@go-to-k/cdkd 0.91.2 → 0.91.4

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/dist/cli.js CHANGED
@@ -47837,22 +47837,32 @@ var ECSProvider = class {
47837
47837
  essential: def["Essential"],
47838
47838
  command: def["Command"],
47839
47839
  entryPoint: def["EntryPoint"],
47840
- environment: def["Environment"],
47841
- environmentFiles: def["EnvironmentFiles"],
47842
- secrets: def["Secrets"],
47840
+ environment: this.convertEnvironment(
47841
+ def["Environment"]
47842
+ ),
47843
+ environmentFiles: this.convertEnvironmentFiles(
47844
+ def["EnvironmentFiles"]
47845
+ ),
47846
+ secrets: this.convertSecrets(def["Secrets"]),
47843
47847
  portMappings: this.convertPortMappings(
47844
47848
  def["PortMappings"]
47845
47849
  ),
47846
- mountPoints: def["MountPoints"],
47847
- volumesFrom: def["VolumesFrom"],
47848
- dependsOn: def["DependsOn"],
47850
+ mountPoints: this.convertMountPoints(
47851
+ def["MountPoints"]
47852
+ ),
47853
+ volumesFrom: this.convertVolumesFrom(
47854
+ def["VolumesFrom"]
47855
+ ),
47856
+ dependsOn: this.convertDependsOn(
47857
+ def["DependsOn"]
47858
+ ),
47849
47859
  links: def["Links"],
47850
47860
  workingDirectory: def["WorkingDirectory"],
47851
47861
  disableNetworking: def["DisableNetworking"],
47852
47862
  privileged: def["Privileged"],
47853
47863
  readonlyRootFilesystem: def["ReadonlyRootFilesystem"],
47854
47864
  user: def["User"],
47855
- ulimits: def["Ulimits"],
47865
+ ulimits: this.convertUlimits(def["Ulimits"]),
47856
47866
  logConfiguration: this.convertLogConfiguration(
47857
47867
  def["LogConfiguration"]
47858
47868
  ),
@@ -47881,6 +47891,103 @@ var ECSProvider = class {
47881
47891
  name: m["Name"]
47882
47892
  }));
47883
47893
  }
47894
+ /**
47895
+ * Convert CFn Environment (KeyValuePair) to ECS SDK format.
47896
+ * CFn template emits `{Name, Value}` (PascalCase); ECS SDK requires
47897
+ * `{name, value}` (camelCase). Pre-fix the cast `as KeyValuePair[]`
47898
+ * silently dropped both fields and AWS rejected RegisterTaskDefinition
47899
+ * with a generic null/empty validation error.
47900
+ */
47901
+ convertEnvironment(entries) {
47902
+ if (!entries)
47903
+ return void 0;
47904
+ return entries.map((e) => ({
47905
+ name: e["Name"],
47906
+ value: e["Value"]
47907
+ }));
47908
+ }
47909
+ /**
47910
+ * Convert CFn EnvironmentFiles (S3-backed env-var files) to ECS SDK format.
47911
+ * CFn: `{Type, Value}` → SDK: `{type, value}`.
47912
+ */
47913
+ convertEnvironmentFiles(entries) {
47914
+ if (!entries)
47915
+ return void 0;
47916
+ return entries.map((e) => ({
47917
+ type: e["Type"],
47918
+ value: e["Value"]
47919
+ }));
47920
+ }
47921
+ /**
47922
+ * Convert CFn Secrets to ECS SDK format.
47923
+ * CFn: `{Name, ValueFrom}` → SDK: `{name, valueFrom}`.
47924
+ * Same PascalCase→camelCase trap as convertEnvironment — discovered
47925
+ * end-to-end via the local-run-task-from-state integ on 2026-05-12
47926
+ * (issue #291 fixture).
47927
+ */
47928
+ convertSecrets(entries) {
47929
+ if (!entries)
47930
+ return void 0;
47931
+ return entries.map((e) => ({
47932
+ name: e["Name"],
47933
+ valueFrom: e["ValueFrom"]
47934
+ }));
47935
+ }
47936
+ /**
47937
+ * Convert CFn MountPoints to ECS SDK format.
47938
+ * CFn: `{SourceVolume, ContainerPath, ReadOnly}` → SDK: `{sourceVolume,
47939
+ * containerPath, readOnly}`.
47940
+ */
47941
+ convertMountPoints(entries) {
47942
+ if (!entries)
47943
+ return void 0;
47944
+ return entries.map((e) => ({
47945
+ sourceVolume: e["SourceVolume"],
47946
+ containerPath: e["ContainerPath"],
47947
+ readOnly: e["ReadOnly"]
47948
+ }));
47949
+ }
47950
+ /**
47951
+ * Convert CFn VolumesFrom to ECS SDK format.
47952
+ * CFn: `{SourceContainer, ReadOnly}` → SDK: `{sourceContainer, readOnly}`.
47953
+ */
47954
+ convertVolumesFrom(entries) {
47955
+ if (!entries)
47956
+ return void 0;
47957
+ return entries.map((e) => ({
47958
+ sourceContainer: e["SourceContainer"],
47959
+ readOnly: e["ReadOnly"]
47960
+ }));
47961
+ }
47962
+ /**
47963
+ * Convert CFn DependsOn to ECS SDK format.
47964
+ * CFn: `{ContainerName, Condition}` → SDK: `{containerName, condition}`.
47965
+ * The pre-existing local-run-task-multi-container integ was relying
47966
+ * on ECS SDK being lenient about the dependsOn key casing on input,
47967
+ * but per the SDK type definition the input is camelCase so this
47968
+ * converter brings the actual wire shape in line with the contract.
47969
+ */
47970
+ convertDependsOn(entries) {
47971
+ if (!entries)
47972
+ return void 0;
47973
+ return entries.map((e) => ({
47974
+ containerName: e["ContainerName"],
47975
+ condition: e["Condition"]
47976
+ }));
47977
+ }
47978
+ /**
47979
+ * Convert CFn Ulimits to ECS SDK format.
47980
+ * CFn: `{Name, SoftLimit, HardLimit}` → SDK: `{name, softLimit, hardLimit}`.
47981
+ */
47982
+ convertUlimits(entries) {
47983
+ if (!entries)
47984
+ return void 0;
47985
+ return entries.map((e) => ({
47986
+ name: e["Name"],
47987
+ softLimit: e["SoftLimit"],
47988
+ hardLimit: e["HardLimit"]
47989
+ }));
47990
+ }
47884
47991
  /**
47885
47992
  * Convert CFn LogConfiguration to ECS SDK format
47886
47993
  */
@@ -70791,7 +70898,8 @@ function isLiteralEnvValue(value) {
70791
70898
  }
70792
70899
 
70793
70900
  // src/local/state-resolver.ts
70794
- function substituteAgainstState(value, resources) {
70901
+ function substituteAgainstState(value, contextOrResources) {
70902
+ const context = isContext(contextOrResources) ? contextOrResources : { resources: contextOrResources };
70795
70903
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
70796
70904
  return { kind: "literal", value };
70797
70905
  }
@@ -70812,24 +70920,40 @@ function substituteAgainstState(value, resources) {
70812
70920
  const intrinsic = keys[0];
70813
70921
  const arg = obj[intrinsic];
70814
70922
  if (intrinsic === "Ref") {
70815
- return resolveRef(arg, resources);
70923
+ return resolveRef(arg, context);
70816
70924
  }
70817
70925
  if (intrinsic === "Fn::GetAtt") {
70818
- return resolveGetAtt(arg, resources);
70926
+ return resolveGetAtt(arg, context);
70819
70927
  }
70820
70928
  if (intrinsic === "Fn::Sub") {
70821
- return resolveSub(arg, resources);
70929
+ return resolveSub(arg, context);
70930
+ }
70931
+ if (intrinsic === "Fn::Join") {
70932
+ return resolveJoin(arg, context);
70822
70933
  }
70823
70934
  return {
70824
70935
  kind: "unresolved",
70825
- reason: `unsupported intrinsic '${intrinsic}' (only Ref, Fn::GetAtt, Fn::Sub are wired in --from-state v1)`
70936
+ reason: `unsupported intrinsic '${intrinsic}' (supported: Ref, Fn::GetAtt, Fn::Sub, Fn::Join)`
70826
70937
  };
70827
70938
  }
70828
- function resolveRef(arg, resources) {
70939
+ function isContext(v) {
70940
+ if (typeof v !== "object" || v === null)
70941
+ return false;
70942
+ const r = v["resources"];
70943
+ if (r === void 0)
70944
+ return false;
70945
+ if (typeof r !== "object" || r === null)
70946
+ return false;
70947
+ return !("physicalId" in r);
70948
+ }
70949
+ function resolveRef(arg, context) {
70829
70950
  if (typeof arg !== "string" || arg.length === 0) {
70830
70951
  return { kind: "unresolved", reason: `Ref expects a non-empty logical ID, got ${typeof arg}` };
70831
70952
  }
70832
- const resource = resources[arg];
70953
+ if (arg.startsWith("AWS::")) {
70954
+ return resolvePseudoParameter(arg, context.pseudoParameters);
70955
+ }
70956
+ const resource = context.resources[arg];
70833
70957
  if (!resource) {
70834
70958
  return {
70835
70959
  kind: "unresolved",
@@ -70838,7 +70962,39 @@ function resolveRef(arg, resources) {
70838
70962
  }
70839
70963
  return { kind: "literal", value: resource.physicalId };
70840
70964
  }
70841
- function resolveGetAtt(arg, resources) {
70965
+ function resolvePseudoParameter(name, pseudo) {
70966
+ if (!pseudo) {
70967
+ return {
70968
+ kind: "unresolved",
70969
+ reason: `Ref '${name}': pseudo parameter not supplied (need --from-state context)`
70970
+ };
70971
+ }
70972
+ switch (name) {
70973
+ case "AWS::AccountId":
70974
+ if (pseudo.accountId !== void 0)
70975
+ return { kind: "literal", value: pseudo.accountId };
70976
+ break;
70977
+ case "AWS::Region":
70978
+ if (pseudo.region !== void 0)
70979
+ return { kind: "literal", value: pseudo.region };
70980
+ break;
70981
+ case "AWS::Partition":
70982
+ if (pseudo.partition !== void 0)
70983
+ return { kind: "literal", value: pseudo.partition };
70984
+ break;
70985
+ case "AWS::URLSuffix":
70986
+ if (pseudo.urlSuffix !== void 0)
70987
+ return { kind: "literal", value: pseudo.urlSuffix };
70988
+ break;
70989
+ default:
70990
+ return {
70991
+ kind: "unresolved",
70992
+ reason: `Ref '${name}': pseudo parameter not supported (supported: AWS::AccountId, AWS::Region, AWS::Partition, AWS::URLSuffix)`
70993
+ };
70994
+ }
70995
+ return { kind: "unresolved", reason: `Ref '${name}': pseudo parameter value not resolved` };
70996
+ }
70997
+ function resolveGetAtt(arg, context) {
70842
70998
  let logicalId;
70843
70999
  let attr;
70844
71000
  if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string") {
@@ -70846,7 +71002,7 @@ function resolveGetAtt(arg, resources) {
70846
71002
  if (typeof arg[1] !== "string") {
70847
71003
  return {
70848
71004
  kind: "unresolved",
70849
- reason: `Fn::GetAtt's second arg must be a string attribute name, got ${typeof arg[1]} (nested intrinsics in attribute names are not supported in --from-state v1)`
71005
+ reason: `Fn::GetAtt's second arg must be a string attribute name, got ${typeof arg[1]} (nested intrinsics in attribute names are not supported)`
70850
71006
  };
70851
71007
  }
70852
71008
  attr = arg[1];
@@ -70866,7 +71022,7 @@ function resolveGetAtt(arg, resources) {
70866
71022
  reason: `Fn::GetAtt expects [LogicalId, Attribute] or 'LogicalId.Attribute', got ${Array.isArray(arg) ? `array of length ${arg.length}` : typeof arg}`
70867
71023
  };
70868
71024
  }
70869
- const resource = resources[logicalId];
71025
+ const resource = context.resources[logicalId];
70870
71026
  if (!resource) {
70871
71027
  return {
70872
71028
  kind: "unresolved",
@@ -70885,7 +71041,7 @@ function resolveGetAtt(arg, resources) {
70885
71041
  }
70886
71042
  return { kind: "literal", value: JSON.stringify(cached) };
70887
71043
  }
70888
- function resolveSub(arg, resources) {
71044
+ function resolveSub(arg, context) {
70889
71045
  let template;
70890
71046
  let bindings = {};
70891
71047
  if (typeof arg === "string") {
@@ -70910,7 +71066,18 @@ function resolveSub(arg, resources) {
70910
71066
  if (resolutions.has(placeholder))
70911
71067
  continue;
70912
71068
  if (placeholder in bindings) {
70913
- const sub = substituteAgainstState(bindings[placeholder], resources);
71069
+ const sub = substituteAgainstState(bindings[placeholder], context);
71070
+ if (sub.kind !== "literal") {
71071
+ return {
71072
+ kind: "unresolved",
71073
+ reason: `Fn::Sub placeholder '\${${placeholder}}': ${sub.reason}`
71074
+ };
71075
+ }
71076
+ resolutions.set(placeholder, String(sub.value));
71077
+ continue;
71078
+ }
71079
+ if (placeholder.startsWith("AWS::")) {
71080
+ const sub = resolvePseudoParameter(placeholder, context.pseudoParameters);
70914
71081
  if (sub.kind !== "literal") {
70915
71082
  return {
70916
71083
  kind: "unresolved",
@@ -70922,7 +71089,7 @@ function resolveSub(arg, resources) {
70922
71089
  }
70923
71090
  const dot = placeholder.indexOf(".");
70924
71091
  if (dot === -1) {
70925
- const sub = resolveRef(placeholder, resources);
71092
+ const sub = resolveRef(placeholder, context);
70926
71093
  if (sub.kind !== "literal") {
70927
71094
  return {
70928
71095
  kind: "unresolved",
@@ -70931,7 +71098,7 @@ function resolveSub(arg, resources) {
70931
71098
  }
70932
71099
  resolutions.set(placeholder, String(sub.value));
70933
71100
  } else {
70934
- const sub = resolveGetAtt(placeholder, resources);
71101
+ const sub = resolveGetAtt(placeholder, context);
70935
71102
  if (sub.kind !== "literal") {
70936
71103
  return {
70937
71104
  kind: "unresolved",
@@ -70946,17 +71113,45 @@ function resolveSub(arg, resources) {
70946
71113
  });
70947
71114
  return { kind: "literal", value: out };
70948
71115
  }
70949
- function substituteEnvVarsFromState(templateEnv, resources) {
71116
+ function resolveJoin(arg, context) {
71117
+ if (!Array.isArray(arg) || arg.length !== 2 || !Array.isArray(arg[1])) {
71118
+ return {
71119
+ kind: "unresolved",
71120
+ reason: `Fn::Join expects [delimiter, [elements]], got ${Array.isArray(arg) ? `array of length ${arg.length}` : typeof arg}`
71121
+ };
71122
+ }
71123
+ const [delimiterRaw, elements] = arg;
71124
+ if (typeof delimiterRaw !== "string") {
71125
+ return {
71126
+ kind: "unresolved",
71127
+ reason: `Fn::Join delimiter must be a string, got ${typeof delimiterRaw}`
71128
+ };
71129
+ }
71130
+ const parts = [];
71131
+ for (let i = 0; i < elements.length; i += 1) {
71132
+ const sub = substituteAgainstState(elements[i], context);
71133
+ if (sub.kind !== "literal") {
71134
+ return {
71135
+ kind: "unresolved",
71136
+ reason: `Fn::Join element [${i}]: ${sub.reason}`
71137
+ };
71138
+ }
71139
+ parts.push(String(sub.value));
71140
+ }
71141
+ return { kind: "literal", value: parts.join(delimiterRaw) };
71142
+ }
71143
+ function substituteEnvVarsFromState(templateEnv, contextOrResources) {
70950
71144
  const env = {};
70951
71145
  const audit = { resolvedKeys: [], unresolved: [] };
70952
71146
  if (!templateEnv)
70953
71147
  return { env, audit };
71148
+ const context = isContext(contextOrResources) ? contextOrResources : { resources: contextOrResources };
70954
71149
  for (const [key, value] of Object.entries(templateEnv)) {
70955
71150
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
70956
71151
  env[key] = value;
70957
71152
  continue;
70958
71153
  }
70959
- const result = substituteAgainstState(value, resources);
71154
+ const result = substituteAgainstState(value, context);
70960
71155
  if (result.kind === "literal") {
70961
71156
  env[key] = result.value;
70962
71157
  audit.resolvedKeys.push(key);
@@ -77063,6 +77258,7 @@ function detectEcsImageResolutionNeeds(stack) {
77063
77258
  const resources = stack.template.Resources ?? {};
77064
77259
  let needsPseudoParameters = false;
77065
77260
  let needsStateResources = false;
77261
+ let needsEnvOrSecretSubstitution = false;
77066
77262
  for (const res of Object.values(resources)) {
77067
77263
  if (res.Type !== "AWS::ECS::TaskDefinition")
77068
77264
  continue;
@@ -77071,19 +77267,42 @@ function detectEcsImageResolutionNeeds(stack) {
77071
77267
  for (const c of containers) {
77072
77268
  if (!c || typeof c !== "object")
77073
77269
  continue;
77074
- const image = c["Image"];
77270
+ const co = c;
77271
+ const image = co["Image"];
77075
77272
  const need = inspectImageForSubstitutions(image, resources);
77076
77273
  if (need.pseudo)
77077
77274
  needsPseudoParameters = true;
77078
77275
  if (need.state)
77079
77276
  needsStateResources = true;
77080
- if (needsPseudoParameters && needsStateResources)
77081
- break;
77277
+ if (containerHasIntrinsicEnvOrSecret(co))
77278
+ needsEnvOrSecretSubstitution = true;
77279
+ }
77280
+ }
77281
+ return { needsPseudoParameters, needsStateResources, needsEnvOrSecretSubstitution };
77282
+ }
77283
+ function containerHasIntrinsicEnvOrSecret(c) {
77284
+ const env = c["Environment"];
77285
+ if (Array.isArray(env)) {
77286
+ for (const entry of env) {
77287
+ if (!entry || typeof entry !== "object")
77288
+ continue;
77289
+ const v = entry["Value"];
77290
+ if (v !== void 0 && typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean") {
77291
+ return true;
77292
+ }
77082
77293
  }
77083
- if (needsPseudoParameters && needsStateResources)
77084
- break;
77085
77294
  }
77086
- return { needsPseudoParameters, needsStateResources };
77295
+ const secrets = c["Secrets"];
77296
+ if (Array.isArray(secrets)) {
77297
+ for (const entry of secrets) {
77298
+ if (!entry || typeof entry !== "object")
77299
+ continue;
77300
+ const v = entry["ValueFrom"];
77301
+ if (v !== void 0 && typeof v !== "string")
77302
+ return true;
77303
+ }
77304
+ }
77305
+ return false;
77087
77306
  }
77088
77307
  function inspectImageForSubstitutions(image, resources) {
77089
77308
  if (!image || typeof image !== "object")
@@ -77284,6 +77503,11 @@ function extractTaskDefinitionProperties(stack, logicalId, resource, context) {
77284
77503
  const containers = rawContainers.map(
77285
77504
  (c, idx) => parseContainerDefinition(c, idx, logicalId, resources, stack, context)
77286
77505
  );
77506
+ for (const ctr of containers) {
77507
+ for (const w of ctr.warnings) {
77508
+ warnings.push(`Container '${ctr.name}': ${w}`);
77509
+ }
77510
+ }
77287
77511
  const rawVolumes = props["Volumes"];
77288
77512
  const volumes = Array.isArray(rawVolumes) ? rawVolumes.map((v, idx) => parseVolume(v, idx, logicalId)) : [];
77289
77513
  const containerNames = new Set(containers.map((c) => c.name));
@@ -77345,7 +77569,9 @@ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack, con
77345
77569
  const command = pickStringArray2(c["Command"]);
77346
77570
  const entryPoint = pickStringArray2(c["EntryPoint"]);
77347
77571
  const workingDirectory = pickString(c["WorkingDirectory"]);
77572
+ const subContext = buildSubstitutionContextFromImageContext(context);
77348
77573
  const environment = {};
77574
+ const droppedEnvKeys = [];
77349
77575
  if (Array.isArray(c["Environment"])) {
77350
77576
  for (const entry of c["Environment"]) {
77351
77577
  if (!entry || typeof entry !== "object")
@@ -77357,19 +77583,54 @@ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack, con
77357
77583
  continue;
77358
77584
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
77359
77585
  environment[key] = String(value);
77586
+ continue;
77587
+ }
77588
+ if (subContext) {
77589
+ const sub = substituteAgainstState(value, subContext);
77590
+ if (sub.kind === "literal") {
77591
+ environment[key] = String(sub.value);
77592
+ continue;
77593
+ }
77594
+ droppedEnvKeys.push({ key, reason: sub.reason });
77595
+ } else {
77596
+ droppedEnvKeys.push({
77597
+ key,
77598
+ reason: "intrinsic-valued; pass --from-state to substitute against deployed state"
77599
+ });
77360
77600
  }
77361
77601
  }
77362
77602
  }
77363
77603
  const secrets = [];
77604
+ const droppedSecretKeys = [];
77364
77605
  if (Array.isArray(c["Secrets"])) {
77365
77606
  for (const entry of c["Secrets"]) {
77366
77607
  if (!entry || typeof entry !== "object")
77367
77608
  continue;
77368
77609
  const e = entry;
77369
77610
  const sName = pickString(e["Name"]);
77370
- const valueFrom = pickString(e["ValueFrom"]);
77371
- if (sName && valueFrom)
77372
- secrets.push({ name: sName, valueFrom });
77611
+ const valueFromRaw = e["ValueFrom"];
77612
+ if (!sName)
77613
+ continue;
77614
+ if (typeof valueFromRaw === "string" && valueFromRaw.length > 0) {
77615
+ secrets.push({ name: sName, valueFrom: valueFromRaw });
77616
+ continue;
77617
+ }
77618
+ if (subContext) {
77619
+ const sub = substituteAgainstState(valueFromRaw, subContext);
77620
+ if (sub.kind === "literal" && typeof sub.value === "string" && sub.value.length > 0) {
77621
+ secrets.push({ name: sName, valueFrom: sub.value });
77622
+ continue;
77623
+ }
77624
+ droppedSecretKeys.push({
77625
+ key: sName,
77626
+ reason: sub.kind === "literal" ? "resolved to non-string / empty value" : sub.reason
77627
+ });
77628
+ } else {
77629
+ droppedSecretKeys.push({
77630
+ key: sName,
77631
+ reason: "intrinsic-valued ValueFrom; pass --from-state to resolve the deployed ARN"
77632
+ });
77633
+ }
77373
77634
  }
77374
77635
  }
77375
77636
  const portMappings = [];
@@ -77459,6 +77720,13 @@ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack, con
77459
77720
  ulimits.push({ name: uName, softLimit: soft, hardLimit: hard });
77460
77721
  }
77461
77722
  }
77723
+ const warnings = [];
77724
+ for (const d of droppedEnvKeys) {
77725
+ warnings.push(`Environment '${d.key}' dropped: ${d.reason}`);
77726
+ }
77727
+ for (const d of droppedSecretKeys) {
77728
+ warnings.push(`Secret '${d.key}' dropped: ${d.reason}`);
77729
+ }
77462
77730
  const out = {
77463
77731
  name,
77464
77732
  image,
@@ -77469,7 +77737,8 @@ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack, con
77469
77737
  dependsOn,
77470
77738
  links,
77471
77739
  essential,
77472
- ulimits
77740
+ ulimits,
77741
+ warnings
77473
77742
  };
77474
77743
  if (command !== void 0)
77475
77744
  out.command = command;
@@ -77487,6 +77756,15 @@ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack, con
77487
77756
  out.readonlyRootFilesystem = readonlyRootFilesystem;
77488
77757
  return out;
77489
77758
  }
77759
+ function buildSubstitutionContextFromImageContext(context) {
77760
+ if (!context?.stateResources)
77761
+ return void 0;
77762
+ const subContext = { resources: context.stateResources };
77763
+ if (context.pseudoParameters) {
77764
+ subContext.pseudoParameters = { ...context.pseudoParameters };
77765
+ }
77766
+ return subContext;
77767
+ }
77490
77768
  function parseContainerImage(raw, containerName, taskLogicalId, resources, _stack, context) {
77491
77769
  const getAttImage = tryResolveImageGetAtt(raw, resources, context);
77492
77770
  if (getAttImage) {
@@ -78681,14 +78959,16 @@ async function buildEcsImageResolutionContext(target, stacks, options) {
78681
78959
  if (!candidate)
78682
78960
  return void 0;
78683
78961
  const needs = detectEcsImageResolutionNeeds(candidate);
78684
- if (!needs.needsPseudoParameters && !needs.needsStateResources)
78962
+ if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) {
78685
78963
  return void 0;
78964
+ }
78686
78965
  const ctx = {};
78687
- if (needs.needsPseudoParameters) {
78966
+ const wantsPseudoForEnvOrSecret = options.fromState && needs.needsEnvOrSecretSubstitution;
78967
+ if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
78688
78968
  const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
78689
78969
  if (!region) {
78690
78970
  logger.warn(
78691
- "Container Image references ${AWS::Region} but cdkd could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack."
78971
+ "Resolver references ${AWS::Region} but cdkd could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack."
78692
78972
  );
78693
78973
  }
78694
78974
  let accountId;
@@ -78696,7 +78976,7 @@ async function buildEcsImageResolutionContext(target, stacks, options) {
78696
78976
  accountId = await resolveCallerAccountId(region);
78697
78977
  } catch (err) {
78698
78978
  logger.warn(
78699
- `Container Image references \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; the resolver will surface its existing error.`
78979
+ `Resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; affected env / secret entries will be dropped with per-key warnings.`
78700
78980
  );
78701
78981
  }
78702
78982
  const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
@@ -78709,7 +78989,8 @@ async function buildEcsImageResolutionContext(target, stacks, options) {
78709
78989
  }
78710
78990
  };
78711
78991
  }
78712
- if (options.fromState && needs.needsStateResources) {
78992
+ const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
78993
+ if (options.fromState && wantsState) {
78713
78994
  const loaded = await loadStateForStack(candidate.stackName, candidate.region, {
78714
78995
  ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
78715
78996
  ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
@@ -78724,6 +79005,10 @@ async function buildEcsImageResolutionContext(target, stacks, options) {
78724
79005
  logger.warn(
78725
79006
  "Container Image references a same-stack AWS::ECR::Repository. Pass --from-state to substitute the deployed repository URI (requires the stack to have been deployed via cdkd deploy). Otherwise the resolver will surface its existing error."
78726
79007
  );
79008
+ } else if (!options.fromState && needs.needsEnvOrSecretSubstitution) {
79009
+ logger.warn(
79010
+ "Container Environment / Secrets entries contain CloudFormation intrinsics (Ref / Fn::GetAtt / Fn::Sub / Fn::Join). Pass --from-state to substitute them against the deployed cdkd state. Without --from-state these entries are dropped (per-key warnings will follow)."
79011
+ );
78727
79012
  }
78728
79013
  return ctx;
78729
79014
  }
@@ -79330,6 +79615,7 @@ import { Command as Command17 } from "commander";
79330
79615
  import {
79331
79616
  CreateChangeSetCommand,
79332
79617
  DescribeChangeSetCommand,
79618
+ DescribeStackEventsCommand,
79333
79619
  ExecuteChangeSetCommand,
79334
79620
  DescribeStacksCommand as DescribeStacksCommand2,
79335
79621
  DescribeTypeCommand,
@@ -79346,10 +79632,13 @@ var NEVER_IMPORTABLE_TYPES = /* @__PURE__ */ new Set([
79346
79632
  function isNeverImportableType(resourceType) {
79347
79633
  if (NEVER_IMPORTABLE_TYPES.has(resourceType))
79348
79634
  return true;
79349
- if (resourceType.startsWith("Custom::"))
79635
+ if (isCustomResourceType(resourceType))
79350
79636
  return true;
79351
79637
  return false;
79352
79638
  }
79639
+ function isCustomResourceType(resourceType) {
79640
+ return resourceType === "AWS::CloudFormation::CustomResource" || resourceType.startsWith("Custom::");
79641
+ }
79353
79642
  var PRIMARY_IDENTIFIER_FALLBACK = {
79354
79643
  "AWS::S3::Bucket": "BucketName",
79355
79644
  "AWS::IAM::Role": "RoleName",
@@ -79613,6 +79902,12 @@ async function exportCommand(stackArg, options) {
79613
79902
  );
79614
79903
  }
79615
79904
  const phase1Template = filterTemplateForImport(template, phase1Imports);
79905
+ const injectedCount = injectDeletionPolicyForImport(phase1Template);
79906
+ if (injectedCount > 0) {
79907
+ logger.info(
79908
+ `Injected DeletionPolicy: Retain on ${injectedCount} resource(s) without an explicit DeletionPolicy (required by CFn IMPORT). The first \`cdk deploy\` after export will reset each to your CDK-declared value.`
79909
+ );
79910
+ }
79616
79911
  await executeImportChangeSet(
79617
79912
  awsClients.cloudFormation,
79618
79913
  cfnStackName,
@@ -79744,7 +80039,7 @@ async function assertCfnStackAbsent(cfnClient, stackName) {
79744
80039
  }
79745
80040
  }
79746
80041
  function isPhase2CreatableType(resourceType) {
79747
- return resourceType.startsWith("Custom::");
80042
+ return isCustomResourceType(resourceType);
79748
80043
  }
79749
80044
  async function buildImportPlan(state, template, cfnClient) {
79750
80045
  const templateResources = template["Resources"];
@@ -79926,32 +80221,49 @@ function resolveTemplateParameters(template, userOverrides) {
79926
80221
  }
79927
80222
  return { parameters, missing };
79928
80223
  }
80224
+ function injectDeletionPolicyForImport(template) {
80225
+ const resources = template["Resources"];
80226
+ if (!resources || typeof resources !== "object" || Array.isArray(resources)) {
80227
+ return 0;
80228
+ }
80229
+ let injected = 0;
80230
+ for (const [, resource] of Object.entries(resources)) {
80231
+ if (!resource || typeof resource !== "object" || Array.isArray(resource))
80232
+ continue;
80233
+ const r = resource;
80234
+ if (r["DeletionPolicy"] === void 0) {
80235
+ r["DeletionPolicy"] = "Retain";
80236
+ injected++;
80237
+ }
80238
+ }
80239
+ return injected;
80240
+ }
79929
80241
  function filterTemplateForImport(template, plan) {
79930
- const allow = new Set(plan.map((p) => p.logicalId));
80242
+ const allow = new Map(plan.map((p) => [p.logicalId, p]));
79931
80243
  const original = template["Resources"];
79932
80244
  const filteredResources = {};
79933
80245
  for (const [logicalId, resource] of Object.entries(original)) {
79934
- if (allow.has(logicalId)) {
79935
- filteredResources[logicalId] = resource;
79936
- }
80246
+ const entry = allow.get(logicalId);
80247
+ if (!entry)
80248
+ continue;
80249
+ filteredResources[logicalId] = overlayResourceIdentifierOnProperties(resource, entry);
79937
80250
  }
79938
80251
  const result = { ...template, Resources: filteredResources };
79939
- const outputs = template["Outputs"];
79940
- if (outputs && typeof outputs === "object" && !Array.isArray(outputs)) {
79941
- const filteredOutputs = {};
79942
- for (const [name, output] of Object.entries(outputs)) {
79943
- if (referencesOnly(output, allow)) {
79944
- filteredOutputs[name] = output;
79945
- }
79946
- }
79947
- if (Object.keys(filteredOutputs).length > 0) {
79948
- result["Outputs"] = filteredOutputs;
79949
- } else {
79950
- delete result["Outputs"];
79951
- }
79952
- }
80252
+ delete result["Outputs"];
79953
80253
  return result;
79954
80254
  }
80255
+ function overlayResourceIdentifierOnProperties(resource, entry) {
80256
+ if (!resource || typeof resource !== "object" || Array.isArray(resource)) {
80257
+ return resource;
80258
+ }
80259
+ const r = resource;
80260
+ const existingProperties = r["Properties"];
80261
+ const properties = existingProperties && typeof existingProperties === "object" && !Array.isArray(existingProperties) ? { ...existingProperties } : {};
80262
+ for (const [field, value] of Object.entries(entry.resourceIdentifier)) {
80263
+ properties[field] = value;
80264
+ }
80265
+ return { ...r, Properties: properties };
80266
+ }
79955
80267
  function reportDriftBaselineGaps(state, logger) {
79956
80268
  const entries = Object.entries(state.resources ?? {});
79957
80269
  if (entries.length === 0)
@@ -80022,29 +80334,6 @@ function walkForGetStackOutput(node, path3, emit) {
80022
80334
  walkForGetStackOutput(value, path3 ? `${path3}.${key}` : key, emit);
80023
80335
  }
80024
80336
  }
80025
- function referencesOnly(node, allow) {
80026
- if (!node || typeof node !== "object")
80027
- return true;
80028
- if (Array.isArray(node)) {
80029
- return node.every((item) => referencesOnly(item, allow));
80030
- }
80031
- for (const [key, value] of Object.entries(node)) {
80032
- if (key === "Ref" && typeof value === "string") {
80033
- if (!allow.has(value))
80034
- return false;
80035
- continue;
80036
- }
80037
- if (key === "Fn::GetAtt") {
80038
- const target = Array.isArray(value) && typeof value[0] === "string" ? value[0] : typeof value === "string" ? value.split(".")[0] : void 0;
80039
- if (target && !allow.has(target))
80040
- return false;
80041
- continue;
80042
- }
80043
- if (!referencesOnly(value, allow))
80044
- return false;
80045
- }
80046
- return true;
80047
- }
80048
80337
  function printPlan(plan, cfnStackName) {
80049
80338
  const logger = getLogger();
80050
80339
  logger.info("");
@@ -80122,11 +80411,41 @@ async function executeImportChangeSet(cfnClient, stackName, template, plan, para
80122
80411
  { StackName: stackName }
80123
80412
  );
80124
80413
  } catch (err) {
80414
+ const failureSummary = await collectImportFailureSummary(cfnClient, stackName).catch(() => "");
80125
80415
  await cfnClient.send(new DeleteChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })).catch(() => {
80126
80416
  });
80417
+ if (failureSummary) {
80418
+ throw new Error(`IMPORT changeset failed:
80419
+ ${failureSummary}`, { cause: err });
80420
+ }
80127
80421
  throw err;
80128
80422
  }
80129
80423
  }
80424
+ async function collectImportFailureSummary(cfnClient, stackName) {
80425
+ const resp = await cfnClient.send(new DescribeStackEventsCommand({ StackName: stackName }));
80426
+ const events = resp.StackEvents ?? [];
80427
+ const failures = [];
80428
+ const seen = /* @__PURE__ */ new Set();
80429
+ for (const e of events) {
80430
+ if (!e.ResourceStatus || !e.ResourceStatus.endsWith("FAILED"))
80431
+ continue;
80432
+ if (!e.LogicalResourceId)
80433
+ continue;
80434
+ if (seen.has(e.LogicalResourceId))
80435
+ continue;
80436
+ seen.add(e.LogicalResourceId);
80437
+ failures.push({
80438
+ logicalId: e.LogicalResourceId,
80439
+ type: e.ResourceType ?? "<unknown>",
80440
+ reason: e.ResourceStatusReason ?? "<no reason reported>"
80441
+ });
80442
+ if (failures.length >= 5)
80443
+ break;
80444
+ }
80445
+ if (failures.length === 0)
80446
+ return "";
80447
+ return failures.map((f) => ` - ${f.logicalId} (${f.type}): ${f.reason}`).join("\n");
80448
+ }
80130
80449
  async function executeUpdateChangeSet(cfnClient, stackName, template, parameters) {
80131
80450
  const logger = getLogger();
80132
80451
  const changeSetName = `cdkd-phase2-${Date.now()}`;
@@ -80308,7 +80627,7 @@ function reorderArgs(argv) {
80308
80627
  }
80309
80628
  async function main() {
80310
80629
  const program = new Command18();
80311
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.91.2");
80630
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.91.4");
80312
80631
  program.addCommand(createBootstrapCommand());
80313
80632
  program.addCommand(createSynthCommand());
80314
80633
  program.addCommand(createListCommand());