11import * as childProcess from 'child_process' ;
2+ import * as os from 'os' ;
23import * as path from 'path' ;
34import { ToolkitError } from '@aws-cdk/toolkit-lib' ;
45import * as chalk from 'chalk' ;
@@ -70,8 +71,14 @@ export interface CliInitOptions {
7071 readonly fromPath ?: string ;
7172
7273 /**
73- * Path to a specific template within a multi-template repository.
74- * This parameter requires --from-path to be specified.
74+ * Git repository URL to clone and use as template source
75+ * @default undefined
76+ */
77+ readonly fromGitUrl ?: string ;
78+
79+ /**
80+ * Path to a template within a multi-template repository.
81+ * This parameter requires an origin to be specified using --from-path or --from-git-url.
7582 * @default undefined
7683 */
7784 readonly templatePath ?: string ;
@@ -89,34 +96,49 @@ export async function cliInit(options: CliInitOptions) {
8996 const workDir = options . workDir ?? process . cwd ( ) ;
9097
9198 // Show available templates only if no fromPath, type, or language provided
92- if ( ! options . fromPath && ! options . type && ! options . language ) {
99+ if ( ! options . fromPath && ! options . fromGitUrl && ! options . type && ! options . language ) {
93100 await printAvailableTemplates ( ioHelper ) ;
94101 return ;
95102 }
96103
97- // Step 1: Load template
98- let template : InitTemplate ;
99- if ( options . fromPath ) {
100- template = await loadLocalTemplate ( options . fromPath , options . templatePath ) ;
101- } else {
102- template = await loadBuiltinTemplate ( ioHelper , options . type , options . language ) ;
103- }
104-
105- // Step 2: Resolve language
106- const language = await resolveLanguage ( ioHelper , template , options . language , options . type ) ;
107-
108- // Step 3: Initialize project following standard process
109- await initializeProject (
110- ioHelper ,
111- template ,
112- language ,
113- canUseNetwork ,
114- generateOnly ,
115- workDir ,
116- options . stackName ,
117- options . migrate ,
118- options . libVersion ,
119- ) ;
104+ // temporarily store git repo if pulling from remote
105+ let gitTempDir : string | undefined ;
106+
107+ try {
108+ // Step 1: Load template
109+ let template : InitTemplate ;
110+ if ( options . fromPath ) {
111+ template = await loadLocalTemplate ( options . fromPath , options . templatePath ) ;
112+ } else if ( options . fromGitUrl ) {
113+ gitTempDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'cdk-init-git-' ) ) ;
114+ template = await loadGitTemplate ( gitTempDir , options . fromGitUrl , options . templatePath ) ;
115+ } else {
116+ template = await loadBuiltinTemplate ( ioHelper , options . type , options . language ) ;
117+ }
118+
119+ // Step 2: Resolve language
120+ const language = await resolveLanguage ( ioHelper , template , options . language , options . type ) ;
121+
122+ // Step 3: Initialize project following standard process
123+ await initializeProject (
124+ ioHelper ,
125+ template ,
126+ language ,
127+ canUseNetwork ,
128+ generateOnly ,
129+ workDir ,
130+ options . stackName ,
131+ options . migrate ,
132+ options . libVersion ,
133+ ) ;
134+ } finally {
135+ // Clean up temporary directory after everything is done
136+ if ( gitTempDir ) {
137+ await fs . remove ( gitTempDir ) . catch ( async ( error : any ) => {
138+ await ioHelper . defaults . warn ( `Could not remove temporary directory ${ gitTempDir } : ${ error . message } ` ) ;
139+ } ) ;
140+ }
141+ }
120142}
121143
122144/**
@@ -160,6 +182,28 @@ async function loadLocalTemplate(fromPath: string, templatePath?: string): Promi
160182 }
161183}
162184
185+ /**
186+ * Load a template from a Git repository URL
187+ * @param gitUrl - Git repository URL to clone
188+ * @param templatePath - Optional path to a specific template within the repository
189+ * @returns Promise resolving to the InitTemplate
190+ */
191+ async function loadGitTemplate ( tempDir : string , gitUrl : string , templatePath ?: string ) : Promise < InitTemplate > {
192+ try {
193+ await executeGitCommand ( 'git' , [ 'clone' , '--depth' , '1' , gitUrl , tempDir ] ) ;
194+
195+ let fullTemplatePath = tempDir ;
196+ if ( templatePath ) {
197+ fullTemplatePath = path . join ( tempDir , templatePath ) ;
198+ }
199+ const template = await InitTemplate . fromPath ( fullTemplatePath ) ;
200+ return template ;
201+ } catch ( error : any ) {
202+ const displayPath = templatePath ? `${ gitUrl } /${ templatePath } ` : gitUrl ;
203+ throw new ToolkitError ( `Failed to load template from Git repository: ${ displayPath } . ${ error . message } ` ) ;
204+ }
205+ }
206+
163207/**
164208 * Load a built-in template by name
165209 */
@@ -188,8 +232,9 @@ async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, reque
188232 return ( async ( ) => {
189233 if ( requestedLanguage ) {
190234 return requestedLanguage ;
191- }
192- if ( template . languages . length === 1 ) {
235+ } else if ( template . languages . length === 0 ) {
236+ throw new ToolkitError ( 'Custom template must contain at least one language directory' ) ;
237+ } else if ( template . languages . length === 1 ) {
193238 const templateLanguage = template . languages [ 0 ] ;
194239 // Only show auto-detection message for built-in templates
195240 if ( template . templateType !== TemplateType . CUSTOM ) {
@@ -892,6 +937,62 @@ function isRoot(dir: string) {
892937 return path . dirname ( dir ) === dir ;
893938}
894939
940+ /**
941+ * Execute a Git command with timeout
942+ * @param cmd - Git command to execute
943+ * @param args - Command arguments
944+ * @returns Promise resolving to stdout
945+ */
946+ async function executeGitCommand ( cmd : string , args : string [ ] ) : Promise < string > {
947+ return new Promise < string > ( ( resolve , reject ) => {
948+ const child = childProcess . spawn ( cmd , args , {
949+ shell : true ,
950+ stdio : [ 'ignore' , 'pipe' , 'pipe' ] ,
951+ } ) ;
952+
953+ let stdout = '' ;
954+ let stderr = '' ;
955+ let killed = false ;
956+
957+ // Handle process errors
958+ child . on ( 'error' , ( err ) => {
959+ reject ( new ToolkitError ( `Failed to execute Git command: ${ err . message } ` ) ) ;
960+ } ) ;
961+
962+ // Collect stdout
963+ child . stdout . on ( 'data' , ( chunk ) => {
964+ stdout += chunk . toString ( ) ;
965+ } ) ;
966+
967+ // Collect stderr
968+ child . stderr . on ( 'data' , ( chunk ) => {
969+ stderr += chunk . toString ( ) ;
970+ } ) ;
971+
972+ // Handle process completion
973+ child . on ( 'exit' , ( code , signal ) => {
974+ if ( killed ) {
975+ return ;
976+ }
977+
978+ if ( code === 0 ) {
979+ resolve ( stdout . trim ( ) ) ;
980+ } else {
981+ const errorMessage = stderr . trim ( ) || stdout . trim ( ) ;
982+ reject ( new ToolkitError (
983+ `Git command failed with ${ signal ? `signal ${ signal } ` : `code ${ code } ` } : ${ errorMessage } ` ,
984+ ) ) ;
985+ }
986+ } ) ;
987+
988+ child . on ( 'close' , ( ) => {
989+ child . stdout . removeAllListeners ( ) ;
990+ child . stderr . removeAllListeners ( ) ;
991+ child . removeAllListeners ( ) ;
992+ } ) ;
993+ } ) ;
994+ }
995+
895996/**
896997 * Executes `command`. STDERR is emitted in real-time.
897998 *
0 commit comments