11import { Command } from 'commander' ;
2- import { existsSync , mkdirSync } from 'node:fs' ;
2+ import { existsSync , mkdirSync , rmSync , cpSync } from 'node:fs' ;
33import { join , resolve } from 'node:path' ;
4- import { execSync } from 'node:child_process' ;
4+ import { execFileSync } from 'node:child_process' ;
55import { loadAgentManifest } from '../utils/loader.js' ;
66import { success , error , info , heading , divider , warn } from '../utils/format.js' ;
77
88interface InstallOptions {
99 dir : string ;
10+ force : boolean ;
11+ }
12+
13+ function cloneGitRepo ( source : string , targetDir : string , version ?: string ) : void {
14+ const args = [ 'clone' , '--depth' , '1' ] ;
15+ if ( version ) {
16+ args . push ( '--branch' , version . replace ( '^' , '' ) ) ;
17+ }
18+ args . push ( source , targetDir ) ;
19+ mkdirSync ( join ( targetDir , '..' ) , { recursive : true } ) ;
20+ execFileSync ( 'git' , args , {
21+ stdio : 'pipe' ,
22+ timeout : 60000 ,
23+ } ) ;
24+ }
25+
26+ function isGitSource ( source : string ) : boolean {
27+ return source . endsWith ( '.git' ) || source . includes ( 'github.com' ) || source . includes ( 'bitbucket.org' ) || source . includes ( 'gitlab.com' ) ;
28+ }
29+
30+ function removeIfExists ( targetDir : string , force : boolean ) : boolean {
31+ if ( existsSync ( targetDir ) ) {
32+ if ( ! force ) {
33+ warn ( `${ targetDir } already exists, skipping (use --force to update)` ) ;
34+ return false ;
35+ }
36+ rmSync ( targetDir , { recursive : true , force : true } ) ;
37+ }
38+ return true ;
1039}
1140
1241export const installCommand = new Command ( 'install' )
13- . description ( 'Resolve and install agent dependencies' )
42+ . description ( 'Resolve and install agent dependencies and extends ' )
1443 . option ( '-d, --dir <dir>' , 'Agent directory' , '.' )
44+ . option ( '-f, --force' , 'Force re-install (remove existing before install)' , false )
1545 . action ( ( options : InstallOptions ) => {
1646 const dir = resolve ( options . dir ) ;
1747
@@ -25,64 +55,101 @@ export const installCommand = new Command('install')
2555
2656 heading ( 'Installing dependencies' ) ;
2757
28- if ( ! manifest . dependencies || manifest . dependencies . length === 0 ) {
29- info ( 'No dependencies to install' ) ;
58+ const hasExtends = ! ! manifest . extends ;
59+ const hasDeps = manifest . dependencies && manifest . dependencies . length > 0 ;
60+
61+ if ( ! hasExtends && ! hasDeps ) {
62+ info ( 'No dependencies or extends to install' ) ;
3063 return ;
3164 }
3265
3366 const depsDir = join ( dir , '.gitagent' , 'deps' ) ;
3467 mkdirSync ( depsDir , { recursive : true } ) ;
3568
36- for ( const dep of manifest . dependencies ) {
69+ // Handle extends — clone parent agent
70+ if ( hasExtends ) {
3771 divider ( ) ;
38- info ( `Installing ${ dep . name } from ${ dep . source } ` ) ;
72+ const extendsSource = manifest . extends ! ;
73+ info ( `Installing parent agent from ${ extendsSource } ` ) ;
3974
40- const targetDir = dep . mount
41- ? join ( dir , dep . mount )
42- : join ( depsDir , dep . name ) ;
43-
44- if ( existsSync ( targetDir ) ) {
45- warn ( `${ dep . name } already exists at ${ targetDir } , skipping` ) ;
46- continue ;
47- }
75+ const parentDir = join ( dir , '.gitagent' , 'parent' ) ;
4876
49- // Check if source is a local path
50- if ( existsSync ( resolve ( dir , dep . source ) ) ) {
51- // Local dependency — symlink or copy
52- const sourcePath = resolve ( dir , dep . source ) ;
77+ if ( ! removeIfExists ( parentDir , options . force ) ) {
78+ // skipped
79+ } else if ( existsSync ( resolve ( dir , extendsSource ) ) ) {
80+ // Local extends
81+ const sourcePath = resolve ( dir , extendsSource ) ;
5382 try {
54- mkdirSync ( join ( targetDir , '..' ) , { recursive : true } ) ;
55- execSync ( `cp -r " ${ sourcePath } " " ${ targetDir } "` , { stdio : 'pipe' } ) ;
56- success ( ` Installed ${ dep . name } (local)` ) ;
83+ mkdirSync ( join ( parentDir , '..' ) , { recursive : true } ) ;
84+ cpSync ( sourcePath , parentDir , { recursive : true } ) ;
85+ success ( ' Installed parent agent (local)' ) ;
5786 } catch ( e ) {
58- error ( `Failed to install ${ dep . name } : ${ ( e as Error ) . message } ` ) ;
87+ error ( `Failed to install parent agent : ${ ( e as Error ) . message } ` ) ;
5988 }
60- } else if ( dep . source . includes ( 'github.com' ) || dep . source . endsWith ( '.git' ) ) {
61- // Git dependency
89+ } else if ( isGitSource ( extendsSource ) ) {
6290 try {
63- const versionFlag = dep . version ? `--branch ${ dep . version . replace ( '^' , '' ) } ` : '' ;
64- mkdirSync ( join ( targetDir , '..' ) , { recursive : true } ) ;
65- execSync ( `git clone --depth 1 ${ versionFlag } "${ dep . source } " "${ targetDir } " 2>&1` , {
66- stdio : 'pipe' ,
67- timeout : 60000 ,
68- } ) ;
69- success ( `Installed ${ dep . name } (git)` ) ;
91+ cloneGitRepo ( extendsSource , parentDir ) ;
92+ success ( 'Installed parent agent (git)' ) ;
7093 } catch ( e ) {
71- error ( `Failed to clone ${ dep . name } : ${ ( e as Error ) . message } ` ) ;
94+ error ( `Failed to clone parent agent : ${ ( e as Error ) . message } ` ) ;
7295 }
7396 } else {
74- warn ( `Unknown source type for ${ dep . name } : ${ dep . source } ` ) ;
97+ warn ( `Unknown source type for extends : ${ extendsSource } ` ) ;
7598 }
7699
77- // Validate installed dependency
78- const depAgentYaml = join ( targetDir , 'agent.yaml' ) ;
79- if ( existsSync ( depAgentYaml ) ) {
80- success ( `${ dep . name } is a valid gitagent` ) ;
81- } else {
82- warn ( `${ dep . name } does not contain agent.yaml — may not be a gitagent` ) ;
100+ // Validate parent
101+ if ( existsSync ( join ( parentDir , 'agent.yaml' ) ) ) {
102+ success ( 'Parent agent is a valid gitagent' ) ;
103+ } else if ( existsSync ( parentDir ) ) {
104+ warn ( 'Parent agent does not contain agent.yaml' ) ;
105+ }
106+ }
107+
108+ // Handle dependencies
109+ if ( hasDeps ) {
110+ for ( const dep of manifest . dependencies ! ) {
111+ divider ( ) ;
112+ info ( `Installing ${ dep . name } from ${ dep . source } ` ) ;
113+
114+ const targetDir = dep . mount
115+ ? join ( dir , dep . mount )
116+ : join ( depsDir , dep . name ) ;
117+
118+ if ( ! removeIfExists ( targetDir , options . force ) ) {
119+ continue ;
120+ }
121+
122+ // Check if source is a local path
123+ if ( existsSync ( resolve ( dir , dep . source ) ) ) {
124+ const sourcePath = resolve ( dir , dep . source ) ;
125+ try {
126+ mkdirSync ( join ( targetDir , '..' ) , { recursive : true } ) ;
127+ cpSync ( sourcePath , targetDir , { recursive : true } ) ;
128+ success ( `Installed ${ dep . name } (local)` ) ;
129+ } catch ( e ) {
130+ error ( `Failed to install ${ dep . name } : ${ ( e as Error ) . message } ` ) ;
131+ }
132+ } else if ( isGitSource ( dep . source ) ) {
133+ try {
134+ cloneGitRepo ( dep . source , targetDir , dep . version ) ;
135+ success ( `Installed ${ dep . name } (git)` ) ;
136+ } catch ( e ) {
137+ error ( `Failed to clone ${ dep . name } : ${ ( e as Error ) . message } ` ) ;
138+ }
139+ } else {
140+ warn ( `Unknown source type for ${ dep . name } : ${ dep . source } ` ) ;
141+ }
142+
143+ // Validate installed dependency
144+ const depAgentYaml = join ( targetDir , 'agent.yaml' ) ;
145+ if ( existsSync ( depAgentYaml ) ) {
146+ success ( `${ dep . name } is a valid gitagent` ) ;
147+ } else {
148+ warn ( `${ dep . name } does not contain agent.yaml — may not be a gitagent` ) ;
149+ }
83150 }
84151 }
85152
86153 divider ( ) ;
87- success ( 'Dependencies installed ' ) ;
154+ success ( 'Installation complete ' ) ;
88155 } ) ;
0 commit comments