feat: list discoverable modules in creation wizard

This commit is contained in:
reaper
2026-08-14 12:19:50 -05:00
parent 31e6150ecd
commit f6c2bbfad6
7 changed files with 69 additions and 7 deletions
+2 -1
View File
@@ -1,10 +1,11 @@
import { dialog, ipcMain } from 'electron'
import { getProjectRoot, listInstalledModules, listModules, scaffoldApplicationModule, setModule } from '../auroraEngine'
import { runCommandStreamed } from '../commandRunner'
import { installModulePackage, uninstallModulePackage } from '../moduleRegistry'
import { getAvailableModulePackages, installModulePackage, uninstallModulePackage } from '../moduleRegistry'
export function registerModulesIpc(): void {
ipcMain.handle('modules:listRegistry', () => listModules())
ipcMain.handle('modules:listAvailable', () => getAvailableModulePackages())
ipcMain.handle('modules:pickAndInstallPackage', async () => {
const picked = await dialog.showOpenDialog({ properties: ['openDirectory'] })
if (picked.canceled || !picked.filePaths[0]) return null
+8 -2
View File
@@ -3,8 +3,8 @@ import { join } from 'path'
import { tmpdir } from 'os'
import { afterEach, describe, expect, it, vi } from 'vitest'
vi.mock('electron', () => ({ app: { getPath: () => '/unused' } }))
import { getModuleRegistry, installModulePackage, uninstallModulePackage, validateModuleManifest } from './moduleRegistry'
vi.mock('electron', () => ({ app: { getPath: () => '/unused', getAppPath: () => '/unused/app' } }))
import { getAvailableModulePackages, getModuleRegistry, installModulePackage, uninstallModulePackage, validateModuleManifest } from './moduleRegistry'
const roots: string[] = []
async function temp(prefix: string): Promise<string> { const root = await mkdtemp(join(tmpdir(), prefix)); roots.push(root); return root }
@@ -15,6 +15,12 @@ afterEach(async () => { const { rm } = await import('fs/promises'); await Promis
describe('external module registry', () => {
it('starts empty', async () => expect(await getModuleRegistry(await temp('aurora-user-'))).toEqual([]))
it('lists discoverable packages for the module library', async () => {
const catalog = await temp('aurora-catalog-'); const source = join(catalog, 'sample-app'); await mkdir(source); await writeFile(join(source, 'manifest.json'), JSON.stringify(manifest))
process.env.AURORA_MODULE_PATH = catalog
expect((await getAvailableModulePackages()).map((item) => item.manifest.id)).toContain('sample-app')
delete process.env.AURORA_MODULE_PATH
})
it('installs a valid local package and refreshes after install', async () => {
const userData = await temp('aurora-user-'); const source = await packageDir()
await installModulePackage(source, userData)
+24 -1
View File
@@ -1,7 +1,7 @@
import { app } from 'electron'
import { cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat } from 'fs/promises'
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'path'
import type { AuroraModuleInstallResult, AuroraModuleManifest, AuroraModuleSetting } from '../shared/types'
import type { AuroraAvailableModule, AuroraModuleInstallResult, AuroraModuleManifest, AuroraModuleSetting } from '../shared/types'
export const CORE_VERSION = '2.0.0-alpha.24'
export const MODULE_API_VERSION = '1.0.0'
@@ -78,6 +78,29 @@ export async function getModuleManifest(id: string): Promise<AuroraModuleManifes
}
export function invalidateModuleRegistry(): void { cache = null }
function catalogDirectories(): string[] {
const configured = (process.env.AURORA_MODULE_PATH ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean)
const besideImage = process.env.APPIMAGE ? [join(dirname(process.env.APPIMAGE), 'modules')] : []
return [...configured, ...besideImage, join(process.cwd(), 'modules'), join(process.cwd(), 'packages'), join(app.getAppPath(), 'packages')]
}
export async function getAvailableModulePackages(): Promise<AuroraAvailableModule[]> {
const found = new Map<string, AuroraAvailableModule>()
for (const directory of catalogDirectories()) {
try {
for (const entry of await readdir(directory, { withFileTypes: true })) {
if (!entry.isDirectory()) continue
const sourcePath = join(directory, entry.name)
try {
const manifest = validateModuleManifest(JSON.parse(await readFile(join(sourcePath, 'manifest.json'), 'utf8')))
if (!found.has(manifest.id)) found.set(manifest.id, { manifest, sourcePath })
} catch { /* unrelated or incompatible package */ }
}
} catch { /* optional catalog directory */ }
}
return [...found.values()].sort((a, b) => a.manifest.name.localeCompare(b.manifest.name))
}
export async function installModulePackage(source: string, userData?: string): Promise<AuroraModuleInstallResult> {
if (!isAbsolute(source)) throw new Error('Module package path must be absolute')
if (!(await stat(source)).isDirectory()) throw new Error('Select an unpacked local module directory')