Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
87d361d
feat: add local template support with --from-path option
rohang9000 Aug 4, 2025
75602da
chore: fix code style and functionality based on code review feedback
rohang9000 Aug 7, 2025
6a487e3
chore: cleaned up some extraneous comments
rohang9000 Aug 7, 2025
dd5139e
chore: modified error handling and fixed flag conflicts with yargs
rohang9000 Aug 8, 2025
54e20f1
fix: correct JSDoc @default comments to show actual values
rohang9000 Aug 8, 2025
8c4058e
chore: self mutation
invalid-email-address Aug 8, 2025
86a8d81
chore: fixed code clarity and documentation based on CR feedback
rohang9000 Aug 8, 2025
c337e83
chore: gate --from-path functionality with unstable flag
rohang9000 Aug 11, 2025
8ba0fa9
chore: correct JSDoc syntax in init.ts
rohang9000 Aug 11, 2025
058e6cf
chore: self mutation
invalid-email-address Aug 11, 2025
33a0ee0
fix: remove auto-detection message for local templates
rohang9000 Aug 12, 2025
0f25a88
chore: revised local template support to also allow multi-template re…
rohang9000 Aug 15, 2025
31423ad
chore: self mutation
invalid-email-address Aug 15, 2025
4bf690c
chore: update code to support multi-template local repository
rohang9000 Aug 15, 2025
3d55b34
chore: seperate test data from test code with fixtures
rohang9000 Aug 18, 2025
34b313a
chore: seperated test data from test code and resolved other issues b…
rohang9000 Aug 18, 2025
1b87c79
chore: add validation for templatePath requiring fromPath at function…
rohang9000 Aug 18, 2025
7ce094d
chore: self mutation
invalid-email-address Aug 18, 2025
b4587fa
chore: refactored check function with implies for template-path valid…
rohang9000 Aug 19, 2025
4660864
chore: self mutation
invalid-email-address Aug 19, 2025
2c5bca3
chore: update test file to remove incorrect test and add test to veri…
rohang9000 Aug 19, 2025
f2e5b03
chore: self mutation
invalid-email-address Aug 19, 2025
5f447d9
chore: remove unused check function code and clean up old validation …
rohang9000 Aug 19, 2025
fcda0b4
chore: self mutation
invalid-email-address Aug 19, 2025
7578eb3
removing "requires --from-path" description from cli-config.ts
rohang9000 Aug 22, 2025
5b132b9
chore: add additional test coverage for misc.
rohang9000 Aug 22, 2025
23dec99
chore: update test for unstable flag
rohang9000 Aug 22, 2025
39041ec
chore: update to unstable flag test
rohang9000 Aug 22, 2025
e26f524
chore: merge conflict
rohang9000 Aug 22, 2025
019f38b
chore: self mutation
invalid-email-address Aug 22, 2025
569128c
chore: updated default cdk init behavior without template type or lan…
rohang9000 Aug 25, 2025
aeef412
chore: self mutation
invalid-email-address Aug 25, 2025
b7d8408
chore: add test for cdk init --language=[language]
rohang9000 Aug 26, 2025
e00e12d
modified test for cdk init --language=[language]
rohang9000 Aug 26, 2025
2d54fe8
chore: update test for cdk init --language=[language]
rohang9000 Aug 26, 2025
82c4734
Merge remote-tracking branch 'origin/main' into feat/git-repository-s…
rohang9000 Aug 26, 2025
e49fe62
WIP: git repository template support - save progress before VSCode re…
rohang9000 Aug 27, 2025
c02abcb
feat(cli): add support for pulling custom templates from git reposito…
rohang9000 Aug 28, 2025
5437650
chore: add CLI-level validation for template-path parameter
rohang9000 Aug 29, 2025
3c79410
chore: self mutation
invalid-email-address Aug 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions packages/@aws-cdk/user-input-gen/lib/yargs-gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,22 @@ 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)) {
optionsExpr = optionsExpr.callMethod('implies', lit(key), lit(value));
}
}

// 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);
}

Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/user-input-gen/lib/yargs-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface YargsCommand {
export interface CliAction extends YargsCommand {
options?: { [optionName: string]: CliOption };
implies?: { [key: string]: string };
check?: (argv: any) => boolean | Promise<boolean>;
}

interface YargsArg {
Expand Down
12 changes: 10 additions & 2 deletions packages/aws-cdk/lib/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,10 +401,18 @@ export async function makeConfig(): Promise<CliConfig> {
'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' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be replaced by a check that ensures that from-path or from-git-url was also used.

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',
Expand Down
15 changes: 11 additions & 4 deletions packages/aws-cdk/lib/cli/cli-type-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -872,17 +872,24 @@
"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": {
"type": "string",
"desc": "Path to a specific template within a multi-template repository",
"requiresArg": true
}
},
"implies": {
"template-path": "from-path"
}
},
"migrate": {
Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
return printAvailableTemplates(ioHelper, language);
} else {
// Gate custom template support with unstable flag
if (args['from-path'] && !configuration.settings.get(['unstable']).includes('init')) {
if ((args['from-path'] || args['from-git-url']) && !configuration.settings.get(['unstable']).includes('init')) {
throw new ToolkitError('Unstable feature use: \'init\' with custom templates is unstable. It must be opted in via \'--unstable\', e.g. \'cdk init --from-path=./my-template --unstable=init\'');
}
return cliInit({
Expand All @@ -528,6 +528,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
generateOnly: args.generateOnly,
libVersion: args.libVersion,
fromPath: args['from-path'],
fromGitUrl: args['from-git-url'],
templatePath: args['template-path'],
});
}
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/cli/convert-to-user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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 = {
Expand Down
17 changes: 16 additions & 1 deletion packages/aws-cdk/lib/cli/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,13 +869,28 @@ export function parseCommandLineArguments(args: Array<string>): 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) =>
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/cli/user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
107 changes: 93 additions & 14 deletions packages/aws-cdk/lib/commands/init/init.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -66,10 +67,16 @@ 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.
* This parameter requires --from-path or --from-git-url to be specified.
* @default undefined - Optional/auto-detected if repository contains only one template, otherwise required
*/
readonly templatePath?: string;

readonly ioHelper: IoHelper;
Expand All @@ -85,15 +92,21 @@ 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;
let gitTempDir: string | undefined;

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);
}
Expand All @@ -102,17 +115,25 @@ export async function cliInit(options: CliInitOptions) {
const language = await resolveLanguage(ioHelper, template, options.language, options.type);

// Step 3: Initialize project following standard process
await initializeProject(
ioHelper,
template,
language,
canUseNetwork,
generateOnly,
workDir,
options.stackName,
options.migrate,
options.libVersion,
);
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(() => {
});
}
}
}

/**
Expand Down Expand Up @@ -156,6 +177,34 @@ 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 and tempDir for cleanup
*/
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]);

// 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}`);
}
}

/**
* Load a built-in template by name
*/
Expand Down Expand Up @@ -843,6 +892,36 @@ function isRoot(dir: string) {
return path.dirname(dir) === dir;
}

/**
* Execute a Git command with error handling
* @param cmd - Command to execute
* @param args - Command arguments
* @returns Promise resolving to stdout
*/
async function executeGitCommand(cmd: string, args: string[]): Promise<string> {
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()));

return new Promise<string>((resolve, reject) => {
child.once('error', (err) => reject(err));
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.
*
Expand Down
39 changes: 17 additions & 22 deletions packages/aws-cdk/test/commands/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,30 +606,25 @@ 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'] },
]);
const projectDir = path.join(workDir, 'my-project');
await fs.mkdirp(projectDir);
cliTest('template-path validation requires from-path or from-git-url', async (workDir) => {
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
const cdkBin = path.join(__dirname, '..', '..', 'bin', 'cdk');

await cliInit({
ioHelper,
fromPath: repoDir,
templatePath: 'implies-test',
language: 'typescript',
canUseNetwork: false,
generateOnly: true,
workDir: projectDir,
});
const emptyDir = path.join(workDir, 'empty-project');
await fs.mkdirp(emptyDir);

expect(await fs.pathExists(path.join(projectDir, 'app.ts'))).toBeTruthy();
// Test that template-path fails when used without 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) => {
Expand Down