fix: complete module creation progress lifecycle
This commit is contained in:
@@ -4,7 +4,7 @@ import { AURORA_ENV, powerOffProjects } from './auroraEngine'
|
|||||||
const running = new Map<string, ChildProcess>(); const cancelledIds = new Set<string>()
|
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 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,'')
|
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 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 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()}
|
export function killAllRunningCommands():void{for(const c of running.values())c.kill();running.clear()}
|
||||||
|
|||||||
@@ -19,15 +19,20 @@ export async function runModuleProjectCreate(moduleId: string, operationId: stri
|
|||||||
if (typeof loaded.projectCreate !== 'function') return
|
if (typeof loaded.projectCreate !== 'function') return
|
||||||
const config = await getProjectConfigByRoot(directory)
|
const config = await getProjectConfigByRoot(directory)
|
||||||
const urls = projectUrls(config.name)
|
const urls = projectUrls(config.name)
|
||||||
await loaded.projectCreate(Object.freeze({
|
try { await loaded.projectCreate(Object.freeze({
|
||||||
moduleId,
|
moduleId,
|
||||||
directory,
|
directory,
|
||||||
projectName,
|
projectName,
|
||||||
settings: Object.freeze({ ...settings }),
|
settings: Object.freeze({ ...settings }),
|
||||||
urls: Object.freeze(urls),
|
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,
|
ensureRouter,
|
||||||
setProjectMetadata: (metadata: Record<string, string | number | boolean>) => setProjectModuleMetadata(directory, metadata),
|
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)
|
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 { ChevronDown, ChevronUp, Eye, EyeOff, Globe2, KeyRound, Mail, RefreshCw, Settings2, Type, UserRound } from 'lucide-react'
|
||||||
import type { AuroraModuleManifest, AuroraModuleSetting } from '@shared/types'
|
import type { AuroraModuleManifest, AuroraModuleSetting } from '@shared/types'
|
||||||
import type { TypeSetupHandle, TypeSetupProps } from './shared'
|
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 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'
|
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 [visibleSecrets, setVisibleSecrets] = useState<Set<string>>(new Set())
|
||||||
const valid = fields.every((field) => !field.required || String(values[field.id] ?? '').trim().length > 0)
|
const valid = fields.every((field) => !field.required || String(values[field.id] ?? '').trim().length > 0)
|
||||||
useEffect(() => onValidityChange(valid), [valid, onValidityChange])
|
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 {
|
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>
|
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>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { AuroraStackOptions } from '@shared/types'
|
import type { AuroraStackOptions } from '@shared/types'
|
||||||
import { useMutation, useQueryClient, type UseMutationResult } from '@tanstack/react-query'
|
import { useMutation, useQueryClient, type UseMutationResult } from '@tanstack/react-query'
|
||||||
import { useTerminalStore } from '../stores/terminalStore'
|
|
||||||
import { useStatusStore } from '../stores/statusStore'
|
import { useStatusStore } from '../stores/statusStore'
|
||||||
|
|
||||||
export interface CreateProjectInput { directory: string; projectName: string; projectType: string; docroot: string; stack?: Partial<AuroraStackOptions> }
|
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({
|
return useMutation({
|
||||||
mutationFn: async ({ directory, projectName, projectType, docroot, stack }) => {
|
mutationFn: async ({ directory, projectName, projectType, docroot, stack }) => {
|
||||||
const operationId = crypto.randomUUID()
|
const operationId = crypto.randomUUID()
|
||||||
useTerminalStore.getState().startOperation(operationId, `Create project ${projectName}`)
|
|
||||||
useStatusStore.getState().begin(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'] })
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['projects'] })
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user