From 87d361d341d3524eb4788f9a45bdef6f3e748821 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Mon, 4 Aug 2025 00:25:25 -0400 Subject: [PATCH 01/38] feat: add local template support with --from-path option --- packages/aws-cdk/lib/cli/cli-config.ts | 1 + packages/aws-cdk/lib/cli/cli.ts | 1 + .../aws-cdk/lib/cli/convert-to-user-input.ts | 2 + .../lib/cli/parse-command-line-arguments.ts | 6 + packages/aws-cdk/lib/commands/init/init.ts | 248 +++++++++++++++--- packages/aws-cdk/test/commands/init.test.ts | 77 ++++++ 6 files changed, 295 insertions(+), 40 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 897ccc090..21e0bcafc 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -395,6 +395,7 @@ export async function makeConfig(): Promise { 'list': { type: 'boolean', desc: 'List the available templates' }, 'generate-only': { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }, 'lib-version': { type: 'string', alias: 'V', default: undefined, desc: 'The version of the CDK library (aws-cdk-lib) to initialize the project with. Defaults to the version that was current when this CLI was built.' }, + 'from-path': { type: 'string', desc: 'Path to a local custom template directory', requiresArg: true }, }, }, 'migrate': { diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index c1d07f9ad..384c9452e 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -520,6 +520,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise): any { type: 'string', alias: 'V', desc: 'The version of the CDK library (aws-cdk-lib) to initialize the project with. Defaults to the version that was current when this CLI was built.', + }) + .option('from-path', { + default: undefined, + type: 'string', + desc: 'Path to a local custom template directory', + requiresArg: true, }), ) .command('migrate', 'Migrate existing AWS resources into a CDK app', (yargs: Argv) => diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index c01761c7c..f8f3ecd70 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -29,6 +29,11 @@ export interface CliInitOptions { */ readonly libVersion?: string; + /** + * Path to a local custom template directory + */ + readonly fromPath?: string; + readonly ioHelper: IoHelper; } @@ -40,33 +45,23 @@ export async function cliInit(options: CliInitOptions) { const canUseNetwork = options.canUseNetwork ?? true; const generateOnly = options.generateOnly ?? false; const workDir = options.workDir ?? process.cwd(); - if (!options.type && !options.language) { - await printAvailableTemplates(ioHelper); - return; - } - - const type = options.type || 'default'; // "default" is the default type (and maps to "app") - const template = (await availableInitTemplates()).find((t) => t.hasName(type!)); - if (!template) { - await printAvailableTemplates(ioHelper, options.language); - throw new ToolkitError(`Unknown init template: ${type}`); - } - if (!options.language && template.languages.length === 1) { - const language = template.languages[0]; - await ioHelper.defaults.warn( - `No --language was provided, but '${type}' supports only '${language}', so defaulting to --language=${language}`, - ); - } - if (!options.language) { - await ioHelper.defaults.info(`Available languages for ${chalk.green(type)}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`); - throw new ToolkitError('No language was selected'); + // Step 1: Load template + let template: InitTemplate; + if (options.fromPath) { + template = await loadLocalTemplate(options.fromPath); + } else { + template = await loadBuiltinTemplate(ioHelper, options.type, options.language); } + // Step 2: Resolve language + const language = await resolveLanguage(ioHelper, template, options.language); + + // Step 3: Initialize project following standard process await initializeProject( ioHelper, template, - options.language, + language, canUseNetwork, generateOnly, workDir, @@ -76,8 +71,116 @@ export async function cliInit(options: CliInitOptions) { ); } +/** + * Load a local custom template from file system path + * @param templatePath - Path to the local template directory + * @returns Promise resolving to the loaded InitTemplate + */ +async function loadLocalTemplate(templatePath: string): Promise { + try { + const template = await InitTemplate.fromPath(templatePath); + + if (template.languages.length === 0) { + throw new ToolkitError('Custom template must contain at least one language directory'); + } + + return template; + } catch (e: any) { + throw new ToolkitError(`Failed to load template from path: ${templatePath}. ${e.message}`); + } +} + +/** + * Load a built-in template by name + * @param ioHelper - IO helper for user interaction + * @param type - Template type name + * @param language - Programming language filter + * @returns Promise resolving to the loaded InitTemplate + */ +async function loadBuiltinTemplate(ioHelper: IoHelper, type?: string, language?: string): Promise { + if (!type && !language) { + await printAvailableTemplates(ioHelper, language); + throw new ToolkitError('No template specified. Please specify a template name.'); + } + + if (!type) { + await printAvailableTemplates(ioHelper, language); + throw new ToolkitError('No template specified. Please specify a template name.'); + } + + const template = (await availableInitTemplates()).find((t) => t.hasName(type)); + + if (!template) { + await printAvailableTemplates(ioHelper, language); + throw new ToolkitError(`Unknown init template: ${type}`); + } + + return template; +} + +/** + * Resolve the programming language for the template + * @param ioHelper - IO helper for user interaction + * @param template - The template to resolve language for + * @param requestedLanguage - User-requested language (optional) + * @returns Promise resolving to the selected language + */ +async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, requestedLanguage?: string): Promise { + let language = requestedLanguage; + + // Auto-detect language for single-language templates + if (!language && template.languages.length === 1) { + language = template.languages[0]; + await ioHelper.defaults.info( + `No --language was provided, but '${template.name}' supports only '${language}', so defaulting to --language=${language}`, + ); + } + + if (!language) { + await ioHelper.defaults.info(`Available languages for ${chalk.green(template.name)}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`); + throw new ToolkitError('No language was selected'); + } + + return language; +} + +/** + * Get valid CDK language directories from a template path + * @param templatePath - Path to the template directory + * @returns Promise resolving to array of supported language names + */ +async function getLanguageDirectories(templatePath: string): Promise { + const result: string[] = []; + const supportedLanguages = ['typescript', 'javascript', 'python', 'java', 'csharp', 'fsharp', 'go']; + + try { + const entries = await fs.readdir(templatePath, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory() && supportedLanguages.includes(entry.name)) { + const langDir = path.join(templatePath, entry.name); + try { + const files = await fs.readdir(langDir); + if (files.length > 0) { + result.push(entry.name); + } + } catch (e) { + // Skip directories we can't read + continue; + } + } + } + } catch (e) { + // If we can't read the directory, return empty array + return []; + } + + return result; +} + /** * Returns the name of the Python executable for this OS + * @returns The Python executable name for the current platform */ function pythonExecutable() { let python = 'python3'; @@ -96,6 +199,19 @@ export class InitTemplate { return new InitTemplate(basePath, name, languages, initInfo); } + public static async fromPath(templatePath: string) { + const basePath = path.resolve(templatePath); + + if (!await fs.pathExists(basePath)) { + throw new ToolkitError(`Template path does not exist: ${basePath}`); + } + + const languages = await getLanguageDirectories(basePath); + const name = path.basename(basePath); + + return new InitTemplate(basePath, name, languages, { description: 'Custom template from local path' }); + } + public readonly description: string; public readonly aliases = new Set(); @@ -145,23 +261,30 @@ export class InitTemplate { } const sourceDirectory = path.join(this.basePath, language); - - await this.installFiles(sourceDirectory, targetDirectory, language, projectInfo); - await this.applyFutureFlags(targetDirectory); - await invokeBuiltinHooks( - ioHelper, - { targetDirectory, language, templateName: this.name }, - { - substitutePlaceholdersIn: async (...fileNames: string[]) => { - for (const fileName of fileNames) { - const fullPath = path.join(targetDirectory, fileName); - const template = await fs.readFile(fullPath, { encoding: 'utf-8' }); - await fs.writeFile(fullPath, expandPlaceholders(template, language, projectInfo)); - } + const isCustomTemplate = this.description === 'Custom template from local path'; + + if (isCustomTemplate) { + // For custom templates, copy files without processing placeholders + await this.installFilesWithoutProcessing(sourceDirectory, targetDirectory); + } else { + // For built-in templates, process placeholders as usual + await this.installFiles(sourceDirectory, targetDirectory, language, projectInfo); + await this.applyFutureFlags(targetDirectory); + await invokeBuiltinHooks( + ioHelper, + { targetDirectory, language, templateName: this.name }, + { + substitutePlaceholdersIn: async (...fileNames: string[]) => { + for (const fileName of fileNames) { + const fullPath = path.join(targetDirectory, fileName); + const template = await fs.readFile(fullPath, { encoding: 'utf-8' }); + await fs.writeFile(fullPath, expandPlaceholders(template, language, projectInfo)); + } + }, + placeholder: (ph: string) => expandPlaceholders(`%${ph}%`, language, projectInfo), }, - placeholder: (ph: string) => expandPlaceholders(`%${ph}%`, language, projectInfo), - }, - ); + ); + } } private async installFiles(sourceDirectory: string, targetDirectory: string, language: string, project: ProjectInfo) { @@ -184,11 +307,38 @@ export class InitTemplate { } } + private isTextFile(filename: string): boolean { + const textExtensions = ['.ts', '.js', '.py', '.java', '.cs', '.fs', '.go', '.json', '.yaml', '.yml', '.md', '.txt', '.xml', '.html', '.css', '.scss', '.less']; + const ext = path.extname(filename).toLowerCase(); + return textExtensions.includes(ext) || !ext; // Include files without extension + } + private async installProcessed(templatePath: string, toFile: string, language: string, project: ProjectInfo) { const template = await fs.readFile(templatePath, { encoding: 'utf-8' }); await fs.writeFile(toFile, expandPlaceholders(template, language, project)); } + /** + * Copy template files without processing placeholders (for custom templates) + */ + private async installFilesWithoutProcessing(sourceDirectory: string, targetDirectory: string) { + for (const file of await fs.readdir(sourceDirectory)) { + const fromFile = path.join(sourceDirectory, file); + const toFile = path.join(targetDirectory, file); + + if ((await fs.stat(fromFile)).isDirectory()) { + await fs.mkdir(toFile); + await this.installFilesWithoutProcessing(fromFile, toFile); + continue; + } else if (file.match(/^.*\.hook\.(d.)?[^.]+$/)) { + // Ignore hook files + continue; + } else { + await fs.copy(fromFile, toFile); + } + } + } + /** * Adds context variables to `cdk.json` in the generated project directory to * enable future behavior for new projects. @@ -340,19 +490,29 @@ async function initializeProject( migrate?: boolean, cdkVersion?: string, ) { + // Step 1: Ensure target directory is empty await assertIsEmptyDirectory(workDir); + + // Step 2: Copy template files await ioHelper.defaults.info(`Applying project template ${chalk.green(template.name)} for ${chalk.blue(language)}`); await template.install(ioHelper, language, workDir, stackName, cdkVersion); + if (migrate) { await template.addMigrateContext(workDir); } + if (await fs.pathExists(`${workDir}/README.md`)) { const readme = await fs.readFile(`${workDir}/README.md`, { encoding: 'utf-8' }); await ioHelper.defaults.info(chalk.green(readme)); } if (!generateOnly) { + // Step 3: Initialize Git repository await initializeGitRepository(ioHelper, workDir); + + // Step 4: Create initial commit (already done in initializeGitRepository) + + // Step 5: Post-install steps await postInstall(ioHelper, language, canUseNetwork, workDir); } @@ -360,9 +520,17 @@ async function initializeProject( } async function assertIsEmptyDirectory(workDir: string) { - const files = await fs.readdir(workDir); - if (files.filter((f) => !f.startsWith('.')).length !== 0) { - throw new ToolkitError('`cdk init` cannot be run in a non-empty directory!'); + try { + const files = await fs.readdir(workDir); + if (files.filter((f) => !f.startsWith('.')).length !== 0) { + throw new ToolkitError('`cdk init` cannot be run in a non-empty directory!'); + } + } catch (e: any) { + if (e.code === 'ENOENT') { + await fs.mkdirp(workDir); + } else { + throw e; + } } } diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index 5c766cf57..0acc5a9c8 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -242,6 +242,83 @@ describe('constructs version', () => { expect(await fs.pathExists(path.join(workDir, 'bin'))).toBeTruthy(); }); + cliTest('create project from local custom template', async (workDir) => { + // Create a simple custom template + const templateDir = path.join(workDir, 'custom-template'); + const tsDir = path.join(templateDir, 'typescript'); + await fs.mkdirp(tsDir); + + // Create template files (custom templates don't process placeholders) + await fs.writeFile(path.join(tsDir, 'package.json'), JSON.stringify({ + name: 'my-custom-project', + version: '1.0.0', + dependencies: { + 'aws-cdk-lib': '^2.0.0', + 'constructs': '^10.0.0', + }, + }, null, 2)); + + await fs.writeFile(path.join(tsDir, 'app.ts'), 'console.log("Hello from custom template!");'); + + // Initialize project from custom template + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + await cliInit({ + ioHelper, + fromPath: templateDir, + language: 'typescript', + canUseNetwork: false, + generateOnly: true, + workDir: projectDir, + }); + + // Verify files were created (custom templates copy files as-is) + expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + + const packageJson = JSON.parse(await fs.readFile(path.join(projectDir, 'package.json'), 'utf8')); + expect(packageJson.name).toBe('my-custom-project'); + + const appTs = await fs.readFile(path.join(projectDir, 'app.ts'), 'utf8'); + expect(appTs).toBe('console.log("Hello from custom template!");'); + }); + + cliTest('custom template with single language auto-detects language', async (workDir) => { + // Create a custom template with only TypeScript + const templateDir = path.join(workDir, 'custom-template'); + const tsDir = path.join(templateDir, 'typescript'); + await fs.mkdirp(tsDir); + + await fs.writeFile(path.join(tsDir, 'package.json'), JSON.stringify({ name: '%name%' }, null, 2)); + + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + // Don't specify language - should auto-detect + await cliInit({ + ioHelper, + fromPath: templateDir, + canUseNetwork: false, + generateOnly: true, + workDir: projectDir, + }); + + expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); + }); + + cliTest('custom template path does not exist throws error', async (workDir) => { + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + await expect(cliInit({ + ioHelper, + fromPath: '/nonexistent/path', + language: 'typescript', + workDir: projectDir, + })).rejects.toThrow(/Template path does not exist/); + }); + cliTest('CLI uses recommended feature flags from data file to initialize context', async (workDir) => { const recommendedFlagsFile = path.join(__dirname, '..', '..', 'lib', 'init-templates', '.recommended-feature-flags.json'); await withReplacedFile(recommendedFlagsFile, JSON.stringify({ banana: 'yellow' }), () => cliInit({ From 75602daab80686ca73e4ff406204617c456d9834 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Thu, 7 Aug 2025 14:41:31 -0400 Subject: [PATCH 02/38] chore: fix code style and functionality based on code review feedback --- .../aws-cdk/lib/cli/cli-type-registry.json | 5 + packages/aws-cdk/lib/cli/user-input.ts | 7 + packages/aws-cdk/lib/commands/init/init.ts | 269 +++++++++++++----- packages/aws-cdk/test/commands/init.test.ts | 2 + 4 files changed, 207 insertions(+), 76 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 07f031730..f74e1cf64 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -831,6 +831,11 @@ "type": "string", "alias": "V", "desc": "The version of the CDK library (aws-cdk-lib) to initialize the project with. Defaults to the version that was current when this CLI was built." + }, + "from-path": { + "type": "string", + "desc": "Path to a local custom template directory", + "requiresArg": true } } }, diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index 79fecb843..5219dc87b 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1326,6 +1326,13 @@ export interface InitOptions { */ readonly libVersion?: string; + /** + * Path to a local custom template directory + * + * @default - undefined + */ + readonly fromPath?: string; + /** * Positional argument for init */ diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index f8f3ecd70..4a8c880ad 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -16,21 +16,50 @@ const camelCase = require('camelcase'); const decamelize = require('decamelize'); export interface CliInitOptions { + /** + * @default - Throws error requiring template specification + */ readonly type?: string; + + /** + * @default - Auto-detects for single-language templates or throws error + */ readonly language?: string; + + /** + * @default true + */ readonly canUseNetwork?: boolean; + + /** + * @default false + */ readonly generateOnly?: boolean; + + /** + * @default - Process.cwd() + */ readonly workDir?: string; + + /** + * @default undefined + */ readonly stackName?: string; + + /** + * @default undefined + */ readonly migrate?: boolean; /** * Override the built-in CDK version + * @default - Uses built-in CDK version */ readonly libVersion?: string; /** * Path to a local custom template directory + * @default - Uses built-in templates */ readonly fromPath?: string; @@ -46,6 +75,19 @@ export async function cliInit(options: CliInitOptions) { const generateOnly = options.generateOnly ?? false; const workDir = options.workDir ?? process.cwd(); + // Validate conflicting options + if (options.fromPath && options.type) { + throw new ToolkitError('Cannot specify both --from-path and template name. Use either --from-path for custom templates or specify a built-in template name.'); + } + + if (options.fromPath && options.libVersion) { + throw new ToolkitError('Cannot specify --lib-version with --from-path. Custom templates do not process version placeholders.'); + } + + if (options.fromPath && options.stackName) { + throw new ToolkitError('Cannot specify --stack-name with --from-path. Custom templates do not process stack name placeholders.'); + } + // Step 1: Load template let template: InitTemplate; if (options.fromPath) { @@ -85,8 +127,8 @@ async function loadLocalTemplate(templatePath: string): Promise { } return template; - } catch (e: any) { - throw new ToolkitError(`Failed to load template from path: ${templatePath}. ${e.message}`); + } catch (error: any) { + throw new ToolkitError(`Failed to load template from path: ${templatePath}. ${error.message}`); } } @@ -94,7 +136,9 @@ async function loadLocalTemplate(templatePath: string): Promise { * Load a built-in template by name * @param ioHelper - IO helper for user interaction * @param type - Template type name + * @default - Throws error requiring template specification * @param language - Programming language filter + * @default - Uses all available languages for template filtering * @returns Promise resolving to the loaded InitTemplate */ async function loadBuiltinTemplate(ioHelper: IoHelper, type?: string, language?: string): Promise { @@ -123,6 +167,7 @@ async function loadBuiltinTemplate(ioHelper: IoHelper, type?: string, language?: * @param ioHelper - IO helper for user interaction * @param template - The template to resolve language for * @param requestedLanguage - User-requested language (optional) + * @default - Auto-detects for single-language templates or throws error * @returns Promise resolving to the selected language */ async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, requestedLanguage?: string): Promise { @@ -150,32 +195,77 @@ async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, reque * @returns Promise resolving to array of supported language names */ async function getLanguageDirectories(templatePath: string): Promise { - const result: string[] = []; - const supportedLanguages = ['typescript', 'javascript', 'python', 'java', 'csharp', 'fsharp', 'go']; + const cdkSupportedLanguages = ['typescript', 'javascript', 'python', 'java', 'csharp', 'fsharp', 'go']; + const languageExtensions: Record = { + typescript: ['.ts'], + javascript: ['.js'], + python: ['.py'], + java: ['.java'], + csharp: ['.cs'], + fsharp: ['.fs'], + go: ['.go'], + }; try { const entries = await fs.readdir(templatePath, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory() && supportedLanguages.includes(entry.name)) { - const langDir = path.join(templatePath, entry.name); + const languageValidationPromises = entries + .filter(directoryEntry => directoryEntry.isDirectory() && cdkSupportedLanguages.includes(directoryEntry.name)) + .map(async (directoryEntry) => { + const languageDirectoryPath = path.join(templatePath, directoryEntry.name); try { - const files = await fs.readdir(langDir); - if (files.length > 0) { - result.push(entry.name); - } - } catch (e) { - // Skip directories we can't read - continue; + const hasValidLanguageFiles = await hasLanguageFiles(languageDirectoryPath, languageExtensions[directoryEntry.name]); + return hasValidLanguageFiles ? directoryEntry.name : null; + } catch (error: any) { + throw new ToolkitError(`Cannot read language directory '${directoryEntry.name}': ${error.message}`); + } + }); + + // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism + const validationResults = await Promise.all(languageValidationPromises); + return validationResults.filter((languageName): languageName is string => languageName !== null); + } catch (error: any) { + if (error instanceof ToolkitError) { + throw error; + } + throw new ToolkitError(`Cannot read template directory '${templatePath}': ${error.message}`); + } +} + +/** + * Iteratively check if a directory contains files with the specified extensions + * @param directoryPath - Path to search for language files + * @param extensions - Array of file extensions to look for + * @returns Promise resolving to true if language files are found + */ +async function hasLanguageFiles(directoryPath: string, extensions: string[]): Promise { + const dirsToCheck = [directoryPath]; + + while (dirsToCheck.length > 0) { + const currentDir = dirsToCheck.pop()!; + + try { + const entries = await fs.readdir(currentDir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) { + return true; + } else if (entry.isDirectory()) { + dirsToCheck.push(path.join(currentDir, entry.name)); } } + } catch (error: any) { + // Skip directories that can't be read (permissions, broken symlinks, etc.) + // but continue searching other directories + if (error.code === 'ENOENT' || error.code === 'EACCES' || error.code === 'ENOTDIR') { + continue; + } + // Re-throw unexpected errors + throw error; } - } catch (e) { - // If we can't read the directory, return empty array - return []; } - return result; + return false; } /** @@ -191,12 +281,22 @@ function pythonExecutable() { } const INFO_DOT_JSON = 'info.json'; +interface TemplateInitInfo { + readonly description: string; + readonly aliases?: string[]; +} + +enum TemplateType { + BUILTIN = 'builtin', + CUSTOM = 'custom', +} + export class InitTemplate { public static async fromName(templatesDir: string, name: string) { const basePath = path.join(templatesDir, name); const languages = await listDirectory(basePath); const initInfo = await fs.readJson(path.join(basePath, INFO_DOT_JSON)); - return new InitTemplate(basePath, name, languages, initInfo); + return new InitTemplate(basePath, name, languages, initInfo, TemplateType.BUILTIN); } public static async fromPath(templatePath: string) { @@ -209,21 +309,26 @@ export class InitTemplate { const languages = await getLanguageDirectories(basePath); const name = path.basename(basePath); - return new InitTemplate(basePath, name, languages, { description: 'Custom template from local path' }); + return new InitTemplate(basePath, name, languages, null, TemplateType.CUSTOM); } public readonly description: string; public readonly aliases = new Set(); + public readonly templateType: TemplateType; constructor( private readonly basePath: string, public readonly name: string, public readonly languages: string[], - initInfo: any, + initInfo: TemplateInitInfo | null, + templateType: TemplateType, ) { - this.description = initInfo.description; - for (const alias of initInfo.aliases || []) { - this.aliases.add(alias); + this.description = initInfo?.description ?? 'Custom template'; + this.templateType = templateType; + if (initInfo?.aliases) { + for (const alias of initInfo.aliases) { + this.aliases.add(alias); + } } } @@ -240,6 +345,10 @@ export class InitTemplate { * * @param language - the language to instantiate this template with * @param targetDirectory - the directory where the template is to be instantiated into + * @param stackName - Name of the stack + * @default undefined + * @param libVersion - Version of the CDK library to use + * @default undefined */ public async install(ioHelper: IoHelper, language: string, targetDirectory: string, stackName?: string, libVersion?: string) { if (this.languages.indexOf(language) === -1) { @@ -261,9 +370,8 @@ export class InitTemplate { } const sourceDirectory = path.join(this.basePath, language); - const isCustomTemplate = this.description === 'Custom template from local path'; - if (isCustomTemplate) { + if (this.templateType === TemplateType.CUSTOM) { // For custom templates, copy files without processing placeholders await this.installFilesWithoutProcessing(sourceDirectory, targetDirectory); } else { @@ -275,11 +383,13 @@ export class InitTemplate { { targetDirectory, language, templateName: this.name }, { substitutePlaceholdersIn: async (...fileNames: string[]) => { - for (const fileName of fileNames) { + const fileProcessingPromises = fileNames.map(async (fileName) => { const fullPath = path.join(targetDirectory, fileName); const template = await fs.readFile(fullPath, { encoding: 'utf-8' }); await fs.writeFile(fullPath, expandPlaceholders(template, language, projectInfo)); - } + }); + // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism + await Promise.all(fileProcessingPromises); }, placeholder: (ph: string) => expandPlaceholders(`%${ph}%`, language, projectInfo), }, @@ -307,12 +417,6 @@ export class InitTemplate { } } - private isTextFile(filename: string): boolean { - const textExtensions = ['.ts', '.js', '.py', '.java', '.cs', '.fs', '.go', '.json', '.yaml', '.yml', '.md', '.txt', '.xml', '.html', '.css', '.scss', '.less']; - const ext = path.extname(filename).toLowerCase(); - return textExtensions.includes(ext) || !ext; // Include files without extension - } - private async installProcessed(templatePath: string, toFile: string, language: string, project: ProjectInfo) { const template = await fs.readFile(templatePath, { encoding: 'utf-8' }); await fs.writeFile(toFile, expandPlaceholders(template, language, project)); @@ -322,21 +426,12 @@ export class InitTemplate { * Copy template files without processing placeholders (for custom templates) */ private async installFilesWithoutProcessing(sourceDirectory: string, targetDirectory: string) { - for (const file of await fs.readdir(sourceDirectory)) { - const fromFile = path.join(sourceDirectory, file); - const toFile = path.join(targetDirectory, file); - - if ((await fs.stat(fromFile)).isDirectory()) { - await fs.mkdir(toFile); - await this.installFilesWithoutProcessing(fromFile, toFile); - continue; - } else if (file.match(/^.*\.hook\.(d.)?[^.]+$/)) { - // Ignore hook files - continue; - } else { - await fs.copy(fromFile, toFile); - } - } + await fs.copy(sourceDirectory, targetDirectory, { + filter: (src: string) => { + const filename = path.basename(src); + return !filename.match(/^.*\.hook\.(d.)?[^.]+$/); + }, + }); } /** @@ -420,32 +515,33 @@ interface ProjectInfo { } export async function availableInitTemplates(): Promise { - return new Promise(async (resolve) => { - try { - const templatesDir = path.join(cliRootDir(), 'lib', 'init-templates'); - const templateNames = await listDirectory(templatesDir); - const templates = new Array(); - for (const templateName of templateNames) { - templates.push(await InitTemplate.fromName(templatesDir, templateName)); - } - resolve(templates); - } catch { - resolve([]); + try { + const templatesDir = path.join(cliRootDir(), 'lib', 'init-templates'); + const templateNames = await listDirectory(templatesDir); + const templatePromises = templateNames.map(templateName => + InitTemplate.fromName(templatesDir, templateName), + ); + // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism + return await Promise.all(templatePromises); + } catch (error: any) { + // Return empty array if templates directory doesn't exist or can't be read + // This allows the CLI to gracefully handle missing built-in templates + if (error.code === 'ENOENT' || error.code === 'EACCES') { + return []; } - }); + throw error; + } } export async function availableInitLanguages(): Promise { - return new Promise(async (resolve) => { - const templates = await availableInitTemplates(); - const result = new Set(); - for (const template of templates) { - for (const language of template.languages) { - result.add(language); - } + const templates = await availableInitTemplates(); + const result = new Set(); + for (const template of templates) { + for (const language of template.languages) { + result.add(language); } - resolve([...result]); - }); + } + return [...result]; } /** @@ -463,6 +559,12 @@ async function listDirectory(dirPath: string) { ); } +/** + * Print available templates to the user + * @param ioHelper - IO helper for user interaction + * @param language - Programming language filter + * @default - Shows all available templates + */ export async function printAvailableTemplates(ioHelper: IoHelper, language?: string) { await ioHelper.defaults.info('Available templates:'); for (const template of await availableInitTemplates()) { @@ -479,6 +581,21 @@ export async function printAvailableTemplates(ioHelper: IoHelper, language?: str } } +/** + * Initialize a new CDK project + * @param ioHelper - IO helper for user interaction + * @param template - Template to use for initialization + * @param language - Programming language for the project + * @param canUseNetwork - Whether network access is available + * @param generateOnly - Whether to only generate files without post-install steps + * @param workDir - Working directory for the project + * @param stackName - Name of the stack + * @default undefined + * @param migrate - Whether this is a migration project + * @default undefined + * @param cdkVersion - Version of the CDK to use + * @default - Uses built-in CDK version + */ async function initializeProject( ioHelper: IoHelper, template: InitTemplate, @@ -543,8 +660,8 @@ async function initializeGitRepository(ioHelper: IoHelper, workDir: string) { await execute(ioHelper, 'git', ['init'], { cwd: workDir }); await execute(ioHelper, 'git', ['add', '.'], { cwd: workDir }); await execute(ioHelper, 'git', ['commit', '--message="Initial commit"', '--no-gpg-sign'], { cwd: workDir }); - } catch { - await ioHelper.defaults.warn('Unable to initialize git repository for your project.'); + } catch (error: any) { + await ioHelper.defaults.warn(`Unable to initialize git repository for your project: ${error.message}`); } } @@ -591,8 +708,8 @@ async function postInstallJava(ioHelper: IoHelper, canUseNetwork: boolean, cwd: await ioHelper.defaults.info("Executing 'mvn package'"); try { await execute(ioHelper, 'mvn', ['package'], { cwd }); - } catch { - await ioHelper.defaults.warn('Unable to package compiled code as JAR'); + } catch (error: any) { + await ioHelper.defaults.warn(`Unable to package compiled code as JAR: ${error.message}`); await ioHelper.defaults.warn(mvnPackageWarning); } } @@ -603,8 +720,8 @@ async function postInstallPython(ioHelper: IoHelper, cwd: string) { await ioHelper.defaults.info(`Executing ${chalk.green('Creating virtualenv...')}`); try { await execute(ioHelper, python, ['-m venv', '.venv'], { cwd }); - } catch { - await ioHelper.defaults.warn('Unable to create virtualenv automatically'); + } catch (error: any) { + await ioHelper.defaults.warn(`Unable to create virtualenv automatically: ${error.message}`); await ioHelper.defaults.warn(`Please run '${python} -m venv .venv'!`); } } diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index 0acc5a9c8..35fbe48ec 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -291,6 +291,7 @@ describe('constructs version', () => { await fs.mkdirp(tsDir); await fs.writeFile(path.join(tsDir, 'package.json'), JSON.stringify({ name: '%name%' }, null, 2)); + await fs.writeFile(path.join(tsDir, 'app.ts'), 'console.log("Hello!");'); const projectDir = path.join(workDir, 'my-project'); await fs.mkdirp(projectDir); @@ -305,6 +306,7 @@ describe('constructs version', () => { }); expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); }); cliTest('custom template path does not exist throws error', async (workDir) => { From 6a487e3852f4e5650ce9d58187abdc4afcac650e Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Thu, 7 Aug 2025 14:51:08 -0400 Subject: [PATCH 03/38] chore: cleaned up some extraneous comments --- packages/aws-cdk/lib/commands/init/init.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index 4a8c880ad..748bac452 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -802,11 +802,9 @@ async function loadInitVersions(): Promise { 'aws-cdk': versionNumber(), }; for (const [key, value] of Object.entries(ret)) { - /* c8 ignore start */ if (!value) { throw new ToolkitError(`Missing init version from ${initVersionFile}: ${key}`); } - /* c8 ignore stop */ } return ret; From dd5139eb0cb8e71bcb9271014b2c60fbf36ad122 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Thu, 7 Aug 2025 20:20:17 -0400 Subject: [PATCH 04/38] chore: modified error handling and fixed flag conflicts with yargs --- packages/aws-cdk/lib/cli/cli-config.ts | 2 +- packages/aws-cdk/lib/commands/init/init.ts | 49 ++++++---------------- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 21e0bcafc..2cc984b99 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -395,7 +395,7 @@ export async function makeConfig(): Promise { 'list': { type: 'boolean', desc: 'List the available templates' }, 'generate-only': { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }, 'lib-version': { type: 'string', alias: 'V', default: undefined, desc: 'The version of the CDK library (aws-cdk-lib) to initialize the project with. Defaults to the version that was current when this CLI was built.' }, - 'from-path': { type: 'string', desc: 'Path to a local custom template directory', requiresArg: true }, + 'from-path': { type: 'string', desc: 'Path to a local custom template directory', requiresArg: true, conflicts: ['lib-version'] }, }, }, 'migrate': { diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index 748bac452..ede66b381 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -17,12 +17,14 @@ const decamelize = require('decamelize'); export interface CliInitOptions { /** - * @default - Throws error requiring template specification + * Template name to initialize + * @default undefined */ readonly type?: string; /** - * @default - Auto-detects for single-language templates or throws error + * Programming language for the project + * @default undefined */ readonly language?: string; @@ -37,7 +39,7 @@ export interface CliInitOptions { readonly generateOnly?: boolean; /** - * @default - Process.cwd() + * @default process.cwd() */ readonly workDir?: string; @@ -53,13 +55,13 @@ export interface CliInitOptions { /** * Override the built-in CDK version - * @default - Uses built-in CDK version + * @default undefined */ readonly libVersion?: string; /** * Path to a local custom template directory - * @default - Uses built-in templates + * @default undefined */ readonly fromPath?: string; @@ -75,19 +77,6 @@ export async function cliInit(options: CliInitOptions) { const generateOnly = options.generateOnly ?? false; const workDir = options.workDir ?? process.cwd(); - // Validate conflicting options - if (options.fromPath && options.type) { - throw new ToolkitError('Cannot specify both --from-path and template name. Use either --from-path for custom templates or specify a built-in template name.'); - } - - if (options.fromPath && options.libVersion) { - throw new ToolkitError('Cannot specify --lib-version with --from-path. Custom templates do not process version placeholders.'); - } - - if (options.fromPath && options.stackName) { - throw new ToolkitError('Cannot specify --stack-name with --from-path. Custom templates do not process stack name placeholders.'); - } - // Step 1: Load template let template: InitTemplate; if (options.fromPath) { @@ -136,17 +125,12 @@ async function loadLocalTemplate(templatePath: string): Promise { * Load a built-in template by name * @param ioHelper - IO helper for user interaction * @param type - Template type name - * @default - Throws error requiring template specification + * @default undefined * @param language - Programming language filter - * @default - Uses all available languages for template filtering + * @default undefined * @returns Promise resolving to the loaded InitTemplate */ async function loadBuiltinTemplate(ioHelper: IoHelper, type?: string, language?: string): Promise { - if (!type && !language) { - await printAvailableTemplates(ioHelper, language); - throw new ToolkitError('No template specified. Please specify a template name.'); - } - if (!type) { await printAvailableTemplates(ioHelper, language); throw new ToolkitError('No template specified. Please specify a template name.'); @@ -167,7 +151,7 @@ async function loadBuiltinTemplate(ioHelper: IoHelper, type?: string, language?: * @param ioHelper - IO helper for user interaction * @param template - The template to resolve language for * @param requestedLanguage - User-requested language (optional) - * @default - Auto-detects for single-language templates or throws error + * @default undefined * @returns Promise resolving to the selected language */ async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, requestedLanguage?: string): Promise { @@ -197,7 +181,7 @@ async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, reque async function getLanguageDirectories(templatePath: string): Promise { const cdkSupportedLanguages = ['typescript', 'javascript', 'python', 'java', 'csharp', 'fsharp', 'go']; const languageExtensions: Record = { - typescript: ['.ts'], + typescript: ['.ts', '.js'], javascript: ['.js'], python: ['.py'], java: ['.java'], @@ -225,9 +209,6 @@ async function getLanguageDirectories(templatePath: string): Promise { const validationResults = await Promise.all(languageValidationPromises); return validationResults.filter((languageName): languageName is string => languageName !== null); } catch (error: any) { - if (error instanceof ToolkitError) { - throw error; - } throw new ToolkitError(`Cannot read template directory '${templatePath}': ${error.message}`); } } @@ -255,12 +236,6 @@ async function hasLanguageFiles(directoryPath: string, extensions: string[]): Pr } } } catch (error: any) { - // Skip directories that can't be read (permissions, broken symlinks, etc.) - // but continue searching other directories - if (error.code === 'ENOENT' || error.code === 'EACCES' || error.code === 'ENOTDIR') { - continue; - } - // Re-throw unexpected errors throw error; } } @@ -644,7 +619,7 @@ async function assertIsEmptyDirectory(workDir: string) { } } catch (e: any) { if (e.code === 'ENOENT') { - await fs.mkdirp(workDir); + throw new ToolkitError(`Directory does not exist: ${workDir}. Please create the directory first.`); } else { throw e; } From 54e20f1beb539327412fb3331692be8fdb0cc9a9 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Fri, 8 Aug 2025 15:22:36 -0400 Subject: [PATCH 05/38] fix: correct JSDoc @default comments to show actual values --- packages/aws-cdk/lib/commands/init/init.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index ede66b381..89a28af75 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -538,7 +538,7 @@ async function listDirectory(dirPath: string) { * Print available templates to the user * @param ioHelper - IO helper for user interaction * @param language - Programming language filter - * @default - Shows all available templates + * @default undefined */ export async function printAvailableTemplates(ioHelper: IoHelper, language?: string) { await ioHelper.defaults.info('Available templates:'); @@ -569,7 +569,7 @@ export async function printAvailableTemplates(ioHelper: IoHelper, language?: str * @param migrate - Whether this is a migration project * @default undefined * @param cdkVersion - Version of the CDK to use - * @default - Uses built-in CDK version + * @default undefined */ async function initializeProject( ioHelper: IoHelper, From 8c4058e28393c3609c09c264c511d596468c2e49 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 8 Aug 2025 19:30:32 +0000 Subject: [PATCH 06/38] chore: self mutation Signed-off-by: github-actions --- packages/aws-cdk/lib/cli/cli-type-registry.json | 5 ++++- packages/aws-cdk/lib/cli/parse-command-line-arguments.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index f74e1cf64..e23ba43c1 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -835,7 +835,10 @@ "from-path": { "type": "string", "desc": "Path to a local custom template directory", - "requiresArg": true + "requiresArg": true, + "conflicts": [ + "lib-version" + ] } } }, diff --git a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts index 788b993a0..a124b1611 100644 --- a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts @@ -830,6 +830,7 @@ export function parseCommandLineArguments(args: Array): any { type: 'string', desc: 'Path to a local custom template directory', requiresArg: true, + conflicts: ['lib-version'], }), ) .command('migrate', 'Migrate existing AWS resources into a CDK app', (yargs: Argv) => From 86a8d81b48878879b601be01bc63c59936b6d7e7 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Fri, 8 Aug 2025 18:36:47 -0400 Subject: [PATCH 07/38] chore: fixed code clarity and documentation based on CR feedback --- packages/aws-cdk/lib/cli/cli-config.ts | 2 +- packages/aws-cdk/lib/commands/init/init.ts | 68 +++++++-------------- packages/aws-cdk/test/commands/init.test.ts | 24 ++++++++ 3 files changed, 48 insertions(+), 46 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 2cc984b99..056133826 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -394,7 +394,7 @@ export async function makeConfig(): Promise { 'language': { type: 'string', alias: 'l', desc: 'The language to be used for the new project (default can be configured in ~/.cdk.json)', choices: await availableInitLanguages() }, 'list': { type: 'boolean', desc: 'List the available templates' }, 'generate-only': { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }, - 'lib-version': { type: 'string', alias: 'V', default: undefined, desc: 'The version of the CDK library (aws-cdk-lib) to initialize the project with. Defaults to the version that was current when this CLI was built.' }, + 'lib-version': { type: 'string', alias: 'V', default: undefined, desc: 'The version of the CDK library (aws-cdk-lib) to initialize built-in templates with. Defaults to the version that was current when this CLI was built.' }, 'from-path': { type: 'string', desc: 'Path to a local custom template directory', requiresArg: true, conflicts: ['lib-version'] }, }, }, diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index 89a28af75..b28a72af8 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -24,7 +24,7 @@ export interface CliInitOptions { /** * Programming language for the project - * @default undefined + * @default - Optional/auto-detected if template supports only one language, otherwise required */ readonly language?: string; @@ -123,12 +123,6 @@ async function loadLocalTemplate(templatePath: string): Promise { /** * Load a built-in template by name - * @param ioHelper - IO helper for user interaction - * @param type - Template type name - * @default undefined - * @param language - Programming language filter - * @default undefined - * @returns Promise resolving to the loaded InitTemplate */ async function loadBuiltinTemplate(ioHelper: IoHelper, type?: string, language?: string): Promise { if (!type) { @@ -205,7 +199,7 @@ async function getLanguageDirectories(templatePath: string): Promise { } }); - // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism + /* eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism */ // Limited to supported CDK languages (7 max) const validationResults = await Promise.all(languageValidationPromises); return validationResults.filter((languageName): languageName is string => languageName !== null); } catch (error: any) { @@ -262,7 +256,7 @@ interface TemplateInitInfo { } enum TemplateType { - BUILTIN = 'builtin', + BUILT_IN = 'builtin', CUSTOM = 'custom', } @@ -271,7 +265,7 @@ export class InitTemplate { const basePath = path.join(templatesDir, name); const languages = await listDirectory(basePath); const initInfo = await fs.readJson(path.join(basePath, INFO_DOT_JSON)); - return new InitTemplate(basePath, name, languages, initInfo, TemplateType.BUILTIN); + return new InitTemplate(basePath, name, languages, initInfo, TemplateType.BUILT_IN); } public static async fromPath(templatePath: string) { @@ -287,7 +281,7 @@ export class InitTemplate { return new InitTemplate(basePath, name, languages, null, TemplateType.CUSTOM); } - public readonly description: string; + public readonly description?: string; public readonly aliases = new Set(); public readonly templateType: TemplateType; @@ -298,10 +292,11 @@ export class InitTemplate { initInfo: TemplateInitInfo | null, templateType: TemplateType, ) { - this.description = initInfo?.description ?? 'Custom template'; this.templateType = templateType; - if (initInfo?.aliases) { - for (const alias of initInfo.aliases) { + // Only built-in templates have descriptions and aliases from info.json + if (templateType === TemplateType.BUILT_IN && initInfo) { + this.description = initInfo.description; + for (const alias of initInfo.aliases || []) { this.aliases.add(alias); } } @@ -318,11 +313,11 @@ export class InitTemplate { /** * Creates a new instance of this ``InitTemplate`` for a given language to a specified folder. * - * @param language - the language to instantiate this template with + * @param language - the language to instantiate this template with * @param targetDirectory - the directory where the template is to be instantiated into - * @param stackName - Name of the stack + * @param stackName * @default undefined - * @param libVersion - Version of the CDK library to use + * @param libVersion * @default undefined */ public async install(ioHelper: IoHelper, language: string, targetDirectory: string, stackName?: string, libVersion?: string) { @@ -363,7 +358,7 @@ export class InitTemplate { const template = await fs.readFile(fullPath, { encoding: 'utf-8' }); await fs.writeFile(fullPath, expandPlaceholders(template, language, projectInfo)); }); - // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism + /* eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism */ // Processing a small, known set of template files await Promise.all(fileProcessingPromises); }, placeholder: (ph: string) => expandPlaceholders(`%${ph}%`, language, projectInfo), @@ -496,7 +491,7 @@ export async function availableInitTemplates(): Promise { const templatePromises = templateNames.map(templateName => InitTemplate.fromName(templatesDir, templateName), ); - // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism + /* eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism */ // Built-in templates are limited in number return await Promise.all(templatePromises); } catch (error: any) { // Return empty array if templates directory doesn't exist or can't be read @@ -546,7 +541,7 @@ export async function printAvailableTemplates(ioHelper: IoHelper, language?: str if (language && template.languages.indexOf(language) === -1) { continue; } - await ioHelper.defaults.info(`* ${chalk.green(template.name)}: ${template.description}`); + await ioHelper.defaults.info(`* ${chalk.green(template.name)}: ${template.description!}`); const languageArg = language ? chalk.bold(language) : template.languages.length > 1 @@ -556,21 +551,6 @@ export async function printAvailableTemplates(ioHelper: IoHelper, language?: str } } -/** - * Initialize a new CDK project - * @param ioHelper - IO helper for user interaction - * @param template - Template to use for initialization - * @param language - Programming language for the project - * @param canUseNetwork - Whether network access is available - * @param generateOnly - Whether to only generate files without post-install steps - * @param workDir - Working directory for the project - * @param stackName - Name of the stack - * @default undefined - * @param migrate - Whether this is a migration project - * @default undefined - * @param cdkVersion - Version of the CDK to use - * @default undefined - */ async function initializeProject( ioHelper: IoHelper, template: InitTemplate, @@ -599,12 +579,10 @@ async function initializeProject( } if (!generateOnly) { - // Step 3: Initialize Git repository + // Step 3: Initialize Git repository and create initial commit await initializeGitRepository(ioHelper, workDir); - // Step 4: Create initial commit (already done in initializeGitRepository) - - // Step 5: Post-install steps + // Step 4: Post-install steps await postInstall(ioHelper, language, canUseNetwork, workDir); } @@ -635,8 +613,8 @@ async function initializeGitRepository(ioHelper: IoHelper, workDir: string) { await execute(ioHelper, 'git', ['init'], { cwd: workDir }); await execute(ioHelper, 'git', ['add', '.'], { cwd: workDir }); await execute(ioHelper, 'git', ['commit', '--message="Initial commit"', '--no-gpg-sign'], { cwd: workDir }); - } catch (error: any) { - await ioHelper.defaults.warn(`Unable to initialize git repository for your project: ${error.message}`); + } catch { + await ioHelper.defaults.warn('Unable to initialize git repository for your project.'); } } @@ -683,8 +661,8 @@ async function postInstallJava(ioHelper: IoHelper, canUseNetwork: boolean, cwd: await ioHelper.defaults.info("Executing 'mvn package'"); try { await execute(ioHelper, 'mvn', ['package'], { cwd }); - } catch (error: any) { - await ioHelper.defaults.warn(`Unable to package compiled code as JAR: ${error.message}`); + } catch { + await ioHelper.defaults.warn('Unable to package compiled code as JAR'); await ioHelper.defaults.warn(mvnPackageWarning); } } @@ -695,8 +673,8 @@ async function postInstallPython(ioHelper: IoHelper, cwd: string) { await ioHelper.defaults.info(`Executing ${chalk.green('Creating virtualenv...')}`); try { await execute(ioHelper, python, ['-m venv', '.venv'], { cwd }); - } catch (error: any) { - await ioHelper.defaults.warn(`Unable to create virtualenv automatically: ${error.message}`); + } catch { + await ioHelper.defaults.warn('Unable to create virtualenv automatically'); await ioHelper.defaults.warn(`Please run '${python} -m venv .venv'!`); } } diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index 35fbe48ec..a76b43444 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -309,6 +309,30 @@ describe('constructs version', () => { expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); }); + cliTest('custom template with multiple languages fails if language not provided', async (workDir) => { + // Create a custom template with both TypeScript and Python + const templateDir = path.join(workDir, 'custom-template'); + const tsDir = path.join(templateDir, 'typescript'); + const pyDir = path.join(templateDir, 'python'); + await fs.mkdirp(tsDir); + await fs.mkdirp(pyDir); + + await fs.writeFile(path.join(tsDir, 'app.ts'), 'console.log("Hello TS!");'); + await fs.writeFile(path.join(pyDir, 'app.py'), 'print("Hello Python!")'); + + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + // Don't specify language - should fail since multiple languages are available + await expect(cliInit({ + ioHelper, + fromPath: templateDir, + canUseNetwork: false, + generateOnly: true, + workDir: projectDir, + })).rejects.toThrow(/No language was selected/); + }); + cliTest('custom template path does not exist throws error', async (workDir) => { const projectDir = path.join(workDir, 'my-project'); await fs.mkdirp(projectDir); From c337e833e8cea06262fa8b1c21fd3288d733ee79 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Mon, 11 Aug 2025 00:30:27 -0400 Subject: [PATCH 08/38] chore: gate --from-path functionality with unstable flag --- packages/aws-cdk/lib/cli/cli.ts | 4 ++++ packages/aws-cdk/lib/commands/init/init.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 384c9452e..5df8c0fa6 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -513,6 +513,10 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise Date: Mon, 11 Aug 2025 14:47:35 -0400 Subject: [PATCH 09/38] chore: correct JSDoc syntax in init.ts --- packages/aws-cdk/lib/commands/init/init.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index 0de5c9216..aca0b23c5 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -315,12 +315,10 @@ export class InitTemplate { * * @param language - the language to instantiate this template with * @param targetDirectory - the directory where the template is to be instantiated into - * /* eslint-disable jsdoc/require-param-description */ - * @param stackName + * @param stackName - the name of the stack to create * @default undefined - * @param libVersion + * @param libVersion - the version of the CDK library to use * @default undefined - * /* eslint-enable jsdoc/require-param-description */ */ public async install(ioHelper: IoHelper, language: string, targetDirectory: string, stackName?: string, libVersion?: string) { if (this.languages.indexOf(language) === -1) { From 058e6cf4c12bbaf3b66cd4058a941b7ff90e19cb Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 11 Aug 2025 18:55:01 +0000 Subject: [PATCH 10/38] chore: self mutation Signed-off-by: github-actions --- packages/aws-cdk/lib/cli/cli-type-registry.json | 2 +- packages/aws-cdk/lib/cli/parse-command-line-arguments.ts | 2 +- packages/aws-cdk/lib/cli/user-input.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index e23ba43c1..73cd21cc0 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -830,7 +830,7 @@ "lib-version": { "type": "string", "alias": "V", - "desc": "The version of the CDK library (aws-cdk-lib) to initialize the project with. Defaults to the version that was current when this CLI was built." + "desc": "The version of the CDK library (aws-cdk-lib) to initialize built-in templates with. Defaults to the version that was current when this CLI was built." }, "from-path": { "type": "string", diff --git a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts index a124b1611..174a5758d 100644 --- a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts @@ -823,7 +823,7 @@ export function parseCommandLineArguments(args: Array): any { default: undefined, type: 'string', alias: 'V', - desc: 'The version of the CDK library (aws-cdk-lib) to initialize the project with. Defaults to the version that was current when this CLI was built.', + desc: 'The version of the CDK library (aws-cdk-lib) to initialize built-in templates with. Defaults to the version that was current when this CLI was built.', }) .option('from-path', { default: undefined, diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index 5219dc87b..64630d6cd 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1318,7 +1318,7 @@ export interface InitOptions { readonly generateOnly?: boolean; /** - * The version of the CDK library (aws-cdk-lib) to initialize the project with. Defaults to the version that was current when this CLI was built. + * The version of the CDK library (aws-cdk-lib) to initialize built-in templates with. Defaults to the version that was current when this CLI was built. * * aliases: V * From 33a0ee011fa5a762a6b067aaa7904bb1607532ab Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Tue, 12 Aug 2025 03:32:37 -0400 Subject: [PATCH 11/38] fix: remove auto-detection message for local templates --- packages/aws-cdk/lib/commands/init/init.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index aca0b23c5..380392519 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -154,9 +154,13 @@ async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, reque // Auto-detect language for single-language templates if (!language && template.languages.length === 1) { language = template.languages[0]; - await ioHelper.defaults.info( - `No --language was provided, but '${template.name}' supports only '${language}', so defaulting to --language=${language}`, - ); + // Only show auto-detection message for built-in templates + // Local templates are inherently single-language for testing purposes + if (template.templateType !== TemplateType.CUSTOM) { + await ioHelper.defaults.info( + `No --language was provided, but '${template.name}' supports only '${language}', so defaulting to --language=${language}`, + ); + } } if (!language) { From 0f25a88a640a8ab6d8545190932a76acfa24d5e9 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Fri, 15 Aug 2025 13:24:15 -0400 Subject: [PATCH 12/38] chore: revised local template support to also allow multi-template repositories that users can select templates from using --template-path param --- packages/aws-cdk/lib/cli/cli-config.ts | 3 +- packages/aws-cdk/lib/cli/cli.ts | 1 + packages/aws-cdk/lib/commands/init/init.ts | 29 ++- packages/aws-cdk/test/commands/init.test.ts | 243 +++++++++++++++++++- 4 files changed, 262 insertions(+), 14 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 056133826..63b571c10 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -395,7 +395,8 @@ export async function makeConfig(): Promise { 'list': { type: 'boolean', desc: 'List the available templates' }, 'generate-only': { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }, 'lib-version': { type: 'string', alias: 'V', default: undefined, desc: 'The version of the CDK library (aws-cdk-lib) to initialize built-in templates with. Defaults to the version that was current when this CLI was built.' }, - 'from-path': { type: 'string', desc: 'Path to a local custom template directory', requiresArg: true, conflicts: ['lib-version'] }, + 'from-path': { type: 'string', desc: 'Path to a local custom template directory or multi-template repository', requiresArg: true, conflicts: ['lib-version'] }, + 'template-path': { type: 'string', desc: 'Path to a specific template within a multi-template repository (used with --from-path)', requiresArg: true }, }, }, 'migrate': { diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 5df8c0fa6..4210a974b 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -525,6 +525,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise { +async function loadLocalTemplate(fromPath: string, templatePath?: string): Promise { try { - const template = await InitTemplate.fromPath(templatePath); + let actualTemplatePath = fromPath; + + // If templatePath is provided, it's a multi-template repository + if (templatePath) { + actualTemplatePath = path.join(fromPath, templatePath); + + if (!await fs.pathExists(actualTemplatePath)) { + throw new ToolkitError(`Template path does not exist: ${actualTemplatePath}`); + } + } + + const template = await InitTemplate.fromPath(actualTemplatePath); if (template.languages.length === 0) { throw new ToolkitError('Custom template must contain at least one language directory'); @@ -117,7 +135,8 @@ async function loadLocalTemplate(templatePath: string): Promise { return template; } catch (error: any) { - throw new ToolkitError(`Failed to load template from path: ${templatePath}. ${error.message}`); + const displayPath = templatePath ? `${fromPath}/${templatePath}` : fromPath; + throw new ToolkitError(`Failed to load template from path: ${displayPath}. ${error.message}`); } } diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index a76b43444..3b4d6412f 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -242,9 +242,9 @@ describe('constructs version', () => { expect(await fs.pathExists(path.join(workDir, 'bin'))).toBeTruthy(); }); - cliTest('create project from local custom template', async (workDir) => { + cliTest('create project from single local custom template', async (workDir) => { // Create a simple custom template - const templateDir = path.join(workDir, 'custom-template'); + const templateDir = path.join(workDir, 'my-cdk-template'); const tsDir = path.join(templateDir, 'typescript'); await fs.mkdirp(tsDir); @@ -284,19 +284,19 @@ describe('constructs version', () => { expect(appTs).toBe('console.log("Hello from custom template!");'); }); - cliTest('custom template with single language auto-detects language', async (workDir) => { - // Create a custom template with only TypeScript - const templateDir = path.join(workDir, 'custom-template'); + cliTest('single template auto-detects language when template has single language', async (workDir) => { + // Create a single custom template with only TypeScript + const templateDir = path.join(workDir, 'my-cdk-template'); const tsDir = path.join(templateDir, 'typescript'); await fs.mkdirp(tsDir); - await fs.writeFile(path.join(tsDir, 'package.json'), JSON.stringify({ name: '%name%' }, null, 2)); - await fs.writeFile(path.join(tsDir, 'app.ts'), 'console.log("Hello!");'); + await fs.writeFile(path.join(tsDir, 'package.json'), JSON.stringify({ name: 'single-lang-project' }, null, 2)); + await fs.writeFile(path.join(tsDir, 'app.ts'), 'console.log("Auto-detected single template!");'); const projectDir = path.join(workDir, 'my-project'); await fs.mkdirp(projectDir); - // Don't specify language - should auto-detect + // Don't specify language - should auto-detect when template has only one language await cliInit({ ioHelper, fromPath: templateDir, @@ -307,6 +307,9 @@ describe('constructs version', () => { expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + + const packageJson = JSON.parse(await fs.readFile(path.join(projectDir, 'package.json'), 'utf8')); + expect(packageJson.name).toBe('single-lang-project'); }); cliTest('custom template with multiple languages fails if language not provided', async (workDir) => { @@ -345,6 +348,230 @@ describe('constructs version', () => { })).rejects.toThrow(/Template path does not exist/); }); + cliTest('create project from multi-template repository with template-path', async (workDir) => { + // Create a multi-template repository structure + const repoDir = path.join(workDir, 'cdk-templates'); + const myCustomTemplateDir = path.join(repoDir, 'my-custom-template'); + const webAppTemplateDir = path.join(repoDir, 'web-app-template'); + + // my-custom-template with TypeScript and Python + const customTsDir = path.join(myCustomTemplateDir, 'typescript'); + const customPyDir = path.join(myCustomTemplateDir, 'python'); + await fs.mkdirp(customTsDir); + await fs.mkdirp(customPyDir); + await fs.writeFile(path.join(customTsDir, 'package.json'), JSON.stringify({ + name: 'my-custom-project', + version: '1.0.0', + }, null, 2)); + await fs.writeFile(path.join(customTsDir, 'app.ts'), 'console.log("My Custom Template!");'); + await fs.writeFile(path.join(customPyDir, 'requirements.txt'), 'aws-cdk-lib>=2.0.0'); + await fs.writeFile(path.join(customPyDir, 'app.py'), 'print("My Custom Template!")'); + + // web-app-template with Java only + const webAppJavaDir = path.join(webAppTemplateDir, 'java'); + await fs.mkdirp(webAppJavaDir); + await fs.writeFile(path.join(webAppJavaDir, 'pom.xml'), 'web-app'); + await fs.writeFile(path.join(webAppJavaDir, 'App.java'), 'public class App { }'); + + // Test 1: Initialize from my-custom-template with TypeScript + const projectDir1 = path.join(workDir, 'project1'); + await fs.mkdirp(projectDir1); + + await cliInit({ + ioHelper, + fromPath: repoDir, + templatePath: 'my-custom-template', + language: 'typescript', + canUseNetwork: false, + generateOnly: true, + workDir: projectDir1, + }); + + // Verify my-custom-template TypeScript files were created + expect(await fs.pathExists(path.join(projectDir1, 'package.json'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir1, 'app.ts'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir1, 'requirements.txt'))).toBeFalsy(); + expect(await fs.pathExists(path.join(projectDir1, 'pom.xml'))).toBeFalsy(); + + const packageJson = JSON.parse(await fs.readFile(path.join(projectDir1, 'package.json'), 'utf8')); + expect(packageJson.name).toBe('my-custom-project'); + + // Test 2: Initialize from web-app-template with Java + const projectDir2 = path.join(workDir, 'project2'); + await fs.mkdirp(projectDir2); + + await cliInit({ + ioHelper, + fromPath: repoDir, + templatePath: 'web-app-template', + language: 'java', + canUseNetwork: false, + generateOnly: true, + workDir: projectDir2, + }); + + // Verify web-app-template Java files were created + expect(await fs.pathExists(path.join(projectDir2, 'pom.xml'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir2, 'App.java'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir2, 'package.json'))).toBeFalsy(); + expect(await fs.pathExists(path.join(projectDir2, 'requirements.txt'))).toBeFalsy(); + }); + + cliTest('multi-template repository with non-existent template-path throws error', async (workDir) => { + const repoDir = path.join(workDir, 'cdk-templates'); + const validTemplateDir = path.join(repoDir, 'valid-template'); + const validTemplateTsDir = path.join(validTemplateDir, 'typescript'); + await fs.mkdirp(validTemplateTsDir); + await fs.writeFile(path.join(validTemplateTsDir, 'app.ts'), 'console.log("Valid template!");'); + + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + await expect(cliInit({ + ioHelper, + fromPath: repoDir, + templatePath: 'nonexistent-template', + language: 'typescript', + workDir: projectDir, + })).rejects.toThrow(/Template path does not exist/); + }); + + cliTest('template validation requires at least one language directory', async (workDir) => { + // Test that templates must contain at least one language subdirectory + const repoDir = path.join(workDir, 'cdk-templates'); + const invalidTemplateDir = path.join(repoDir, 'invalid-template'); + await fs.mkdirp(invalidTemplateDir); + // Create a file but no language directories + await fs.writeFile(path.join(invalidTemplateDir, 'README.md'), 'This template has no language directories'); + + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + await expect(cliInit({ + ioHelper, + fromPath: repoDir, + templatePath: 'invalid-template', + language: 'typescript', + workDir: projectDir, + })).rejects.toThrow(/Custom template must contain at least one language directory/); + }); + + cliTest('template validation requires language files in language directory', async (workDir) => { + // Test that language directories must contain files of the matching language type + const repoDir = path.join(workDir, 'cdk-templates'); + const invalidTemplateDir = path.join(repoDir, 'empty-lang-template'); + const emptyTsDir = path.join(invalidTemplateDir, 'typescript'); + await fs.mkdirp(emptyTsDir); + // Create language directory but no files with matching extensions + await fs.writeFile(path.join(emptyTsDir, 'README.md'), 'No TypeScript files here'); + + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + await expect(cliInit({ + ioHelper, + fromPath: repoDir, + templatePath: 'empty-lang-template', + language: 'typescript', + workDir: projectDir, + })).rejects.toThrow(/Custom template must contain at least one language directory/); + }); + + cliTest('multi-template repository auto-detects language when template has single language', async (workDir) => { + // Create a multi-template repository with a single-language template + const repoDir = path.join(workDir, 'cdk-templates'); + const templateDir = path.join(repoDir, 'single-lang-template'); + const templateTsDir = path.join(templateDir, 'typescript'); + await fs.mkdirp(templateTsDir); + await fs.writeFile(path.join(templateTsDir, 'package.json'), JSON.stringify({ name: 'single-lang-project' }, null, 2)); + await fs.writeFile(path.join(templateTsDir, 'app.ts'), 'console.log("Auto-detected TypeScript!");'); + + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + // Don't specify language - should auto-detect when template has only one language + await cliInit({ + ioHelper, + fromPath: repoDir, + templatePath: 'single-lang-template', + canUseNetwork: false, + generateOnly: true, + workDir: projectDir, + }); + + expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + + const packageJson = JSON.parse(await fs.readFile(path.join(projectDir, 'package.json'), 'utf8')); + expect(packageJson.name).toBe('single-lang-project'); + }); + + cliTest('multi-template repository supports all CDK languages', async (workDir) => { + // Create a multi-template repository with comprehensive language support + const repoDir = path.join(workDir, 'cdk-templates'); + const templateDir = path.join(repoDir, 'multi-lang-template'); + + // Create language directories for all supported CDK languages + const languages = { + typescript: { file: 'app.ts', content: 'console.log("TypeScript!");' }, + javascript: { file: 'app.js', content: 'console.log("JavaScript!");' }, + python: { file: 'app.py', content: 'print("Python!")' }, + java: { file: 'App.java', content: 'public class App { }' }, + csharp: { file: 'App.cs', content: 'public class App { }' }, + fsharp: { file: 'App.fs', content: 'module App' }, + go: { file: 'app.go', content: 'package main' }, + }; + + for (const [lang, config] of Object.entries(languages)) { + const langDir = path.join(templateDir, lang); + await fs.mkdirp(langDir); + await fs.writeFile(path.join(langDir, config.file), config.content); + } + + // Test TypeScript selection + const tsProjectDir = path.join(workDir, 'ts-project'); + await fs.mkdirp(tsProjectDir); + + await cliInit({ + ioHelper, + fromPath: repoDir, + templatePath: 'multi-lang-template', + language: 'typescript', + canUseNetwork: false, + generateOnly: true, + workDir: tsProjectDir, + }); + + // Verify only TypeScript files were created + expect(await fs.pathExists(path.join(tsProjectDir, 'app.ts'))).toBeTruthy(); + expect(await fs.pathExists(path.join(tsProjectDir, 'app.js'))).toBeFalsy(); + expect(await fs.pathExists(path.join(tsProjectDir, 'app.py'))).toBeFalsy(); + + const tsContent = await fs.readFile(path.join(tsProjectDir, 'app.ts'), 'utf8'); + expect(tsContent).toBe('console.log("TypeScript!");'); + + // Test Python selection + const pyProjectDir = path.join(workDir, 'py-project'); + await fs.mkdirp(pyProjectDir); + + await cliInit({ + ioHelper, + fromPath: repoDir, + templatePath: 'multi-lang-template', + language: 'python', + canUseNetwork: false, + generateOnly: true, + workDir: pyProjectDir, + }); + + // Verify only Python files were created + expect(await fs.pathExists(path.join(pyProjectDir, 'app.py'))).toBeTruthy(); + expect(await fs.pathExists(path.join(pyProjectDir, 'app.ts'))).toBeFalsy(); + + const pyContent = await fs.readFile(path.join(pyProjectDir, 'app.py'), 'utf8'); + expect(pyContent).toBe('print("Python!")'); + }); + cliTest('CLI uses recommended feature flags from data file to initialize context', async (workDir) => { const recommendedFlagsFile = path.join(__dirname, '..', '..', 'lib', 'init-templates', '.recommended-feature-flags.json'); await withReplacedFile(recommendedFlagsFile, JSON.stringify({ banana: 'yellow' }), () => cliInit({ From 31423ad4e93c8820f2348495d240a401fc1453ac Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 15 Aug 2025 17:31:43 +0000 Subject: [PATCH 13/38] chore: self mutation Signed-off-by: github-actions --- packages/aws-cdk/lib/cli/cli-type-registry.json | 7 ++++++- packages/aws-cdk/lib/cli/convert-to-user-input.ts | 2 ++ .../aws-cdk/lib/cli/parse-command-line-arguments.ts | 8 +++++++- packages/aws-cdk/lib/cli/user-input.ts | 9 ++++++++- packages/aws-cdk/test/commands/init.test.ts | 10 +++++----- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 73cd21cc0..9fba20bd4 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -834,11 +834,16 @@ }, "from-path": { "type": "string", - "desc": "Path to a local custom template directory", + "desc": "Path to a local custom template directory or multi-template repository", "requiresArg": true, "conflicts": [ "lib-version" ] + }, + "template-path": { + "type": "string", + "desc": "Path to a specific template within a multi-template repository (used with --from-path)", + "requiresArg": true } } }, diff --git a/packages/aws-cdk/lib/cli/convert-to-user-input.ts b/packages/aws-cdk/lib/cli/convert-to-user-input.ts index a1c4cb2bd..63c5ec014 100644 --- a/packages/aws-cdk/lib/cli/convert-to-user-input.ts +++ b/packages/aws-cdk/lib/cli/convert-to-user-input.ts @@ -240,6 +240,7 @@ export function convertYargsToUserInput(args: any): UserInput { generateOnly: args.generateOnly, libVersion: args.libVersion, fromPath: args.fromPath, + templatePath: args.templatePath, TEMPLATE: args.TEMPLATE, }; break; @@ -471,6 +472,7 @@ export function convertConfigToUserInput(config: any): UserInput { generateOnly: config.init?.generateOnly, libVersion: config.init?.libVersion, fromPath: config.init?.fromPath, + templatePath: config.init?.templatePath, }; const migrateOptions = { stackName: config.migrate?.stackName, diff --git a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts index 174a5758d..d3f416cf3 100644 --- a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts @@ -828,9 +828,15 @@ export function parseCommandLineArguments(args: Array): any { .option('from-path', { default: undefined, type: 'string', - desc: 'Path to a local custom template directory', + desc: 'Path to a local custom template directory or multi-template repository', requiresArg: true, conflicts: ['lib-version'], + }) + .option('template-path', { + default: undefined, + type: 'string', + desc: 'Path to a specific template within a multi-template repository (used with --from-path)', + requiresArg: true, }), ) .command('migrate', 'Migrate existing AWS resources into a CDK app', (yargs: Argv) => diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index 64630d6cd..d4930a884 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1327,12 +1327,19 @@ export interface InitOptions { readonly libVersion?: string; /** - * Path to a local custom template directory + * Path to a local custom template directory or multi-template repository * * @default - undefined */ readonly fromPath?: string; + /** + * Path to a specific template within a multi-template repository (used with --from-path) + * + * @default - undefined + */ + readonly templatePath?: string; + /** * Positional argument for init */ diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index 3b4d6412f..bc2e2f3eb 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -307,7 +307,7 @@ describe('constructs version', () => { expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); - + const packageJson = JSON.parse(await fs.readFile(path.join(projectDir, 'package.json'), 'utf8')); expect(packageJson.name).toBe('single-lang-project'); }); @@ -501,7 +501,7 @@ describe('constructs version', () => { expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); - + const packageJson = JSON.parse(await fs.readFile(path.join(projectDir, 'package.json'), 'utf8')); expect(packageJson.name).toBe('single-lang-project'); }); @@ -510,7 +510,7 @@ describe('constructs version', () => { // Create a multi-template repository with comprehensive language support const repoDir = path.join(workDir, 'cdk-templates'); const templateDir = path.join(repoDir, 'multi-lang-template'); - + // Create language directories for all supported CDK languages const languages = { typescript: { file: 'app.ts', content: 'console.log("TypeScript!");' }, @@ -546,7 +546,7 @@ describe('constructs version', () => { expect(await fs.pathExists(path.join(tsProjectDir, 'app.ts'))).toBeTruthy(); expect(await fs.pathExists(path.join(tsProjectDir, 'app.js'))).toBeFalsy(); expect(await fs.pathExists(path.join(tsProjectDir, 'app.py'))).toBeFalsy(); - + const tsContent = await fs.readFile(path.join(tsProjectDir, 'app.ts'), 'utf8'); expect(tsContent).toBe('console.log("TypeScript!");'); @@ -567,7 +567,7 @@ describe('constructs version', () => { // Verify only Python files were created expect(await fs.pathExists(path.join(pyProjectDir, 'app.py'))).toBeTruthy(); expect(await fs.pathExists(path.join(pyProjectDir, 'app.ts'))).toBeFalsy(); - + const pyContent = await fs.readFile(path.join(pyProjectDir, 'app.py'), 'utf8'); expect(pyContent).toBe('print("Python!")'); }); From 4bf690c9ce4f0a9a53251520e4ae1710df735a76 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Fri, 15 Aug 2025 17:32:17 -0400 Subject: [PATCH 14/38] chore: update code to support multi-template local repository --- packages/aws-cdk/lib/commands/init/init.ts | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index d01955c95..1c956127f 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -130,6 +130,15 @@ async function loadLocalTemplate(fromPath: string, templatePath?: string): Promi const template = await InitTemplate.fromPath(actualTemplatePath); if (template.languages.length === 0) { + // Check if this might be a multi-template repository + if (!templatePath) { + const availableTemplates = await findPotentialTemplates(fromPath); + if (availableTemplates.length > 0) { + throw new ToolkitError( + 'Use --template-path to specify which template to use.', + ); + } + } throw new ToolkitError('Custom template must contain at least one language directory'); } @@ -190,6 +199,32 @@ async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, reque return language; } +/** + * Find potential template directories in a multi-template repository + * @param repositoryPath - Path to the repository root + * @returns Promise resolving to array of potential template directory names + */ +async function findPotentialTemplates(repositoryPath: string): Promise { + try { + const entries = await fs.readdir(repositoryPath, { withFileTypes: true }); + const potentialTemplates: string[] = []; + + for (const entry of entries) { + if (entry.isDirectory() && !entry.name.startsWith('.')) { + const templatePath = path.join(repositoryPath, entry.name); + const languages = await getLanguageDirectories(templatePath); + if (languages.length > 0) { + potentialTemplates.push(entry.name); + } + } + } + + return potentialTemplates; + } catch (error: any) { + return []; + } +} + /** * Get valid CDK language directories from a template path * @param templatePath - Path to the template directory From 3d55b3494b8ec2ee96b4b4242cd3ac8d32b4c1ee Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Mon, 18 Aug 2025 13:49:10 -0400 Subject: [PATCH 15/38] chore: seperate test data from test code with fixtures --- .../test/_fixtures/init-templates/app.ts | 7 +++ .../_fixtures/init-templates/package.json | 8 +++ .../test/_fixtures/init-templates/stack.ts | 8 +++ .../init-templates/template-helpers.ts | 62 +++++++++++++++++++ packages/aws-cdk/test/commands/init.test.ts | 40 ++++-------- 5 files changed, 96 insertions(+), 29 deletions(-) create mode 100644 packages/aws-cdk/test/_fixtures/init-templates/app.ts create mode 100644 packages/aws-cdk/test/_fixtures/init-templates/package.json create mode 100644 packages/aws-cdk/test/_fixtures/init-templates/stack.ts create mode 100644 packages/aws-cdk/test/_fixtures/init-templates/template-helpers.ts diff --git a/packages/aws-cdk/test/_fixtures/init-templates/app.ts b/packages/aws-cdk/test/_fixtures/init-templates/app.ts new file mode 100644 index 000000000..2f8a1cc8b --- /dev/null +++ b/packages/aws-cdk/test/_fixtures/init-templates/app.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { MyCustomStack } from '../lib/my-custom-stack'; + +const app = new cdk.App(); +new MyCustomStack(app, 'MyCustomStack'); \ No newline at end of file diff --git a/packages/aws-cdk/test/_fixtures/init-templates/package.json b/packages/aws-cdk/test/_fixtures/init-templates/package.json new file mode 100644 index 000000000..a9672bffb --- /dev/null +++ b/packages/aws-cdk/test/_fixtures/init-templates/package.json @@ -0,0 +1,8 @@ +{ + "name": "my-custom-project", + "version": "1.0.0", + "dependencies": { + "aws-cdk-lib": "^2.0.0", + "constructs": "^10.0.0" + } +} \ No newline at end of file diff --git a/packages/aws-cdk/test/_fixtures/init-templates/stack.ts b/packages/aws-cdk/test/_fixtures/init-templates/stack.ts new file mode 100644 index 000000000..ba35dd66b --- /dev/null +++ b/packages/aws-cdk/test/_fixtures/init-templates/stack.ts @@ -0,0 +1,8 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +export class MyCustomStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + } +} \ No newline at end of file diff --git a/packages/aws-cdk/test/_fixtures/init-templates/template-helpers.ts b/packages/aws-cdk/test/_fixtures/init-templates/template-helpers.ts new file mode 100644 index 000000000..b0dc36e2e --- /dev/null +++ b/packages/aws-cdk/test/_fixtures/init-templates/template-helpers.ts @@ -0,0 +1,62 @@ +import * as path from 'path'; +import * as fs from 'fs-extra'; + +const FIXTURES_DIR = path.join(__dirname, '.'); + +/** + * Helper function to create a custom template structure for testing + */ +export async function createCustomTemplate(baseDir: string, templateName: string, languages: string[]): Promise { + const templateDir = path.join(baseDir, templateName); + + for (const language of languages) { + const langDir = path.join(templateDir, language); + await fs.mkdirp(langDir); + + // Copy appropriate fixtures based on language + if (language === 'typescript') { + await copyTypescriptFixtures(langDir); + } + // Add other languages as needed + } + + return templateDir; +} + +/** + * Copy TypeScript template fixtures to the specified directory + */ +async function copyTypescriptFixtures(targetDir: string): Promise { + // Create directory structure + await fs.mkdirp(path.join(targetDir, 'bin')); + await fs.mkdirp(path.join(targetDir, 'lib')); + + // Copy fixture files + await fs.copy( + path.join(FIXTURES_DIR, 'package.json'), + path.join(targetDir, 'package.json') + ); + + await fs.copy( + path.join(FIXTURES_DIR, 'app.ts'), + path.join(targetDir, 'bin', 'app.ts') + ); + + await fs.copy( + path.join(FIXTURES_DIR, 'stack.ts'), + path.join(targetDir, 'lib', 'my-custom-stack.ts') + ); +} + +/** + * Create a multi-template repository structure for testing + */ +export async function createMultiTemplateRepository(baseDir: string): Promise { + const repoDir = path.join(baseDir, 'multi-template-repo'); + + // Create multiple templates + await createCustomTemplate(repoDir, 'template1', ['typescript']); + await createCustomTemplate(repoDir, 'template2', ['typescript']); + + return repoDir; +} \ No newline at end of file diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index bc2e2f3eb..a0626239a 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -4,6 +4,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import { availableInitLanguages, availableInitTemplates, cliInit, currentlyRecommendedAwsCdkLibFlags, expandPlaceholders, printAvailableTemplates } from '../../lib/commands/init'; import { TestIoHost } from '../_helpers/io-host'; +import { createCustomTemplate, createMultiTemplateRepository } from '../_fixtures/init-templates/template-helpers'; const ioHost = new TestIoHost(); const ioHelper = ioHost.asHelper('init'); @@ -243,22 +244,8 @@ describe('constructs version', () => { }); cliTest('create project from single local custom template', async (workDir) => { - // Create a simple custom template - const templateDir = path.join(workDir, 'my-cdk-template'); - const tsDir = path.join(templateDir, 'typescript'); - await fs.mkdirp(tsDir); - - // Create template files (custom templates don't process placeholders) - await fs.writeFile(path.join(tsDir, 'package.json'), JSON.stringify({ - name: 'my-custom-project', - version: '1.0.0', - dependencies: { - 'aws-cdk-lib': '^2.0.0', - 'constructs': '^10.0.0', - }, - }, null, 2)); - - await fs.writeFile(path.join(tsDir, 'app.ts'), 'console.log("Hello from custom template!");'); + // Create a simple custom template using fixtures + const templateDir = await createCustomTemplate(workDir, 'my-cdk-template', ['typescript']); // Initialize project from custom template const projectDir = path.join(workDir, 'my-project'); @@ -275,23 +262,18 @@ describe('constructs version', () => { // Verify files were created (custom templates copy files as-is) expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); - expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir, 'bin', 'app.ts'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir, 'lib', 'my-custom-stack.ts'))).toBeTruthy(); const packageJson = JSON.parse(await fs.readFile(path.join(projectDir, 'package.json'), 'utf8')); expect(packageJson.name).toBe('my-custom-project'); - - const appTs = await fs.readFile(path.join(projectDir, 'app.ts'), 'utf8'); - expect(appTs).toBe('console.log("Hello from custom template!");'); + expect(packageJson.dependencies['aws-cdk-lib']).toBe('^2.0.0'); + expect(packageJson.dependencies.constructs).toBe('^10.0.0'); }); cliTest('single template auto-detects language when template has single language', async (workDir) => { - // Create a single custom template with only TypeScript - const templateDir = path.join(workDir, 'my-cdk-template'); - const tsDir = path.join(templateDir, 'typescript'); - await fs.mkdirp(tsDir); - - await fs.writeFile(path.join(tsDir, 'package.json'), JSON.stringify({ name: 'single-lang-project' }, null, 2)); - await fs.writeFile(path.join(tsDir, 'app.ts'), 'console.log("Auto-detected single template!");'); + // Create a single custom template with only TypeScript using fixtures + const templateDir = await createCustomTemplate(workDir, 'my-cdk-template', ['typescript']); const projectDir = path.join(workDir, 'my-project'); await fs.mkdirp(projectDir); @@ -306,10 +288,10 @@ describe('constructs version', () => { }); expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); - expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir, 'bin', 'app.ts'))).toBeTruthy(); const packageJson = JSON.parse(await fs.readFile(path.join(projectDir, 'package.json'), 'utf8')); - expect(packageJson.name).toBe('single-lang-project'); + expect(packageJson.name).toBe('my-custom-project'); }); cliTest('custom template with multiple languages fails if language not provided', async (workDir) => { From 34b313ada37ee4d5f6711e319926d22f70a0157c Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Mon, 18 Aug 2025 16:54:19 -0400 Subject: [PATCH 16/38] chore: seperated test data from test code and resolved other issues based on CR feedback --- .../@aws-cdk/user-input-gen/lib/yargs-gen.ts | 13 ++ .../user-input-gen/lib/yargs-types.ts | 1 + .../user-input-gen/test/yargs-gen.test.ts | 6 +- packages/aws-cdk/lib/cli/cli-config.ts | 3 +- packages/aws-cdk/lib/commands/init/init.ts | 3 +- .../test/_fixtures/init-templates/app.ts | 7 - .../_fixtures/init-templates/package.json | 8 - .../test/_fixtures/init-templates/stack.ts | 8 - .../init-templates/template-helpers.ts | 116 ++++++++------ packages/aws-cdk/test/commands/init.test.ts | 144 ++++-------------- 10 files changed, 120 insertions(+), 189 deletions(-) delete mode 100644 packages/aws-cdk/test/_fixtures/init-templates/app.ts delete mode 100644 packages/aws-cdk/test/_fixtures/init-templates/package.json delete mode 100644 packages/aws-cdk/test/_fixtures/init-templates/stack.ts diff --git a/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts b/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts index 850750d8c..a41d5f393 100644 --- a/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts +++ b/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts @@ -52,6 +52,7 @@ export async function renderYargs(config: CliConfig, helpers: CliHelpers): Promi disabledEsLintRules: [ EsLintRules.MAX_LEN, // the default disabled rules result in 'Definition for rule 'prettier/prettier' was not found '@typescript-eslint/consistent-type-imports', // (ironically) typewriter does not support type imports + '@cdklabs/no-throw-default-error', // generated check functions throw proper Error objects ], }).render(scope); @@ -84,6 +85,8 @@ function makeYargs(config: CliConfig, helpers: CliHelpers): Statement { // we must compute global options first, as they are not part of an argument to a command call yargsExpr = makeOptions(yargsExpr, config.globalOptions, helpers); + let checkFunctions: string[] = []; + for (const command of Object.keys(config.commands)) { const commandFacts = config.commands[command]; const commandArg = commandFacts.arg @@ -110,6 +113,16 @@ function makeYargs(config: CliConfig, helpers: CliHelpers): Statement { } yargsExpr = yargsExpr.callMethod('command', ...commandCallArgs); + + // Collect check functions to add at global level + if (commandFacts.check) { + checkFunctions.push(commandFacts.check); + } + } + + // Add check functions at global level + for (const checkFunction of checkFunctions) { + yargsExpr = yargsExpr.callMethod('check', code.expr.directCode(checkFunction)); } return code.stmt.ret(makeEpilogue(yargsExpr, helpers)); diff --git a/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts b/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts index 91c23b288..f7e8efa7a 100644 --- a/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts +++ b/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts @@ -7,6 +7,7 @@ interface YargsCommand { export interface CliAction extends YargsCommand { options?: { [optionName: string]: CliOption }; + check?: string; } interface YargsArg { diff --git a/packages/@aws-cdk/user-input-gen/test/yargs-gen.test.ts b/packages/@aws-cdk/user-input-gen/test/yargs-gen.test.ts index 36276dff6..4c5e6a1e6 100644 --- a/packages/@aws-cdk/user-input-gen/test/yargs-gen.test.ts +++ b/packages/@aws-cdk/user-input-gen/test/yargs-gen.test.ts @@ -29,7 +29,7 @@ describe('render', () => { // GENERATED FROM packages/aws-cdk/lib/cli/cli-config.ts. // Do not edit by hand; all changes will be overwritten at build time from the config file. // ------------------------------------------------------------------------------------------- - /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports */ + /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports, @cdklabs/no-throw-default-error */ import { Argv } from 'yargs'; import * as helpers from './util/yargs-helpers'; @@ -96,7 +96,7 @@ describe('render', () => { // GENERATED FROM packages/aws-cdk/lib/cli/cli-config.ts. // Do not edit by hand; all changes will be overwritten at build time from the config file. // ------------------------------------------------------------------------------------------- - /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports */ + /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports, @cdklabs/no-throw-default-error */ import { Argv } from 'yargs'; import * as helpers from './util/yargs-helpers'; @@ -177,7 +177,7 @@ describe('render', () => { // GENERATED FROM packages/aws-cdk/lib/cli/cli-config.ts. // Do not edit by hand; all changes will be overwritten at build time from the config file. // ------------------------------------------------------------------------------------------- - /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports */ + /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports, @cdklabs/no-throw-default-error */ import { Argv } from 'yargs'; import * as helpers from './util/yargs-helpers'; diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 63b571c10..4a97b8df9 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -396,8 +396,9 @@ export async function makeConfig(): Promise { 'generate-only': { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }, 'lib-version': { type: 'string', alias: 'V', default: undefined, desc: 'The version of the CDK library (aws-cdk-lib) to initialize built-in templates with. Defaults to the version that was current when this CLI was built.' }, 'from-path': { type: 'string', desc: 'Path to a local custom template directory or multi-template repository', requiresArg: true, conflicts: ['lib-version'] }, - 'template-path': { type: 'string', desc: 'Path to a specific template within a multi-template repository (used with --from-path)', requiresArg: true }, + 'template-path': { type: 'string', desc: 'Path to a specific template within a multi-template repository (requires --from-path)', requiresArg: true }, }, + check: '(argv: any) => { if (argv.templatePath && !argv.fromPath) { throw new Error("--template-path requires --from-path to be specified"); } return true; }', }, 'migrate': { description: 'Migrate existing AWS resources into a CDK app', diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index 1c956127f..e5352c38e 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -66,7 +66,8 @@ export interface CliInitOptions { readonly fromPath?: string; /** - * Path to a specific template within a multi-template repository + * Path to a specific template within a multi-template repository. + * This parameter requires --from-path to be specified. * @default undefined */ readonly templatePath?: string; diff --git a/packages/aws-cdk/test/_fixtures/init-templates/app.ts b/packages/aws-cdk/test/_fixtures/init-templates/app.ts deleted file mode 100644 index 2f8a1cc8b..000000000 --- a/packages/aws-cdk/test/_fixtures/init-templates/app.ts +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env node -import 'source-map-support/register'; -import * as cdk from 'aws-cdk-lib'; -import { MyCustomStack } from '../lib/my-custom-stack'; - -const app = new cdk.App(); -new MyCustomStack(app, 'MyCustomStack'); \ No newline at end of file diff --git a/packages/aws-cdk/test/_fixtures/init-templates/package.json b/packages/aws-cdk/test/_fixtures/init-templates/package.json deleted file mode 100644 index a9672bffb..000000000 --- a/packages/aws-cdk/test/_fixtures/init-templates/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "my-custom-project", - "version": "1.0.0", - "dependencies": { - "aws-cdk-lib": "^2.0.0", - "constructs": "^10.0.0" - } -} \ No newline at end of file diff --git a/packages/aws-cdk/test/_fixtures/init-templates/stack.ts b/packages/aws-cdk/test/_fixtures/init-templates/stack.ts deleted file mode 100644 index ba35dd66b..000000000 --- a/packages/aws-cdk/test/_fixtures/init-templates/stack.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as cdk from 'aws-cdk-lib'; -import { Construct } from 'constructs'; - -export class MyCustomStack extends cdk.Stack { - constructor(scope: Construct, id: string, props?: cdk.StackProps) { - super(scope, id, props); - } -} \ No newline at end of file diff --git a/packages/aws-cdk/test/_fixtures/init-templates/template-helpers.ts b/packages/aws-cdk/test/_fixtures/init-templates/template-helpers.ts index b0dc36e2e..0f680b7d2 100644 --- a/packages/aws-cdk/test/_fixtures/init-templates/template-helpers.ts +++ b/packages/aws-cdk/test/_fixtures/init-templates/template-helpers.ts @@ -1,62 +1,82 @@ import * as path from 'path'; import * as fs from 'fs-extra'; -const FIXTURES_DIR = path.join(__dirname, '.'); +export async function createSingleLanguageTemplate(baseDir: string, templateName: string, language: string): Promise { + const templateDir = path.join(baseDir, templateName); + const langDir = path.join(templateDir, language); + await fs.mkdirp(langDir); + + const fileContent = getLanguageFileContent(language); + const fileName = getLanguageFileName(language); + + await fs.writeFile(path.join(langDir, fileName), fileContent); + return templateDir; +} -/** - * Helper function to create a custom template structure for testing - */ -export async function createCustomTemplate(baseDir: string, templateName: string, languages: string[]): Promise { +export async function createMultiLanguageTemplate(baseDir: string, templateName: string, languages: string[]): Promise { const templateDir = path.join(baseDir, templateName); - + for (const language of languages) { const langDir = path.join(templateDir, language); await fs.mkdirp(langDir); - - // Copy appropriate fixtures based on language - if (language === 'typescript') { - await copyTypescriptFixtures(langDir); - } - // Add other languages as needed + + const fileContent = getLanguageFileContent(language); + const fileName = getLanguageFileName(language); + + await fs.writeFile(path.join(langDir, fileName), fileContent); } - + return templateDir; } -/** - * Copy TypeScript template fixtures to the specified directory - */ -async function copyTypescriptFixtures(targetDir: string): Promise { - // Create directory structure - await fs.mkdirp(path.join(targetDir, 'bin')); - await fs.mkdirp(path.join(targetDir, 'lib')); - - // Copy fixture files - await fs.copy( - path.join(FIXTURES_DIR, 'package.json'), - path.join(targetDir, 'package.json') - ); - - await fs.copy( - path.join(FIXTURES_DIR, 'app.ts'), - path.join(targetDir, 'bin', 'app.ts') - ); - - await fs.copy( - path.join(FIXTURES_DIR, 'stack.ts'), - path.join(targetDir, 'lib', 'my-custom-stack.ts') - ); -} +export async function createMultiTemplateRepository(baseDir: string, templates: Array<{ name: string; languages: string[] }>): Promise { + const repoDir = path.join(baseDir, 'template-repo'); + + for (const template of templates) { + await createMultiLanguageTemplate(repoDir, template.name, template.languages); + } -/** - * Create a multi-template repository structure for testing - */ -export async function createMultiTemplateRepository(baseDir: string): Promise { - const repoDir = path.join(baseDir, 'multi-template-repo'); - - // Create multiple templates - await createCustomTemplate(repoDir, 'template1', ['typescript']); - await createCustomTemplate(repoDir, 'template2', ['typescript']); - return repoDir; -} \ No newline at end of file +} + +function getLanguageFileContent(language: string): string { + switch (language) { + case 'typescript': + return 'console.log("TypeScript template");'; + case 'javascript': + return 'console.log("JavaScript template");'; + case 'python': + return 'print("Python template")'; + case 'java': + return 'public class App { }'; + case 'csharp': + return 'public class App { }'; + case 'fsharp': + return 'module App'; + case 'go': + return 'package main'; + default: + return `// ${language} template`; + } +} + +function getLanguageFileName(language: string): string { + switch (language) { + case 'typescript': + return 'app.ts'; + case 'javascript': + return 'app.js'; + case 'python': + return 'app.py'; + case 'java': + return 'App.java'; + case 'csharp': + return 'App.cs'; + case 'fsharp': + return 'App.fs'; + case 'go': + return 'app.go'; + default: + return 'app.txt'; + } +} diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index a0626239a..d1141a774 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -3,8 +3,8 @@ import * as path from 'path'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import { availableInitLanguages, availableInitTemplates, cliInit, currentlyRecommendedAwsCdkLibFlags, expandPlaceholders, printAvailableTemplates } from '../../lib/commands/init'; +import { createSingleLanguageTemplate, createMultiLanguageTemplate, createMultiTemplateRepository } from '../_fixtures/init-templates/template-helpers'; import { TestIoHost } from '../_helpers/io-host'; -import { createCustomTemplate, createMultiTemplateRepository } from '../_fixtures/init-templates/template-helpers'; const ioHost = new TestIoHost(); const ioHelper = ioHost.asHelper('init'); @@ -243,11 +243,18 @@ describe('constructs version', () => { expect(await fs.pathExists(path.join(workDir, 'bin'))).toBeTruthy(); }); - cliTest('create project from single local custom template', async (workDir) => { - // Create a simple custom template using fixtures - const templateDir = await createCustomTemplate(workDir, 'my-cdk-template', ['typescript']); + cliTest('fails when --template-path is used without --from-path', async (workDir) => { + await expect(cliInit({ + ioHelper, + type: undefined, + language: 'typescript', + workDir, + templatePath: 'some-template', + })).rejects.toThrow('--template-path requires --from-path to be specified'); + }); - // Initialize project from custom template + cliTest('create project from single local custom template', async (workDir) => { + const templateDir = await createSingleLanguageTemplate(workDir, 'my-template', 'typescript'); const projectDir = path.join(workDir, 'my-project'); await fs.mkdirp(projectDir); @@ -260,25 +267,14 @@ describe('constructs version', () => { workDir: projectDir, }); - // Verify files were created (custom templates copy files as-is) - expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); - expect(await fs.pathExists(path.join(projectDir, 'bin', 'app.ts'))).toBeTruthy(); - expect(await fs.pathExists(path.join(projectDir, 'lib', 'my-custom-stack.ts'))).toBeTruthy(); - - const packageJson = JSON.parse(await fs.readFile(path.join(projectDir, 'package.json'), 'utf8')); - expect(packageJson.name).toBe('my-custom-project'); - expect(packageJson.dependencies['aws-cdk-lib']).toBe('^2.0.0'); - expect(packageJson.dependencies.constructs).toBe('^10.0.0'); + expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); }); cliTest('single template auto-detects language when template has single language', async (workDir) => { - // Create a single custom template with only TypeScript using fixtures - const templateDir = await createCustomTemplate(workDir, 'my-cdk-template', ['typescript']); - + const templateDir = await createSingleLanguageTemplate(workDir, 'my-template', 'typescript'); const projectDir = path.join(workDir, 'my-project'); await fs.mkdirp(projectDir); - // Don't specify language - should auto-detect when template has only one language await cliInit({ ioHelper, fromPath: templateDir, @@ -287,28 +283,14 @@ describe('constructs version', () => { workDir: projectDir, }); - expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); - expect(await fs.pathExists(path.join(projectDir, 'bin', 'app.ts'))).toBeTruthy(); - - const packageJson = JSON.parse(await fs.readFile(path.join(projectDir, 'package.json'), 'utf8')); - expect(packageJson.name).toBe('my-custom-project'); + expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); }); cliTest('custom template with multiple languages fails if language not provided', async (workDir) => { - // Create a custom template with both TypeScript and Python - const templateDir = path.join(workDir, 'custom-template'); - const tsDir = path.join(templateDir, 'typescript'); - const pyDir = path.join(templateDir, 'python'); - await fs.mkdirp(tsDir); - await fs.mkdirp(pyDir); - - await fs.writeFile(path.join(tsDir, 'app.ts'), 'console.log("Hello TS!");'); - await fs.writeFile(path.join(pyDir, 'app.py'), 'print("Hello Python!")'); - + const templateDir = await createMultiLanguageTemplate(workDir, 'multi-lang-template', ['typescript', 'python']); const projectDir = path.join(workDir, 'my-project'); await fs.mkdirp(projectDir); - // Don't specify language - should fail since multiple languages are available await expect(cliInit({ ioHelper, fromPath: templateDir, @@ -331,29 +313,10 @@ describe('constructs version', () => { }); cliTest('create project from multi-template repository with template-path', async (workDir) => { - // Create a multi-template repository structure - const repoDir = path.join(workDir, 'cdk-templates'); - const myCustomTemplateDir = path.join(repoDir, 'my-custom-template'); - const webAppTemplateDir = path.join(repoDir, 'web-app-template'); - - // my-custom-template with TypeScript and Python - const customTsDir = path.join(myCustomTemplateDir, 'typescript'); - const customPyDir = path.join(myCustomTemplateDir, 'python'); - await fs.mkdirp(customTsDir); - await fs.mkdirp(customPyDir); - await fs.writeFile(path.join(customTsDir, 'package.json'), JSON.stringify({ - name: 'my-custom-project', - version: '1.0.0', - }, null, 2)); - await fs.writeFile(path.join(customTsDir, 'app.ts'), 'console.log("My Custom Template!");'); - await fs.writeFile(path.join(customPyDir, 'requirements.txt'), 'aws-cdk-lib>=2.0.0'); - await fs.writeFile(path.join(customPyDir, 'app.py'), 'print("My Custom Template!")'); - - // web-app-template with Java only - const webAppJavaDir = path.join(webAppTemplateDir, 'java'); - await fs.mkdirp(webAppJavaDir); - await fs.writeFile(path.join(webAppJavaDir, 'pom.xml'), 'web-app'); - await fs.writeFile(path.join(webAppJavaDir, 'App.java'), 'public class App { }'); + const repoDir = await createMultiTemplateRepository(workDir, [ + { name: 'my-custom-template', languages: ['typescript', 'python'] }, + { name: 'web-app-template', languages: ['java'] }, + ]); // Test 1: Initialize from my-custom-template with TypeScript const projectDir1 = path.join(workDir, 'project1'); @@ -369,14 +332,8 @@ describe('constructs version', () => { workDir: projectDir1, }); - // Verify my-custom-template TypeScript files were created - expect(await fs.pathExists(path.join(projectDir1, 'package.json'))).toBeTruthy(); expect(await fs.pathExists(path.join(projectDir1, 'app.ts'))).toBeTruthy(); - expect(await fs.pathExists(path.join(projectDir1, 'requirements.txt'))).toBeFalsy(); - expect(await fs.pathExists(path.join(projectDir1, 'pom.xml'))).toBeFalsy(); - - const packageJson = JSON.parse(await fs.readFile(path.join(projectDir1, 'package.json'), 'utf8')); - expect(packageJson.name).toBe('my-custom-project'); + expect(await fs.pathExists(path.join(projectDir1, 'app.py'))).toBeFalsy(); // Test 2: Initialize from web-app-template with Java const projectDir2 = path.join(workDir, 'project2'); @@ -392,19 +349,14 @@ describe('constructs version', () => { workDir: projectDir2, }); - // Verify web-app-template Java files were created - expect(await fs.pathExists(path.join(projectDir2, 'pom.xml'))).toBeTruthy(); expect(await fs.pathExists(path.join(projectDir2, 'App.java'))).toBeTruthy(); - expect(await fs.pathExists(path.join(projectDir2, 'package.json'))).toBeFalsy(); - expect(await fs.pathExists(path.join(projectDir2, 'requirements.txt'))).toBeFalsy(); + expect(await fs.pathExists(path.join(projectDir2, 'app.ts'))).toBeFalsy(); }); cliTest('multi-template repository with non-existent template-path throws error', async (workDir) => { - const repoDir = path.join(workDir, 'cdk-templates'); - const validTemplateDir = path.join(repoDir, 'valid-template'); - const validTemplateTsDir = path.join(validTemplateDir, 'typescript'); - await fs.mkdirp(validTemplateTsDir); - await fs.writeFile(path.join(validTemplateTsDir, 'app.ts'), 'console.log("Valid template!");'); + const repoDir = await createMultiTemplateRepository(workDir, [ + { name: 'valid-template', languages: ['typescript'] }, + ]); const projectDir = path.join(workDir, 'my-project'); await fs.mkdirp(projectDir); @@ -460,18 +412,13 @@ describe('constructs version', () => { }); cliTest('multi-template repository auto-detects language when template has single language', async (workDir) => { - // Create a multi-template repository with a single-language template - const repoDir = path.join(workDir, 'cdk-templates'); - const templateDir = path.join(repoDir, 'single-lang-template'); - const templateTsDir = path.join(templateDir, 'typescript'); - await fs.mkdirp(templateTsDir); - await fs.writeFile(path.join(templateTsDir, 'package.json'), JSON.stringify({ name: 'single-lang-project' }, null, 2)); - await fs.writeFile(path.join(templateTsDir, 'app.ts'), 'console.log("Auto-detected TypeScript!");'); + const repoDir = await createMultiTemplateRepository(workDir, [ + { name: 'single-lang-template', languages: ['typescript'] }, + ]); const projectDir = path.join(workDir, 'my-project'); await fs.mkdirp(projectDir); - // Don't specify language - should auto-detect when template has only one language await cliInit({ ioHelper, fromPath: repoDir, @@ -481,34 +428,13 @@ describe('constructs version', () => { workDir: projectDir, }); - expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); - - const packageJson = JSON.parse(await fs.readFile(path.join(projectDir, 'package.json'), 'utf8')); - expect(packageJson.name).toBe('single-lang-project'); }); cliTest('multi-template repository supports all CDK languages', async (workDir) => { - // Create a multi-template repository with comprehensive language support - const repoDir = path.join(workDir, 'cdk-templates'); - const templateDir = path.join(repoDir, 'multi-lang-template'); - - // Create language directories for all supported CDK languages - const languages = { - typescript: { file: 'app.ts', content: 'console.log("TypeScript!");' }, - javascript: { file: 'app.js', content: 'console.log("JavaScript!");' }, - python: { file: 'app.py', content: 'print("Python!")' }, - java: { file: 'App.java', content: 'public class App { }' }, - csharp: { file: 'App.cs', content: 'public class App { }' }, - fsharp: { file: 'App.fs', content: 'module App' }, - go: { file: 'app.go', content: 'package main' }, - }; - - for (const [lang, config] of Object.entries(languages)) { - const langDir = path.join(templateDir, lang); - await fs.mkdirp(langDir); - await fs.writeFile(path.join(langDir, config.file), config.content); - } + const repoDir = await createMultiTemplateRepository(workDir, [ + { name: 'multi-lang-template', languages: ['typescript', 'javascript', 'python', 'java', 'csharp', 'fsharp', 'go'] }, + ]); // Test TypeScript selection const tsProjectDir = path.join(workDir, 'ts-project'); @@ -524,14 +450,10 @@ describe('constructs version', () => { workDir: tsProjectDir, }); - // Verify only TypeScript files were created expect(await fs.pathExists(path.join(tsProjectDir, 'app.ts'))).toBeTruthy(); expect(await fs.pathExists(path.join(tsProjectDir, 'app.js'))).toBeFalsy(); expect(await fs.pathExists(path.join(tsProjectDir, 'app.py'))).toBeFalsy(); - const tsContent = await fs.readFile(path.join(tsProjectDir, 'app.ts'), 'utf8'); - expect(tsContent).toBe('console.log("TypeScript!");'); - // Test Python selection const pyProjectDir = path.join(workDir, 'py-project'); await fs.mkdirp(pyProjectDir); @@ -546,12 +468,8 @@ describe('constructs version', () => { workDir: pyProjectDir, }); - // Verify only Python files were created expect(await fs.pathExists(path.join(pyProjectDir, 'app.py'))).toBeTruthy(); expect(await fs.pathExists(path.join(pyProjectDir, 'app.ts'))).toBeFalsy(); - - const pyContent = await fs.readFile(path.join(pyProjectDir, 'app.py'), 'utf8'); - expect(pyContent).toBe('print("Python!")'); }); cliTest('CLI uses recommended feature flags from data file to initialize context', async (workDir) => { From 1b87c7984833dc11e592f79c4f4c1c755a02f055 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Mon, 18 Aug 2025 17:03:28 -0400 Subject: [PATCH 17/38] chore: add validation for templatePath requiring fromPath at function level --- packages/aws-cdk/lib/commands/init/init.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index e5352c38e..0aaed2d1d 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -84,6 +84,11 @@ export async function cliInit(options: CliInitOptions) { const generateOnly = options.generateOnly ?? false; const workDir = options.workDir ?? process.cwd(); + // Validate that templatePath requires fromPath + if (options.templatePath && !options.fromPath) { + throw new ToolkitError('--template-path requires --from-path to be specified'); + } + // Step 1: Load template let template: InitTemplate; if (options.fromPath) { From 7ce094d56cf4584c7b40bcbeb2a50c75b319d55c Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 18 Aug 2025 21:11:41 +0000 Subject: [PATCH 18/38] chore: self mutation Signed-off-by: github-actions --- packages/aws-cdk/lib/cli/cli-type-registry.json | 5 +++-- .../aws-cdk/lib/cli/parse-command-line-arguments.ts | 10 ++++++++-- packages/aws-cdk/lib/cli/user-input.ts | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 9fba20bd4..44b7adb7c 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -842,10 +842,11 @@ }, "template-path": { "type": "string", - "desc": "Path to a specific template within a multi-template repository (used with --from-path)", + "desc": "Path to a specific template within a multi-template repository (requires --from-path)", "requiresArg": true } - } + }, + "check": "(argv: any) => { if (argv.templatePath && !argv.fromPath) { throw new Error(\"--template-path requires --from-path to be specified\"); } return true; }" }, "migrate": { "description": "Migrate existing AWS resources into a CDK app", diff --git a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts index d3f416cf3..e3e6b178e 100644 --- a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts @@ -2,7 +2,7 @@ // GENERATED FROM packages/aws-cdk/lib/cli/cli-config.ts. // Do not edit by hand; all changes will be overwritten at build time from the config file. // ------------------------------------------------------------------------------------------- -/* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports */ +/* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports, @cdklabs/no-throw-default-error */ import { Argv } from 'yargs'; import * as helpers from './util/yargs-helpers'; @@ -835,7 +835,7 @@ export function parseCommandLineArguments(args: Array): any { .option('template-path', { default: undefined, type: 'string', - desc: 'Path to a specific template within a multi-template repository (used with --from-path)', + desc: 'Path to a specific template within a multi-template repository (requires --from-path)', requiresArg: true, }), ) @@ -973,6 +973,12 @@ export function parseCommandLineArguments(args: Array): any { conflicts: ['enable', 'disable'], }), ) + .check((argv: any) => { + if (argv.templatePath && !argv.fromPath) { + throw new Error('--template-path requires --from-path to be specified'); + } + return true; + }) .version(helpers.cliVersion()) .demandCommand(1, '') .recommendCommands() diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index d4930a884..72327d04a 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1334,7 +1334,7 @@ export interface InitOptions { readonly fromPath?: string; /** - * Path to a specific template within a multi-template repository (used with --from-path) + * Path to a specific template within a multi-template repository (requires --from-path) * * @default - undefined */ From b4587fa8abf4858ff744ab8225de043fe7159163 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Tue, 19 Aug 2025 13:32:07 -0400 Subject: [PATCH 19/38] chore: refactored check function with implies for template-path validation --- packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts | 8 +++++++- packages/@aws-cdk/user-input-gen/lib/yargs-types.ts | 1 + packages/@aws-cdk/user-input-gen/test/yargs-gen.test.ts | 6 +++--- packages/aws-cdk/lib/cli/cli-config.ts | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts b/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts index a41d5f393..9d755cf83 100644 --- a/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts +++ b/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts @@ -52,7 +52,6 @@ export async function renderYargs(config: CliConfig, helpers: CliHelpers): Promi disabledEsLintRules: [ EsLintRules.MAX_LEN, // the default disabled rules result in 'Definition for rule 'prettier/prettier' was not found '@typescript-eslint/consistent-type-imports', // (ironically) typewriter does not support type imports - '@cdklabs/no-throw-default-error', // generated check functions throw proper Error objects ], }).render(scope); @@ -112,6 +111,13 @@ function makeYargs(config: CliConfig, helpers: CliHelpers): Statement { commandCallArgs.push(optionsExpr); } + // Add implies calls if present + if (commandFacts.implies) { + for (const [key, value] of Object.entries(commandFacts.implies)) { + optionsExpr = optionsExpr.callMethod('implies', lit(key), lit(value)); + } + } + yargsExpr = yargsExpr.callMethod('command', ...commandCallArgs); // Collect check functions to add at global level diff --git a/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts b/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts index f7e8efa7a..917d015ce 100644 --- a/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts +++ b/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts @@ -8,6 +8,7 @@ interface YargsCommand { export interface CliAction extends YargsCommand { options?: { [optionName: string]: CliOption }; check?: string; + implies?: { [key: string]: string }; } interface YargsArg { diff --git a/packages/@aws-cdk/user-input-gen/test/yargs-gen.test.ts b/packages/@aws-cdk/user-input-gen/test/yargs-gen.test.ts index 4c5e6a1e6..36276dff6 100644 --- a/packages/@aws-cdk/user-input-gen/test/yargs-gen.test.ts +++ b/packages/@aws-cdk/user-input-gen/test/yargs-gen.test.ts @@ -29,7 +29,7 @@ describe('render', () => { // GENERATED FROM packages/aws-cdk/lib/cli/cli-config.ts. // Do not edit by hand; all changes will be overwritten at build time from the config file. // ------------------------------------------------------------------------------------------- - /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports, @cdklabs/no-throw-default-error */ + /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports */ import { Argv } from 'yargs'; import * as helpers from './util/yargs-helpers'; @@ -96,7 +96,7 @@ describe('render', () => { // GENERATED FROM packages/aws-cdk/lib/cli/cli-config.ts. // Do not edit by hand; all changes will be overwritten at build time from the config file. // ------------------------------------------------------------------------------------------- - /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports, @cdklabs/no-throw-default-error */ + /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports */ import { Argv } from 'yargs'; import * as helpers from './util/yargs-helpers'; @@ -177,7 +177,7 @@ describe('render', () => { // GENERATED FROM packages/aws-cdk/lib/cli/cli-config.ts. // Do not edit by hand; all changes will be overwritten at build time from the config file. // ------------------------------------------------------------------------------------------- - /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports, @cdklabs/no-throw-default-error */ + /* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports */ import { Argv } from 'yargs'; import * as helpers from './util/yargs-helpers'; diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 4a97b8df9..ca4d3e265 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -398,7 +398,7 @@ export async function makeConfig(): Promise { 'from-path': { type: 'string', desc: 'Path to a local custom template directory or multi-template repository', requiresArg: true, conflicts: ['lib-version'] }, 'template-path': { type: 'string', desc: 'Path to a specific template within a multi-template repository (requires --from-path)', requiresArg: true }, }, - check: '(argv: any) => { if (argv.templatePath && !argv.fromPath) { throw new Error("--template-path requires --from-path to be specified"); } return true; }', + implies: { 'template-path': 'from-path' }, }, 'migrate': { description: 'Migrate existing AWS resources into a CDK app', From 46608640daa2827899471788daa22d92ceeafea7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 19 Aug 2025 17:41:03 +0000 Subject: [PATCH 20/38] chore: self mutation Signed-off-by: github-actions --- packages/aws-cdk/lib/cli/cli-type-registry.json | 4 +++- packages/aws-cdk/lib/cli/parse-command-line-arguments.ts | 8 +------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 44b7adb7c..1b71d332a 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -846,7 +846,9 @@ "requiresArg": true } }, - "check": "(argv: any) => { if (argv.templatePath && !argv.fromPath) { throw new Error(\"--template-path requires --from-path to be specified\"); } return true; }" + "implies": { + "template-path": "from-path" + } }, "migrate": { "description": "Migrate existing AWS resources into a CDK app", diff --git a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts index e3e6b178e..d793cd926 100644 --- a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts @@ -2,7 +2,7 @@ // GENERATED FROM packages/aws-cdk/lib/cli/cli-config.ts. // Do not edit by hand; all changes will be overwritten at build time from the config file. // ------------------------------------------------------------------------------------------- -/* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports, @cdklabs/no-throw-default-error */ +/* eslint-disable @stylistic/max-len, @typescript-eslint/consistent-type-imports */ import { Argv } from 'yargs'; import * as helpers from './util/yargs-helpers'; @@ -973,12 +973,6 @@ export function parseCommandLineArguments(args: Array): any { conflicts: ['enable', 'disable'], }), ) - .check((argv: any) => { - if (argv.templatePath && !argv.fromPath) { - throw new Error('--template-path requires --from-path to be specified'); - } - return true; - }) .version(helpers.cliVersion()) .demandCommand(1, '') .recommendCommands() From 2c5bca33473028ca417a360f55aa958a8dcaba6d Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Tue, 19 Aug 2025 15:04:11 -0400 Subject: [PATCH 21/38] chore: update test file to remove incorrect test and add test to verify implies constraint works correctly --- packages/aws-cdk/lib/commands/init/init.ts | 5 +---- packages/aws-cdk/test/commands/init.test.ts | 22 +++++++++++++++------ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index 0aaed2d1d..114305b0f 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -84,10 +84,7 @@ export async function cliInit(options: CliInitOptions) { const generateOnly = options.generateOnly ?? false; const workDir = options.workDir ?? process.cwd(); - // Validate that templatePath requires fromPath - if (options.templatePath && !options.fromPath) { - throw new ToolkitError('--template-path requires --from-path to be specified'); - } + // Step 1: Load template let template: InitTemplate; diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index d1141a774..432315c4c 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -243,14 +243,24 @@ describe('constructs version', () => { expect(await fs.pathExists(path.join(workDir, 'bin'))).toBeTruthy(); }); - cliTest('fails when --template-path is used without --from-path', async (workDir) => { - await expect(cliInit({ + cliTest('template-path implies from-path constraint works correctly', async (workDir) => { + const repoDir = await createMultiTemplateRepository(workDir, [ + { name: 'test-template', languages: ['typescript'] }, + ]); + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + await cliInit({ ioHelper, - type: undefined, + fromPath: repoDir, + templatePath: 'test-template', language: 'typescript', - workDir, - templatePath: 'some-template', - })).rejects.toThrow('--template-path requires --from-path to be specified'); + canUseNetwork: false, + generateOnly: true, + workDir: projectDir, + }); + + expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); }); cliTest('create project from single local custom template', async (workDir) => { From f2e5b03e4623ee1055875804ac2ce3bdf47e4c0c Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 19 Aug 2025 19:12:07 +0000 Subject: [PATCH 22/38] chore: self mutation Signed-off-by: github-actions --- packages/aws-cdk/lib/commands/init/init.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index 114305b0f..e5352c38e 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -84,8 +84,6 @@ export async function cliInit(options: CliInitOptions) { const generateOnly = options.generateOnly ?? false; const workDir = options.workDir ?? process.cwd(); - - // Step 1: Load template let template: InitTemplate; if (options.fromPath) { From 5f447d9ee0d55daafa77e8d75fd5dea6e371adf1 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Tue, 19 Aug 2025 16:36:48 -0400 Subject: [PATCH 23/38] chore: remove unused check function code and clean up old validation and test --- .../@aws-cdk/user-input-gen/lib/yargs-gen.ts | 12 ------------ .../@aws-cdk/user-input-gen/lib/yargs-types.ts | 1 - packages/aws-cdk/test/commands/init.test.ts | 18 ------------------ 3 files changed, 31 deletions(-) diff --git a/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts b/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts index 9d755cf83..a40f0c789 100644 --- a/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts +++ b/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts @@ -84,8 +84,6 @@ function makeYargs(config: CliConfig, helpers: CliHelpers): Statement { // we must compute global options first, as they are not part of an argument to a command call yargsExpr = makeOptions(yargsExpr, config.globalOptions, helpers); - let checkFunctions: string[] = []; - for (const command of Object.keys(config.commands)) { const commandFacts = config.commands[command]; const commandArg = commandFacts.arg @@ -119,16 +117,6 @@ function makeYargs(config: CliConfig, helpers: CliHelpers): Statement { } yargsExpr = yargsExpr.callMethod('command', ...commandCallArgs); - - // Collect check functions to add at global level - if (commandFacts.check) { - checkFunctions.push(commandFacts.check); - } - } - - // Add check functions at global level - for (const checkFunction of checkFunctions) { - yargsExpr = yargsExpr.callMethod('check', code.expr.directCode(checkFunction)); } return code.stmt.ret(makeEpilogue(yargsExpr, helpers)); diff --git a/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts b/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts index 917d015ce..a19fff94e 100644 --- a/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts +++ b/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts @@ -7,7 +7,6 @@ interface YargsCommand { export interface CliAction extends YargsCommand { options?: { [optionName: string]: CliOption }; - check?: string; implies?: { [key: string]: string }; } diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index 432315c4c..f3d0ff6b5 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -243,25 +243,7 @@ describe('constructs version', () => { expect(await fs.pathExists(path.join(workDir, 'bin'))).toBeTruthy(); }); - cliTest('template-path implies from-path constraint works correctly', async (workDir) => { - const repoDir = await createMultiTemplateRepository(workDir, [ - { name: 'test-template', languages: ['typescript'] }, - ]); - const projectDir = path.join(workDir, 'my-project'); - await fs.mkdirp(projectDir); - await cliInit({ - ioHelper, - fromPath: repoDir, - templatePath: 'test-template', - language: 'typescript', - canUseNetwork: false, - generateOnly: true, - workDir: projectDir, - }); - - expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); - }); cliTest('create project from single local custom template', async (workDir) => { const templateDir = await createSingleLanguageTemplate(workDir, 'my-template', 'typescript'); From fcda0b4c46cb935f2b59358ee0429cc666566b14 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 19 Aug 2025 20:44:19 +0000 Subject: [PATCH 24/38] chore: self mutation Signed-off-by: github-actions --- packages/aws-cdk/test/commands/init.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index f3d0ff6b5..45ff84155 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -243,8 +243,6 @@ describe('constructs version', () => { expect(await fs.pathExists(path.join(workDir, 'bin'))).toBeTruthy(); }); - - cliTest('create project from single local custom template', async (workDir) => { const templateDir = await createSingleLanguageTemplate(workDir, 'my-template', 'typescript'); const projectDir = path.join(workDir, 'my-project'); From 7578eb32369bc9278c2b0ede151a9755f9b70651 Mon Sep 17 00:00:00 2001 From: Rohan Gupta <44989017+rohang9000@users.noreply.github.com> Date: Fri, 22 Aug 2025 07:14:06 -0700 Subject: [PATCH 25/38] removing "requires --from-path" description from cli-config.ts Co-authored-by: Ian Hou <45278651+iankhou@users.noreply.github.com> --- packages/aws-cdk/lib/cli/cli-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index ca4d3e265..dc6c53661 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -396,7 +396,7 @@ export async function makeConfig(): Promise { 'generate-only': { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }, 'lib-version': { type: 'string', alias: 'V', default: undefined, desc: 'The version of the CDK library (aws-cdk-lib) to initialize built-in templates with. Defaults to the version that was current when this CLI was built.' }, 'from-path': { type: 'string', desc: 'Path to a local custom template directory or multi-template repository', requiresArg: true, conflicts: ['lib-version'] }, - 'template-path': { type: 'string', desc: 'Path to a specific template within a multi-template repository (requires --from-path)', requiresArg: true }, + 'template-path': { type: 'string', desc: 'Path to a specific template within a multi-template repository', requiresArg: true }, }, implies: { 'template-path': 'from-path' }, }, From 5b132b93a37ef5b191924f9a7b9604e23d44ee56 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Fri, 22 Aug 2025 13:38:27 -0400 Subject: [PATCH 26/38] chore: add additional test coverage for misc. --- .../aws-cdk/lib/cli/cli-type-registry.json | 2 +- packages/aws-cdk/test/commands/init.test.ts | 155 ++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 1b71d332a..9cb070ca1 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -842,7 +842,7 @@ }, "template-path": { "type": "string", - "desc": "Path to a specific template within a multi-template repository (requires --from-path)", + "desc": "Path to a specific template within a multi-template repository", "requiresArg": true } }, diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index 45ff84155..e6ff90dee 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -529,6 +529,161 @@ describe('constructs version', () => { }, // This is a lot to test, and it can be slow-ish, especially when ran with other tests. 30_000); + + cliTest('unstable flag functionality works correctly', async (workDir) => { + const { exec } = await import('child_process'); + const { promisify } = await import('util'); + const execAsync = promisify(exec); + + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + const cdkBin = path.join(__dirname, '..', '..', 'bin', 'cdk'); + + // Test that unstable flags are accepted during actual init + const { stderr } = await execAsync(`node ${cdkBin} init app --language typescript --unstable feature1 --unstable feature2 --generate-only`, { + cwd: projectDir, + env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' } + }); + + // Should complete without error + expect(stderr).not.toContain('error'); + expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + }); + + cliTest('conflict between lib-version and from-path is enforced', async (workDir) => { + const { exec } = await import('child_process'); + const { promisify } = await import('util'); + const execAsync = promisify(exec); + + const templateDir = await createSingleLanguageTemplate(workDir, 'conflict-test', 'typescript'); + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + const cdkBin = path.join(__dirname, '..', '..', 'bin', 'cdk'); + + // Test that using both flags together causes an error + await expect(execAsync(`node ${cdkBin} init app --language typescript --lib-version 2.0.0 --from-path ${templateDir} --generate-only`, { + cwd: projectDir, + env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' } + })).rejects.toThrow(); + }); + + cliTest('template-path implies from-path validation works', async (workDir) => { + const { exec } = await import('child_process'); + const { promisify } = await import('util'); + const execAsync = promisify(exec); + + // Test that the implication is properly configured + const { makeConfig } = await import('../../lib/cli/cli-config'); + const config = await makeConfig(); + expect(config.commands.init.implies).toEqual({ 'template-path': 'from-path' }); + + const repoDir = await createMultiTemplateRepository(workDir, [ + { name: 'implies-test', languages: ['typescript'] }, + ]); + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + const cdkBin = path.join(__dirname, '..', '..', 'bin', 'cdk'); + + // Test CLI enforcement: template-path without from-path should fail + await expect(execAsync(`node ${cdkBin} init app --language typescript --template-path implies-test --generate-only`, { + cwd: projectDir, + env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' } + })).rejects.toThrow(); + + // Test success case: template-path WITH from-path should work + const { stderr } = await execAsync(`node ${cdkBin} init app --language typescript --from-path ${repoDir} --template-path implies-test --generate-only`, { + cwd: projectDir, + env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' } + }); + + expect(stderr).not.toContain('error'); + expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + }); + + cliTest('hook files are ignored during template copy', async (workDir) => { + const templateDir = path.join(workDir, 'template-with-hooks'); + const tsDir = path.join(templateDir, 'typescript'); + await fs.mkdirp(tsDir); + + await fs.writeFile(path.join(tsDir, 'app.ts'), 'console.log("Hello CDK");'); + await fs.writeFile(path.join(tsDir, 'package.json'), '{}'); + await fs.writeFile(path.join(tsDir, 'setup.hook.js'), 'console.log("setup hook");'); + await fs.writeFile(path.join(tsDir, 'build.hook.d.ts'), 'export {};'); + await fs.writeFile(path.join(tsDir, 'deploy.hook.sh'), '#!/bin/bash\necho "deploy"'); + + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + await cliInit({ + ioHelper, + fromPath: templateDir, + language: 'typescript', + canUseNetwork: false, + generateOnly: true, + workDir: projectDir, + }); + + expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir, 'package.json'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir, 'setup.hook.js'))).toBeFalsy(); + expect(await fs.pathExists(path.join(projectDir, 'build.hook.d.ts'))).toBeFalsy(); + expect(await fs.pathExists(path.join(projectDir, 'deploy.hook.sh'))).toBeFalsy(); + }); + + cliTest('handles file permission failures gracefully', async (workDir) => { + const templateDir = await createSingleLanguageTemplate(workDir, 'permission-test-template', 'typescript'); + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + await fs.chmod(projectDir, 0o444); + + try { + await expect(cliInit({ + ioHelper, + fromPath: templateDir, + language: 'typescript', + canUseNetwork: false, + generateOnly: true, + workDir: projectDir, + })).rejects.toThrow(); + } finally { + await fs.chmod(projectDir, 0o755); + } + }); + + cliTest('handles relative vs absolute paths correctly', async (workDir) => { + const templateDir = await createSingleLanguageTemplate(workDir, 'path-test-template', 'typescript'); + const projectDir = path.join(workDir, 'my-project'); + await fs.mkdirp(projectDir); + + await cliInit({ + ioHelper, + fromPath: path.resolve(templateDir), + language: 'typescript', + canUseNetwork: false, + generateOnly: true, + workDir: projectDir, + }); + + expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + + await fs.remove(projectDir); + await fs.mkdirp(projectDir); + + const relativePath = path.relative(process.cwd(), templateDir); + await cliInit({ + ioHelper, + fromPath: relativePath, + language: 'typescript', + canUseNetwork: false, + generateOnly: true, + workDir: projectDir, + }); + + expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + }); }); test('when no version number is present (e.g., local development), the v2 templates are chosen by default', async () => { From 23dec990e8309327219fa6076a134881afb95780 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Fri, 22 Aug 2025 13:48:50 -0400 Subject: [PATCH 27/38] chore: update test for unstable flag --- packages/aws-cdk/test/commands/init.test.ts | 38 ++++++++------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index e6ff90dee..6d685939a 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -535,20 +535,18 @@ describe('constructs version', () => { const { promisify } = await import('util'); const execAsync = promisify(exec); - const projectDir = path.join(workDir, 'my-project'); - await fs.mkdirp(projectDir); - const cdkBin = path.join(__dirname, '..', '..', 'bin', 'cdk'); // Test that unstable flags are accepted during actual init const { stderr } = await execAsync(`node ${cdkBin} init app --language typescript --unstable feature1 --unstable feature2 --generate-only`, { - cwd: projectDir, + cwd: workDir, env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' } }); // Should complete without error expect(stderr).not.toContain('error'); - expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + expect(await fs.pathExists(path.join(workDir, 'cdk.json'))).toBeTruthy(); + expect(await fs.pathExists(path.join(workDir, 'bin'))).toBeTruthy(); }); cliTest('conflict between lib-version and from-path is enforced', async (workDir) => { @@ -557,48 +555,40 @@ describe('constructs version', () => { const execAsync = promisify(exec); const templateDir = await createSingleLanguageTemplate(workDir, 'conflict-test', 'typescript'); - const projectDir = path.join(workDir, 'my-project'); - await fs.mkdirp(projectDir); const cdkBin = path.join(__dirname, '..', '..', 'bin', 'cdk'); // Test that using both flags together causes an error await expect(execAsync(`node ${cdkBin} init app --language typescript --lib-version 2.0.0 --from-path ${templateDir} --generate-only`, { - cwd: projectDir, + cwd: workDir, env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' } })).rejects.toThrow(); }); cliTest('template-path implies from-path validation works', async (workDir) => { - const { exec } = await import('child_process'); - const { promisify } = await import('util'); - const execAsync = promisify(exec); - // Test that the implication is properly configured const { makeConfig } = await import('../../lib/cli/cli-config'); const config = await makeConfig(); expect(config.commands.init.implies).toEqual({ 'template-path': 'from-path' }); + // Test functional behavior: template-path works when from-path is provided const repoDir = await createMultiTemplateRepository(workDir, [ { name: 'implies-test', languages: ['typescript'] }, ]); const projectDir = path.join(workDir, 'my-project'); await fs.mkdirp(projectDir); - const cdkBin = path.join(__dirname, '..', '..', 'bin', 'cdk'); - // Test CLI enforcement: template-path without from-path should fail - await expect(execAsync(`node ${cdkBin} init app --language typescript --template-path implies-test --generate-only`, { - cwd: projectDir, - env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' } - })).rejects.toThrow(); - - // Test success case: template-path WITH from-path should work - const { stderr } = await execAsync(`node ${cdkBin} init app --language typescript --from-path ${repoDir} --template-path implies-test --generate-only`, { - cwd: projectDir, - env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' } + // This should work - using template-path WITH from-path (satisfies implication) + await cliInit({ + ioHelper, + fromPath: repoDir, + templatePath: 'implies-test', + language: 'typescript', + canUseNetwork: false, + generateOnly: true, + workDir: projectDir, }); - expect(stderr).not.toContain('error'); expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); }); From 39041ecaf0fa3edc82e5a32aec160edcb6e36c69 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Fri, 22 Aug 2025 14:22:33 -0400 Subject: [PATCH 28/38] chore: update to unstable flag test --- packages/aws-cdk/lib/cli/user-input.ts | 2 +- packages/aws-cdk/test/commands/init.test.ts | 62 ++++++++++++++------- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index 72327d04a..4011119b3 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1334,7 +1334,7 @@ export interface InitOptions { readonly fromPath?: string; /** - * Path to a specific template within a multi-template repository (requires --from-path) + * Path to a specific template within a multi-template repository * * @default - undefined */ diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index 6d685939a..ab720587c 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -534,34 +534,45 @@ describe('constructs version', () => { const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); - const cdkBin = path.join(__dirname, '..', '..', 'bin', 'cdk'); - - // Test that unstable flags are accepted during actual init - const { stderr } = await execAsync(`node ${cdkBin} init app --language typescript --unstable feature1 --unstable feature2 --generate-only`, { - cwd: workDir, - env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' } + + const repoDir = await createMultiTemplateRepository(workDir, [ + { name: 'unstable-test', languages: ['typescript'] }, + ]); + const projectDir1 = path.join(workDir, 'project-without-unstable'); + const projectDir2 = path.join(workDir, 'project-with-unstable'); + await fs.mkdirp(projectDir1); + await fs.mkdirp(projectDir2); + + // Test that template-path fails WITHOUT --unstable=init flag + await expect(execAsync(`node ${cdkBin} init --from-path ${repoDir} --template-path unstable-test --language typescript --generate-only`, { + cwd: projectDir1, + env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' }, + })).rejects.toThrow(); + + // Test that template-path succeeds WITH --unstable=init flag + const { stderr } = await execAsync(`node ${cdkBin} init --from-path ${repoDir} --template-path unstable-test --language typescript --unstable init --generate-only`, { + cwd: projectDir2, + env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' }, }); - - // Should complete without error + expect(stderr).not.toContain('error'); - expect(await fs.pathExists(path.join(workDir, 'cdk.json'))).toBeTruthy(); - expect(await fs.pathExists(path.join(workDir, 'bin'))).toBeTruthy(); + expect(await fs.pathExists(path.join(projectDir2, 'app.ts'))).toBeTruthy(); }); cliTest('conflict between lib-version and from-path is enforced', async (workDir) => { const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); - + const templateDir = await createSingleLanguageTemplate(workDir, 'conflict-test', 'typescript'); - + const cdkBin = path.join(__dirname, '..', '..', 'bin', 'cdk'); - + // Test that using both flags together causes an error await expect(execAsync(`node ${cdkBin} init app --language typescript --lib-version 2.0.0 --from-path ${templateDir} --generate-only`, { cwd: workDir, - env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' } + env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' }, })).rejects.toThrow(); }); @@ -570,15 +581,15 @@ describe('constructs version', () => { const { makeConfig } = await import('../../lib/cli/cli-config'); const config = await makeConfig(); expect(config.commands.init.implies).toEqual({ 'template-path': 'from-path' }); - - // Test functional behavior: template-path works when from-path is provided + + // Test that template-path functionality requires from-path to work const repoDir = await createMultiTemplateRepository(workDir, [ { name: 'implies-test', languages: ['typescript'] }, ]); const projectDir = path.join(workDir, 'my-project'); await fs.mkdirp(projectDir); - - // This should work - using template-path WITH from-path (satisfies implication) + + // Test that template-path works when from-path is provided (satisfying the implication) await cliInit({ ioHelper, fromPath: repoDir, @@ -588,8 +599,21 @@ describe('constructs version', () => { generateOnly: true, workDir: projectDir, }); - + expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + + // Test that template-path without from-path fails at the function level + const projectDir2 = path.join(workDir, 'my-project-2'); + await fs.mkdirp(projectDir2); + + await expect(cliInit({ + ioHelper, + templatePath: 'implies-test', // template-path without from-path + language: 'typescript', + canUseNetwork: false, + generateOnly: true, + workDir: projectDir2, + })).rejects.toThrow(); // Should fail because template-path requires from-path }); cliTest('hook files are ignored during template copy', async (workDir) => { From 019f38b271b1c7694895c3d08042cb912f64229a Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 22 Aug 2025 20:02:56 +0000 Subject: [PATCH 29/38] chore: self mutation Signed-off-by: github-actions --- packages/aws-cdk/lib/cli/parse-command-line-arguments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts index 3bed8359e..1b29c21ee 100644 --- a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts @@ -874,7 +874,7 @@ export function parseCommandLineArguments(args: Array): any { .option('template-path', { default: undefined, type: 'string', - desc: 'Path to a specific template within a multi-template repository (requires --from-path)', + desc: 'Path to a specific template within a multi-template repository', requiresArg: true, }), ) From 569128ccf36bb93f5160d1927b389963d3cb55d0 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Mon, 25 Aug 2025 13:02:04 -0400 Subject: [PATCH 30/38] chore: updated default cdk init behavior without template type or language specified, and fixed post-install for CDK languages --- packages/aws-cdk/lib/commands/init/init.ts | 144 +++++++++++++++------ 1 file changed, 106 insertions(+), 38 deletions(-) diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index 24cb94189..69b1f1253 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -84,6 +84,12 @@ export async function cliInit(options: CliInitOptions) { const generateOnly = options.generateOnly ?? false; const workDir = options.workDir ?? process.cwd(); + // Show available templates if no type and no language provided (main branch logic) + if (!options.fromPath && !options.type && !options.language) { + await printAvailableTemplates(ioHelper); + return; + } + // Step 1: Load template let template: InitTemplate; if (options.fromPath) { @@ -93,7 +99,7 @@ export async function cliInit(options: CliInitOptions) { } // Step 2: Resolve language - const language = await resolveLanguage(ioHelper, template, options.language); + const language = await resolveLanguage(ioHelper, template, options.language, options.type); // Step 3: Initialize project following standard process await initializeProject( @@ -170,27 +176,30 @@ async function loadBuiltinTemplate(ioHelper: IoHelper, type?: string, language?: * @param ioHelper - IO helper for user interaction * @param template - The template to resolve language for * @param requestedLanguage - User-requested language (optional) + * @param type - The template type name for messages * @default undefined * @returns Promise resolving to the selected language */ -async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, requestedLanguage?: string): Promise { - if (requestedLanguage) { - return requestedLanguage; - } - if (template.languages.length === 1) { - const templateLanguage = template.languages[0]; - // Only show auto-detection message for built-in templates - if (template.templateType !== TemplateType.CUSTOM) { - await ioHelper.defaults.warn( - `No --language was provided, but '${template.name}' supports only '${templateLanguage}', so defaulting to --language=${templateLanguage}`, - ); +async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, requestedLanguage?: string, type?: string): Promise { + return await (async () => { + if (requestedLanguage) { + return requestedLanguage; } - return templateLanguage; - } - await ioHelper.defaults.info( - `Available languages for ${chalk.green(template.name)}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`, - ); - throw new ToolkitError('No language was selected'); + if (template.languages.length === 1) { + const templateLanguage = template.languages[0]; + // Only show auto-detection message for built-in templates + if (template.templateType !== TemplateType.CUSTOM) { + await ioHelper.defaults.warn( + `No --language was provided, but '${type || template.name}' supports only '${templateLanguage}', so defaulting to --language=${templateLanguage}`, + ); + } + return templateLanguage; + } + await ioHelper.defaults.info( + `Available languages for ${chalk.green(type || template.name)}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`, + ); + throw new ToolkitError('No language was selected'); + })(); } /** @@ -683,9 +692,9 @@ async function postInstall(ioHelper: IoHelper, language: string, canUseNetwork: case 'go': return postInstallGo(ioHelper, canUseNetwork, workDir); case 'csharp': + return postInstallCSharp(ioHelper, canUseNetwork, workDir); case 'fsharp': - // .NET languages don't need post-install steps for custom templates - return; + return postInstallFSharp(ioHelper, canUseNetwork, workDir); } } @@ -710,30 +719,66 @@ async function postInstallTypescript(ioHelper: IoHelper, canUseNetwork: boolean, } async function postInstallJava(ioHelper: IoHelper, canUseNetwork: boolean, cwd: string) { - const mvnPackageWarning = "Please run 'mvn package'!"; - if (!canUseNetwork) { - await ioHelper.defaults.warn(mvnPackageWarning); - return; - } + // Check if this is a Gradle or Maven project + const hasGradleBuild = await fs.pathExists(path.join(cwd, 'build.gradle')); + const hasMavenPom = await fs.pathExists(path.join(cwd, 'pom.xml')); + + if (hasGradleBuild) { + // Gradle project + const gradleWarning = "Please run './gradlew build'!"; + if (!canUseNetwork) { + await ioHelper.defaults.warn(gradleWarning); + return; + } - await ioHelper.defaults.info("Executing 'mvn package'"); - try { - await execute(ioHelper, 'mvn', ['package'], { cwd }); - } catch { - await ioHelper.defaults.warn('Unable to package compiled code as JAR'); - await ioHelper.defaults.warn(mvnPackageWarning); + await ioHelper.defaults.info("Executing './gradlew build'"); + try { + await execute(ioHelper, './gradlew', ['build'], { cwd }); + } catch { + await ioHelper.defaults.warn('Unable to build Gradle project'); + await ioHelper.defaults.warn(gradleWarning); + } + } else if (hasMavenPom) { + // Maven project + const mvnPackageWarning = "Please run 'mvn package'!"; + if (!canUseNetwork) { + await ioHelper.defaults.warn(mvnPackageWarning); + return; + } + + await ioHelper.defaults.info("Executing 'mvn package'"); + try { + await execute(ioHelper, 'mvn', ['package'], { cwd }); + } catch { + await ioHelper.defaults.warn('Unable to package compiled code as JAR'); + await ioHelper.defaults.warn(mvnPackageWarning); + } + } else { + // No recognized build file + await ioHelper.defaults.warn('No build.gradle or pom.xml found. Please set up your build system manually.'); } } async function postInstallPython(ioHelper: IoHelper, cwd: string) { const python = pythonExecutable(); - await ioHelper.defaults.warn(`Please run '${python} -m venv .venv'!`); - await ioHelper.defaults.info(`Executing ${chalk.green('Creating virtualenv...')}`); - try { - await execute(ioHelper, python, ['-m venv', '.venv'], { cwd }); - } catch { - await ioHelper.defaults.warn('Unable to create virtualenv automatically'); - await ioHelper.defaults.warn(`Please run '${python} -m venv .venv'!`); + + // Check if requirements.txt exists + const hasRequirements = await fs.pathExists(path.join(cwd, 'requirements.txt')); + + if (hasRequirements) { + await ioHelper.defaults.info(`Executing ${chalk.green('Creating virtualenv...')}`); + try { + await execute(ioHelper, python, ['-m', 'venv', '.venv'], { cwd }); + await ioHelper.defaults.info(`Executing ${chalk.green('Installing dependencies...')}`); + // Install dependencies in the virtual environment + const pipPath = process.platform === 'win32' ? '.venv\\Scripts\\pip' : '.venv/bin/pip'; + await execute(ioHelper, pipPath, ['install', '-r', 'requirements.txt'], { cwd }); + } catch { + await ioHelper.defaults.warn('Unable to create virtualenv or install dependencies automatically'); + await ioHelper.defaults.warn(`Please run '${python} -m venv .venv && .venv/bin/pip install -r requirements.txt'!`); + } + } else { + await ioHelper.defaults.warn('No requirements.txt found. Please set up your Python environment manually.'); } } @@ -751,6 +796,29 @@ async function postInstallGo(ioHelper: IoHelper, canUseNetwork: boolean, cwd: st } } +async function postInstallCSharp(ioHelper: IoHelper, canUseNetwork: boolean, cwd: string) { + const dotnetWarning = "Please run 'dotnet restore && dotnet build'!"; + if (!canUseNetwork) { + await ioHelper.defaults.warn(dotnetWarning); + return; + } + + await ioHelper.defaults.info(`Executing ${chalk.green('dotnet restore')}...`); + try { + await execute(ioHelper, 'dotnet', ['restore'], { cwd }); + await ioHelper.defaults.info(`Executing ${chalk.green('dotnet build')}...`); + await execute(ioHelper, 'dotnet', ['build'], { cwd }); + } catch (e: any) { + await ioHelper.defaults.warn('Unable to restore/build .NET project: ' + formatErrorMessage(e)); + await ioHelper.defaults.warn(dotnetWarning); + } +} + +async function postInstallFSharp(ioHelper: IoHelper, canUseNetwork: boolean, cwd: string) { + // F# uses the same build system as C# + return postInstallCSharp(ioHelper, canUseNetwork, cwd); +} + /** * @param dir - a directory to be checked * @returns true if ``dir`` is within a git repository. From aeef412fea392366c0c708dfeb42627e4dbe67eb Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 25 Aug 2025 17:10:56 +0000 Subject: [PATCH 31/38] chore: self mutation Signed-off-by: github-actions --- packages/aws-cdk/lib/commands/init/init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index 69b1f1253..61666ddc0 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -181,7 +181,7 @@ async function loadBuiltinTemplate(ioHelper: IoHelper, type?: string, language?: * @returns Promise resolving to the selected language */ async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, requestedLanguage?: string, type?: string): Promise { - return await (async () => { + return (async () => { if (requestedLanguage) { return requestedLanguage; } From b7d8408a9c95c690b7bc39b8c69e1f98c5b54a20 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Tue, 26 Aug 2025 15:06:40 -0400 Subject: [PATCH 32/38] chore: add test for cdk init --language=[language] --- packages/aws-cdk/test/commands/init.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index 904cbeebd..902a460c0 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -66,6 +66,22 @@ describe('constructs version', () => { })).rejects.toThrow(/No language/); }); + cliTest('specifying language without template type creates default app template with specified language', async (workDir) => { + await cliInit({ + ioHelper, + language: 'python', + canUseNetwork: false, + generateOnly: true, + workDir, + }); + + // Verify that an app template was created with the specified language (Python) + expect(await fs.pathExists(path.join(workDir, 'requirements.txt'))).toBeTruthy(); + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining('Applying project template app for python'), + })); + }); + cliTest('create a TypeScript app project', async (workDir) => { await cliInit({ ioHelper, From e00e12dbdfe1cb1cbe4ed97f51599eb9f6628466 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Tue, 26 Aug 2025 15:32:02 -0400 Subject: [PATCH 33/38] modified test for cdk init --language=[language] --- packages/aws-cdk/test/commands/init.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index 902a460c0..f1d675f3c 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -66,20 +66,22 @@ describe('constructs version', () => { })).rejects.toThrow(/No language/); }); - cliTest('specifying language without template type creates default app template with specified language', async (workDir) => { + cliTest('cdk init --language defaults to app template with specified language', async (workDir) => { await cliInit({ ioHelper, - language: 'python', + language: 'typescript', canUseNetwork: false, generateOnly: true, workDir, }); - // Verify that an app template was created with the specified language (Python) - expect(await fs.pathExists(path.join(workDir, 'requirements.txt'))).toBeTruthy(); - expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ - message: expect.stringContaining('Applying project template app for python'), - })); + // Verify app template structure was created + expect(await fs.pathExists(path.join(workDir, 'package.json'))).toBeTruthy(); + expect(await fs.pathExists(path.join(workDir, 'bin'))).toBeTruthy(); + + // Verify it uses the specified language (TypeScript) + const binFiles = await fs.readdir(path.join(workDir, 'bin')); + expect(binFiles.some(file => file.endsWith('.ts'))).toBeTruthy(); }); cliTest('create a TypeScript app project', async (workDir) => { From 2d54fe8481829fa219bc768c600e0e63fbdd47d2 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Tue, 26 Aug 2025 15:58:52 -0400 Subject: [PATCH 34/38] chore: update test for cdk init --language=[language] --- packages/aws-cdk/test/commands/init.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index f1d675f3c..247cad552 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -78,7 +78,7 @@ describe('constructs version', () => { // Verify app template structure was created expect(await fs.pathExists(path.join(workDir, 'package.json'))).toBeTruthy(); expect(await fs.pathExists(path.join(workDir, 'bin'))).toBeTruthy(); - + // Verify it uses the specified language (TypeScript) const binFiles = await fs.readdir(path.join(workDir, 'bin')); expect(binFiles.some(file => file.endsWith('.ts'))).toBeTruthy(); From e49fe62bef7157def68da505df0af68367fd076c Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Wed, 27 Aug 2025 02:48:25 -0400 Subject: [PATCH 35/38] WIP: git repository template support - save progress before VSCode restart --- .../user-input-gen/lib/yargs-types.ts | 1 + packages/aws-cdk/lib/cli/cli-config.ts | 5 +- packages/aws-cdk/lib/commands/init/init.ts | 233 +++++++++++++++--- 3 files changed, 202 insertions(+), 37 deletions(-) diff --git a/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts b/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts index a19fff94e..d6518222c 100644 --- a/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts +++ b/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts @@ -8,6 +8,7 @@ interface YargsCommand { export interface CliAction extends YargsCommand { options?: { [optionName: string]: CliOption }; implies?: { [key: string]: string }; + //check?: string; } interface YargsArg { diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 873749f25..7d50acb72 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -401,10 +401,11 @@ export async function makeConfig(): Promise { 'list': { type: 'boolean', desc: 'List the available templates' }, 'generate-only': { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }, 'lib-version': { type: 'string', alias: 'V', default: undefined, desc: 'The version of the CDK library (aws-cdk-lib) to initialize built-in templates with. Defaults to the version that was current when this CLI was built.' }, - 'from-path': { type: 'string', desc: 'Path to a local custom template directory or multi-template repository', requiresArg: true, conflicts: ['lib-version'] }, + 'from-path': { type: 'string', desc: 'Path to a local custom template directory or multi-template repository', requiresArg: true, conflicts: ['lib-version', 'from-git-url'] }, + 'from-git-url': { type: 'string', desc: 'Git repository URL to clone custom template from', requiresArg: true, conflicts: ['lib-version', 'from-path'] }, 'template-path': { type: 'string', desc: 'Path to a specific template within a multi-template repository', requiresArg: true }, }, - implies: { 'template-path': 'from-path' }, + //implies: { 'template-path': 'from-path' }, }, 'migrate': { description: 'Migrate existing AWS resources into a CDK app', diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index 61666ddc0..1bdc83e9b 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -1,4 +1,5 @@ import * as childProcess from 'child_process'; +import * as os from 'os'; import * as path from 'path'; import { ToolkitError } from '@aws-cdk/toolkit-lib'; import * as chalk from 'chalk'; @@ -66,10 +67,15 @@ export interface CliInitOptions { readonly fromPath?: string; /** - * Path to a specific template within a multi-template repository. - * This parameter requires --from-path to be specified. + * Git repository URL to clone and use as template source * @default undefined */ + readonly fromGitUrl?: string; + + /** + * Path to a specific template within a multi-template repository + * @default - Optional/auto-detected if repository contains only one template, otherwise required + */ readonly templatePath?: string; readonly ioHelper: IoHelper; @@ -85,27 +91,75 @@ export async function cliInit(options: CliInitOptions) { const workDir = options.workDir ?? process.cwd(); // Show available templates if no type and no language provided (main branch logic) - if (!options.fromPath && !options.type && !options.language) { + if (!options.fromPath && !options.fromGitUrl && !options.type && !options.language) { await printAvailableTemplates(ioHelper); return; } - // Step 1: Load template - let template: InitTemplate; - if (options.fromPath) { - template = await loadLocalTemplate(options.fromPath, options.templatePath); - } else { - template = await loadBuiltinTemplate(ioHelper, options.type, options.language); + // Validate mutually exclusive options + const customSources = [options.fromPath, options.fromGitUrl].filter(Boolean); + if (customSources.length > 1) { + throw new ToolkitError('Only one custom template source can be specified at a time'); + } + + // Handle custom template sources if referenced in command + if (customSources.length > 0) { + let template: InitTemplate; + try { + if (options.fromPath) { + template = await loadLocalTemplate(options.fromPath, options.templatePath); + } else if (options.fromGitUrl) { + template = await loadGitTemplate(options.fromGitUrl, options.templatePath); + } else { + throw new ToolkitError('Invalid template source configuration'); + } + } catch (error: any) { + if (error instanceof ToolkitError) { + throw error; + } + throw new ToolkitError(`Failed to load custom template: ${error.message}`); + } + + const language = await resolveLanguage(ioHelper, template, options.language); + try { + await initializeProject( + ioHelper, + template, + language, + canUseNetwork, + generateOnly, + workDir, + options.stackName, + options.migrate, + options.libVersion, + ); + } finally { + // Clean up temporary template files (only git templates have cleanup) + if (template.cleanup) { + await template.cleanup(); + } + } + return; } - // Step 2: Resolve language - const language = await resolveLanguage(ioHelper, template, options.language, options.type); + // If not custom template, handle built-in templates + const template = await loadBuiltinTemplate(ioHelper, options.type, options.language); + + if (!options.language && template.languages.length === 1) { + const language = template.languages[0]; + await ioHelper.defaults.warn( + `No --language was provided, but '${options.type || 'default'}' supports only '${language}', so defaulting to --language=${language}`, + ); + } + if (!options.language) { + await ioHelper.defaults.info(`Available languages for ${chalk.green(options.type || 'default')}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`); + throw new ToolkitError('No language was selected'); + } - // Step 3: Initialize project following standard process await initializeProject( ioHelper, template, - language, + options.language, canUseNetwork, generateOnly, workDir, @@ -156,6 +210,29 @@ async function loadLocalTemplate(fromPath: string, templatePath?: string): Promi } } +/** + * Load a template from a Git repository URL + * @param gitUrl - Git repository URL to clone + * @param templatePath - Optional path to a specific template within the repository + * @returns Promise resolving to the loaded InitTemplate + */ +async function loadGitTemplate(gitUrl: string, templatePath?: string): Promise { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-init-git-')); + + try { + // Clone repository (defaults to main/master branch) + await executeGitCommand('git', ['clone', '--depth', '1', gitUrl, tempDir]); + + // Loading template from tempDir using local custom template logic + return await loadLocalTemplate(tempDir, templatePath); + } catch (error: any) { + const displayPath = templatePath ? `${gitUrl}/${templatePath}` : gitUrl; + throw new ToolkitError(`Failed to load template from Git repository: ${displayPath}. ${error.message}`); + } finally { + await fs.remove(tempDir).catch(() => {}); + } +} + /** * Load a built-in template by name */ @@ -176,30 +253,51 @@ async function loadBuiltinTemplate(ioHelper: IoHelper, type?: string, language?: * @param ioHelper - IO helper for user interaction * @param template - The template to resolve language for * @param requestedLanguage - User-requested language (optional) - * @param type - The template type name for messages * @default undefined * @returns Promise resolving to the selected language */ -async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, requestedLanguage?: string, type?: string): Promise { - return (async () => { - if (requestedLanguage) { - return requestedLanguage; - } - if (template.languages.length === 1) { - const templateLanguage = template.languages[0]; - // Only show auto-detection message for built-in templates - if (template.templateType !== TemplateType.CUSTOM) { - await ioHelper.defaults.warn( - `No --language was provided, but '${type || template.name}' supports only '${templateLanguage}', so defaulting to --language=${templateLanguage}`, - ); - } - return templateLanguage; +async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, requestedLanguage?: string): Promise { + // Validate inputs + if (!template || !template.languages || !Array.isArray(template.languages)) { + throw new ToolkitError('Invalid template: missing language information'); + } + + if (template.languages.length === 0) { + throw new ToolkitError('Template does not support any languages'); + } + + let language = requestedLanguage; + + // Validate requested language if provided + if (language) { + if (typeof language !== 'string' || language.trim() === '') { + throw new ToolkitError('Language must be a non-empty string'); } - await ioHelper.defaults.info( - `Available languages for ${chalk.green(type || template.name)}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`, - ); - throw new ToolkitError('No language was selected'); - })(); + language = language.trim().toLowerCase(); + + if (!template.languages.includes(language)) { + await ioHelper.defaults.info(`Available languages for ${chalk.green(template.name)}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`); + throw new ToolkitError(`Language '${language}' is not supported by this template`); + } + + return language; + } + + // Auto-detect language for single-language templates + if (template.languages.length === 1) { + language = template.languages[0]; + // Only show auto-detection message for built-in templates + if (template.templateType !== TemplateType.CUSTOM) { + await ioHelper.defaults.info( + `No --language was provided, but '${template.name}' supports only '${language}', so defaulting to --language=${language}`, + ); + } + return language; + } + + // Multiple languages available, user must choose + await ioHelper.defaults.info(`Available languages for ${chalk.green(template.name)}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`); + throw new ToolkitError('No language was selected. Please specify --language with one of the supported languages'); } /** @@ -326,7 +424,7 @@ export class InitTemplate { const basePath = path.join(templatesDir, name); const languages = await listDirectory(basePath); const initInfo = await fs.readJson(path.join(basePath, INFO_DOT_JSON)); - return new InitTemplate(basePath, name, languages, initInfo, TemplateType.BUILT_IN); + return new InitTemplate(basePath, name, languages, initInfo, TemplateType.BUILT_IN, false); } public static async fromPath(templatePath: string) { @@ -339,12 +437,26 @@ export class InitTemplate { const languages = await getLanguageDirectories(basePath); const name = path.basename(basePath); - return new InitTemplate(basePath, name, languages, null, TemplateType.CUSTOM); + return new InitTemplate(basePath, name, languages, null, TemplateType.CUSTOM, false); + } + + public static async fromTemporaryPath(templatePath: string, customName?: string) { + const basePath = path.resolve(templatePath); + + if (!await fs.pathExists(basePath)) { + throw new ToolkitError(`Template path does not exist: ${basePath}`); + } + + const languages = await getLanguageDirectories(basePath); + const name = customName || path.basename(basePath); + + return new InitTemplate(basePath, name, languages, null, TemplateType.CUSTOM, true); } public readonly description?: string; public readonly aliases = new Set(); public readonly templateType: TemplateType; + private readonly isTemporary: boolean; constructor( private readonly basePath: string, @@ -352,8 +464,10 @@ export class InitTemplate { public readonly languages: string[], initInfo: TemplateInitInfo | null, templateType: TemplateType, + isTemporary: boolean, ) { this.templateType = templateType; + this.isTemporary = isTemporary; // Only built-in templates have descriptions and aliases from info.json if (templateType === TemplateType.BUILT_IN && initInfo) { this.description = initInfo.description; @@ -498,6 +612,15 @@ export class InitTemplate { await fs.writeJson(cdkJson, config, { spaces: 2 }); } + + /** + * Clean up temporary template directory if this template was loaded from a temporary source + */ + public async cleanup() { + if (this.isTemporary) { + await fs.remove(this.basePath).catch(() => {}); + } + } } export function expandPlaceholders(template: string, language: string, project: ProjectInfo) { @@ -843,6 +966,46 @@ function isRoot(dir: string) { return path.dirname(dir) === dir; } +/** + * Execute a Git command with error handling + * @param cmd - Command to execute (should be 'git') + * @param args - Command arguments + * @returns Promise resolving to stdout + */ +async function executeGitCommand(cmd: string, args: string[]): Promise { + // Security: Only allow git commands + if (cmd !== 'git') { + throw new ToolkitError('Only git commands are allowed'); + } + + // Security: Validate git arguments to prevent command injection + const allowedGitCommands = ['clone', 'checkout', 'branch', 'tag']; + if (args.length === 0 || !allowedGitCommands.includes(args[0])) { + throw new ToolkitError(`Git command not allowed: ${args[0]}`); + } + + const child = childProcess.spawn(cmd, args, { + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let stdout = ''; + let stderr = ''; + + child.stdout?.on('data', (chunk) => (stdout += chunk.toString())); + child.stderr?.on('data', (chunk) => (stderr += chunk.toString())); + + return new Promise((resolve, reject) => { + child.once('error', (err) => reject(new ToolkitError(`Failed to execute git command: ${err.message}`))); + child.once('exit', (status) => { + if (status === 0) { + resolve(stdout); + } else { + reject(new ToolkitError(`Git command failed: ${stderr || stdout}`)); + } + }); + }); +} + /** * Executes `command`. STDERR is emitted in real-time. * From c02abcbc59c21bf86366c8391ce34e9959c6f81e Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Thu, 28 Aug 2025 13:19:54 -0400 Subject: [PATCH 36/38] feat(cli): add support for pulling custom templates from git repositories --- .../user-input-gen/lib/yargs-types.ts | 2 +- packages/aws-cdk/lib/cli/cli-config.ts | 2 +- .../aws-cdk/lib/cli/cli-type-registry.json | 15 +- packages/aws-cdk/lib/cli/cli.ts | 3 +- packages/aws-cdk/lib/commands/init/init.ts | 233 ++++++------------ packages/aws-cdk/test/commands/init.test.ts | 21 +- 6 files changed, 97 insertions(+), 179 deletions(-) diff --git a/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts b/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts index d6518222c..2d5ed4c98 100644 --- a/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts +++ b/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts @@ -8,7 +8,7 @@ interface YargsCommand { export interface CliAction extends YargsCommand { options?: { [optionName: string]: CliOption }; implies?: { [key: string]: string }; - //check?: string; + // check?: string; } interface YargsArg { diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 7d50acb72..fbb6f3336 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -405,7 +405,7 @@ export async function makeConfig(): Promise { 'from-git-url': { type: 'string', desc: 'Git repository URL to clone custom template from', requiresArg: true, conflicts: ['lib-version', 'from-path'] }, 'template-path': { type: 'string', desc: 'Path to a specific template within a multi-template repository', requiresArg: true }, }, - //implies: { 'template-path': 'from-path' }, + }, 'migrate': { description: 'Migrate existing AWS resources into a CDK app', diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index f78fd2fc3..799c3ba8b 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -872,7 +872,17 @@ "desc": "Path to a local custom template directory or multi-template repository", "requiresArg": true, "conflicts": [ - "lib-version" + "lib-version", + "from-git-url" + ] + }, + "from-git-url": { + "type": "string", + "desc": "Git repository URL to clone custom template from", + "requiresArg": true, + "conflicts": [ + "lib-version", + "from-path" ] }, "template-path": { @@ -880,9 +890,6 @@ "desc": "Path to a specific template within a multi-template repository", "requiresArg": true } - }, - "implies": { - "template-path": "from-path" } }, "migrate": { diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index e1ba9710a..1eefead91 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -517,7 +517,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise 1) { - throw new ToolkitError('Only one custom template source can be specified at a time'); - } + // Step 1: Load template + let template: InitTemplate; + let gitTempDir: string | undefined; - // Handle custom template sources if referenced in command - if (customSources.length > 0) { - let template: InitTemplate; - try { - if (options.fromPath) { - template = await loadLocalTemplate(options.fromPath, options.templatePath); - } else if (options.fromGitUrl) { - template = await loadGitTemplate(options.fromGitUrl, options.templatePath); - } else { - throw new ToolkitError('Invalid template source configuration'); - } - } catch (error: any) { - if (error instanceof ToolkitError) { - throw error; - } - throw new ToolkitError(`Failed to load custom template: ${error.message}`); - } - - const language = await resolveLanguage(ioHelper, template, options.language); - try { - await initializeProject( - ioHelper, - template, - language, - canUseNetwork, - generateOnly, - workDir, - options.stackName, - options.migrate, - options.libVersion, - ); - } finally { - // Clean up temporary template files (only git templates have cleanup) - if (template.cleanup) { - await template.cleanup(); - } - } - return; + if (options.fromPath) { + template = await loadLocalTemplate(options.fromPath, options.templatePath); + } else if (options.fromGitUrl) { + const result = await loadGitTemplate(options.fromGitUrl, options.templatePath); + template = result.template; + gitTempDir = result.tempDir; + } else { + template = await loadBuiltinTemplate(ioHelper, options.type, options.language); } - // If not custom template, handle built-in templates - const template = await loadBuiltinTemplate(ioHelper, options.type, options.language); + // Step 2: Resolve language + const language = await resolveLanguage(ioHelper, template, options.language, options.type); - if (!options.language && template.languages.length === 1) { - const language = template.languages[0]; - await ioHelper.defaults.warn( - `No --language was provided, but '${options.type || 'default'}' supports only '${language}', so defaulting to --language=${language}`, + // Step 3: Initialize project following standard process + try { + await initializeProject( + ioHelper, + template, + language, + canUseNetwork, + generateOnly, + workDir, + options.stackName, + options.migrate, + options.libVersion, ); + } finally { + // Clean up git temp directory if it exists + if (gitTempDir) { + await fs.remove(gitTempDir).catch(() => { + }); + } } - if (!options.language) { - await ioHelper.defaults.info(`Available languages for ${chalk.green(options.type || 'default')}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`); - throw new ToolkitError('No language was selected'); - } - - await initializeProject( - ioHelper, - template, - options.language, - canUseNetwork, - generateOnly, - workDir, - options.stackName, - options.migrate, - options.libVersion, - ); } /** @@ -214,22 +186,27 @@ async function loadLocalTemplate(fromPath: string, templatePath?: string): Promi * Load a template from a Git repository URL * @param gitUrl - Git repository URL to clone * @param templatePath - Optional path to a specific template within the repository - * @returns Promise resolving to the loaded InitTemplate + * @returns Promise resolving to the loaded InitTemplate and tempDir for cleanup */ -async function loadGitTemplate(gitUrl: string, templatePath?: string): Promise { +async function loadGitTemplate(gitUrl: string, templatePath?: string): Promise<{ template: InitTemplate; tempDir: string }> { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-init-git-')); try { // Clone repository (defaults to main/master branch) await executeGitCommand('git', ['clone', '--depth', '1', gitUrl, tempDir]); - // Loading template from tempDir using local custom template logic - return await loadLocalTemplate(tempDir, templatePath); + // Now that we have the repository locally, use the same logic as loadLocalTemplate + const template = await loadLocalTemplate(tempDir, templatePath); + + return { template, tempDir }; } catch (error: any) { + await fs.remove(tempDir).catch(() => { + }); + if (error.message?.includes('not found') || error.code === 'ENOENT') { + throw new ToolkitError('Git is not installed or not available in PATH. Please install Git to use --from-git-url.'); + } const displayPath = templatePath ? `${gitUrl}/${templatePath}` : gitUrl; throw new ToolkitError(`Failed to load template from Git repository: ${displayPath}. ${error.message}`); - } finally { - await fs.remove(tempDir).catch(() => {}); } } @@ -253,51 +230,30 @@ async function loadBuiltinTemplate(ioHelper: IoHelper, type?: string, language?: * @param ioHelper - IO helper for user interaction * @param template - The template to resolve language for * @param requestedLanguage - User-requested language (optional) + * @param type - The template type name for messages * @default undefined * @returns Promise resolving to the selected language */ -async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, requestedLanguage?: string): Promise { - // Validate inputs - if (!template || !template.languages || !Array.isArray(template.languages)) { - throw new ToolkitError('Invalid template: missing language information'); - } - - if (template.languages.length === 0) { - throw new ToolkitError('Template does not support any languages'); - } - - let language = requestedLanguage; - - // Validate requested language if provided - if (language) { - if (typeof language !== 'string' || language.trim() === '') { - throw new ToolkitError('Language must be a non-empty string'); - } - language = language.trim().toLowerCase(); - - if (!template.languages.includes(language)) { - await ioHelper.defaults.info(`Available languages for ${chalk.green(template.name)}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`); - throw new ToolkitError(`Language '${language}' is not supported by this template`); - } - - return language; - } - - // Auto-detect language for single-language templates - if (template.languages.length === 1) { - language = template.languages[0]; - // Only show auto-detection message for built-in templates - if (template.templateType !== TemplateType.CUSTOM) { - await ioHelper.defaults.info( - `No --language was provided, but '${template.name}' supports only '${language}', so defaulting to --language=${language}`, - ); +async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, requestedLanguage?: string, type?: string): Promise { + return (async () => { + if (requestedLanguage) { + return requestedLanguage; + } + if (template.languages.length === 1) { + const templateLanguage = template.languages[0]; + // Only show auto-detection message for built-in templates + if (template.templateType !== TemplateType.CUSTOM) { + await ioHelper.defaults.warn( + `No --language was provided, but '${type || template.name}' supports only '${templateLanguage}', so defaulting to --language=${templateLanguage}`, + ); + } + return templateLanguage; } - return language; - } - - // Multiple languages available, user must choose - await ioHelper.defaults.info(`Available languages for ${chalk.green(template.name)}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`); - throw new ToolkitError('No language was selected. Please specify --language with one of the supported languages'); + await ioHelper.defaults.info( + `Available languages for ${chalk.green(type || template.name)}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`, + ); + throw new ToolkitError('No language was selected'); + })(); } /** @@ -424,7 +380,7 @@ export class InitTemplate { const basePath = path.join(templatesDir, name); const languages = await listDirectory(basePath); const initInfo = await fs.readJson(path.join(basePath, INFO_DOT_JSON)); - return new InitTemplate(basePath, name, languages, initInfo, TemplateType.BUILT_IN, false); + return new InitTemplate(basePath, name, languages, initInfo, TemplateType.BUILT_IN); } public static async fromPath(templatePath: string) { @@ -437,26 +393,12 @@ export class InitTemplate { const languages = await getLanguageDirectories(basePath); const name = path.basename(basePath); - return new InitTemplate(basePath, name, languages, null, TemplateType.CUSTOM, false); - } - - public static async fromTemporaryPath(templatePath: string, customName?: string) { - const basePath = path.resolve(templatePath); - - if (!await fs.pathExists(basePath)) { - throw new ToolkitError(`Template path does not exist: ${basePath}`); - } - - const languages = await getLanguageDirectories(basePath); - const name = customName || path.basename(basePath); - - return new InitTemplate(basePath, name, languages, null, TemplateType.CUSTOM, true); + return new InitTemplate(basePath, name, languages, null, TemplateType.CUSTOM); } public readonly description?: string; public readonly aliases = new Set(); public readonly templateType: TemplateType; - private readonly isTemporary: boolean; constructor( private readonly basePath: string, @@ -464,10 +406,8 @@ export class InitTemplate { public readonly languages: string[], initInfo: TemplateInitInfo | null, templateType: TemplateType, - isTemporary: boolean, ) { this.templateType = templateType; - this.isTemporary = isTemporary; // Only built-in templates have descriptions and aliases from info.json if (templateType === TemplateType.BUILT_IN && initInfo) { this.description = initInfo.description; @@ -612,15 +552,6 @@ export class InitTemplate { await fs.writeJson(cdkJson, config, { spaces: 2 }); } - - /** - * Clean up temporary template directory if this template was loaded from a temporary source - */ - public async cleanup() { - if (this.isTemporary) { - await fs.remove(this.basePath).catch(() => {}); - } - } } export function expandPlaceholders(template: string, language: string, project: ProjectInfo) { @@ -968,34 +899,24 @@ function isRoot(dir: string) { /** * Execute a Git command with error handling - * @param cmd - Command to execute (should be 'git') + * @param cmd - Command to execute * @param args - Command arguments * @returns Promise resolving to stdout */ async function executeGitCommand(cmd: string, args: string[]): Promise { - // Security: Only allow git commands - if (cmd !== 'git') { - throw new ToolkitError('Only git commands are allowed'); - } - - // Security: Validate git arguments to prevent command injection - const allowedGitCommands = ['clone', 'checkout', 'branch', 'tag']; - if (args.length === 0 || !allowedGitCommands.includes(args[0])) { - throw new ToolkitError(`Git command not allowed: ${args[0]}`); - } - const child = childProcess.spawn(cmd, args, { + shell: true, stdio: ['ignore', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; - child.stdout?.on('data', (chunk) => (stdout += chunk.toString())); - child.stderr?.on('data', (chunk) => (stderr += chunk.toString())); + child.stdout.on('data', (chunk) => (stdout += chunk.toString())); + child.stderr.on('data', (chunk) => (stderr += chunk.toString())); return new Promise((resolve, reject) => { - child.once('error', (err) => reject(new ToolkitError(`Failed to execute git command: ${err.message}`))); + child.once('error', (err) => reject(err)); child.once('exit', (status) => { if (status === 0) { resolve(stdout); diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index 247cad552..a37bd669b 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -606,30 +606,19 @@ describe('constructs version', () => { })).rejects.toThrow(); }); - cliTest('template-path implies from-path validation works', async (workDir) => { - // Test that the implication is properly configured - const { makeConfig } = await import('../../lib/cli/cli-config'); - const config = await makeConfig(); - expect(config.commands.init.implies).toEqual({ 'template-path': 'from-path' }); - - // Test that template-path functionality works when from-path is provided - const repoDir = await createMultiTemplateRepository(workDir, [ - { name: 'implies-test', languages: ['typescript'] }, - ]); + cliTest('template-path validation requires from-path or from-git-url', async (workDir) => { const projectDir = path.join(workDir, 'my-project'); await fs.mkdirp(projectDir); - await cliInit({ + // Test that template-path fails when used without from-path or from-git-url + await expect(cliInit({ ioHelper, - fromPath: repoDir, - templatePath: 'implies-test', + templatePath: 'some-template', language: 'typescript', canUseNetwork: false, generateOnly: true, workDir: projectDir, - }); - - expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy(); + })).rejects.toThrow('--template-path can only be used with --from-path or --from-git-url'); }); cliTest('hook files are ignored during template copy', async (workDir) => { From 5437650de871c5af1a74f3fef1e4e58172591669 Mon Sep 17 00:00:00 2001 From: Rohan Gupta Date: Fri, 29 Aug 2025 15:33:19 -0400 Subject: [PATCH 37/38] chore: add CLI-level validation for template-path parameter --- .../@aws-cdk/user-input-gen/lib/yargs-gen.ts | 13 +++++++--- .../user-input-gen/lib/yargs-types.ts | 2 +- packages/aws-cdk/lib/cli/cli-config.ts | 9 ++++++- packages/aws-cdk/lib/commands/init/init.ts | 5 ---- packages/aws-cdk/test/commands/init.test.ts | 26 ++++++++++++------- 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts b/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts index a40f0c789..af492bca8 100644 --- a/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts +++ b/packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts @@ -105,10 +105,6 @@ function makeYargs(config: CliConfig, helpers: CliHelpers): Statement { } commandCallArgs.push(lit(commandFacts.description)); - if (commandFacts.options) { - commandCallArgs.push(optionsExpr); - } - // Add implies calls if present if (commandFacts.implies) { for (const [key, value] of Object.entries(commandFacts.implies)) { @@ -116,6 +112,15 @@ function makeYargs(config: CliConfig, helpers: CliHelpers): Statement { } } + // Add check function if present + if (commandFacts.check) { + optionsExpr = optionsExpr.callMethod('check', code.expr.directCode(commandFacts.check.toString())); + } + + if (commandFacts.options || commandFacts.check) { + commandCallArgs.push(optionsExpr); + } + yargsExpr = yargsExpr.callMethod('command', ...commandCallArgs); } diff --git a/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts b/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts index 2d5ed4c98..7c074c193 100644 --- a/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts +++ b/packages/@aws-cdk/user-input-gen/lib/yargs-types.ts @@ -8,7 +8,7 @@ interface YargsCommand { export interface CliAction extends YargsCommand { options?: { [optionName: string]: CliOption }; implies?: { [key: string]: string }; - // check?: string; + check?: (argv: any) => boolean | Promise; } interface YargsArg { diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index fbb6f3336..44f40cec4 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -405,7 +405,14 @@ export async function makeConfig(): Promise { 'from-git-url': { type: 'string', desc: 'Git repository URL to clone custom template from', requiresArg: true, conflicts: ['lib-version', 'from-path'] }, 'template-path': { type: 'string', desc: 'Path to a specific template within a multi-template repository', requiresArg: true }, }, - + check: (argv: any) => { + if (argv['template-path'] && !argv['from-path'] && !argv['from-git-url']) { + const error = new Error('--template-path can only be used with --from-path or --from-git-url'); + error.name = 'ValidationError'; + throw error; + } + return true; + }, }, 'migrate': { description: 'Migrate existing AWS resources into a CDK app', diff --git a/packages/aws-cdk/lib/commands/init/init.ts b/packages/aws-cdk/lib/commands/init/init.ts index 3ef40500a..f5f944229 100644 --- a/packages/aws-cdk/lib/commands/init/init.ts +++ b/packages/aws-cdk/lib/commands/init/init.ts @@ -91,11 +91,6 @@ export async function cliInit(options: CliInitOptions) { const generateOnly = options.generateOnly ?? false; const workDir = options.workDir ?? process.cwd(); - // Validate that if template-path is provided, exactly one custom template source must be provided - if (options.templatePath && !options.fromPath && !options.fromGitUrl) { - throw new ToolkitError('--template-path can only be used with --from-path or --from-git-url'); - } - // Show available templates if no type and no language provided (main branch logic) if (!options.fromPath && !options.fromGitUrl && !options.type && !options.language) { await printAvailableTemplates(ioHelper); diff --git a/packages/aws-cdk/test/commands/init.test.ts b/packages/aws-cdk/test/commands/init.test.ts index a37bd669b..d498a635d 100644 --- a/packages/aws-cdk/test/commands/init.test.ts +++ b/packages/aws-cdk/test/commands/init.test.ts @@ -607,18 +607,24 @@ describe('constructs version', () => { }); cliTest('template-path validation requires from-path or from-git-url', async (workDir) => { - const projectDir = path.join(workDir, 'my-project'); - await fs.mkdirp(projectDir); + const { exec } = await import('child_process'); + const { promisify } = await import('util'); + const execAsync = promisify(exec); + const cdkBin = path.join(__dirname, '..', '..', 'bin', 'cdk'); + + const emptyDir = path.join(workDir, 'empty-project'); + await fs.mkdirp(emptyDir); // Test that template-path fails when used without from-path or from-git-url - await expect(cliInit({ - ioHelper, - templatePath: 'some-template', - language: 'typescript', - canUseNetwork: false, - generateOnly: true, - workDir: projectDir, - })).rejects.toThrow('--template-path can only be used with --from-path or --from-git-url'); + try { + await execAsync(`node ${cdkBin} init --template-path some-template --language typescript --generate-only`, { + cwd: emptyDir, + env: { ...process.env, CDK_DISABLE_VERSION_CHECK: '1' }, + }); + throw new Error('Expected command to fail but it succeeded'); + } catch (error: any) { + expect(error.stderr || error.message).toContain('--template-path can only be used with --from-path or --from-git-url'); + } }); cliTest('hook files are ignored during template copy', async (workDir) => { From 3c794109eb15f4d238c19a02ab67f264d28dd7f1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 29 Aug 2025 19:41:37 +0000 Subject: [PATCH 38/38] chore: self mutation Signed-off-by: github-actions --- .../aws-cdk/lib/cli/convert-to-user-input.ts | 2 ++ .../lib/cli/parse-command-line-arguments.ts | 17 ++++++++++++++++- packages/aws-cdk/lib/cli/user-input.ts | 7 +++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cli/convert-to-user-input.ts b/packages/aws-cdk/lib/cli/convert-to-user-input.ts index 9456ffa43..f6f4db708 100644 --- a/packages/aws-cdk/lib/cli/convert-to-user-input.ts +++ b/packages/aws-cdk/lib/cli/convert-to-user-input.ts @@ -246,6 +246,7 @@ export function convertYargsToUserInput(args: any): UserInput { generateOnly: args.generateOnly, libVersion: args.libVersion, fromPath: args.fromPath, + fromGitUrl: args.fromGitUrl, templatePath: args.templatePath, TEMPLATE: args.TEMPLATE, }; @@ -484,6 +485,7 @@ export function convertConfigToUserInput(config: any): UserInput { generateOnly: config.init?.generateOnly, libVersion: config.init?.libVersion, fromPath: config.init?.fromPath, + fromGitUrl: config.init?.fromGitUrl, templatePath: config.init?.templatePath, }; const migrateOptions = { diff --git a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts index 1b29c21ee..3ebd359a9 100644 --- a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts @@ -869,13 +869,28 @@ export function parseCommandLineArguments(args: Array): any { type: 'string', desc: 'Path to a local custom template directory or multi-template repository', requiresArg: true, - conflicts: ['lib-version'], + conflicts: ['lib-version', 'from-git-url'], + }) + .option('from-git-url', { + default: undefined, + type: 'string', + desc: 'Git repository URL to clone custom template from', + requiresArg: true, + conflicts: ['lib-version', 'from-path'], }) .option('template-path', { default: undefined, type: 'string', desc: 'Path to a specific template within a multi-template repository', requiresArg: true, + }) + .check((argv) => { + if (argv['template-path'] && !argv['from-path'] && !argv['from-git-url']) { + const error = new Error('--template-path can only be used with --from-path or --from-git-url'); + error.name = 'ValidationError'; + throw error; + } + return true; }), ) .command('migrate', 'Migrate existing AWS resources into a CDK app', (yargs: Argv) => diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index 5402ef19b..3d0cc664d 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1378,6 +1378,13 @@ export interface InitOptions { */ readonly fromPath?: string; + /** + * Git repository URL to clone custom template from + * + * @default - undefined + */ + readonly fromGitUrl?: string; + /** * Path to a specific template within a multi-template repository *