@owloops/cdko
Version:
Multi-region AWS CDK deployment tool
1,037 lines (1,022 loc) • 35.3 kB
JavaScript
// src/cli/index.ts
import { fs as fs2, path } from "zx";
// src/utils/logger.ts
import { chalk } from "zx";
var logger = {
error: (msg) => console.error(chalk.red("x"), msg),
warn: (msg) => console.log(chalk.yellow("!"), msg),
info: (msg) => console.log(chalk.blue("\u2022"), msg),
success: (msg) => console.log(chalk.green("\u2713"), msg),
region: (region) => chalk.cyan(region),
stack: (stackName) => chalk.bold(stackName),
dim: (text) => chalk.dim(text)
};
// src/utils/prerequisites.ts
import { $ } from "zx";
async function checkPrerequisites() {
const requiredTools = ["aws", "cdk"];
const missing = [];
for (const tool of requiredTools) {
try {
await $`which ${tool}`.quiet();
} catch {
missing.push(tool);
}
}
if (missing.length > 0) {
logger.error(`Missing required tools: ${missing.join(", ")}`);
logger.info("Please install the missing tools to continue");
process.exit(1);
}
try {
const cdkVersion = await $`cdk --version`.quiet();
logger.info(`Using CDK version: ${cdkVersion.toString().trim()}`);
} catch {
}
}
// src/core/stack-manager.ts
import { $ as $2 } from "zx";
import fs from "fs/promises";
import { minimatch } from "minimatch";
var StackManager = class {
configPath;
constructor(configPath = ".cdko.json") {
this.configPath = configPath;
}
async detect() {
try {
await fs.access("cdk.json");
} catch {
throw new Error(
"No CDK project found. Run this command in a CDK project directory."
);
}
const configExists = await fs.access(this.configPath).then(() => true).catch(() => false);
if (configExists) {
logger.info("Updating existing CDKO configuration...");
} else {
logger.info("Creating new CDKO configuration...");
}
const config = await this.detectStacks();
await this.saveConfig(config);
const action = configExists ? "updated" : "created";
logger.success(`CDKO configuration ${action} successfully!`);
logger.info(`Found ${Object.keys(config.stackGroups).length} stack groups`);
return config;
}
async loadConfig() {
try {
const data = await fs.readFile(this.configPath, "utf8");
const config = JSON.parse(data);
if (config.stackGroups && typeof config.stackGroups !== "object") {
logger.warn("Config: stackGroups must be an object, ignoring");
config.stackGroups = void 0;
}
return config;
} catch (error) {
if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
logger.warn(`Failed to load ${this.configPath}: ${error.message}`);
}
}
return {};
}
async detectStacks() {
logger.info("Detecting CDK stacks...");
try {
const result = await $2`cdk list --long --json`.quiet();
const stacks = JSON.parse(result.stdout);
if (!Array.isArray(stacks) || stacks.length === 0) {
throw new Error(
"No stacks found. Ensure your CDK app synthesizes correctly."
);
}
const stackGroups = {};
for (const stack of stacks) {
const stackName = stack.name || stack.id;
const account = stack.environment?.account;
const region = stack.environment?.region;
let constructId = stack.id;
const parenIndex = constructId.indexOf(" (");
if (parenIndex > -1) {
constructId = constructId.substring(0, parenIndex);
}
if (!stackGroups[stackName]) {
stackGroups[stackName] = {};
}
const deploymentKey = `${account}/${region}`;
stackGroups[stackName][deploymentKey] = {
constructId,
account,
region
};
}
return {
version: "0.1",
stackGroups,
cdkTimeout: process.env.CDK_TIMEOUT,
suppressNotices: process.env.CDK_CLI_NOTICES !== "true",
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
updatedBy: `cdko@${await this.getCdkoVersion()}`
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("No stacks found")) {
throw error;
}
logger.error(`Stack detection failed: ${errorMessage}`);
throw new Error(
"Failed to detect stacks. Ensure CDK app synthesizes correctly."
);
}
}
async getCdkoVersion() {
try {
const packagePath = new URL("../../package.json", import.meta.url);
const data = await fs.readFile(packagePath, "utf8");
const packageJson = JSON.parse(data);
return packageJson.version || "0.0.0";
} catch {
return "0.0.0";
}
}
async saveConfig(config) {
await fs.writeFile(this.configPath, JSON.stringify(config, null, 2) + "\n");
logger.info(`Configuration saved to ${this.configPath}`);
}
getRegions(config, cliRegions) {
if (cliRegions === "all") {
if (config?.stackGroups) {
const allRegions = Object.values(config.stackGroups).flatMap(
(group) => Object.values(group).map((deployment) => deployment.region)
);
const uniqueRegions = [...new Set(allRegions)];
if (uniqueRegions.length > 0) {
return uniqueRegions;
}
}
return ["us-east-1"];
}
return cliRegions.split(",").map((r) => r.trim());
}
matchStacks(stackPattern, stackConfig) {
if (!stackConfig?.stackGroups) {
return [];
}
const patterns = stackPattern.split(",").map((p) => p.trim());
const matchedGroups = [];
for (const pattern of patterns) {
for (const [stackGroupName, deployments] of Object.entries(
stackConfig.stackGroups
)) {
if (minimatch(stackGroupName, pattern)) {
matchedGroups.push({
name: stackGroupName,
pattern,
deployments
});
}
}
}
return matchedGroups;
}
filterDeployments(stackGroup, requestedRegions) {
const filteredDeployments = [];
for (const [, deployment] of Object.entries(stackGroup.deployments)) {
const { region, constructId } = deployment;
if (region === "unknown-region") {
for (const requestedRegion of requestedRegions) {
filteredDeployments.push({
region: requestedRegion,
constructId,
stackName: stackGroup.name
});
}
} else {
if (requestedRegions.includes(region)) {
filteredDeployments.push({
region,
constructId,
stackName: stackGroup.name
});
}
}
}
return filteredDeployments;
}
resolveDeployments(stackPattern, stackConfig, requestedRegions) {
const matchedGroups = this.matchStacks(stackPattern, stackConfig);
if (matchedGroups.length === 0) {
logger.warn(`No stacks found matching pattern: ${stackPattern}`);
return [];
}
logger.info(`Found ${matchedGroups.length} stack(s) matching pattern`);
const allDeployments = [];
for (const stackGroup of matchedGroups) {
const deployments = this.filterDeployments(stackGroup, requestedRegions);
if (deployments.length === 0) {
logger.warn(
`No valid deployments for ${stackGroup.name} in requested regions`
);
} else {
logger.info(
`${stackGroup.name}: ${deployments.length} deployment(s) planned`
);
allDeployments.push(...deployments);
}
}
return allDeployments;
}
};
// src/core/executor.ts
import { $ as $3 } from "zx";
async function runCdkCommand(region, stackName, command, profile, options = {}) {
const {
verbose = false,
parameters = [],
includeDeps = false,
context = [],
executeChangeset = false,
cdkOptions = "",
signal,
cloudAssemblyPath = null
} = options;
const cdkArgs = [command, "--profile", profile];
if (process.env.CDK_CLI_NOTICES !== "true") {
cdkArgs.push("--no-notices");
}
if (verbose) {
cdkArgs.push("-v");
}
if (!cloudAssemblyPath) {
throw new Error("Cloud assembly path is required");
}
cdkArgs.push("--app", cloudAssemblyPath);
context.forEach((ctx) => {
cdkArgs.push("--context", ctx);
});
if (command === "deploy") {
if (!includeDeps) cdkArgs.push("--exclusively");
if (!executeChangeset) cdkArgs.push("--no-execute");
cdkArgs.push("--require-approval=never");
if (executeChangeset) {
cdkArgs.push("--progress", "events");
}
}
if (command === "diff") {
if (!includeDeps) cdkArgs.push("--exclusively");
}
parameters.forEach((param) => {
if (includeDeps && !param.includes(":")) {
cdkArgs.push("--parameters", `${stackName}:${param}`);
} else {
cdkArgs.push("--parameters", param);
}
});
if (cdkOptions) {
const additionalArgs = cdkOptions.split(/\s+/).filter((arg) => arg);
cdkArgs.push(...additionalArgs);
}
cdkArgs.push(stackName);
process.env.AWS_REGION = region;
process.env.FORCE_COLOR = "1";
const cdkProcess = $3({
signal,
quiet: ["diff", "deploy"].includes(command) ? false : !verbose
})`cdk ${cdkArgs}`;
return process.env.CDK_TIMEOUT ? await cdkProcess.timeout(process.env.CDK_TIMEOUT) : await cdkProcess;
}
// src/core/account-manager.ts
import { $ as $4 } from "zx";
import { minimatch as minimatch2 } from "minimatch";
var AccountManager = class {
configPath;
accountCache;
constructor(configPath = ".cdko.json") {
this.configPath = configPath;
this.accountCache = /* @__PURE__ */ new Map();
}
async getAvailableProfiles() {
try {
const result = await $4`aws configure list-profiles`.quiet();
return result.stdout.split("\n").map((profile) => profile.trim()).filter((profile) => profile.length > 0);
} catch {
logger.warn("Could not list AWS profiles, using exact matching only");
return [];
}
}
async matchProfiles(profilePattern) {
const patterns = profilePattern.split(",").map((p) => p.trim());
const availableProfiles = await this.getAvailableProfiles();
const matchedProfiles = [];
for (const pattern of patterns) {
if (availableProfiles.length > 0) {
for (const profile of availableProfiles) {
if (minimatch2(profile, pattern)) {
if (!matchedProfiles.includes(profile)) {
matchedProfiles.push(profile);
}
}
}
} else {
if (!matchedProfiles.includes(pattern)) {
matchedProfiles.push(pattern);
}
}
}
if (matchedProfiles.length === 0) {
logger.warn(`No profiles found matching pattern: ${profilePattern}`);
return patterns;
}
logger.info(
`Found ${matchedProfiles.length} profile(s) matching pattern: ${matchedProfiles.join(", ")}`
);
return matchedProfiles;
}
async getAccountInfo(profile) {
if (this.accountCache.has(profile)) {
return this.accountCache.get(profile);
}
try {
logger.info(`Discovering account for profile: ${profile}`);
const result = await $4`aws sts get-caller-identity --profile ${profile} --output json`.quiet();
const identity = JSON.parse(result.stdout);
const accountInfo = {
profile,
accountId: identity.Account,
userId: identity.UserId,
arn: identity.Arn
};
this.accountCache.set(profile, accountInfo);
logger.info(`Profile ${profile} \u2192 Account ${identity.Account}`);
return accountInfo;
} catch (error) {
const errorMsg = error instanceof Error && "stderr" in error ? error.stderr : error instanceof Error ? error.message : String(error);
logger.error(
`Failed to get account info for profile ${profile}: ${errorMsg}`
);
throw new Error(
`Profile '${profile}' authentication failed: ${errorMsg}`
);
}
}
async getMultiAccountInfo(profiles) {
logger.info(`Discovering accounts for ${profiles.length} profile(s)...`);
const accountPromises = profiles.map(
(profile) => this.getAccountInfo(profile).catch(
(error) => ({
profile,
error: error instanceof Error ? error.message : String(error),
failed: true
})
)
);
const results = await Promise.all(accountPromises);
const successful = results.filter(
(result) => !("failed" in result)
);
const failed = results.filter(
(result) => "failed" in result
);
if (failed.length > 0) {
logger.error(`Failed to authenticate ${failed.length} profile(s):`);
failed.forEach(({ profile, error }) => {
logger.error(` ${profile}: ${error}`);
});
if (successful.length === 0) {
throw new Error("All profiles failed authentication");
}
logger.warn(`Continuing with ${successful.length} successful profile(s)`);
}
return successful;
}
createDeploymentTargets(accountInfo, regions) {
const targets = [];
accountInfo.forEach(({ profile, accountId }) => {
regions.forEach((region) => {
targets.push({
profile,
accountId,
region,
key: `${accountId}/${region}`
});
});
});
return targets;
}
clearCache() {
this.accountCache.clear();
}
};
// src/core/cloud-assembly.ts
import { $ as $5 } from "zx";
import { existsSync, mkdirSync, rmSync } from "fs";
import { join } from "path";
var CloudAssemblyManager = class {
cloudAssemblyPath;
constructor() {
this.cloudAssemblyPath = null;
}
getCloudAssemblyPath() {
return join(process.cwd(), "cdk.out");
}
async synthesize(options = {}) {
const { profile, environment, outputDir } = options;
const cloudAssemblyPath = outputDir ? join(process.cwd(), outputDir) : this.getCloudAssemblyPath();
try {
if (existsSync(cloudAssemblyPath)) {
logger.info(`Cleaning existing cloud assembly at ${cloudAssemblyPath}`);
rmSync(cloudAssemblyPath, { recursive: true, force: true });
}
mkdirSync(cloudAssemblyPath, { recursive: true });
logger.info("Synthesizing cloud assembly...");
const cdkArgs = ["synth"];
if (process.env.CDK_CLI_NOTICES !== "true") {
cdkArgs.push("--no-notices");
}
cdkArgs.push("--output", cloudAssemblyPath);
if (profile) {
cdkArgs.push("--profile", profile);
}
if (environment) {
cdkArgs.push("--context", `environment=${environment}`);
}
const result = await $5({ quiet: true })`cdk ${cdkArgs}`;
if (result.exitCode === 0) {
logger.success(`Cloud assembly synthesized to ${cloudAssemblyPath}`);
this.cloudAssemblyPath = cloudAssemblyPath;
return cloudAssemblyPath;
} else {
throw new Error(`CDK synth failed with exit code ${result.exitCode}`);
}
} catch (error) {
logger.error("Failed to synthesize cloud assembly");
if (error instanceof Error && "stderr" in error) {
console.error(error.stderr);
}
throw error;
}
}
getCdkArgs(baseArgs) {
if (!this.cloudAssemblyPath || !existsSync(this.cloudAssemblyPath)) {
throw new Error("Cloud assembly not available. Run synthesize() first.");
}
const args = [...baseArgs];
const appIndex = args.indexOf("--app");
if (appIndex !== -1) {
args.splice(appIndex, 2);
}
args.push("--app", this.cloudAssemblyPath);
return args;
}
isAvailable() {
return !!this.cloudAssemblyPath && existsSync(this.cloudAssemblyPath);
}
};
// src/core/orchestrator.ts
async function deployStack(deployment, args, signal, cloudAssemblyPath = null) {
const { region, constructId, stackName, profile } = deployment;
const deployProfile = profile || args.profile;
console.log();
console.log(`${logger.region(region)} \u2192 ${logger.stack(stackName)}`);
if (args.dryRun) {
logger.info(`Would deploy: ${constructId} to ${region}`);
return { success: true, region, stackName };
}
const startTime = Date.now();
try {
const executorOptions = {
...args,
signal,
cloudAssemblyPath
};
switch (args.mode) {
case "diff":
await runCdkCommand(
region,
constructId,
"diff",
deployProfile,
executorOptions
);
break;
case "changeset":
logger.info(`Creating changeset for ${stackName}`);
await runCdkCommand(
region,
constructId,
"deploy",
deployProfile,
executorOptions
);
logger.success("Changeset created");
break;
case "execute":
logger.info(`Deploying ${stackName}`);
const executeOptions = { ...executorOptions, executeChangeset: true };
await runCdkCommand(
region,
constructId,
"deploy",
deployProfile,
executeOptions
);
logger.success(`Deployed ${stackName}`);
break;
}
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
logger.info(`Completed in ${duration}s`);
return { success: true, region, stackName, duration };
} catch (e) {
logger.error(`Failed to deploy ${stackName}`);
return { success: false, region, stackName, error: e };
}
}
async function deployToAllRegions(regions, args, signal) {
const stackManager = new StackManager();
const stackConfig = await stackManager.loadConfig();
let deployments = [];
const isMultiAccount = args.profile.includes(",") || args.profile.includes("*") || args.profile.includes("{");
if (isMultiAccount) {
deployments = await resolveMultiAccountDeployments(
args.profile,
args.stackPattern,
stackConfig,
regions
);
} else {
if (stackConfig) {
deployments = stackManager.resolveDeployments(
args.stackPattern,
stackConfig,
regions
);
if (deployments.length === 0) {
logger.warn(
"No matching stacks found in .cdko.json, falling back to traditional deployment"
);
}
}
if (deployments.length === 0) {
logger.info("Using traditional pattern-based deployment");
for (const region of regions) {
const stackName = args.stackPattern;
deployments.push({
region,
constructId: stackName,
stackName,
profile: args.profile
});
}
} else {
deployments = deployments.map((deployment) => ({
...deployment,
profile: args.profile
}));
}
}
const profileAssemblies = /* @__PURE__ */ new Map();
if (isMultiAccount) {
const uniqueProfiles = [
...new Set(deployments.map((d) => d.profile || args.profile))
];
logger.info(
`Synthesizing cloud assemblies for ${uniqueProfiles.length} profile(s)...`
);
const synthesisPromises = uniqueProfiles.map(async (profile) => {
try {
const cloudAssembly = new CloudAssemblyManager();
const assemblyPath = await cloudAssembly.synthesize({
profile,
environment: args.environment,
outputDir: `cdk.out-${profile}`
});
profileAssemblies.set(profile, assemblyPath);
logger.success(
`Cloud assembly for profile ${profile} \u2192 ${assemblyPath}`
);
return { profile, assemblyPath, success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(
`Failed to synthesize for profile ${profile}: ${errorMessage}`
);
return { profile, error, success: false };
}
});
const synthesisResults = await Promise.all(synthesisPromises);
const failedSynthesis = synthesisResults.filter(
(result) => !result.success
);
if (failedSynthesis.length > 0) {
throw new Error(
`Failed to synthesize ${failedSynthesis.length} profile(s): ${failedSynthesis.map((f) => f.profile).join(", ")}`
);
}
} else {
try {
const cloudAssembly = new CloudAssemblyManager();
const assemblyPath = await cloudAssembly.synthesize({
profile: args.profile,
environment: args.environment
});
profileAssemblies.set(args.profile, assemblyPath);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(`Failed to synthesize cloud assembly: ${errorMessage}`);
throw error;
}
}
if (!isMultiAccount) {
console.log();
logger.info(`Planning to deploy ${deployments.length} stack(s):`);
const stackGroups = {};
deployments.forEach((d) => {
if (!stackGroups[d.stackName]) {
stackGroups[d.stackName] = [];
}
stackGroups[d.stackName].push(d.region);
});
Object.entries(stackGroups).forEach(([stack, regions2]) => {
logger.info(`${stack} \u2192 ${regions2.join(", ")}`);
});
}
const results = [];
if (args.sequential) {
for (const deployment of deployments) {
const assemblyPath = profileAssemblies.get(
deployment.profile || args.profile
);
results.push(await deployStack(deployment, args, signal, assemblyPath));
}
} else {
logger.info(
`Processing ${deployments.length} deployment(s) in parallel...`
);
const deploymentPromises = deployments.map((deployment) => {
const assemblyPath = profileAssemblies.get(
deployment.profile || args.profile
);
return deployStack(deployment, args, signal, assemblyPath).then(
(result) => ({ ...result, status: "fulfilled" }),
(error) => ({
region: deployment.region,
stackName: deployment.stackName,
success: false,
error,
status: "rejected"
})
);
});
const parallelResults = await Promise.all(deploymentPromises);
results.push(...parallelResults);
console.log();
results.forEach((result) => {
if (result.success) {
logger.success(
`${result.region}: ${result.stackName} completed successfully`
);
} else {
const error = result.error;
let errorMsg = "Unknown error";
if (error instanceof Error) {
errorMsg = error.message;
} else if (error && typeof error === "object" && "stderr" in error) {
errorMsg = error.stderr;
} else if (error && typeof error === "object" && "stdout" in error) {
errorMsg = error.stdout;
} else if (typeof error === "string") {
errorMsg = error;
}
const lines = errorMsg.split("\n").filter((line) => line.trim());
const meaningfulError = lines.find(
(line) => line.includes("No stacks match") || line.includes("already exists") || line.includes("AccessDenied") || line.includes("is not authorized") || line.includes("CloudFormation error") || line.includes("Error:") || line.includes("failed:")
) || lines[0] || "Check output above for details";
logger.error(
`${result.region}: ${result.stackName} - ${meaningfulError.trim()}`
);
}
});
}
return results;
}
async function resolveMultiAccountDeployments(profilePattern, stackPattern, stackConfig, requestedRegions) {
const accountManager = new AccountManager();
const profiles = await accountManager.matchProfiles(profilePattern);
if (profiles.length === 0) {
throw new Error(`No profiles found matching pattern: ${profilePattern}`);
}
const accountInfo = await accountManager.getMultiAccountInfo(profiles);
if (accountInfo.length === 0) {
throw new Error("Failed to authenticate any profiles");
}
const deploymentTargets = accountManager.createDeploymentTargets(
accountInfo,
requestedRegions
);
const stackManager = new StackManager();
const matchedStacks = stackManager.matchStacks(stackPattern, stackConfig);
const allDeployments = [];
if (matchedStacks.length === 0) {
logger.info("No stack configuration found, using pattern-based deployment");
deploymentTargets.forEach(({ profile, accountId, region }) => {
allDeployments.push({
profile,
accountId,
region,
constructId: stackPattern,
stackName: stackPattern,
source: "pattern"
});
});
} else {
logger.info(`Found ${matchedStacks.length} stack(s) matching pattern`);
matchedStacks.forEach((stackGroup) => {
deploymentTargets.forEach(({ profile, accountId, region, key }) => {
if (stackGroup.deployments[key]) {
const deployment = stackGroup.deployments[key];
allDeployments.push({
profile,
accountId,
region,
constructId: deployment.constructId,
stackName: stackGroup.name,
source: "configured"
});
} else {
const unknownRegionKey = `${accountId}/unknown-region`;
if (stackGroup.deployments[unknownRegionKey]) {
const deployment = stackGroup.deployments[unknownRegionKey];
allDeployments.push({
profile,
accountId,
region,
constructId: deployment.constructId,
stackName: stackGroup.name,
source: "region-agnostic"
});
}
}
});
});
}
if (allDeployments.length === 0) {
logger.warn(
"No deployments resolved - check stack configuration and account/region combinations"
);
return [];
}
logMultiAccountDeploymentSummary(allDeployments);
return allDeployments;
}
function logMultiAccountDeploymentSummary(deployments) {
console.log();
logger.info(`Planning ${deployments.length} deployment(s):`);
const stackGroups = {};
deployments.forEach((deployment) => {
if (!stackGroups[deployment.stackName]) {
stackGroups[deployment.stackName] = [];
}
stackGroups[deployment.stackName].push(deployment);
});
Object.entries(stackGroups).forEach(([stackName, stackDeployments]) => {
const targets = stackDeployments.map((d) => `${d.accountId}/${d.region}`).join(", ");
logger.info(`${stackName} \u2192 ${targets}`);
});
const accounts = [...new Set(deployments.map((d) => d.accountId))];
const profiles = [...new Set(deployments.map((d) => d.profile))];
console.log();
logger.info(`Using ${profiles.length} profile(s): ${profiles.join(", ")}`);
logger.info(
`Targeting ${accounts.length} account(s): ${accounts.join(", ")}`
);
}
// src/cli/args.ts
import { argv } from "zx";
function parseArgs() {
const args = {
_: argv._ || [],
profile: argv.p || argv.profile || "",
stackPattern: argv.s || argv.stack || "",
regions: argv.r || argv.region || "us-east-1",
mode: argv.m || argv.mode || "changeset",
sequential: argv.x || argv.sequential || false,
dryRun: argv.d || argv["dry-run"] || false,
help: argv.h || argv.help || false,
version: argv.version || false,
verbose: argv.v || argv.verbose || false,
includeDeps: argv["include-deps"] || false,
parameters: argv.parameters || [],
context: argv.context || [],
cdkOptions: argv["cdk-opts"] || ""
};
args.parameters = Array.isArray(argv.parameters) ? argv.parameters : argv.parameters ? [argv.parameters] : [];
args.context = Array.isArray(argv.context) ? argv.context : argv.context ? [argv.context] : [];
const cdkOptsIndex = process.argv.findIndex((arg) => arg === "--cdk-opts");
if (cdkOptsIndex !== -1 && cdkOptsIndex + 1 < process.argv.length) {
args.cdkOptions = process.argv[cdkOptsIndex + 1];
}
return args;
}
function validateArgs(args) {
if (!args.profile || args.profile.trim() === "") {
logger.error("AWS profile is required. Use -p or --profile to specify.");
printUsage();
process.exit(1);
}
if (!args.stackPattern || args.stackPattern.trim() === "") {
logger.error("Stack pattern is required. Use -s or --stack to specify.");
printUsage();
process.exit(1);
}
const validModes = ["diff", "changeset", "execute"];
if (!validModes.includes(args.mode)) {
logger.error(
`Mode must be one of: ${validModes.join(", ")}. Provided: ${args.mode}`
);
process.exit(1);
}
for (const param of args.parameters) {
if (!param.includes("=")) {
logger.error(
`Invalid parameter format: ${param}. Use KEY=VALUE or STACK:KEY=VALUE`
);
process.exit(1);
}
}
for (const ctx of args.context) {
if (!ctx.includes("=")) {
logger.error(`Invalid context format: ${ctx}. Use KEY=VALUE`);
process.exit(1);
}
}
}
function printUsage() {
const scriptName = "cdko";
console.log(`
Multi-Account & Multi-Region CDK Orchestrator
Deploy CDK stacks across multiple AWS accounts and regions with enhanced control.
Usage:
${scriptName} init Initialize CDKO configuration
${scriptName} [OPTIONS] Deploy stacks
Options:
-p, --profile PROFILE AWS profile to use (required)
Supports patterns: dev-*, or comma-separated: dev,prod,staging
-s, --stack PATTERN Stack name pattern to deploy (required)
-r, --region REGION Comma-separated regions or 'all' (default: from config)
-m, --mode MODE Deployment mode: diff, changeset, execute
(default: changeset)
-x, --sequential Deploy regions sequentially (default: parallel)
-d, --dry-run Show what would be deployed without executing
-v, --verbose Enable verbose CDK output
--include-deps Include dependency stacks (default: exclude dependencies)
--parameters KEY=VAL CDK parameters (can be used multiple times)
Format: KEY=VALUE or STACK:KEY=VALUE
--context KEY=VAL CDK context values (can be used multiple times)
--cdk-opts "OPTIONS" Pass CDK command flags directly (for diff/deploy/global flags)
Example: --cdk-opts "--force --quiet --outputs-file out.json"
-h, --help Show this help message
--version Show version number
Examples:
${scriptName} init # Initialize configuration
${scriptName} -p "dev,prod" -s MyStack # Multi-account deployment
${scriptName} -p MyProfile -s MyStack -r us-east-1,eu-west-1 # Multi-region
${scriptName} -p MyProfile -s "Production-*" -m diff # Preview changes
Run '${scriptName} init' to auto-detect CDK stacks and create .cdko.json configuration.
`);
}
// src/cli/commands/init.ts
async function init() {
try {
const stackManager = new StackManager();
await stackManager.detect();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(errorMessage);
process.exit(1);
}
}
// src/cli/index.ts
import { fileURLToPath } from "url";
import { dirname, join as join2 } from "path";
var controller = new AbortController();
process.on("SIGINT", () => {
console.log("\n\nCancelling deployments...");
controller.abort();
process.exit(130);
});
async function getVersion() {
try {
const __filename2 = fileURLToPath(import.meta.url);
const __dirname2 = dirname(__filename2);
const packagePath = join2(__dirname2, "..", "..", "package.json");
const packageJson = JSON.parse(await fs2.readFile(packagePath, "utf-8"));
const version = packageJson.version || "unknown";
const buildInfo = [];
if ("1b1b7a9d6b28966c4235229cc40e38d3a48aa8e7") {
buildInfo.push(`commit: ${"1b1b7a9d6b28966c4235229cc40e38d3a48aa8e7".substring(0, 7)}`);
}
if ("2025-07-28T17:00:44+04:00") {
buildInfo.push(`built: ${"2025-07-28T17:00:44+04:00"}`);
}
const buildString = buildInfo.length > 0 ? ` (${buildInfo.join(", ")})` : "";
return `cdko version ${version}${buildString}`;
} catch {
return "cdko version unknown";
}
}
async function main() {
const args = parseArgs();
if (args.help) {
printUsage();
process.exit(0);
}
if (args.version) {
const version = await getVersion();
console.log(version);
process.exit(0);
}
if (args._.includes("init")) {
await init();
process.exit(0);
}
validateArgs(args);
const stackManager = new StackManager();
const config = await stackManager.loadConfig();
if (config.cdkTimeout) {
process.env.CDK_TIMEOUT = config.cdkTimeout;
}
const regions = stackManager.getRegions(config, args.regions);
displayHeader(args, regions);
if (args.dryRun) {
logger.warn("DRY RUN MODE - No actual deployments will occur");
}
console.log();
await checkPrerequisites();
const projectRoot = process.cwd();
if (!await fs2.exists(path.join(projectRoot, "cdk.json"))) {
logger.error("No cdk.json found in current directory:");
logger.error(` ${projectRoot}`);
logger.info("Make sure you're in a CDK project directory");
logger.info("If this is a new CDK project, run: cdk init");
process.exit(1);
}
console.log();
const modeMessages = {
diff: "Showing differences",
changeset: "Creating changesets",
execute: "Deploying stacks"
};
logger.info(
modeMessages[args.mode] || "Processing stacks"
);
const results = await deployToAllRegions(regions, args, controller.signal);
displayResults(args, results);
}
function displayHeader(args, regions) {
console.log(`
Multi-Region CDK Deployment
${Object.entries({
Profile: args.profile,
Stack: args.stackPattern,
Regions: regions.join(" "),
Mode: args.mode,
Deployment: args.sequential ? "sequential" : "parallel",
Dependencies: args.includeDeps ? "included" : "excluded",
Timeout: process.env.CDK_TIMEOUT || "not set"
}).map(([k, v]) => `${logger.dim(k + ":")} ${v}`).join("\n")}`);
}
function displayResults(args, results) {
const failedRegions = results.filter((r) => !r.success);
if (failedRegions.length > 0) {
console.log();
logger.error(`${failedRegions.length} region(s) failed`);
process.exit(1);
}
console.log();
if (args.dryRun) {
logger.success("Dry run completed - no actual changes made");
} else if (args.mode === "changeset") {
logger.success("Changesets created for review in CloudFormation console");
logger.info("Execute changesets manually after review");
} else if (args.mode === "execute") {
const totalDuration = results.filter((r) => r.duration).reduce((sum, r) => sum + parseFloat(r.duration), 0);
logger.success(`All deployments completed in ${totalDuration.toFixed(1)}s`);
} else {
logger.success("Differences shown for all regions");
}
}
main().catch((err) => {
console.log();
logger.error("CDKO encountered an unexpected error:");
logger.error(` ${err.message}`);
if (process.env.DEBUG) {
console.log("\nStack trace:");
console.error(err);
} else {
logger.info("Run with DEBUG=1 for detailed error information");
}
logger.info("If this issue persists, please report it at:");
logger.info(" https://github.com/owloops/cdko/issues");
process.exit(1);
});
//# sourceMappingURL=index.js.map