Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/build-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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]');
});
});
15 changes: 12 additions & 3 deletions src/build-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<string>();
return runtimeCommands
Expand Down Expand Up @@ -178,10 +183,14 @@ export async function buildManifest(): Promise<ManifestEntry[]> {
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<void> {
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}`);

Expand Down
5 changes: 3 additions & 2 deletions src/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down Expand Up @@ -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();

Expand All @@ -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();
Expand Down