From 90f7106ffb3b26477a03b361875c4b795e616128 Mon Sep 17 00:00:00 2001 From: qwan30 Date: Wed, 8 Jul 2026 20:49:10 +0700 Subject: [PATCH] fix(db): ensure migrate and seed scripts work on Windows (#7)%0A%0AIntroduced isMainModule helper to robustly handle path normalization across platforms. --- src/db/migrate.ts | 3 ++- src/db/seed.ts | 3 ++- src/mcp/server.ts | 3 ++- src/utils/is-main.ts | 24 ++++++++++++++++++ test/is-main.test.ts | 59 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 src/utils/is-main.ts create mode 100644 test/is-main.test.ts diff --git a/src/db/migrate.ts b/src/db/migrate.ts index 8496ef2..f8dd380 100644 --- a/src/db/migrate.ts +++ b/src/db/migrate.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { pool, closePool } from "./pool.js"; import { logger } from "../observability/logger.js"; +import { isMainModule } from "../utils/is-main.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.resolve(__dirname, "../.."); @@ -51,7 +52,7 @@ export async function migrate(): Promise { } } -if (import.meta.url === `file://${process.argv[1]}`) { +if (isMainModule(import.meta.url)) { migrate() .then(async () => closePool()) .catch(async (error: unknown) => { diff --git a/src/db/seed.ts b/src/db/seed.ts index ce0c400..a4be202 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -1,5 +1,6 @@ import { pool, closePool } from "./pool.js"; import { logger } from "../observability/logger.js"; +import { isMainModule } from "../utils/is-main.js"; export const defaultEntityTypes = [ { @@ -124,7 +125,7 @@ export async function seed(): Promise { logger.info({ count: defaultEntityTypes.length }, "seed complete"); } -if (import.meta.url === `file://${process.argv[1]}`) { +if (isMainModule(import.meta.url)) { seed() .then(async () => closePool()) .catch(async (error: unknown) => { diff --git a/src/mcp/server.ts b/src/mcp/server.ts index d3f6b9d..c3d2fe0 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -7,6 +7,7 @@ import { graphService } from "../services/graph-service.js"; import { logger } from "../observability/logger.js"; import { subscribeModelCallLogs, type ModelCallLogRecord } from "../observability/model-call-log.js"; import type { SearchProgressEvent } from "../types.js"; +import { isMainModule } from "../utils/is-main.js"; export function buildMcpServer(): McpServer { const server = new McpServer({ @@ -192,7 +193,7 @@ export async function startMcpServer(): Promise { logger.info("SAG MCP stdio server started"); } -if (import.meta.url === `file://${process.argv[1]}`) { +if (isMainModule(import.meta.url)) { startMcpServer().catch((error: unknown) => { logger.error({ error }, "mcp server failed"); process.exit(1); diff --git a/src/utils/is-main.ts b/src/utils/is-main.ts new file mode 100644 index 0000000..7302932 --- /dev/null +++ b/src/utils/is-main.ts @@ -0,0 +1,24 @@ +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +export function isMainModule(metaUrl: string): boolean { + if (!process.argv[1]) return false; + + try { + const modulePath = fileURLToPath(metaUrl); + let entryPath = process.argv[1]; + + const stripExt = (p: string) => { + const ext = path.extname(p); + return ext ? p.slice(0, -ext.length) : p; + }; + + // Normalize path separators and lowercase on Windows for case-insensitivity + const normalize = (p: string) => + process.platform === 'win32' ? path.resolve(p).toLowerCase() : path.resolve(p); + + return stripExt(normalize(modulePath)) === stripExt(normalize(entryPath)); + } catch { + return false; + } +} diff --git a/test/is-main.test.ts b/test/is-main.test.ts new file mode 100644 index 0000000..8759f9d --- /dev/null +++ b/test/is-main.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { isMainModule } from "../src/utils/is-main.js"; +import { pathToFileURL } from "node:url"; + +describe("isMainModule", () => { + const originalArgv = process.argv; + const originalPlatform = process.platform; + + afterEach(() => { + Object.defineProperty(process, 'argv', { value: originalArgv }); + Object.defineProperty(process, 'platform', { value: originalPlatform }); + }); + + it("should return true when module matches argv[1] on Linux", () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + Object.defineProperty(process, 'argv', { + value: ['node', '/app/src/db/migrate.ts'] + }); + + const metaUrl = pathToFileURL('/app/src/db/migrate.ts').href; + expect(isMainModule(metaUrl)).toBe(true); + }); + + it("should return false when module does not match argv[1]", () => { + Object.defineProperty(process, 'argv', { + value: ['node', '/app/src/db/seed.ts'] + }); + + const metaUrl = pathToFileURL('/app/src/db/migrate.ts').href; + expect(isMainModule(metaUrl)).toBe(false); + }); + + it("should return true when module matches argv[1] on Windows (different case)", () => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + Object.defineProperty(process, 'argv', { + value: ['node', 'c:\\app\\src\\db\\migrate.ts'] + }); + + // simulate import.meta.url which has uppercase drive letter + const metaUrl = pathToFileURL('C:\\app\\src\\db\\migrate.ts').href; + expect(isMainModule(metaUrl)).toBe(true); + }); + + it("should return true even when argv[1] lacks file extension", () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + Object.defineProperty(process, 'argv', { + value: ['node', '/app/src/db/migrate'] + }); + + const metaUrl = pathToFileURL('/app/src/db/migrate.ts').href; + expect(isMainModule(metaUrl)).toBe(true); + }); + + it("should return false if process.argv[1] is undefined", () => { + Object.defineProperty(process, 'argv', { value: ['node'] }); + const metaUrl = pathToFileURL('/app/src/db/migrate.ts').href; + expect(isMainModule(metaUrl)).toBe(false); + }); +});