Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 | undefined;
}

interface YargsArg {
Expand Down
23 changes: 22 additions & 1 deletion packages/aws-cdk/lib/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import { getLanguageAlias } from '../commands/language';

export const YARGS_HELPERS = new CliHelpers('./util/yargs-helpers');

interface InitCommandArgs {
'template-path'?: string;
'from-path'?: string;
'from-git-url'?: string;
[key: string]: unknown;
}

/**
* Source of truth for all CDK CLI commands. `user-input-gen` translates this into:
*
Expand Down Expand Up @@ -406,8 +413,22 @@ export async function makeConfig(): Promise<CliConfig> {
'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', requiresArg: true },
'from-git-url': { type: 'string', desc: 'Git repository URL to clone custom template from', requiresArg: true, conflicts: ['lib-version', 'from-path'] },
},
check: (argv: InitCommandArgs) => {
const hasTemplatePath = Boolean(argv['template-path']);
const hasValidSource = Boolean(argv['from-path'] || argv['from-git-url']);

if (hasTemplatePath && !hasValidSource) {
const e = new Error(
'--template-path can only be used with --from-path or --from-git-url',
);
e.name = 'ValidationError';
throw e;
}

return 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.

},
'migrate': {
description: 'Migrate existing AWS resources into a CDK app',
Expand Down
12 changes: 9 additions & 3 deletions packages/aws-cdk/lib/cli/cli-type-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -897,10 +897,16 @@
"type": "string",
"desc": "Path to a specific template within a multi-template repository",
"requiresArg": true
},
"from-git-url": {
"type": "string",
"desc": "Git repository URL to clone custom template from",
"requiresArg": true,
"conflicts": [
"lib-version",
"from-path"
]
}
},
"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 @@ -547,7 +547,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 @@ -558,6 +558,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 @@ -249,6 +249,7 @@ export function convertYargsToUserInput(args: any): UserInput {
libVersion: args.libVersion,
fromPath: args.fromPath,
templatePath: args.templatePath,
fromGitUrl: args.fromGitUrl,
TEMPLATE: args.TEMPLATE,
};
break;
Expand Down Expand Up @@ -491,6 +492,7 @@ export function convertConfigToUserInput(config: any): UserInput {
libVersion: config.init?.libVersion,
fromPath: config.init?.fromPath,
templatePath: config.init?.templatePath,
fromGitUrl: config.init?.fromGitUrl,
};
const migrateOptions = {
stackName: config.migrate?.stackName,
Expand Down
17 changes: 17 additions & 0 deletions packages/aws-cdk/lib/cli/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,23 @@ export function parseCommandLineArguments(args: Array<string>): any {
type: 'string',
desc: 'Path to a specific template within a multi-template repository',
requiresArg: true,
})
.option('from-git-url', {
default: undefined,
type: 'string',
desc: 'Git repository URL to clone custom template from',
requiresArg: true,
conflicts: ['lib-version', 'from-path'],
})
.check((argv) => {
const hasTemplatePath = Boolean(argv['template-path']);
const hasValidSource = Boolean(argv['from-path'] || argv['from-git-url']);
if (hasTemplatePath && !hasValidSource) {
const e = new Error('--template-path can only be used with --from-path or --from-git-url');
e.name = 'ValidationError';
throw e;
}
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 @@ -1401,6 +1401,13 @@ export interface InitOptions {
*/
readonly templatePath?: string;

/**
* Git repository URL to clone custom template from
*
* @default - undefined
*/
readonly fromGitUrl?: string;

/**
* Positional argument for init
*/
Expand Down
157 changes: 129 additions & 28 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 @@ -70,8 +71,14 @@ 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 template within a multi-template repository.
* This parameter requires an origin to be specified using --from-path or --from-git-url.
* @default undefined
*/
readonly templatePath?: string;
Expand All @@ -89,34 +96,49 @@ export async function cliInit(options: CliInitOptions) {
const workDir = options.workDir ?? process.cwd();

// Show available templates only if no fromPath, type, or language provided
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);
}

// Step 2: Resolve language
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,
);
// temporarily store git repo if pulling from remote
let gitTempDir: string | undefined;

try {
// Step 1: Load template
let template: InitTemplate;
if (options.fromPath) {
template = await loadLocalTemplate(options.fromPath, options.templatePath);
} else if (options.fromGitUrl) {
gitTempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-init-git-'));
template = await loadGitTemplate(gitTempDir, options.fromGitUrl, options.templatePath);
} else {
template = await loadBuiltinTemplate(ioHelper, options.type, options.language);
}

// Step 2: Resolve language
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,
);
} finally {
// Clean up temporary directory after everything is done
if (gitTempDir) {
await fs.remove(gitTempDir).catch(async (error: any) => {
await ioHelper.defaults.warn(`Could not remove temporary directory ${gitTempDir}: ${error.message}`);
});
}
}
}

/**
Expand Down Expand Up @@ -160,6 +182,28 @@ 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 InitTemplate
*/
async function loadGitTemplate(tempDir: string, gitUrl: string, templatePath?: string): Promise<InitTemplate> {
try {
await executeGitCommand('git', ['clone', '--depth', '1', gitUrl, tempDir]);

let fullTemplatePath = tempDir;
if (templatePath) {
fullTemplatePath = path.join(tempDir, templatePath);
}
const template = await InitTemplate.fromPath(fullTemplatePath);
return template;
} catch (error: any) {
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 @@ -188,8 +232,9 @@ async function resolveLanguage(ioHelper: IoHelper, template: InitTemplate, reque
return (async () => {
if (requestedLanguage) {
return requestedLanguage;
}
if (template.languages.length === 1) {
} else if (template.languages.length === 0) {
throw new ToolkitError('Custom template must contain at least one language directory');
} else if (template.languages.length === 1) {
const templateLanguage = template.languages[0];
// Only show auto-detection message for built-in templates
if (template.templateType !== TemplateType.CUSTOM) {
Expand Down Expand Up @@ -892,6 +937,62 @@ function isRoot(dir: string) {
return path.dirname(dir) === dir;
}

/**
* Execute a Git command with timeout
* @param cmd - Git command to execute
* @param args - Command arguments
* @returns Promise resolving to stdout
*/
async function executeGitCommand(cmd: string, args: string[]): Promise<string> {
return new Promise<string>((resolve, reject) => {
const child = childProcess.spawn(cmd, args, {
shell: true,
stdio: ['ignore', 'pipe', 'pipe'],
});

let stdout = '';
let stderr = '';
let killed = false;

// Handle process errors
child.on('error', (err) => {
reject(new ToolkitError(`Failed to execute Git command: ${err.message}`));
});

// Collect stdout
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});

// Collect stderr
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});

// Handle process completion
child.on('exit', (code, signal) => {
if (killed) {
return;
}

if (code === 0) {
resolve(stdout.trim());
} else {
const errorMessage = stderr.trim() || stdout.trim();
reject(new ToolkitError(
`Git command failed with ${signal ? `signal ${signal}` : `code ${code}`}: ${errorMessage}`,
));
}
});

child.on('close', () => {
child.stdout.removeAllListeners();
child.stderr.removeAllListeners();
child.removeAllListeners();
});
});
}

/**
* Executes `command`. STDERR is emitted in real-time.
*
Expand Down
Loading
Loading