@go-to-k/cdkd 0.147.2 → 0.148.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
@@ -306,6 +306,8 @@ cdkd state info --state-bucket my-bucket # explicit bucket; reports Source: --s
306
306
  cdkd state list
307
307
  cdkd state ls --long # include resource count, last-modified, lock status
308
308
  cdkd state list --json # JSON output (alone, or combined with --long)
309
+ cdkd state list --tree # parent → child stack tree (nested stacks; #555 A3)
310
+ cdkd state list --tree --json # tree as nested JSON
309
311
 
310
312
  # List resources of a single stack from state
311
313
  cdkd state resources MyStack # aligned columns: LogicalID, Type, PhysicalID
package/dist/cli.js CHANGED
@@ -34897,6 +34897,95 @@ function createStateMigrateCommand() {
34897
34897
  return cmd;
34898
34898
  }
34899
34899
 
34900
+ //#endregion
34901
+ //#region src/cli/commands/state-list-tree.ts
34902
+ /**
34903
+ * Build a parent → child tree from the flat list of state records.
34904
+ *
34905
+ * Children are linked to their parent by `(parentStack, parentRegion)`
34906
+ * matching another entry's `(stackName, region)`. Children whose parent
34907
+ * isn't present in the input (orphans — parent state was hand-deleted,
34908
+ * or destroyed out-of-band) are reported at the root level so they stay
34909
+ * visible to `cdkd state list` rather than vanishing silently.
34910
+ *
34911
+ * A self-link (parent equals self) is treated as a missing parent — the
34912
+ * node lands at the root rather than building an infinite tree.
34913
+ *
34914
+ * The roots and every child list are sorted alphabetically by `stackName`,
34915
+ * then by `region` (legacy `undefined` last), so output is stable across
34916
+ * runs.
34917
+ */
34918
+ function buildStackTree(entries) {
34919
+ const refKey = (stackName, region) => `${stackName}\0${region ?? ""}`;
34920
+ const byKey = /* @__PURE__ */ new Map();
34921
+ for (const entry of entries) byKey.set(refKey(entry.stackName, entry.region), {
34922
+ ...entry,
34923
+ children: []
34924
+ });
34925
+ const roots = [];
34926
+ for (const node of byKey.values()) {
34927
+ if (node.parentStack !== void 0) {
34928
+ const parent = byKey.get(refKey(node.parentStack, node.parentRegion));
34929
+ if (parent && parent !== node) {
34930
+ parent.children.push(node);
34931
+ continue;
34932
+ }
34933
+ }
34934
+ roots.push(node);
34935
+ }
34936
+ const cmp = (a, b) => {
34937
+ if (a.stackName < b.stackName) return -1;
34938
+ if (a.stackName > b.stackName) return 1;
34939
+ const ar = a.region ?? "￿";
34940
+ const br = b.region ?? "￿";
34941
+ if (ar < br) return -1;
34942
+ if (ar > br) return 1;
34943
+ return 0;
34944
+ };
34945
+ const sortRecursive = (list) => {
34946
+ list.sort(cmp);
34947
+ for (const node of list) sortRecursive(node.children);
34948
+ };
34949
+ sortRecursive(roots);
34950
+ return roots;
34951
+ }
34952
+ /**
34953
+ * Render the tree using `tree(1)`-style box-drawing prefixes.
34954
+ *
34955
+ * Each line is built by `formatLine(node)`. The caller picks the
34956
+ * human-readable shape (e.g. `Stack (region)`); this helper only owns the
34957
+ * indentation.
34958
+ */
34959
+ function renderStackTreeAscii(roots, formatLine) {
34960
+ const lines = [];
34961
+ for (const root of roots) {
34962
+ lines.push(formatLine(root));
34963
+ renderChildren(root.children, "", lines, formatLine);
34964
+ }
34965
+ return lines.join("\n");
34966
+ }
34967
+ function renderChildren(children, prefix, out, formatLine) {
34968
+ const lastIdx = children.length - 1;
34969
+ for (let i = 0; i < children.length; i++) {
34970
+ const child = children[i];
34971
+ const isLast = i === lastIdx;
34972
+ const branch = isLast ? "└── " : "├── ";
34973
+ const childPrefix = isLast ? " " : "│ ";
34974
+ out.push(`${prefix}${branch}${formatLine(child)}`);
34975
+ renderChildren(child.children, prefix + childPrefix, out, formatLine);
34976
+ }
34977
+ }
34978
+ function stackTreeToJson(roots) {
34979
+ return roots.map((node) => ({
34980
+ stackName: node.stackName,
34981
+ region: node.region ?? null,
34982
+ parentStack: node.parentStack ?? null,
34983
+ parentLogicalId: node.parentLogicalId ?? null,
34984
+ parentRegion: node.parentRegion ?? null,
34985
+ children: stackTreeToJson(node.children)
34986
+ }));
34987
+ }
34988
+
34900
34989
  //#endregion
34901
34990
  //#region src/cli/commands/state.ts
34902
34991
  /**
@@ -35001,6 +35090,10 @@ function sortRefs(refs) {
35001
35090
  * `version: 1` records (no region) appear as plain `Stack` rows.
35002
35091
  * - `--long`/`-l`: include resource count, last-modified time, and lock status.
35003
35092
  * - `--json`: emit a JSON array (alongside or instead of the long form).
35093
+ * - `--tree`: render parent → child stack tree (issue #555 A3). Loads each
35094
+ * state record to read the v6 `parentStack` / `parentRegion` fields, then
35095
+ * reconstructs the hierarchy. Flat default is preserved so tooling that
35096
+ * greps the existing one-per-line shape keeps working.
35004
35097
  */
35005
35098
  async function stateListCommand(options) {
35006
35099
  const logger = getLogger();
@@ -35008,6 +35101,10 @@ async function stateListCommand(options) {
35008
35101
  const setup = await setupStateBackend(options);
35009
35102
  try {
35010
35103
  const refs = sortRefs(await setup.stateBackend.listStacks());
35104
+ if (options.tree) {
35105
+ await renderTreeMode(refs, setup.stateBackend, options.json);
35106
+ return;
35107
+ }
35011
35108
  if (!options.long && !options.json) {
35012
35109
  for (const ref of refs) process.stdout.write(`${formatStackRef(ref)}\n`);
35013
35110
  return;
@@ -35057,10 +35154,56 @@ async function stateListCommand(options) {
35057
35154
  }
35058
35155
  }
35059
35156
  /**
35157
+ * Render the parent → child stack tree for `cdkd state list --tree`.
35158
+ *
35159
+ * Loads each state record in parallel to read the v6 `parentStack` /
35160
+ * `parentLogicalId` / `parentRegion` fields, then hands the enriched flat
35161
+ * list to {@link buildStackTree}. A missing state record (NoSuchKey returns
35162
+ * `null` from getState) OR an unreadable one (transient S3 503 / throttle /
35163
+ * per-stack IAM hiccup throws) degrades to a top-level entry (no parent
35164
+ * link) — the row still appears in the tree at the root level rather than
35165
+ * vanishing, and one bad record never kills the whole view.
35166
+ *
35167
+ * `tree --json` emits the nested {@link import('./state-list-tree.js').StackTreeJson}
35168
+ * shape; plain `tree` renders `tree(1)`-style box-drawing.
35169
+ */
35170
+ async function renderTreeMode(refs, stateBackend, asJson) {
35171
+ const roots = buildStackTree(await Promise.all(refs.map(async (ref) => {
35172
+ if (!ref.region) return { stackName: ref.stackName };
35173
+ let state;
35174
+ try {
35175
+ state = (await stateBackend.getState(ref.stackName, ref.region))?.state;
35176
+ } catch {
35177
+ return {
35178
+ stackName: ref.stackName,
35179
+ region: ref.region
35180
+ };
35181
+ }
35182
+ return {
35183
+ stackName: ref.stackName,
35184
+ region: ref.region,
35185
+ ...state?.parentStack !== void 0 && { parentStack: state.parentStack },
35186
+ ...state?.parentLogicalId !== void 0 && { parentLogicalId: state.parentLogicalId },
35187
+ ...state?.parentRegion !== void 0 && { parentRegion: state.parentRegion }
35188
+ };
35189
+ })));
35190
+ if (asJson) {
35191
+ process.stdout.write(`${JSON.stringify(stackTreeToJson(roots), null, 2)}\n`);
35192
+ return;
35193
+ }
35194
+ if (roots.length === 0) return;
35195
+ const rendered = renderStackTreeAscii(roots, (node) => formatStackRef({
35196
+ stackName: node.stackName,
35197
+ ...node.region ? { region: node.region } : {}
35198
+ }));
35199
+ process.stdout.write(`${rendered}\n`);
35200
+ }
35201
+ /**
35060
35202
  * Create the `state list` subcommand.
35061
35203
  */
35062
35204
  function createStateListCommand() {
35063
- const cmd = new Command("list").alias("ls").description("List stacks registered in the cdkd state bucket").option("-l, --long", "Show resource count, last-modified time, and lock status", false).option("--json", "Output as JSON", false).action(withErrorHandling(stateListCommand));
35205
+ const treeOption = new Option("--tree", "Render parent child stack tree (loads each state record to read the v6 parent link)").default(false).conflicts("long");
35206
+ const cmd = new Command("list").alias("ls").description("List stacks registered in the cdkd state bucket").option("-l, --long", "Show resource count, last-modified time, and lock status", false).option("--json", "Output as JSON", false).addOption(treeOption).action(withErrorHandling(stateListCommand));
35064
35207
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
35065
35208
  cmd.addOption(deprecatedRegionOption);
35066
35209
  return cmd;
@@ -56684,7 +56827,7 @@ function reorderArgs(argv) {
56684
56827
  */
56685
56828
  async function main() {
56686
56829
  const program = new Command();
56687
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.147.2");
56830
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.148.0");
56688
56831
  program.addCommand(createBootstrapCommand());
56689
56832
  program.addCommand(createSynthCommand());
56690
56833
  program.addCommand(createListCommand());