fix: complete module creation progress lifecycle

This commit is contained in:
reaper
2026-08-14 12:42:09 -05:00
parent 3d77f0ac93
commit c518c33952
4 changed files with 19 additions and 7 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import { AURORA_ENV, powerOffProjects } from './auroraEngine'
const running = new Map<string, ChildProcess>(); const cancelledIds = new Set<string>()
const ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g
const stripAnsi=(s:string)=>s.replace(ANSI_PATTERN,'')
export function runCommandStreamed(operationId:string, command:string, args:string[], sender:WebContents, options:{cwd?:string}={}):Promise<void>{return new Promise((resolve,reject)=>{const child=spawn(command,args,{env:AURORA_ENV,cwd:options.cwd,stdio:['ignore','pipe','pipe']});running.set(operationId,child);const tail:string[]=[];const forward=(stream:'stdout'|'stderr')=>(data:Buffer)=>{const chunk=stripAnsi(data.toString());tail.push(chunk);if(tail.length>20)tail.shift();if(!sender.isDestroyed())sender.send('terminal:data',{operationId,stream,chunk})};child.stdout.on('data',forward('stdout'));child.stderr.on('data',forward('stderr'));child.on('error',e=>{running.delete(operationId);sender.send('terminal:exit',{operationId,exitCode:null,cancelled:false});reject(e)});child.on('close',code=>{running.delete(operationId);const cancelled=cancelledIds.delete(operationId);if(!sender.isDestroyed())sender.send('terminal:exit',{operationId,exitCode:code,cancelled});if(cancelled)reject(new Error('Command cancelled'));else if(code===0)resolve();else reject(new Error(tail.join('').trim()||`${command} exited with code ${code}`))})})}
export function runCommandStreamed(operationId:string, command:string, args:string[], sender:WebContents, options:{cwd?:string;emitExit?:boolean}={}):Promise<void>{return new Promise((resolve,reject)=>{const child=spawn(command,args,{env:AURORA_ENV,cwd:options.cwd,stdio:['ignore','pipe','pipe']});running.set(operationId,child);const tail:string[]=[];const forward=(stream:'stdout'|'stderr')=>(data:Buffer)=>{const chunk=stripAnsi(data.toString());tail.push(chunk);if(tail.length>20)tail.shift();if(!sender.isDestroyed())sender.send('terminal:data',{operationId,stream,chunk})};child.stdout.on('data',forward('stdout'));child.stderr.on('data',forward('stderr'));child.on('error',e=>{running.delete(operationId);if(options.emitExit!==false&&!sender.isDestroyed())sender.send('terminal:exit',{operationId,exitCode:null,cancelled:false});reject(e)});child.on('close',code=>{running.delete(operationId);const cancelled=cancelledIds.delete(operationId);if(options.emitExit!==false&&!sender.isDestroyed())sender.send('terminal:exit',{operationId,exitCode:code,cancelled});if(cancelled)reject(new Error('Command cancelled'));else if(code===0)resolve();else reject(new Error(tail.join('').trim()||`${command} exited with code ${code}`))})})}
export function cancelCommand(id:string):boolean{const c=running.get(id);if(!c)return false;cancelledIds.add(id);c.kill();return true}
export function startLogStream(operationId:string, command:string,args:string[],sender:WebContents,options:{cwd?:string}={}):void{const child=spawn(command,args,{env:AURORA_ENV,cwd:options.cwd,stdio:['ignore','pipe','pipe']});running.set(operationId,child);const f=(stream:'stdout'|'stderr')=>(d:Buffer)=>{if(!sender.isDestroyed())sender.send('logs:data',{operationId,stream,chunk:stripAnsi(d.toString())})};child.stdout.on('data',f('stdout'));child.stderr.on('data',f('stderr'));const done=()=>{running.delete(operationId);if(!sender.isDestroyed())sender.send('logs:exit',{operationId})};child.on('close',done);child.on('error',done)}
export function killAllRunningCommands():void{for(const c of running.values())c.kill();running.clear()}
+7 -2
View File
@@ -19,15 +19,20 @@ export async function runModuleProjectCreate(moduleId: string, operationId: stri
if (typeof loaded.projectCreate !== 'function') return
const config = await getProjectConfigByRoot(directory)
const urls = projectUrls(config.name)
await loaded.projectCreate(Object.freeze({
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}-${suffix}`, command, args, sender, { cwd: directory }),
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 })
} catch (error) {
if (!sender.isDestroyed()) sender.send('terminal:exit', { operationId, exitCode: 1, cancelled: false })
throw error
}
}
@@ -2,6 +2,8 @@ import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
import { ChevronDown, ChevronUp, Eye, EyeOff, Globe2, KeyRound, Mail, RefreshCw, Settings2, Type, UserRound } from 'lucide-react'
import type { AuroraModuleManifest, AuroraModuleSetting } from '@shared/types'
import type { TypeSetupHandle, TypeSetupProps } from './shared'
import { useTerminalStore } from '../../../stores/terminalStore'
import { useStatusStore } from '../../../stores/statusStore'
const inputClass = 'w-full rounded-lg border border-neutral-300 bg-white/80 px-3 py-2 text-sm shadow-sm transition placeholder:text-neutral-400 focus:border-cyan-400 focus:outline-none focus:ring-2 focus:ring-cyan-400/15 dark:border-white/10 dark:bg-neutral-950/70 dark:placeholder:text-neutral-600'
const labelClass = 'mb-1.5 block text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400'
@@ -21,7 +23,13 @@ export const ExternalModuleSetup = forwardRef<TypeSetupHandle, TypeSetupProps &
const [visibleSecrets, setVisibleSecrets] = useState<Set<string>>(new Set())
const valid = fields.every((field) => !field.required || String(values[field.id] ?? '').trim().length > 0)
useEffect(() => onValidityChange(valid), [valid, onValidityChange])
useImperativeHandle(ref, () => ({ runPostCreate: async ({ directory }) => window.api.create.runModuleProjectCreate(crypto.randomUUID(), module.id, directory, projectName, values) }), [module.id, projectName, values])
useImperativeHandle(ref, () => ({ runPostCreate: async ({ directory }) => {
const operationId = crypto.randomUUID()
const label = `Install ${module.name} for ${projectName}`
useTerminalStore.getState().startOperation(operationId, label)
useStatusStore.getState().begin(operationId, label)
await window.api.create.runModuleProjectCreate(operationId, module.id, directory, projectName, values)
} }), [module.id, module.name, projectName, values])
function fieldControl(field: AuroraModuleSetting): React.JSX.Element {
if (field.type === 'boolean') return <label className="flex items-center gap-2 rounded-lg border border-neutral-200 bg-white/70 px-3 py-2 text-sm font-medium dark:border-white/10 dark:bg-neutral-950/50"><input type="checkbox" checked={Boolean(values[field.id])} onChange={(event) => setValues({ ...values, [field.id]: event.target.checked })}/>{field.label}</label>
+2 -3
View File
@@ -1,6 +1,5 @@
import type { AuroraStackOptions } from '@shared/types'
import { useMutation, useQueryClient, type UseMutationResult } from '@tanstack/react-query'
import { useTerminalStore } from '../stores/terminalStore'
import { useStatusStore } from '../stores/statusStore'
export interface CreateProjectInput { directory: string; projectName: string; projectType: string; docroot: string; stack?: Partial<AuroraStackOptions> }
@@ -10,9 +9,9 @@ export function useCreateProject(): UseMutationResult<void, Error, CreateProject
return useMutation({
mutationFn: async ({ directory, projectName, projectType, docroot, stack }) => {
const operationId = crypto.randomUUID()
useTerminalStore.getState().startOperation(operationId, `Create project ${projectName}`)
useStatusStore.getState().begin(operationId, `Create project ${projectName}`)
await window.api.create.configure(operationId, directory, projectName, projectType, docroot, stack)
try { await window.api.create.configure(operationId, directory, projectName, projectType, docroot, stack) }
finally { if (useStatusStore.getState().operationId === operationId) useStatusStore.getState().end() }
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['projects'] })
})