feat: complete external module lifecycle SDK
This commit is contained in:
@@ -10,7 +10,7 @@ const execFileAsync = promisify(execFile)
|
||||
const EXTRA_PATH_DIRS = ['/opt/homebrew/bin', '/usr/local/bin', '/opt/local/bin']
|
||||
export const AURORA_ENV = { ...process.env, PATH: [...EXTRA_PATH_DIRS, process.env.PATH].join(':') }
|
||||
|
||||
type AuroraConfig = {
|
||||
export type AuroraConfig = {
|
||||
name: string
|
||||
type: string
|
||||
docroot: string
|
||||
|
||||
+22
-17
@@ -1,7 +1,8 @@
|
||||
import { dialog, ipcMain } from 'electron'
|
||||
import { getProjectRoot, listInstalledModules, listModules, scaffoldApplicationModule, setModule } from '../auroraEngine'
|
||||
import { getProjectConfig, getProjectRoot, listInstalledModules, listModules, scaffoldApplicationModule, setModule } from '../auroraEngine'
|
||||
import { runCommandStreamed } from '../commandRunner'
|
||||
import { getAvailableModulePackages, installModulePackage, uninstallModulePackage } from '../moduleRegistry'
|
||||
import { runModuleLifecycleHook, runModuleProjectTool } from '../moduleRuntime'
|
||||
|
||||
export function registerModulesIpc(): void {
|
||||
ipcMain.handle('modules:listRegistry', () => listModules())
|
||||
@@ -15,7 +16,10 @@ export function registerModulesIpc(): void {
|
||||
return installModulePackage(picked.filePaths[0])
|
||||
})
|
||||
ipcMain.handle('modules:installPackage', (_event, source: string) => installModulePackage(source))
|
||||
ipcMain.handle('modules:uninstallPackage', (_event, id: string) => uninstallModulePackage(id))
|
||||
ipcMain.handle('modules:uninstallPackage', async (event, operationId: string, id: string) => {
|
||||
try { await runModuleLifecycleHook(id, 'packageUninstall', { operationId, sender: event.sender }); await uninstallModulePackage(id); if (!event.sender.isDestroyed()) event.sender.send('terminal:exit', { operationId, exitCode: 0, cancelled: false }) }
|
||||
catch (error) { if (!event.sender.isDestroyed()) event.sender.send('terminal:exit', { operationId, exitCode: 1, cancelled: false }); throw error }
|
||||
})
|
||||
ipcMain.handle('modules:listInstalled', (_event, name: string) => listInstalledModules(name))
|
||||
ipcMain.handle(
|
||||
'modules:install',
|
||||
@@ -29,27 +33,28 @@ export function registerModulesIpc(): void {
|
||||
await setModule(name, moduleId, true, settings)
|
||||
await scaffoldApplicationModule(name, moduleId)
|
||||
const root = await getProjectRoot(name)
|
||||
return runCommandStreamed(
|
||||
operationId,
|
||||
'docker',
|
||||
['compose', '-f', `${root}/.aurora/compose.yaml`, 'up', '-d', '--remove-orphans'],
|
||||
event.sender,
|
||||
{ cwd: root }
|
||||
)
|
||||
try {
|
||||
await runCommandStreamed(operationId, 'docker', ['compose', '-f', `${root}/.aurora/compose.yaml`, 'up', '-d', '--remove-orphans'], event.sender, { cwd: root, emitExit: false })
|
||||
await runModuleLifecycleHook(moduleId, 'projectCreate', { directory: root, projectName: name, settings, operationId, sender: event.sender })
|
||||
if (!event.sender.isDestroyed()) event.sender.send('terminal:exit', { operationId, exitCode: 0, cancelled: false })
|
||||
} catch (error) { if (!event.sender.isDestroyed()) event.sender.send('terminal:exit', { operationId, exitCode: 1, cancelled: false }); throw error }
|
||||
}
|
||||
)
|
||||
ipcMain.handle(
|
||||
'modules:remove',
|
||||
async (event, operationId: string, name: string, moduleId: string) => {
|
||||
await setModule(name, moduleId, false)
|
||||
const root = await getProjectRoot(name)
|
||||
return runCommandStreamed(
|
||||
operationId,
|
||||
'docker',
|
||||
['compose', '-f', `${root}/.aurora/compose.yaml`, 'up', '-d', '--remove-orphans'],
|
||||
event.sender,
|
||||
{ cwd: root }
|
||||
)
|
||||
try {
|
||||
const config = await getProjectConfig(root)
|
||||
await runModuleLifecycleHook(moduleId, 'projectRemove', { directory: root, projectName: name, settings: config.moduleSettings?.[moduleId], operationId, sender: event.sender })
|
||||
await setModule(name, moduleId, false)
|
||||
await runCommandStreamed(operationId, 'docker', ['compose', '-f', `${root}/.aurora/compose.yaml`, 'up', '-d', '--remove-orphans'], event.sender, { cwd: root, emitExit: false })
|
||||
if (!event.sender.isDestroyed()) event.sender.send('terminal:exit', { operationId, exitCode: 0, cancelled: false })
|
||||
} catch (error) { if (!event.sender.isDestroyed()) event.sender.send('terminal:exit', { operationId, exitCode: 1, cancelled: false }); throw error }
|
||||
}
|
||||
)
|
||||
ipcMain.handle('modules:runProjectTool', async (event, operationId: string, name: string, moduleId: string, toolId: string) => {
|
||||
const root = await getProjectRoot(name)
|
||||
return runModuleProjectTool(moduleId, toolId, operationId, root, event.sender)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { ipcMain, type WebContents } from 'electron'
|
||||
import { spawn } from 'child_process'
|
||||
import { listProjects, describeProject, getProjectRoot, unregisterProject, updateEnvironment, ensureRouter, trustAuroraCA } from '../auroraEngine'
|
||||
import { runCommandStreamed } from '../commandRunner'
|
||||
const composeArgs=(root:string,...args:string[])=>['compose','-f',`${root}/.aurora/compose.yaml`,...args]
|
||||
import { runProjectLifecycleHooks } from '../moduleRuntime'
|
||||
import type { EnvironmentUpdate } from '../../shared/types'
|
||||
const composeArgs=(root:string,...args:string[]):string[]=>['compose','-f',`${root}/.aurora/compose.yaml`,...args]
|
||||
const allowedServices = new Set(['web','php','db','node','adminer','redis','mailpit'])
|
||||
function launchTerminal(command:string,args:string[]):Promise<void>{return new Promise((resolve,reject)=>{const child=spawn(command,args,{detached:true,stdio:'ignore'});child.once('error',reject);child.once('spawn',()=>{child.unref();resolve()})})}
|
||||
async function startProject(operationId:string,name:string,sender:WebContents,forceRecreate=false):Promise<void>{
|
||||
const root=await getProjectRoot(name); await updateEnvironment(root,{}); await ensureRouter()
|
||||
try { await runCommandStreamed(operationId,'docker',composeArgs(root,'up','-d','--build',...(forceRecreate?['--force-recreate']:[]),'--remove-orphans'),sender,{cwd:root,emitExit:false}); await runProjectLifecycleHooks(root,'projectStart',operationId,sender); if(!sender.isDestroyed())sender.send('terminal:exit',{operationId,exitCode:0,cancelled:false}) }
|
||||
catch(error){if(!sender.isDestroyed())sender.send('terminal:exit',{operationId,exitCode:1,cancelled:false});throw error}
|
||||
}
|
||||
export function registerProjectsIpc():void{
|
||||
ipcMain.handle('projects:list',()=>listProjects()); ipcMain.handle('projects:describe',(_e,name:string)=>describeProject(name))
|
||||
ipcMain.handle('projects:start',async(e,id:string,name:string)=>{const root=await getProjectRoot(name);await updateEnvironment(root,{});await ensureRouter();return runCommandStreamed(id,'docker',composeArgs(root,'up','-d','--build','--remove-orphans'),e.sender,{cwd:root})})
|
||||
ipcMain.handle('projects:start',(e,id:string,name:string)=>startProject(id,name,e.sender))
|
||||
ipcMain.handle('projects:stop',async(e,id:string,name:string)=>{const root=await getProjectRoot(name);return runCommandStreamed(id,'docker',composeArgs(root,'down'),e.sender,{cwd:root})})
|
||||
ipcMain.handle('projects:restart',async(e,id:string,name:string)=>{const root=await getProjectRoot(name);await updateEnvironment(root,{});await ensureRouter();return runCommandStreamed(id,'docker',composeArgs(root,'up','-d','--build','--force-recreate','--remove-orphans'),e.sender,{cwd:root})})
|
||||
ipcMain.handle('projects:restart',(e,id:string,name:string)=>startProject(id,name,e.sender,true))
|
||||
ipcMain.handle('projects:restartService',async(e,id:string,name:string,service:string)=>{if(!allowedServices.has(service))throw new Error('Invalid service');const root=await getProjectRoot(name);return runCommandStreamed(id,'docker',composeArgs(root,'restart',service),e.sender,{cwd:root})})
|
||||
ipcMain.handle('projects:phpInfo',async(e,id:string,name:string)=>{const root=await getProjectRoot(name);return runCommandStreamed(id,'docker',composeArgs(root,'exec','-T','php','php','-i'),e.sender,{cwd:root})})
|
||||
ipcMain.handle('projects:openTerminal',async(_e,name:string)=>{const root=await getProjectRoot(name);const candidates: Array<[string,string[]]>=process.platform==='linux'?[['ptyxis',['--new-window','--working-directory',root,'--title',`Aurora · ${name}`]],['x-terminal-emulator',['--new-window','--working-directory',root]],['gnome-terminal',[`--working-directory=${root}`]],['kgx',['--working-directory',root]],['konsole',['--workdir',root]]]:[];for(const [cmd,args] of candidates){try{await launchTerminal(cmd,args);return}catch{}}throw new Error('No supported terminal application was found.')})
|
||||
ipcMain.handle('projects:openTerminal',async(_e,name:string)=>{const root=await getProjectRoot(name);const candidates: Array<[string,string[]]>=process.platform==='linux'?[['ptyxis',['--new-window','--working-directory',root,'--title',`Aurora · ${name}`]],['x-terminal-emulator',['--new-window','--working-directory',root]],['gnome-terminal',[`--working-directory=${root}`]],['kgx',['--working-directory',root]],['konsole',['--workdir',root]]]:[];for(const [cmd,args] of candidates){try{await launchTerminal(cmd,args);return}catch{/* try the next supported terminal */}}throw new Error('No supported terminal application was found.')})
|
||||
ipcMain.handle('projects:delete',async(e,id:string,name:string,approot:string,deleteFiles:boolean)=>{
|
||||
let root=approot
|
||||
try { root=await getProjectRoot(name) } catch { /* use renderer-provided root for stale entries */ }
|
||||
try { root=await getProjectRoot(name) } catch { /* use renderer-provided root for stale entries */ }
|
||||
try { await runProjectLifecycleHooks(root,'projectRemove',id,e.sender) } catch (error) { console.warn(`Aurora module cleanup for '${name}' failed:`, error) }
|
||||
try {
|
||||
await runCommandStreamed(id,'docker',composeArgs(root,'down','-v','--remove-orphans'),e.sender,{cwd:root})
|
||||
} catch (error) {
|
||||
@@ -25,7 +33,7 @@ export function registerProjectsIpc():void{
|
||||
await unregisterProject(name,deleteFiles)
|
||||
})
|
||||
ipcMain.handle('projects:trustCA',async()=>{ await trustAuroraCA(); await ensureRouter() })
|
||||
ipcMain.handle('projects:updateEnvironment',async(_e,_id:string,_name:string,root:string,updates:any)=>{
|
||||
ipcMain.handle('projects:updateEnvironment',async(_e,_id:string,_name:string,root:string,updates:EnvironmentUpdate)=>{
|
||||
await updateEnvironment(root,updates)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -65,6 +65,13 @@ export function validateModuleManifest(value: unknown): AuroraModuleManifest {
|
||||
if (action.hiddenValues !== undefined && !Array.isArray(action.hiddenValues)) throw new Error('Invalid project action hiddenValues')
|
||||
}
|
||||
}
|
||||
if (project.tools !== undefined) {
|
||||
if (!Array.isArray(project.tools)) throw new Error('project.tools must be an array')
|
||||
for (const toolValue of project.tools) {
|
||||
const tool = toolValue as Record<string, unknown>
|
||||
if (!tool || typeof tool !== 'object' || typeof tool.id !== 'string' || !/^[a-z][a-z0-9_-]*$/.test(tool.id) || typeof tool.label !== 'string' || !tool.label.trim()) throw new Error('Invalid project tool')
|
||||
}
|
||||
}
|
||||
}
|
||||
return value as AuroraModuleManifest
|
||||
}
|
||||
|
||||
+84
-24
@@ -1,38 +1,98 @@
|
||||
import { execFile } from 'child_process'
|
||||
import { createRequire } from 'module'
|
||||
import { promisify } from 'util'
|
||||
import { dirname, join, resolve, sep } from 'path'
|
||||
import type { WebContents } from 'electron'
|
||||
import type { AuroraModuleLifecycleHook } from '../shared/types'
|
||||
import { getModuleManifest, moduleDirectory } from './moduleRegistry'
|
||||
import { runCommandStreamed } from './commandRunner'
|
||||
import { ensureRouter, getProjectConfigByRoot, projectUrls, setProjectModuleMetadata } from './auroraEngine'
|
||||
import { AURORA_ENV, ensureRouter, getProjectConfig, projectUrls, setProjectModuleMetadata } from './auroraEngine'
|
||||
import { saveSiteCredentials } from './ipc/secrets'
|
||||
|
||||
type Settings = Record<string, string | number | boolean>
|
||||
type ExternalHook = (context: Record<string, unknown>) => Promise<void>
|
||||
type ExternalHook = (context: Record<string, unknown>) => Promise<void> | void
|
||||
type LoadedModule = Partial<Record<AuroraModuleLifecycleHook, ExternalHook>>
|
||||
type HookOptions = { directory?: string; projectName?: string; settings?: Settings; toolId?: string; operationId?: string; sender?: WebContents }
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
const CORE_MODULE_IDS = new Set(['adminer', 'redis', 'mailpit'])
|
||||
|
||||
function sendExit(sender: WebContents, operationId: string, exitCode: number): void {
|
||||
if (!sender.isDestroyed()) sender.send('terminal:exit', { operationId, exitCode, cancelled: false })
|
||||
}
|
||||
|
||||
function loadModule(moduleId: string, main: string): LoadedModule {
|
||||
const root = resolve(moduleDirectory(), moduleId)
|
||||
const entry = resolve(root, main)
|
||||
if (entry !== root && !entry.startsWith(`${root}${sep}`)) throw new Error('Unsafe module main entry')
|
||||
return createRequire(join(dirname(entry), 'loader.cjs'))(entry) as LoadedModule
|
||||
}
|
||||
|
||||
export async function runModuleLifecycleHook(moduleId: string, hook: AuroraModuleLifecycleHook, options: HookOptions = {}): Promise<boolean> {
|
||||
const manifest = await getModuleManifest(moduleId)
|
||||
if (!manifest.main) return false
|
||||
const handler = loadModule(moduleId, manifest.main)[hook]
|
||||
if (typeof handler !== 'function') return false
|
||||
const directory = options.directory
|
||||
const urls = options.projectName ? projectUrls(options.projectName) : undefined
|
||||
const run = async (label: string, command: string, args: string[]): Promise<void> => {
|
||||
if (options.sender && options.operationId) {
|
||||
if (!options.sender.isDestroyed()) options.sender.send('terminal:data', { operationId: options.operationId, stream: 'stdout', chunk: `\n[${manifest.name}] ${label}\n` })
|
||||
return runCommandStreamed(options.operationId, command, args, options.sender, { cwd: directory ?? resolve(moduleDirectory(), moduleId), emitExit: false })
|
||||
}
|
||||
await execFileAsync(command, args, { cwd: directory ?? resolve(moduleDirectory(), moduleId), env: AURORA_ENV })
|
||||
}
|
||||
await handler(Object.freeze({
|
||||
moduleId,
|
||||
hook,
|
||||
directory,
|
||||
projectName: options.projectName,
|
||||
toolId: options.toolId,
|
||||
settings: Object.freeze({ ...(options.settings ?? {}) }),
|
||||
urls: urls ? Object.freeze(urls) : undefined,
|
||||
run,
|
||||
ensureRouter,
|
||||
setProjectMetadata: async (metadata: Settings) => {
|
||||
if (!directory) throw new Error('Project metadata is unavailable outside a project lifecycle hook')
|
||||
await setProjectModuleMetadata(directory, metadata)
|
||||
},
|
||||
saveCredentials: async (credentials: { platform: string; adminUrl: string; username: string; password: string; email: string }) => {
|
||||
if (!directory) throw new Error('Project credentials are unavailable outside a project lifecycle hook')
|
||||
await saveSiteCredentials(directory, credentials)
|
||||
}
|
||||
}))
|
||||
return true
|
||||
}
|
||||
|
||||
export async function runProjectLifecycleHooks(directory: string, hook: 'projectStart' | 'projectRemove', operationId: string, sender: WebContents): Promise<void> {
|
||||
const config = await getProjectConfig(directory)
|
||||
for (const moduleId of config.modules) {
|
||||
if (CORE_MODULE_IDS.has(moduleId)) continue
|
||||
await runModuleLifecycleHook(moduleId, hook, { directory, projectName: config.name, settings: config.moduleSettings?.[moduleId], operationId, sender })
|
||||
}
|
||||
}
|
||||
|
||||
export async function runModuleProjectCreate(moduleId: string, operationId: string, directory: string, projectName: string, settings: Settings, sender: WebContents): Promise<void> {
|
||||
const manifest = await getModuleManifest(moduleId)
|
||||
if (!manifest.main) return
|
||||
const root = resolve(moduleDirectory(), moduleId)
|
||||
const entry = resolve(root, manifest.main)
|
||||
if (entry !== root && !entry.startsWith(`${root}${sep}`)) throw new Error('Unsafe module main entry')
|
||||
const loaded = createRequire(join(dirname(entry), 'loader.cjs'))(entry) as { projectCreate?: ExternalHook }
|
||||
if (typeof loaded.projectCreate !== 'function') return
|
||||
const config = await getProjectConfigByRoot(directory)
|
||||
const urls = projectUrls(config.name)
|
||||
try { await loaded.projectCreate(Object.freeze({
|
||||
moduleId,
|
||||
directory,
|
||||
projectName,
|
||||
settings: Object.freeze({ ...settings }),
|
||||
urls: Object.freeze(urls),
|
||||
run: (_suffix: string, command: string, args: string[]) => runCommandStreamed(operationId, command, args, sender, { cwd: directory, emitExit: false }),
|
||||
ensureRouter,
|
||||
setProjectMetadata: (metadata: Record<string, string | number | boolean>) => setProjectModuleMetadata(directory, metadata),
|
||||
saveCredentials: (credentials: { platform: string; adminUrl: string; username: string; password: string; email: string }) => saveSiteCredentials(directory, credentials)
|
||||
}))
|
||||
if (!sender.isDestroyed()) sender.send('terminal:exit', { operationId, exitCode: 0, cancelled: false })
|
||||
try {
|
||||
await runModuleLifecycleHook(moduleId, 'projectCreate', { directory, projectName, settings, operationId, sender })
|
||||
sendExit(sender, operationId, 0)
|
||||
} catch (error) {
|
||||
if (!sender.isDestroyed()) sender.send('terminal:exit', { operationId, exitCode: 1, cancelled: false })
|
||||
sendExit(sender, operationId, 1)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function runModuleProjectTool(moduleId: string, toolId: string, operationId: string, directory: string, sender: WebContents): Promise<void> {
|
||||
const manifest = await getModuleManifest(moduleId)
|
||||
if (!manifest.project?.tools?.some((tool) => tool.id === toolId)) throw new Error(`Unknown module tool '${toolId}'`)
|
||||
const config = await getProjectConfig(directory)
|
||||
if (!config.modules.includes(moduleId)) throw new Error(`Module '${moduleId}' is not installed in this project`)
|
||||
try {
|
||||
const handled = await runModuleLifecycleHook(moduleId, 'projectTool', { directory, projectName: config.name, settings: config.moduleSettings?.[moduleId], toolId, operationId, sender })
|
||||
if (!handled) throw new Error(`Module '${moduleId}' does not implement projectTool`)
|
||||
sendExit(sender, operationId, 0)
|
||||
} catch (error) {
|
||||
sendExit(sender, operationId, 1)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ const api = {
|
||||
ipcRenderer.invoke('modules:pickAndInstallPackage'),
|
||||
installPackage: (source: string): Promise<AuroraModuleInstallResult> =>
|
||||
ipcRenderer.invoke('modules:installPackage', source),
|
||||
uninstallPackage: (id: string): Promise<void> => ipcRenderer.invoke('modules:uninstallPackage', id),
|
||||
uninstallPackage: (operationId: string, id: string): Promise<void> => ipcRenderer.invoke('modules:uninstallPackage', operationId, id),
|
||||
install: (
|
||||
operationId: string,
|
||||
name: string,
|
||||
@@ -98,7 +98,9 @@ const api = {
|
||||
settings: Record<string, string | number | boolean>
|
||||
): Promise<void> => ipcRenderer.invoke('modules:install', operationId, name, moduleId, settings),
|
||||
remove: (operationId: string, name: string, moduleId: string): Promise<void> =>
|
||||
ipcRenderer.invoke('modules:remove', operationId, name, moduleId)
|
||||
ipcRenderer.invoke('modules:remove', operationId, name, moduleId),
|
||||
runProjectTool: (operationId: string, name: string, moduleId: string, toolId: string): Promise<void> =>
|
||||
ipcRenderer.invoke('modules:runProjectTool', operationId, name, moduleId, toolId)
|
||||
},
|
||||
logs: {
|
||||
start: (operationId: string, name: string, service: string): Promise<void> =>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Globe2,
|
||||
LockKeyhole,
|
||||
Radio
|
||||
,Wrench
|
||||
} from 'lucide-react'
|
||||
import type { AuroraSiteCredentials, EnvironmentUpdate } from '@shared/types'
|
||||
import {
|
||||
@@ -37,7 +38,7 @@ import { DeveloperServices } from './DeveloperServices'
|
||||
import { DeleteProjectModal } from './DeleteProjectModal'
|
||||
import { LogViewer } from '../logs/LogViewer'
|
||||
import { useAppStore } from '../../stores/appStore'
|
||||
import { useModuleRegistry } from '../../hooks/useModules'
|
||||
import { useModuleRegistry, useRunModuleTool } from '../../hooks/useModules'
|
||||
|
||||
const NODE_VERSIONS = ['20', '22', '24']
|
||||
|
||||
@@ -85,6 +86,7 @@ export function ProjectDetail({ name }: { name: string }): React.JSX.Element {
|
||||
const restartProject = useRestartProject()
|
||||
const deleteProject = useDeleteProject()
|
||||
const updateEnvironment = useUpdateEnvironment()
|
||||
const runModuleTool = useRunModuleTool(name)
|
||||
const selectProject = useAppStore((s) => s.selectProject)
|
||||
const [isLogsOpen, setIsLogsOpen] = useState(false)
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false)
|
||||
@@ -112,7 +114,8 @@ export function ProjectDetail({ name }: { name: string }): React.JSX.Element {
|
||||
startProject.isPending ||
|
||||
stopProject.isPending ||
|
||||
restartProject.isPending ||
|
||||
deleteProject.isPending
|
||||
deleteProject.isPending ||
|
||||
runModuleTool.isPending
|
||||
|
||||
const isEnvUpdating = updateEnvironment.isPending || restartProject.isPending
|
||||
|
||||
@@ -259,6 +262,11 @@ export function ProjectDetail({ name }: { name: string }): React.JSX.Element {
|
||||
<Globe2 size={14} /> {action.label}
|
||||
</a>
|
||||
))}
|
||||
{applicationManifest?.project?.tools?.map((tool) => (
|
||||
<button key={tool.id} type="button" disabled={isBusy} onClick={() => runModuleTool.mutate({ moduleId: applicationManifest.id, toolId: tool.id, label: tool.label })} className="inline-flex items-center gap-1.5 rounded-md border border-neutral-200 bg-white/80 px-3 py-1.5 text-sm font-medium text-neutral-700 shadow-sm transition hover:border-cyan-200 hover:bg-cyan-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-white/10 dark:bg-white/10 dark:text-white dark:hover:bg-white/[0.15]">
|
||||
<Wrench size={14} /> {tool.label}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
|
||||
@@ -35,11 +35,15 @@ export function useInstallAvailableModule(): UseMutationResult<void, Error, stri
|
||||
export function useUninstallModulePackage(): UseMutationResult<void, Error, string> {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id) => window.api.modules.uninstallPackage(id),
|
||||
mutationFn: (id) => window.api.modules.uninstallPackage(beginOperation(`Uninstall ${id}`), id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['modules', 'registry'] })
|
||||
})
|
||||
}
|
||||
|
||||
export function useRunModuleTool(name: string): UseMutationResult<void, Error, { moduleId: string; toolId: string; label: string }> {
|
||||
return useMutation({ mutationFn: ({ moduleId, toolId, label }) => window.api.modules.runProjectTool(beginOperation(label), name, moduleId, toolId) })
|
||||
}
|
||||
|
||||
export function useInstalledModules(name: string): UseQueryResult<AuroraInstalledModule[], Error> {
|
||||
return useQuery({ queryKey: installedModulesKey(name), queryFn: () => window.api.modules.listInstalled(name) })
|
||||
}
|
||||
|
||||
+2
-2
@@ -161,7 +161,7 @@ export interface AuroraModuleManifest {
|
||||
labels: Record<string, string>
|
||||
fallback?: string
|
||||
}
|
||||
tools?: Array<{ id: string; label: string; hook: AuroraModuleLifecycleHook }>
|
||||
tools?: Array<{ id: string; label: string }>
|
||||
}
|
||||
compose?: {
|
||||
service: string
|
||||
@@ -171,7 +171,7 @@ export interface AuroraModuleManifest {
|
||||
}
|
||||
}
|
||||
|
||||
export type AuroraModuleLifecycleHook = 'projectCreate' | 'projectStart' | 'projectRemove' | 'packageUninstall'
|
||||
export type AuroraModuleLifecycleHook = 'projectCreate' | 'projectStart' | 'projectRemove' | 'packageUninstall' | 'projectTool'
|
||||
|
||||
export interface AuroraModuleCommand {
|
||||
command: string
|
||||
|
||||
Reference in New Issue
Block a user