@@ -11,6 +11,14 @@ import {
1111} from './shared/types/Process.types' ;
1212import { Profile } from './shared/types/Profile.types' ;
1313import { ProcessIPC } from './ipc/Process.ipc' ;
14+ import { DEFAULT_JAR_RESOLUTION } from './shared/config/JarResolution.config' ;
15+
16+ // Inline resolution to avoid circular IPC dependency
17+ import fs from 'fs' ;
18+ import { patternToRegex } from './shared/config/JarResolution.config' ;
19+ import type { JarResolutionConfig } from './shared/types/JarResolution.types' ;
20+ import { ProfileIPC } from './ipc/Profile.ipc' ;
21+ import { saveProfile } from './Store' ;
1422
1523const SELF_PROCESS_NAME = 'Java Client Runner' ;
1624
@@ -34,6 +42,83 @@ interface ManagedProcess {
3442 intentionallyStopped : boolean ;
3543}
3644
45+ function parseVersion ( str : string ) : number [ ] {
46+ return str
47+ . split ( / [ . \- _ ] / )
48+ . map ( ( p ) => parseInt ( p , 10 ) )
49+ . filter ( ( n ) => ! isNaN ( n ) ) ;
50+ }
51+
52+ function compareVersionArrays ( a : number [ ] , b : number [ ] ) : number {
53+ const len = Math . max ( a . length , b . length ) ;
54+ for ( let i = 0 ; i < len ; i ++ ) {
55+ const diff = ( b [ i ] ?? 0 ) - ( a [ i ] ?? 0 ) ;
56+ if ( diff !== 0 ) return diff ;
57+ }
58+ return 0 ;
59+ }
60+
61+ function resolveJarPath ( profile : Profile ) : { jarPath : string ; error ?: string } {
62+ const res = profile . jarResolution ?? DEFAULT_JAR_RESOLUTION ;
63+
64+ if ( ! res . enabled ) {
65+ return { jarPath : profile . jarPath } ;
66+ }
67+
68+ if ( ! res . baseDir ) return { jarPath : '' , error : 'Dynamic JAR: no base directory set.' } ;
69+
70+ let entries : fs . Dirent [ ] ;
71+ try {
72+ entries = fs . readdirSync ( res . baseDir , { withFileTypes : true } ) ;
73+ } catch {
74+ return { jarPath : '' , error : `Dynamic JAR: cannot read directory "${ res . baseDir } ".` } ;
75+ }
76+
77+ const jars = entries . filter ( ( e ) => e . isFile ( ) && e . name . endsWith ( '.jar' ) ) ;
78+
79+ let matchRegex : RegExp ;
80+ if ( res . strategy === 'regex' && res . regexOverride ?. trim ( ) ) {
81+ try {
82+ matchRegex = new RegExp ( res . regexOverride . trim ( ) , 'i' ) ;
83+ } catch {
84+ return { jarPath : '' , error : 'Dynamic JAR: invalid regular expression.' } ;
85+ }
86+ } else {
87+ matchRegex = patternToRegex ( res . pattern ) ;
88+ }
89+
90+ const matched = jars . filter ( ( e ) => matchRegex . test ( e . name ) ) ;
91+ if ( matched . length === 0 ) {
92+ return { jarPath : '' , error : 'Dynamic JAR: no files matched the pattern.' } ;
93+ }
94+
95+ let chosen : string ;
96+
97+ if ( res . strategy === 'latest-modified' ) {
98+ const withMtime = matched . map ( ( e ) => {
99+ const full = path . join ( res . baseDir , e . name ) ;
100+ try {
101+ return { name : e . name , mtime : fs . statSync ( full ) . mtimeMs } ;
102+ } catch {
103+ return { name : e . name , mtime : 0 } ;
104+ }
105+ } ) ;
106+ chosen = withMtime . sort ( ( a , b ) => b . mtime - a . mtime ) [ 0 ] . name ;
107+ } else if ( res . strategy === 'regex' ) {
108+ chosen = matched [ 0 ] . name ;
109+ } else {
110+ const versionRegex = patternToRegex ( res . pattern ) ;
111+ const withVersions = matched . map ( ( e ) => {
112+ const m = versionRegex . exec ( e . name ) ;
113+ return { name : e . name , version : parseVersion ( m ?. [ 1 ] ?? '' ) } ;
114+ } ) ;
115+ withVersions . sort ( ( a , b ) => compareVersionArrays ( a . version , b . version ) ) ;
116+ chosen = withVersions [ 0 ] . name ;
117+ }
118+
119+ return { jarPath : path . join ( res . baseDir , chosen ) } ;
120+ }
121+
37122class ProcessManager {
38123 private processes = new Map < string , ManagedProcess > ( ) ;
39124 private activityLog : ProcessLogEntry [ ] = [ ] ;
@@ -48,27 +133,30 @@ class ProcessManager {
48133 this . window = win ;
49134 }
50135
51- private buildArgs ( profile : Profile ) : { cmd : string ; args : string [ ] } {
136+ private buildArgs ( profile : Profile , resolvedJarPath : string ) : { cmd : string ; args : string [ ] } {
52137 const cmd = profile . javaPath || 'java' ;
53138 const args : string [ ] = [ ] ;
54139 for ( const a of profile . jvmArgs ) if ( a . enabled && a . value . trim ( ) ) args . push ( a . value . trim ( ) ) ;
55140 for ( const p of profile . systemProperties )
56141 if ( p . enabled && p . key . trim ( ) )
57142 args . push ( p . value . trim ( ) ? `-D${ p . key . trim ( ) } =${ p . value . trim ( ) } ` : `-D${ p . key . trim ( ) } ` ) ;
58- args . push ( '-jar' , profile . jarPath ) ;
143+ args . push ( '-jar' , resolvedJarPath ) ;
59144 for ( const a of profile . programArgs ) if ( a . enabled && a . value . trim ( ) ) args . push ( a . value . trim ( ) ) ;
60145 return { cmd, args } ;
61146 }
62147
63148 start ( profile : Profile ) : { ok : boolean ; error ?: string } {
64149 if ( this . processes . has ( profile . id ) ) return { ok : false , error : 'Process already running' } ;
65- if ( ! profile . jarPath ) return { ok : false , error : 'No JAR file specified' } ;
150+
151+ const { jarPath, error : resolveError } = resolveJarPath ( profile ) ;
152+ if ( resolveError ) return { ok : false , error : resolveError } ;
153+ if ( ! jarPath ) return { ok : false , error : 'No JAR file specified' } ;
66154
67155 this . cancelRestartTimer ( profile . id ) ;
68156 this . profileSnapshots . set ( profile . id , profile ) ;
69157
70- const { cmd, args } = this . buildArgs ( profile ) ;
71- const cwd = profile . workingDir || path . dirname ( profile . jarPath ) ;
158+ const { cmd, args } = this . buildArgs ( profile , jarPath ) ;
159+ const cwd = profile . workingDir || path . dirname ( jarPath ) ;
72160
73161 this . pushSystem ( 'start' , profile . id , 'pending' , `Starting: ${ cmd } ${ args . join ( ' ' ) } ` ) ;
74162 this . pushSystem ( 'info-workdir' , profile . id , 'pending' , `Working dir: ${ cwd } ` ) ;
@@ -92,7 +180,7 @@ class ProcessManager {
92180 process : proc ,
93181 profileId : profile . id ,
94182 profileName : profile . name ,
95- jarPath : profile . jarPath ,
183+ jarPath,
96184 startedAt : Date . now ( ) ,
97185 intentionallyStopped : false ,
98186 } ;
@@ -110,7 +198,7 @@ class ProcessManager {
110198 id : uuidv4 ( ) ,
111199 profileId : profile . id ,
112200 profileName : profile . name ,
113- jarPath : profile . jarPath ,
201+ jarPath,
114202 pid,
115203 startedAt : managed . startedAt ,
116204 } ;
@@ -253,8 +341,6 @@ class ProcessManager {
253341 this . activityLog = [ ] ;
254342 }
255343
256- // ── Process Scanner ──────────────────────────────────────────────────────────
257-
258344 private isProtected ( name : string , cmd : string ) : boolean {
259345 return PROTECTED_PROCESS_NAMES . some (
260346 ( n ) =>
@@ -416,7 +502,6 @@ class ProcessManager {
416502 }
417503 }
418504
419- // Only kills non-protected java processes
420505 killAllJava ( ) : { ok : boolean ; killed : number } {
421506 const procs = this . scanAllProcesses ( ) . filter ( ( p ) => p . isJava && ! p . protected ) ;
422507 let killed = 0 ;
0 commit comments