diff --git a/src/build-manifest.test.ts b/src/build-manifest.test.ts index 64a9580b..45b2e862 100644 --- a/src/build-manifest.test.ts +++ b/src/build-manifest.test.ts @@ -3,7 +3,7 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { cli, getRegistry, Strategy } from './registry.js'; -import { loadManifestEntries } from './build-manifest.js'; +import { loadManifestEntries, serializeManifest } from './build-manifest.js'; describe('manifest helper rules', () => { const tempDirs: string[] = []; @@ -83,8 +83,9 @@ describe('manifest helper rules', () => { replacedBy: 'opencli demo new', }, ]); - // Verify sourceFile is included + // Verify sourceFile is included and normalized for cross-platform builds. expect(entries[0].sourceFile).toBeDefined(); + expect(entries[0].sourceFile).not.toContain('\\'); getRegistry().delete(key); }); @@ -155,4 +156,11 @@ describe('manifest helper rules', () => { getRegistry().delete(screenKey); getRegistry().delete(statusKey); }); + + it('serializes manifest json with a trailing newline', () => { + const serialized = serializeManifest([{ site: 'demo', name: 'status', description: '', strategy: 'public', browser: false, args: [], type: 'js' }]); + + expect(serialized.endsWith('\n')).toBe(true); + expect(serialized).toContain('\n]'); + }); }); diff --git a/src/build-manifest.ts b/src/build-manifest.ts index e5e3db22..9eaaa46b 100644 --- a/src/build-manifest.ts +++ b/src/build-manifest.ts @@ -75,6 +75,10 @@ function toModulePath(filePath: string, site: string): string { return `${site}/${baseName}.js`; } +function toManifestRelativePath(filePath: string): string { + return path.relative(CLIS_DIR, filePath).split(path.sep).join('/'); +} + function isCliCommandValue(value: unknown, site: string): value is CliCommand { return isRecord(value) && typeof value.site === 'string' @@ -133,8 +137,9 @@ export async function loadManifestEntries( }) .map(([, cmd]) => cmd); - // Resolve sourceFile relative to clis/. - const sourceRelative = path.relative(CLIS_DIR, filePath); + // Resolve sourceFile relative to clis/ using POSIX separators so the + // generated manifest stays stable across Windows and Unix builds. + const sourceRelative = toManifestRelativePath(filePath); const seen = new Set(); return runtimeCommands @@ -178,10 +183,14 @@ export async function buildManifest(): Promise { return [...manifest.values()].sort((a, b) => a.site.localeCompare(b.site) || a.name.localeCompare(b.name)); } +export function serializeManifest(manifest: ManifestEntry[]): string { + return `${JSON.stringify(manifest, null, 2)}\n`; +} + async function main(): Promise { const manifest = await buildManifest(); fs.mkdirSync(path.dirname(OUTPUT), { recursive: true }); - fs.writeFileSync(OUTPUT, JSON.stringify(manifest, null, 2)); + fs.writeFileSync(OUTPUT, serializeManifest(manifest)); console.log(`✅ Manifest compiled: ${manifest.length} entries → ${OUTPUT}`); diff --git a/src/engine.test.ts b/src/engine.test.ts index a8310bd8..dcd87b71 100644 --- a/src/engine.test.ts +++ b/src/engine.test.ts @@ -148,6 +148,7 @@ describe('discoverPlugins', () => { const symlinkTargetDir = path.join(os.tmpdir(), '__test-plugin-symlink-target__'); const symlinkPluginDir = path.join(PLUGINS_DIR, '__test-plugin-symlink__'); const brokenSymlinkDir = path.join(PLUGINS_DIR, '__test-plugin-broken__'); + const dirLinkType: fs.symlink.Type = process.platform === 'win32' ? 'junction' : 'dir'; afterEach(async () => { try { await fs.promises.rm(testPluginDir, { recursive: true }); } catch {} @@ -188,7 +189,7 @@ description: Test plugin greeting via symlink strategy: public browser: false `); - await fs.promises.symlink(symlinkTargetDir, symlinkPluginDir, 'dir'); + await fs.promises.symlink(symlinkTargetDir, symlinkPluginDir, dirLinkType); await discoverPlugins(); @@ -198,7 +199,7 @@ browser: false it('skips broken plugin symlinks without throwing', async () => { await fs.promises.mkdir(PLUGINS_DIR, { recursive: true }); - await fs.promises.symlink(path.join(os.tmpdir(), '__missing-plugin-target__'), brokenSymlinkDir, 'dir'); + await fs.promises.symlink(path.join(os.tmpdir(), '__missing-plugin-target__'), brokenSymlinkDir, dirLinkType); await expect(discoverPlugins()).resolves.not.toThrow(); expect(getRegistry().get('__test-plugin-broken__/hello')).toBeUndefined();