refactor(omo-codex): sync telemetry component sources
This commit is contained in:
+1
-1
@@ -63,7 +63,7 @@
|
|||||||
"typecheck:packages": "tsgo --noEmit -p packages/rules-engine/tsconfig.json && tsgo --noEmit -p packages/ast-grep-core/tsconfig.json && tsgo --noEmit -p packages/ast-grep-mcp/tsconfig.json && tsgo --noEmit -p packages/utils/tsconfig.json && tsgo --noEmit -p packages/model-core/tsconfig.json && tsgo --noEmit -p packages/prompts-core/tsconfig.json && tsgo --noEmit -p packages/comment-checker-core/tsconfig.json && tsgo --noEmit -p packages/hashline-core/tsconfig.json && tsgo --noEmit -p packages/boulder-state/tsconfig.json && tsgo --noEmit -p packages/agents-md-core/tsconfig.json && tsgo --noEmit -p packages/omo-codex/tsconfig.json",
|
"typecheck:packages": "tsgo --noEmit -p packages/rules-engine/tsconfig.json && tsgo --noEmit -p packages/ast-grep-core/tsconfig.json && tsgo --noEmit -p packages/ast-grep-mcp/tsconfig.json && tsgo --noEmit -p packages/utils/tsconfig.json && tsgo --noEmit -p packages/model-core/tsconfig.json && tsgo --noEmit -p packages/prompts-core/tsconfig.json && tsgo --noEmit -p packages/comment-checker-core/tsconfig.json && tsgo --noEmit -p packages/hashline-core/tsconfig.json && tsgo --noEmit -p packages/boulder-state/tsconfig.json && tsgo --noEmit -p packages/agents-md-core/tsconfig.json && tsgo --noEmit -p packages/omo-codex/tsconfig.json",
|
||||||
"typecheck:script": "tsgo --noEmit -p script/tsconfig.json",
|
"typecheck:script": "tsgo --noEmit -p script/tsconfig.json",
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"test:codex": "bun test src/cli/install-codex/codex-cache.test.ts src/cli/install-codex/install-codex.test.ts packages/omo-codex/src/**/*.test.ts packages/utils/src/jsonc-parser.test.ts packages/utils/src/frontmatter.test.ts packages/hashline-core/src/hash-computation.test.ts packages/hashline-core/src/smoke-untested-modules.test.ts packages/rules-engine/src/index.test.ts packages/rules-engine/src/security-boundary.test.ts packages/agents-md-core/src/injector.test.ts && node --test packages/omo-codex/plugin/test/*.test.mjs packages/omo-codex/scripts/install-local.test.mjs",
|
"test:codex": "bun test src/cli/install-codex/codex-cache.test.ts src/cli/install-codex/install-codex.test.ts packages/omo-codex/src/**/*.test.ts packages/utils/src/jsonc-parser.test.ts packages/utils/src/frontmatter.test.ts packages/hashline-core/src/hash-computation.test.ts packages/hashline-core/src/smoke-untested-modules.test.ts packages/rules-engine/src/index.test.ts packages/rules-engine/src/security-boundary.test.ts packages/agents-md-core/src/injector.test.ts && node --test packages/omo-codex/plugin/test/*.test.mjs packages/omo-codex/scripts/install-local.test.mjs packages/omo-codex/scripts/sync-telemetry-component.test.mjs",
|
||||||
"test:windows-codex": "bun run test:codex",
|
"test:windows-codex": "bun run test:codex",
|
||||||
"build:ast-grep-mcp": "bun run --cwd packages/ast-grep-mcp build"
|
"build:ast-grep-mcp": "bun run --cwd packages/ast-grep-mcp build"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import { renameSync, unlinkSync, writeFileSync } from "node:fs";
|
import { renameSync, unlinkSync, writeFileSync } from "node:fs"
|
||||||
|
|
||||||
export function writeFileAtomically(filePath: string, content: string): void {
|
export function writeFileAtomically(filePath: string, content: string): void {
|
||||||
const tempPath = `${filePath}.tmp`;
|
const tempPath = `${filePath}.tmp`
|
||||||
writeFileSync(tempPath, content, "utf-8");
|
writeFileSync(tempPath, content, "utf-8")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
renameSync(tempPath, filePath);
|
renameSync(tempPath, filePath)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const isPermissionError =
|
const isPermissionError =
|
||||||
error instanceof Error && (error.message.includes("EPERM") || error.message.includes("EACCES"));
|
error instanceof Error &&
|
||||||
|
(error.message.includes("EPERM") || error.message.includes("EACCES"))
|
||||||
|
|
||||||
if (process.platform === "win32" && isPermissionError) {
|
if (process.platform === "win32" && isPermissionError) {
|
||||||
unlinkSync(filePath);
|
unlinkSync(filePath)
|
||||||
renameSync(tempPath, filePath);
|
renameSync(tempPath, filePath)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,45 @@
|
|||||||
import { accessSync, constants, mkdirSync } from "node:fs";
|
import { accessSync, constants, mkdirSync } from "node:fs"
|
||||||
import os from "node:os";
|
import os from "node:os"
|
||||||
import path from "node:path";
|
import path from "node:path"
|
||||||
|
|
||||||
import { CACHE_DIR_NAME } from "./product-identity.js";
|
import { CACHE_DIR_NAME } from "./product-identity.js"
|
||||||
|
|
||||||
type OsProvider = Pick<typeof os, "homedir" | "tmpdir">;
|
type OsProvider = Pick<typeof os, "homedir" | "tmpdir">
|
||||||
|
|
||||||
let osProviderOverride: OsProvider | null = null;
|
let osProviderOverride: OsProvider | null = null
|
||||||
|
|
||||||
export function getOsProvider(): OsProvider {
|
export function getOsProvider(): OsProvider {
|
||||||
return osProviderOverride ?? os;
|
return osProviderOverride ?? os
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal test-only */
|
/** @internal test-only */
|
||||||
export function __setOsProviderForTesting(provider: OsProvider): void {
|
export function __setOsProviderForTesting(provider: OsProvider): void {
|
||||||
osProviderOverride = provider;
|
osProviderOverride = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal test-only */
|
/** @internal test-only */
|
||||||
export function __resetOsProviderForTesting(): void {
|
export function __resetOsProviderForTesting(): void {
|
||||||
osProviderOverride = null;
|
osProviderOverride = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveWritableDirectory(preferredDir: string, fallbackSuffix: string): string {
|
function resolveWritableDirectory(preferredDir: string, fallbackSuffix: string): string {
|
||||||
try {
|
try {
|
||||||
mkdirSync(preferredDir, { recursive: true });
|
mkdirSync(preferredDir, { recursive: true })
|
||||||
accessSync(preferredDir, constants.W_OK);
|
accessSync(preferredDir, constants.W_OK)
|
||||||
return preferredDir;
|
return preferredDir
|
||||||
} catch {
|
} catch {
|
||||||
const fallbackDir = path.join(getOsProvider().tmpdir(), fallbackSuffix);
|
const fallbackDir = path.join(getOsProvider().tmpdir(), fallbackSuffix)
|
||||||
mkdirSync(fallbackDir, { recursive: true });
|
mkdirSync(fallbackDir, { recursive: true })
|
||||||
return fallbackDir;
|
return fallbackDir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDataDir(): string {
|
export function getDataDir(): string {
|
||||||
const preferredDataDir = process.env["XDG_DATA_HOME"] ?? path.join(getOsProvider().homedir(), ".local", "share");
|
const preferredDataDir =
|
||||||
return resolveWritableDirectory(preferredDataDir, "omo-codex-data");
|
process.env["XDG_DATA_HOME"] ?? path.join(getOsProvider().homedir(), ".local", "share")
|
||||||
|
return resolveWritableDirectory(preferredDataDir, "omo-codex-data")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getActivityStateDir(): string {
|
export function getActivityStateDir(): string {
|
||||||
return path.join(getDataDir(), CACHE_DIR_NAME);
|
return path.join(getDataDir(), CACHE_DIR_NAME)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,43 @@
|
|||||||
import { DEFAULT_POSTHOG_API_KEY, DEFAULT_POSTHOG_HOST } from "./product-identity.js";
|
import {
|
||||||
|
DEFAULT_POSTHOG_API_KEY,
|
||||||
|
DEFAULT_POSTHOG_HOST,
|
||||||
|
} from "./product-identity.js"
|
||||||
|
|
||||||
function normalizeEnvValue(value: string | undefined): string | undefined {
|
function normalizeEnvValue(value: string | undefined): string | undefined {
|
||||||
return value?.trim().toLowerCase();
|
return value?.trim().toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDisableFlag(value: string | undefined): boolean {
|
function isDisableFlag(value: string | undefined): boolean {
|
||||||
const normalized = normalizeEnvValue(value);
|
const normalized = normalizeEnvValue(value)
|
||||||
return normalized === "1" || normalized === "true";
|
return normalized === "1" || normalized === "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTelemetryOptOutFlag(value: string | undefined): boolean {
|
function isTelemetryOptOutFlag(value: string | undefined): boolean {
|
||||||
const normalized = normalizeEnvValue(value);
|
const normalized = normalizeEnvValue(value)
|
||||||
return normalized === "0" || normalized === "false" || normalized === "no";
|
return normalized === "0" || normalized === "false" || normalized === "no"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldDisablePostHog(): boolean {
|
export function shouldDisablePostHog(): boolean {
|
||||||
return (
|
return (
|
||||||
isDisableFlag(process.env["OMO_DISABLE_POSTHOG"]) ||
|
isDisableFlag(process.env["OMO_DISABLE_POSTHOG"]) ||
|
||||||
isTelemetryOptOutFlag(process.env["OMO_SEND_ANONYMOUS_TELEMETRY"]) ||
|
isTelemetryOptOutFlag(process.env["OMO_SEND_ANONYMOUS_TELEMETRY"]) ||
|
||||||
isDisableFlag(process.env["OMO_CODEX_DISABLE_POSTHOG"]) ||
|
isDisableFlag(process.env["OMO_CODEX_DISABLE_POSTHOG"]) ||
|
||||||
isTelemetryOptOutFlag(process.env["OMO_CODEX_SEND_ANONYMOUS_TELEMETRY"])
|
isTelemetryOptOutFlag(process.env["OMO_CODEX_SEND_ANONYMOUS_TELEMETRY"])
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPostHogApiKey(): string {
|
export function getPostHogApiKey(): string {
|
||||||
const explicit = process.env["POSTHOG_API_KEY"];
|
const explicit = process.env["POSTHOG_API_KEY"]
|
||||||
if (explicit === undefined) {
|
if (explicit === undefined) {
|
||||||
return DEFAULT_POSTHOG_API_KEY;
|
return DEFAULT_POSTHOG_API_KEY
|
||||||
}
|
}
|
||||||
return explicit.trim();
|
return explicit.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasPostHogApiKey(): boolean {
|
export function hasPostHogApiKey(): boolean {
|
||||||
return getPostHogApiKey().length > 0;
|
return getPostHogApiKey().length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPostHogHost(): string {
|
export function getPostHogHost(): string {
|
||||||
return process.env["POSTHOG_HOST"]?.trim() || DEFAULT_POSTHOG_HOST;
|
return process.env["POSTHOG_HOST"]?.trim() || DEFAULT_POSTHOG_HOST
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +1,81 @@
|
|||||||
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readFileSync } from "node:fs"
|
||||||
import { join } from "node:path";
|
import { join } from "node:path"
|
||||||
|
|
||||||
import { writeFileAtomically } from "./atomic-write.js";
|
import { writeFileAtomically } from "./atomic-write.js"
|
||||||
import { getActivityStateDir } from "./data-path.js";
|
import { getActivityStateDir } from "./data-path.js"
|
||||||
|
|
||||||
export type PostHogActivityState = {
|
export type PostHogActivityState = {
|
||||||
readonly lastActiveDayUTC?: string;
|
readonly lastActiveDayUTC?: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type PostHogActivityCaptureState = {
|
export type PostHogActivityCaptureState = {
|
||||||
readonly dayUTC: string;
|
readonly dayUTC: string
|
||||||
readonly captureDaily: boolean;
|
readonly captureDaily: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
const POSTHOG_ACTIVITY_STATE_FILE = "posthog-activity.json";
|
const POSTHOG_ACTIVITY_STATE_FILE = "posthog-activity.json"
|
||||||
|
|
||||||
function getPostHogActivityStateFilePath(): string {
|
function getPostHogActivityStateFilePath(): string {
|
||||||
return join(getActivityStateDir(), POSTHOG_ACTIVITY_STATE_FILE);
|
return join(getActivityStateDir(), POSTHOG_ACTIVITY_STATE_FILE)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUtcDayString(date: Date): string {
|
function getUtcDayString(date: Date): string {
|
||||||
return date.toISOString().slice(0, 10);
|
return date.toISOString().slice(0, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPostHogActivityState(value: unknown): value is PostHogActivityState {
|
function isPostHogActivityState(value: unknown): value is PostHogActivityState {
|
||||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPostHogActivityState(): PostHogActivityState {
|
function readPostHogActivityState(): PostHogActivityState {
|
||||||
const stateFilePath = getPostHogActivityStateFilePath();
|
const stateFilePath = getPostHogActivityStateFilePath()
|
||||||
|
|
||||||
if (!existsSync(stateFilePath)) {
|
if (!existsSync(stateFilePath)) {
|
||||||
return {};
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stateContent = readFileSync(stateFilePath, "utf-8");
|
const stateContent = readFileSync(stateFilePath, "utf-8")
|
||||||
const stateJson: unknown = JSON.parse(stateContent);
|
const stateJson: unknown = JSON.parse(stateContent)
|
||||||
|
|
||||||
if (!isPostHogActivityState(stateJson)) {
|
if (!isPostHogActivityState(stateJson)) {
|
||||||
return {};
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return stateJson;
|
return stateJson
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function writePostHogActivityState(nextState: PostHogActivityState): void {
|
function writePostHogActivityState(nextState: PostHogActivityState): void {
|
||||||
const stateDir = getActivityStateDir();
|
const stateDir = getActivityStateDir()
|
||||||
const stateFilePath = getPostHogActivityStateFilePath();
|
const stateFilePath = getPostHogActivityStateFilePath()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mkdirSync(stateDir, { recursive: true });
|
mkdirSync(stateDir, { recursive: true })
|
||||||
writeFileAtomically(stateFilePath, `${JSON.stringify(nextState, null, 2)}\n`);
|
writeFileAtomically(stateFilePath, `${JSON.stringify(nextState, null, 2)}\n`)
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPostHogActivityCaptureState(now: Date = new Date()): PostHogActivityCaptureState {
|
export function getPostHogActivityCaptureState(
|
||||||
const state = readPostHogActivityState();
|
now: Date = new Date(),
|
||||||
const dayUTC = getUtcDayString(now);
|
): PostHogActivityCaptureState {
|
||||||
const captureDaily = state.lastActiveDayUTC !== dayUTC;
|
const state = readPostHogActivityState()
|
||||||
|
const dayUTC = getUtcDayString(now)
|
||||||
|
const captureDaily = state.lastActiveDayUTC !== dayUTC
|
||||||
|
|
||||||
if (captureDaily) {
|
if (captureDaily) {
|
||||||
writePostHogActivityState({
|
writePostHogActivityState({
|
||||||
...state,
|
...state,
|
||||||
lastActiveDayUTC: dayUTC,
|
lastActiveDayUTC: dayUTC,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dayUTC,
|
dayUTC,
|
||||||
captureDaily,
|
captureDaily,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"components/ultrawork"
|
"components/ultrawork"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/sync-skills.mjs && npm run build --workspaces --if-present",
|
"build": "node scripts/sync-skills.mjs && node ../scripts/sync-telemetry-component.mjs && npm run build --workspaces --if-present",
|
||||||
"check": "npm run build && npm test",
|
"check": "npm run build && npm test",
|
||||||
"sync:skills": "node scripts/sync-skills.mjs",
|
"sync:skills": "node scripts/sync-skills.mjs",
|
||||||
"test": "node --test test/*.test.mjs"
|
"test": "node --test test/*.test.mjs"
|
||||||
|
|||||||
@@ -62,6 +62,19 @@ test("#given aggregate MCP config #when inspected #then lsp server stays compone
|
|||||||
assert.equal(server.cwd, ".");
|
assert.equal(server.cwd, ".");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("#given aggregate plugin build script #when inspected #then telemetry sync runs before workspace builds", async () => {
|
||||||
|
// given
|
||||||
|
const packageJson = await readJson("package.json");
|
||||||
|
const telemetrySyncScript = await readFile(join(root, "..", "scripts", "sync-telemetry-component.mjs"), "utf8");
|
||||||
|
|
||||||
|
// when
|
||||||
|
const buildScript = packageJson.scripts.build;
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert.equal(buildScript, "node scripts/sync-skills.mjs && node ../scripts/sync-telemetry-component.mjs && npm run build --workspaces --if-present");
|
||||||
|
assert.match(telemetrySyncScript, /syncTelemetryComponent/);
|
||||||
|
});
|
||||||
|
|
||||||
test("#given component directories #when scanned #then only root owns plugin identity", async () => {
|
test("#given component directories #when scanned #then only root owns plugin identity", async () => {
|
||||||
// given
|
// given
|
||||||
const components = await readdir(join(root, "components"), { withFileTypes: true });
|
const components = await readdir(join(root, "components"), { withFileTypes: true });
|
||||||
|
|||||||
@@ -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-codex-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