@go-to-k/cdkd 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -99,6 +99,8 @@ Reproduce with `./tests/benchmark/run-benchmark.sh all`. See [tests/benchmark/RE
99
99
  └── Initialize AWS clients
100
100
 
101
101
  2. Synthesis (self-implemented, no CDK CLI dependency)
102
+ ├── Short-circuit: if --app is an existing directory, treat it as a
103
+ │ pre-synthesized cloud assembly and skip the steps below
102
104
  ├── Load context (merge order, later wins):
103
105
  │ ├── CDK defaults (path-metadata, asset-metadata, version-reporting, bundling-stacks)
104
106
  │ ├── ~/.cdk.json "context" field (user defaults)
@@ -364,6 +366,9 @@ cdkd bootstrap \
364
366
  # Synthesize only
365
367
  cdkd synth --app "npx ts-node app.ts"
366
368
 
369
+ # Deploy from a pre-synthesized cloud assembly directory
370
+ cdkd deploy --app cdk.out
371
+
367
372
  # Deploy (single stack auto-detected, reads --app from cdk.json)
368
373
  cdkd deploy
369
374
 
package/dist/cli.js CHANGED
@@ -477,12 +477,16 @@ function parseContextOptions(contextArgs) {
477
477
  var commonOptions = [
478
478
  new Option("--verbose", "Enable verbose logging").default(false),
479
479
  new Option("--region <region>", "AWS region"),
480
- new Option("--profile <profile>", "AWS profile")
480
+ new Option("--profile <profile>", "AWS profile"),
481
+ new Option(
482
+ "-y, --yes",
483
+ "Automatically answer interactive prompts with the recommended response (e.g. confirm destroy)"
484
+ ).default(false)
481
485
  ];
482
486
  var appOptions = [
483
487
  new Option(
484
- "--app <command>",
485
- 'CDK app command (e.g., "npx ts-node app.ts"). Falls back to cdk.json or CDKD_APP env'
488
+ "-a, --app <command>",
489
+ 'CDK app command (e.g., "npx ts-node app.ts") or path to a pre-synthesized cloud assembly directory. Falls back to cdk.json or CDKD_APP env'
486
490
  ),
487
491
  new Option("--output <path>", "Output directory for synthesis").default("cdk.out")
488
492
  ];
@@ -517,7 +521,11 @@ var contextOptions = [
517
521
  "Set context values (can be specified multiple times)"
518
522
  )
519
523
  ];
520
- var destroyOptions = [new Option("--force", "Skip confirmation prompt").default(false)];
524
+ var destroyOptions = [
525
+ new Option("-f, --force", "Do not ask for confirmation before destroying the stacks").default(
526
+ false
527
+ )
528
+ ];
521
529
 
522
530
  // src/utils/logger.ts
523
531
  var colors = {
@@ -950,7 +958,7 @@ import { writeFileSync as writeFileSync3 } from "fs";
950
958
  import { join as join4 } from "path";
951
959
 
952
960
  // src/synthesis/synthesizer.ts
953
- import { mkdirSync } from "node:fs";
961
+ import { existsSync as existsSync3, mkdirSync, statSync } from "node:fs";
954
962
  import { resolve as resolve3 } from "node:path";
955
963
  import { GetCallerIdentityCommand as GetCallerIdentityCommand2, STSClient as STSClient2 } from "@aws-sdk/client-sts";
956
964
 
@@ -2118,6 +2126,14 @@ var Synthesizer = class {
2118
2126
  * 5. Return assembly with stacks
2119
2127
  */
2120
2128
  async synthesize(options) {
2129
+ const appPath = resolve3(options.app);
2130
+ if (existsSync3(appPath) && statSync(appPath).isDirectory()) {
2131
+ this.logger.debug(`Using pre-synthesized cloud assembly at ${appPath}`);
2132
+ const manifest = this.assemblyReader.readManifest(appPath);
2133
+ const stacks = this.assemblyReader.getAllStacks(appPath, manifest);
2134
+ this.logger.debug(`Loaded ${stacks.length} stack(s) from pre-synthesized assembly`);
2135
+ return { manifest, assemblyDir: appPath, stacks };
2136
+ }
2121
2137
  const outputDir = resolve3(options.output || "cdk.out");
2122
2138
  mkdirSync(outputDir, { recursive: true });
2123
2139
  const userCdkJson = loadUserCdkJson();
@@ -2318,7 +2334,7 @@ import { Command as Command3 } from "commander";
2318
2334
  import { readFileSync as readFileSync4 } from "node:fs";
2319
2335
 
2320
2336
  // src/assets/file-asset-publisher.ts
2321
- import { createReadStream, statSync } from "node:fs";
2337
+ import { createReadStream, statSync as statSync2 } from "node:fs";
2322
2338
  import { join as join5, basename } from "node:path";
2323
2339
  import { S3Client as S3Client2, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
2324
2340
  var FileAssetPublisher = class {
@@ -2388,7 +2404,7 @@ var FileAssetPublisher = class {
2388
2404
  * Upload a single file to S3
2389
2405
  */
2390
2406
  async uploadFile(client, filePath, bucket, key) {
2391
- const stat = statSync(filePath);
2407
+ const stat = statSync2(filePath);
2392
2408
  const stream = createReadStream(filePath);
2393
2409
  await client.send(
2394
2410
  new PutObjectCommand({
@@ -2410,7 +2426,7 @@ var FileAssetPublisher = class {
2410
2426
  archive.on("data", (chunk) => chunks.push(chunk));
2411
2427
  archive.on("end", () => resolve4(Buffer.concat(chunks)));
2412
2428
  archive.on("error", reject);
2413
- const stat = statSync(dirPath);
2429
+ const stat = statSync2(dirPath);
2414
2430
  if (stat.isDirectory()) {
2415
2431
  archive.directory(dirPath, false);
2416
2432
  } else {
@@ -3519,6 +3535,11 @@ var TemplateParser = class {
3519
3535
 
3520
3536
  // src/analyzer/dag-builder.ts
3521
3537
  var { Graph, alg } = graphlib;
3538
+ var IAM_ROLE_POLICY_TYPES = /* @__PURE__ */ new Set([
3539
+ "AWS::IAM::Policy",
3540
+ "AWS::IAM::RolePolicy",
3541
+ "AWS::IAM::ManagedPolicy"
3542
+ ]);
3522
3543
  var DagBuilder = class {
3523
3544
  logger = getLogger().child("DagBuilder");
3524
3545
  parser = new TemplateParser();
@@ -3559,6 +3580,7 @@ var DagBuilder = class {
3559
3580
  }
3560
3581
  }
3561
3582
  this.logger.debug(`Dependency graph built: ${resourceIds.length} nodes, ${edgeCount} edges`);
3583
+ edgeCount += this.addCustomResourcePolicyEdges(graph, template);
3562
3584
  if (!alg.isAcyclic(graph)) {
3563
3585
  const cycles = this.findCycles(graph);
3564
3586
  throw new DependencyError(
@@ -3701,6 +3723,123 @@ var DagBuilder = class {
3701
3723
  const deps = this.getAllDependencies(graph, resourceA);
3702
3724
  return deps.has(resourceB);
3703
3725
  }
3726
+ /**
3727
+ * Add implicit edges from IAM::Policy resources to Custom Resources whose
3728
+ * ServiceToken Lambda's execution role those policies attach to.
3729
+ *
3730
+ * Returns the number of edges added.
3731
+ */
3732
+ addCustomResourcePolicyEdges(graph, template) {
3733
+ const rolePolicies = this.buildRolePoliciesMap(template);
3734
+ if (rolePolicies.size === 0) {
3735
+ return 0;
3736
+ }
3737
+ let added = 0;
3738
+ for (const logicalId of this.parser.getResourceIds(template)) {
3739
+ const resource = this.parser.getResource(template, logicalId);
3740
+ if (!resource || !this.isCustomResourceType(resource.Type)) {
3741
+ continue;
3742
+ }
3743
+ const serviceToken = (resource.Properties ?? {})["ServiceToken"];
3744
+ const lambdaId = this.extractLogicalIdFromReference(serviceToken);
3745
+ if (!lambdaId)
3746
+ continue;
3747
+ const lambdaResource = this.parser.getResource(template, lambdaId);
3748
+ if (!lambdaResource || lambdaResource.Type !== "AWS::Lambda::Function") {
3749
+ continue;
3750
+ }
3751
+ const roleId = this.extractLogicalIdFromReference((lambdaResource.Properties ?? {})["Role"]);
3752
+ if (!roleId)
3753
+ continue;
3754
+ const policies = rolePolicies.get(roleId);
3755
+ if (!policies)
3756
+ continue;
3757
+ for (const policyId of policies) {
3758
+ if (policyId === logicalId)
3759
+ continue;
3760
+ if (!graph.hasNode(policyId))
3761
+ continue;
3762
+ if (graph.hasEdge(policyId, logicalId))
3763
+ continue;
3764
+ graph.setEdge(policyId, logicalId);
3765
+ added++;
3766
+ this.logger.debug(
3767
+ `Added implicit edge (custom resource policy): ${policyId} -> ${logicalId}`
3768
+ );
3769
+ }
3770
+ }
3771
+ if (added > 0) {
3772
+ this.logger.debug(`Added ${added} implicit edges for custom resource policies`);
3773
+ }
3774
+ return added;
3775
+ }
3776
+ isCustomResourceType(type) {
3777
+ return type === "AWS::CloudFormation::CustomResource" || type.startsWith("Custom::");
3778
+ }
3779
+ /**
3780
+ * Build a map of roleLogicalId -> Set<policyLogicalId> by scanning the
3781
+ * template for IAM::Policy / IAM::RolePolicy / IAM::ManagedPolicy resources
3782
+ * that attach to a role by Ref/GetAtt.
3783
+ */
3784
+ buildRolePoliciesMap(template) {
3785
+ const map = /* @__PURE__ */ new Map();
3786
+ for (const [policyId, resource] of Object.entries(template.Resources)) {
3787
+ if (!IAM_ROLE_POLICY_TYPES.has(resource.Type))
3788
+ continue;
3789
+ for (const roleId of this.extractAttachedRoleIds(resource)) {
3790
+ let set = map.get(roleId);
3791
+ if (!set) {
3792
+ set = /* @__PURE__ */ new Set();
3793
+ map.set(roleId, set);
3794
+ }
3795
+ set.add(policyId);
3796
+ }
3797
+ }
3798
+ return map;
3799
+ }
3800
+ /**
3801
+ * Extract the logical IDs of IAM::Role resources that a policy resource
3802
+ * attaches to. Supports both `Roles: [Ref]` (IAM::Policy / IAM::ManagedPolicy)
3803
+ * and `RoleName: Ref` (IAM::RolePolicy) shapes.
3804
+ */
3805
+ extractAttachedRoleIds(resource) {
3806
+ const ids = [];
3807
+ const props = resource.Properties ?? {};
3808
+ const roles = props["Roles"];
3809
+ if (Array.isArray(roles)) {
3810
+ for (const entry of roles) {
3811
+ const id = this.extractLogicalIdFromReference(entry);
3812
+ if (id)
3813
+ ids.push(id);
3814
+ }
3815
+ }
3816
+ const roleName = props["RoleName"];
3817
+ const roleNameId = this.extractLogicalIdFromReference(roleName);
3818
+ if (roleNameId)
3819
+ ids.push(roleNameId);
3820
+ return ids;
3821
+ }
3822
+ /**
3823
+ * Extract a resource logical ID from a direct Ref or Fn::GetAtt expression.
3824
+ * Returns undefined for literals or intrinsics we can't statically resolve
3825
+ * (Fn::Join, Fn::ImportValue, etc.) — callers should skip in that case.
3826
+ */
3827
+ extractLogicalIdFromReference(value) {
3828
+ if (typeof value !== "object" || value === null)
3829
+ return void 0;
3830
+ const obj = value;
3831
+ if ("Ref" in obj && typeof obj["Ref"] === "string") {
3832
+ const ref = obj["Ref"];
3833
+ return ref.startsWith("AWS::") ? void 0 : ref;
3834
+ }
3835
+ if ("Fn::GetAtt" in obj) {
3836
+ const getAtt = obj["Fn::GetAtt"];
3837
+ if (Array.isArray(getAtt) && typeof getAtt[0] === "string") {
3838
+ return getAtt[0];
3839
+ }
3840
+ }
3841
+ return void 0;
3842
+ }
3704
3843
  };
3705
3844
 
3706
3845
  // src/analyzer/replacement-rules.ts
@@ -26588,7 +26727,7 @@ Resources to be deleted (${resourceCount}):`);
26588
26727
  for (const [logicalId, resource] of Object.entries(currentState.resources)) {
26589
26728
  logger.info(` - ${logicalId} (${resource.resourceType})`);
26590
26729
  }
26591
- if (!options.force) {
26730
+ if (!options.yes && !options.force) {
26592
26731
  const rl = readline.createInterface({
26593
26732
  input: process.stdin,
26594
26733
  output: process.stdout
@@ -26890,7 +27029,7 @@ function reorderArgs(argv) {
26890
27029
  }
26891
27030
  async function main() {
26892
27031
  const program = new Command8();
26893
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.0.3");
27032
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.1.0");
26894
27033
  program.addCommand(createBootstrapCommand());
26895
27034
  program.addCommand(createSynthCommand());
26896
27035
  program.addCommand(createDeployCommand());