@shepai/cli 1.142.1 → 1.144.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.
Files changed (139) hide show
  1. package/dist/src/presentation/cli/commands/feat/ls.command.d.ts +44 -2
  2. package/dist/src/presentation/cli/commands/feat/ls.command.d.ts.map +1 -1
  3. package/dist/src/presentation/cli/commands/feat/ls.command.js +182 -82
  4. package/dist/tsconfig.build.tsbuildinfo +1 -1
  5. package/package.json +1 -1
  6. package/web/.next/BUILD_ID +1 -1
  7. package/web/.next/build-manifest.json +2 -2
  8. package/web/.next/fallback-build-manifest.json +2 -2
  9. package/web/.next/prerender-manifest.json +3 -3
  10. package/web/.next/required-server-files.js +2 -2
  11. package/web/.next/required-server-files.json +2 -2
  12. package/web/.next/server/app/(dashboard)/@drawer/adopt/page/server-reference-manifest.json +28 -28
  13. package/web/.next/server/app/(dashboard)/@drawer/adopt/page.js.nft.json +1 -1
  14. package/web/.next/server/app/(dashboard)/@drawer/adopt/page_client-reference-manifest.js +1 -1
  15. package/web/.next/server/app/(dashboard)/@drawer/create/page/server-reference-manifest.json +28 -28
  16. package/web/.next/server/app/(dashboard)/@drawer/create/page.js.nft.json +1 -1
  17. package/web/.next/server/app/(dashboard)/@drawer/create/page_client-reference-manifest.js +1 -1
  18. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page/server-reference-manifest.json +36 -36
  19. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page.js.nft.json +1 -1
  20. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page_client-reference-manifest.js +1 -1
  21. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page/server-reference-manifest.json +36 -36
  22. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page.js.nft.json +1 -1
  23. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page_client-reference-manifest.js +1 -1
  24. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page/server-reference-manifest.json +26 -26
  25. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page.js.nft.json +1 -1
  26. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
  27. package/web/.next/server/app/(dashboard)/create/page/server-reference-manifest.json +28 -28
  28. package/web/.next/server/app/(dashboard)/create/page.js.nft.json +1 -1
  29. package/web/.next/server/app/(dashboard)/create/page_client-reference-manifest.js +1 -1
  30. package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page/server-reference-manifest.json +36 -36
  31. package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page.js.nft.json +1 -1
  32. package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page_client-reference-manifest.js +1 -1
  33. package/web/.next/server/app/(dashboard)/feature/[featureId]/page/server-reference-manifest.json +36 -36
  34. package/web/.next/server/app/(dashboard)/feature/[featureId]/page.js.nft.json +1 -1
  35. package/web/.next/server/app/(dashboard)/feature/[featureId]/page_client-reference-manifest.js +1 -1
  36. package/web/.next/server/app/(dashboard)/page/server-reference-manifest.json +26 -26
  37. package/web/.next/server/app/(dashboard)/page.js.nft.json +1 -1
  38. package/web/.next/server/app/(dashboard)/page_client-reference-manifest.js +1 -1
  39. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page/server-reference-manifest.json +26 -26
  40. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page.js.nft.json +1 -1
  41. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
  42. package/web/.next/server/app/_global-error.html +2 -2
  43. package/web/.next/server/app/_global-error.rsc +1 -1
  44. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  45. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  46. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  47. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  48. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  49. package/web/.next/server/app/_not-found/page/server-reference-manifest.json +3 -3
  50. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  51. package/web/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  52. package/web/.next/server/app/settings/page.js.nft.json +1 -1
  53. package/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  54. package/web/.next/server/app/skills/page/server-reference-manifest.json +8 -8
  55. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  56. package/web/.next/server/app/tools/page/server-reference-manifest.json +8 -8
  57. package/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
  58. package/web/.next/server/app/version/page/server-reference-manifest.json +3 -3
  59. package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  60. package/web/.next/server/chunks/[root-of-the-server]__a402b567._.js +1 -1
  61. package/web/.next/server/chunks/ssr/744ca_web_components_common_control-center-drawer_create-drawer-client_tsx_5e26fc0a._.js +1 -1
  62. package/web/.next/server/chunks/ssr/744ca_web_components_common_control-center-drawer_create-drawer-client_tsx_5e26fc0a._.js.map +1 -1
  63. package/web/.next/server/chunks/ssr/[root-of-the-server]__2138fa7e._.js +2 -2
  64. package/web/.next/server/chunks/ssr/[root-of-the-server]__29580090._.js +1 -1
  65. package/web/.next/server/chunks/ssr/[root-of-the-server]__29580090._.js.map +1 -1
  66. package/web/.next/server/chunks/ssr/[root-of-the-server]__357d99f9._.js +1 -1
  67. package/web/.next/server/chunks/ssr/[root-of-the-server]__3ef34e4c._.js +1 -1
  68. package/web/.next/server/chunks/ssr/[root-of-the-server]__43f51aa6._.js +1 -1
  69. package/web/.next/server/chunks/ssr/[root-of-the-server]__43f51aa6._.js.map +1 -1
  70. package/web/.next/server/chunks/ssr/[root-of-the-server]__815546bd._.js +1 -1
  71. package/web/.next/server/chunks/ssr/[root-of-the-server]__815546bd._.js.map +1 -1
  72. package/web/.next/server/chunks/ssr/[root-of-the-server]__aad040c0._.js +2 -2
  73. package/web/.next/server/chunks/ssr/[root-of-the-server]__aad040c0._.js.map +1 -1
  74. package/web/.next/server/chunks/ssr/[root-of-the-server]__c094882b._.js +1 -1
  75. package/web/.next/server/chunks/ssr/[root-of-the-server]__c094882b._.js.map +1 -1
  76. package/web/.next/server/chunks/ssr/[root-of-the-server]__d48c5b11._.js +1 -1
  77. package/web/.next/server/chunks/ssr/[root-of-the-server]__d48c5b11._.js.map +1 -1
  78. package/web/.next/server/chunks/ssr/[root-of-the-server]__dac5dbf1._.js +1 -1
  79. package/web/.next/server/chunks/ssr/[root-of-the-server]__dac5dbf1._.js.map +1 -1
  80. package/web/.next/server/chunks/ssr/[root-of-the-server]__fae8b355._.js +1 -1
  81. package/web/.next/server/chunks/ssr/[root-of-the-server]__fae8b355._.js.map +1 -1
  82. package/web/.next/server/chunks/ssr/_01046927._.js +1 -1
  83. package/web/.next/server/chunks/ssr/_0c5f56e3._.js +2 -2
  84. package/web/.next/server/chunks/ssr/_0c5f56e3._.js.map +1 -1
  85. package/web/.next/server/chunks/ssr/_1b719e7f._.js +1 -1
  86. package/web/.next/server/chunks/ssr/_1b719e7f._.js.map +1 -1
  87. package/web/.next/server/chunks/ssr/_37e8548b._.js +1 -1
  88. package/web/.next/server/chunks/ssr/_37e8548b._.js.map +1 -1
  89. package/web/.next/server/chunks/ssr/_55d763e2._.js +1 -1
  90. package/web/.next/server/chunks/ssr/_55d763e2._.js.map +1 -1
  91. package/web/.next/server/chunks/ssr/_6256a985._.js +1 -1
  92. package/web/.next/server/chunks/ssr/_6256a985._.js.map +1 -1
  93. package/web/.next/server/chunks/ssr/_64bdfc6f._.js +2 -2
  94. package/web/.next/server/chunks/ssr/_64bdfc6f._.js.map +1 -1
  95. package/web/.next/server/chunks/ssr/_7dca1882._.js +1 -1
  96. package/web/.next/server/chunks/ssr/_7dca1882._.js.map +1 -1
  97. package/web/.next/server/chunks/ssr/_a9f57758._.js +1 -1
  98. package/web/.next/server/chunks/ssr/_b71645b4._.js +1 -1
  99. package/web/.next/server/chunks/ssr/_b71645b4._.js.map +1 -1
  100. package/web/.next/server/chunks/ssr/{_be90dd43._.js → _c64f06d5._.js} +2 -2
  101. package/web/.next/server/chunks/ssr/{_be90dd43._.js.map → _c64f06d5._.js.map} +1 -1
  102. package/web/.next/server/chunks/ssr/{_05b4c375._.js → _c67ad133._.js} +2 -2
  103. package/web/.next/server/chunks/ssr/{_05b4c375._.js.map → _c67ad133._.js.map} +1 -1
  104. package/web/.next/server/chunks/ssr/_d8575088._.js +1 -1
  105. package/web/.next/server/chunks/ssr/_d8575088._.js.map +1 -1
  106. package/web/.next/server/chunks/ssr/_f39a1adb._.js +1 -1
  107. package/web/.next/server/chunks/ssr/_f39a1adb._.js.map +1 -1
  108. package/web/.next/server/chunks/ssr/b1a17_presentation_web_components_features_settings_settings-page-client_tsx_6ed9d5f8._.js +1 -1
  109. package/web/.next/server/chunks/ssr/b1a17_presentation_web_components_features_settings_settings-page-client_tsx_6ed9d5f8._.js.map +1 -1
  110. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_skills_page_actions_1b176e3c.js +1 -1
  111. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_skills_page_actions_1b176e3c.js.map +1 -1
  112. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_tools_page_actions_bd9f0dda.js +1 -1
  113. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_tools_page_actions_bd9f0dda.js.map +1 -1
  114. package/web/.next/server/chunks/ssr/src_presentation_web_app_actions_open-ide_ts_baaca5d5._.js +1 -1
  115. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
  116. package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
  117. package/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_7ac3562e._.js +1 -1
  118. package/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_7ac3562e._.js.map +1 -1
  119. package/web/.next/server/chunks/ssr/{src_presentation_web_93e5fd0d._.js → src_presentation_web_dd85ad88._.js} +2 -2
  120. package/web/.next/server/chunks/ssr/{src_presentation_web_93e5fd0d._.js.map → src_presentation_web_dd85ad88._.js.map} +1 -1
  121. package/web/.next/server/pages/500.html +2 -2
  122. package/web/.next/server/server-reference-manifest.js +1 -1
  123. package/web/.next/server/server-reference-manifest.json +44 -44
  124. package/web/.next/static/chunks/{3cd98a2f7a8a33a8.js → 0bce83a11c7a9383.js} +1 -1
  125. package/web/.next/static/chunks/{7fe2bb39ee56a00a.js → 1867fdb7f2c6035f.js} +1 -1
  126. package/web/.next/static/chunks/{6b099ae0735b3c21.js → 21502baa33ad728a.js} +2 -2
  127. package/web/.next/static/chunks/{09084b604a4022a2.js → 53478ed65db63030.js} +1 -1
  128. package/web/.next/static/chunks/{f7251d9aebce001c.js → 60c6c3c30fba3b7c.js} +1 -1
  129. package/web/.next/static/chunks/{8b7ba729b53257a1.js → 683ec435a34ca112.js} +1 -1
  130. package/web/.next/static/chunks/{41d77b193b5df1b7.js → 88300dbc7c91abb2.js} +1 -1
  131. package/web/.next/static/chunks/{86afa5ddfcbbbf94.js → 8cc1aea0d82835be.js} +1 -1
  132. package/web/.next/static/chunks/{9f57fc63be1397a3.js → 904cdf4c47654f32.js} +1 -1
  133. package/web/.next/static/chunks/9e3e916a22d1121f.js +1 -0
  134. package/web/.next/static/chunks/{a05f5913e43443d6.js → de0704c3e73118cf.js} +1 -1
  135. package/web/.next/static/chunks/{d5475a9f1eed113d.js → ecfbc35ad11c83bb.js} +1 -1
  136. package/web/.next/static/chunks/d2eb3a9e6fa8cdad.js +0 -1
  137. /package/web/.next/static/{Z9eZhnwItIVi_t0WGZSA4 → rsS9eOHzCHbGCCwBS1fRh}/_buildManifest.js +0 -0
  138. /package/web/.next/static/{Z9eZhnwItIVi_t0WGZSA4 → rsS9eOHzCHbGCCwBS1fRh}/_clientMiddlewareManifest.json +0 -0
  139. /package/web/.next/static/{Z9eZhnwItIVi_t0WGZSA4 → rsS9eOHzCHbGCCwBS1fRh}/_ssgManifest.js +0 -0
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Feature List Command
3
3
  *
4
- * Lists features in a formatted list with optional filtering.
5
- * Derives real-time status from the agent run (not stale Feature.lifecycle).
4
+ * Lists features in a hierarchical tree view: repo → feature → child → child…
5
+ * Repos and features are ordered by creation date descending.
6
6
  *
7
7
  * Usage: shep feat ls [options]
8
8
  *
@@ -11,5 +11,47 @@
11
11
  * $ shep feat ls --repo /path/to/project
12
12
  */
13
13
  import { Command } from 'commander';
14
+ import type { Feature, AgentRun, PhaseTiming, Repository } from '../../../../../packages/core/src/domain/generated/output.js';
15
+ /** Convert a date value (any type from domain) to a timestamp for sorting. */
16
+ export declare function toTimestamp(val: unknown): number;
17
+ interface Entry {
18
+ feature: Feature;
19
+ run: AgentRun | null;
20
+ phases: PhaseTiming[];
21
+ }
22
+ interface TreeNode {
23
+ entry: Entry;
24
+ children: TreeNode[];
25
+ }
26
+ interface RepoGroup {
27
+ repoPath: string;
28
+ repoName: string;
29
+ repoCreatedAt: number;
30
+ roots: TreeNode[];
31
+ }
32
+ interface FlatRow {
33
+ entry: Entry;
34
+ parentIsLast: boolean[];
35
+ isLast: boolean;
36
+ }
37
+ /**
38
+ * Build a recursive feature tree within a single repo group.
39
+ * Features with a parentId that matches another feature in the group become children.
40
+ * All levels are sorted by createdAt descending.
41
+ */
42
+ export declare function buildTree(entries: Entry[]): TreeNode[];
43
+ /**
44
+ * Group entries by repositoryPath and sort repos by createdAt desc.
45
+ * Uses the Repository entity's createdAt if available; falls back to the
46
+ * newest feature's createdAt in that repo group.
47
+ */
48
+ export declare function groupByRepo(entries: Entry[], repos: Repository[]): RepoGroup[];
49
+ /** Flatten a tree into rows with prefix context for rendering. */
50
+ export declare function flattenTree(nodes: TreeNode[], parentIsLast: boolean[]): FlatRow[];
51
+ /** Build the tree-drawing prefix string for a given depth context. */
52
+ export declare function buildTreePrefix(parentIsLast: boolean[], isLast: boolean): string;
53
+ /** Render a single feature row as a formatted string. */
54
+ export declare function renderFeatureRow(entry: Entry, treePrefix: string): string;
14
55
  export declare function createLsCommand(): Command;
56
+ export {};
15
57
  //# sourceMappingURL=ls.command.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ls.command.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/cli/commands/feat/ls.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkLpC,wBAAgB,eAAe,IAAI,OAAO,CAwGzC"}
1
+ {"version":3,"file":"ls.command.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/cli/commands/feat/ls.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAyJ/F,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAOhD;AAiBD,UAAU,KAAK;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,UAAU,QAAQ;IAChB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,QAAQ,EAAE,CAAC;CACtB;AAED,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED,UAAU,OAAO;IACf,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,EAAE,OAAO,EAAE,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,QAAQ,EAAE,CAgCtD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,SAAS,EAAE,CA8B9E;AAED,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAWjF;AAED,sEAAsE;AACtE,wBAAgB,eAAe,CAAC,YAAY,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAOhF;AAQD,yDAAyD;AACzD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAazE;AAeD,wBAAgB,eAAe,IAAI,OAAO,CAiFzC"}
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Feature List Command
3
3
  *
4
- * Lists features in a formatted list with optional filtering.
5
- * Derives real-time status from the agent run (not stale Feature.lifecycle).
4
+ * Lists features in a hierarchical tree view: repo → feature → child → child…
5
+ * Repos and features are ordered by creation date descending.
6
6
  *
7
7
  * Usage: shep feat ls [options]
8
8
  *
@@ -14,7 +14,7 @@ import path from 'node:path';
14
14
  import { Command } from 'commander';
15
15
  import { container } from '../../../../../packages/core/src/infrastructure/di/container.js';
16
16
  import { ListFeaturesUseCase } from '../../../../../packages/core/src/application/use-cases/features/list-features.use-case.js';
17
- import { colors, symbols, messages, renderListView } from '../../ui/index.js';
17
+ import { colors, symbols, messages, fmt } from '../../ui/index.js';
18
18
  /** Map graph node names to human-readable phase labels (active). */
19
19
  const NODE_TO_PHASE = {
20
20
  analyze: 'Analyzing',
@@ -33,21 +33,10 @@ const NODE_TO_REVIEW = {
33
33
  implement: 'Review Merge',
34
34
  merge: 'Review Merge',
35
35
  };
36
- /** Status priority for sorting (lower = higher priority). */
37
- const STATUS_PRIORITY = {
38
- running: 0,
39
- pending: 0,
40
- waiting_approval: 1,
41
- failed: 2,
42
- interrupted: 2,
43
- completed: 3,
44
- };
45
36
  /**
46
37
  * Derive the display status from the agent run.
47
- * Returns the current graph node as a phase label with an activity indicator.
48
38
  */
49
39
  function formatStatus(feature, run) {
50
- // Blocked features show "Waiting" — the parent relationship is conveyed by tree indentation
51
40
  if (feature.lifecycle === 'Archived') {
52
41
  return `${colors.muted(symbols.dotEmpty)} ${colors.muted('Archived')}`;
53
42
  }
@@ -124,7 +113,6 @@ function formatElapsed(run, phaseTimings) {
124
113
  const totalMs = phaseTimings.reduce((sum, pt) => {
125
114
  if (pt.durationMs != null)
126
115
  return sum + Number(pt.durationMs);
127
- // Phase still running — add live delta
128
116
  if (pt.startedAt)
129
117
  return sum + (now - new Date(pt.startedAt).getTime());
130
118
  return sum;
@@ -132,7 +120,6 @@ function formatElapsed(run, phaseTimings) {
132
120
  const isRunning = run?.status === 'running' || run?.status === 'pending';
133
121
  return isRunning ? colors.info(formatDuration(totalMs)) : colors.muted(formatDuration(totalMs));
134
122
  }
135
- // Fallback to run timestamps if no phase timings
136
123
  if (!run?.startedAt)
137
124
  return '';
138
125
  const started = new Date(run.startedAt).getTime();
@@ -160,11 +147,149 @@ function formatGates(feature) {
160
147
  const push = feature.push ? colors.accent('■') : colors.muted('□');
161
148
  return `${gate(allowPrd)} ${gate(allowPlan)} ${gate(allowMerge)} ${push}`;
162
149
  }
163
- /** Get sort priority for a run status (lower = shown first). */
164
- function getStatusPriority(run) {
165
- if (!run)
166
- return 4;
167
- return STATUS_PRIORITY[run.status] ?? 4;
150
+ /** Convert a date value (any type from domain) to a timestamp for sorting. */
151
+ export function toTimestamp(val) {
152
+ if (!val)
153
+ return 0;
154
+ try {
155
+ return new Date(val).getTime();
156
+ }
157
+ catch {
158
+ return 0;
159
+ }
160
+ }
161
+ /** Strip ANSI escape sequences to get visible character count. */
162
+ function stripAnsi(text) {
163
+ // eslint-disable-next-line no-control-regex
164
+ return text.replace(/\x1B\[[0-9;]*m/g, '');
165
+ }
166
+ /** Pad a string that may contain ANSI codes to a given visible width. */
167
+ function ansiPad(text, width) {
168
+ const visible = stripAnsi(text).length;
169
+ if (visible >= width)
170
+ return text;
171
+ return text + ' '.repeat(width - visible);
172
+ }
173
+ /**
174
+ * Build a recursive feature tree within a single repo group.
175
+ * Features with a parentId that matches another feature in the group become children.
176
+ * All levels are sorted by createdAt descending.
177
+ */
178
+ export function buildTree(entries) {
179
+ const byId = new Map();
180
+ for (const e of entries)
181
+ byId.set(e.feature.id, e);
182
+ const childrenByParent = new Map();
183
+ const rootEntries = [];
184
+ for (const e of entries) {
185
+ const pid = e.feature.parentId;
186
+ if (pid && byId.has(pid)) {
187
+ const list = childrenByParent.get(pid) ?? [];
188
+ list.push(e);
189
+ childrenByParent.set(pid, list);
190
+ }
191
+ else {
192
+ rootEntries.push(e);
193
+ }
194
+ }
195
+ function sortDesc(arr) {
196
+ return [...arr].sort((a, b) => toTimestamp(b.feature.createdAt) - toTimestamp(a.feature.createdAt));
197
+ }
198
+ function buildNodes(arr) {
199
+ return sortDesc(arr).map((e) => ({
200
+ entry: e,
201
+ children: buildNodes(childrenByParent.get(e.feature.id) ?? []),
202
+ }));
203
+ }
204
+ return buildNodes(rootEntries);
205
+ }
206
+ /**
207
+ * Group entries by repositoryPath and sort repos by createdAt desc.
208
+ * Uses the Repository entity's createdAt if available; falls back to the
209
+ * newest feature's createdAt in that repo group.
210
+ */
211
+ export function groupByRepo(entries, repos) {
212
+ const repoByPath = new Map();
213
+ for (const r of repos) {
214
+ repoByPath.set(r.path.replace(/\\/g, '/'), r);
215
+ }
216
+ const groupMap = new Map();
217
+ for (const e of entries) {
218
+ const repoPath = e.feature.repositoryPath.replace(/\\/g, '/');
219
+ const list = groupMap.get(repoPath) ?? [];
220
+ list.push(e);
221
+ groupMap.set(repoPath, list);
222
+ }
223
+ const groups = [];
224
+ for (const [repoPath, groupEntries] of groupMap) {
225
+ const repo = repoByPath.get(repoPath);
226
+ const repoCreatedAt = repo
227
+ ? toTimestamp(repo.createdAt)
228
+ : Math.max(...groupEntries.map((e) => toTimestamp(e.feature.createdAt)));
229
+ groups.push({
230
+ repoPath,
231
+ repoName: path.basename(repoPath),
232
+ repoCreatedAt,
233
+ roots: buildTree(groupEntries),
234
+ });
235
+ }
236
+ groups.sort((a, b) => b.repoCreatedAt - a.repoCreatedAt);
237
+ return groups;
238
+ }
239
+ /** Flatten a tree into rows with prefix context for rendering. */
240
+ export function flattenTree(nodes, parentIsLast) {
241
+ const result = [];
242
+ for (let i = 0; i < nodes.length; i++) {
243
+ const node = nodes[i];
244
+ const isLast = i === nodes.length - 1;
245
+ result.push({ entry: node.entry, parentIsLast, isLast });
246
+ if (node.children.length > 0) {
247
+ result.push(...flattenTree(node.children, [...parentIsLast, isLast]));
248
+ }
249
+ }
250
+ return result;
251
+ }
252
+ /** Build the tree-drawing prefix string for a given depth context. */
253
+ export function buildTreePrefix(parentIsLast, isLast) {
254
+ let prefix = '';
255
+ for (const wasLast of parentIsLast) {
256
+ prefix += wasLast ? ' ' : '│ ';
257
+ }
258
+ prefix += isLast ? '└─ ' : '├─ ';
259
+ return prefix;
260
+ }
261
+ // Column widths for feature rows
262
+ const FIRST_COL_WIDTH = 44; // tree prefix + id + 2 spaces + name
263
+ const STATUS_WIDTH = 22;
264
+ const GATES_WIDTH = 10;
265
+ const ELAPSED_WIDTH = 9;
266
+ /** Render a single feature row as a formatted string. */
267
+ export function renderFeatureRow(entry, treePrefix) {
268
+ const { feature, run, phases } = entry;
269
+ const shortId = colors.muted(feature.id.slice(0, 8));
270
+ const prefixPlusId = `${treePrefix}${shortId} `;
271
+ const prefixPlusIdLen = stripAnsi(prefixPlusId).length;
272
+ const nameMax = Math.max(FIRST_COL_WIDTH - prefixPlusIdLen, 8);
273
+ const name = truncate(feature.name, nameMax);
274
+ const firstCol = ansiPad(prefixPlusId + name, FIRST_COL_WIDTH);
275
+ const status = ansiPad(formatStatus(feature, run), STATUS_WIDTH);
276
+ const gates = ansiPad(formatGates(feature), GATES_WIDTH);
277
+ const elapsed = ansiPad(formatElapsed(run, phases), ELAPSED_WIDTH);
278
+ const done = formatDone(run);
279
+ return ` ${firstCol} ${status} ${gates} ${elapsed} ${done}`;
280
+ }
281
+ /** Count total features across all repo groups. */
282
+ function countFeatures(groups) {
283
+ let count = 0;
284
+ function countNodes(nodes) {
285
+ for (const n of nodes) {
286
+ count++;
287
+ countNodes(n.children);
288
+ }
289
+ }
290
+ for (const g of groups)
291
+ countNodes(g.roots);
292
+ return count;
168
293
  }
169
294
  export function createLsCommand() {
170
295
  return new Command('ls')
@@ -177,79 +302,54 @@ export function createLsCommand() {
177
302
  const useCase = container.resolve(ListFeaturesUseCase);
178
303
  const runRepo = container.resolve('IAgentRunRepository');
179
304
  const phaseRepo = container.resolve('IPhaseTimingRepository');
305
+ const repoRepo = container.resolve('IRepositoryRepository');
180
306
  const filters = {
181
307
  ...(options.repo && { repositoryPath: options.repo }),
182
308
  ...(options.includeDeleted && { includeDeleted: true }),
183
309
  ...(options.showArchived && { includeArchived: true }),
184
310
  };
185
311
  const features = await useCase.execute(Object.keys(filters).length > 0 ? filters : undefined);
186
- // Load agent runs and phase timings for all features in parallel
187
- const [runs, timings] = await Promise.all([
312
+ // Load agent runs, phase timings, and repos in parallel
313
+ const [runs, timings, repos] = await Promise.all([
188
314
  Promise.all(features.map((f) => f.agentRunId ? runRepo.findById(f.agentRunId) : Promise.resolve(null))),
189
315
  Promise.all(features.map((f) => phaseRepo.findByFeatureId(f.id))),
316
+ repoRepo.list(),
190
317
  ]);
191
- // Pair features with runs/timings and sort by status priority
192
- const paired = features
193
- .map((feature, i) => ({ feature, run: runs[i], phases: timings[i] }))
194
- .sort((a, b) => getStatusPriority(a.run) - getStatusPriority(b.run));
195
- const childrenByParent = new Map();
196
- const roots = [];
197
- for (const entry of paired) {
198
- const pid = entry.feature.parentId;
199
- if (pid && entry.feature.lifecycle === 'Blocked') {
200
- const list = childrenByParent.get(pid) ?? [];
201
- list.push(entry);
202
- childrenByParent.set(pid, list);
203
- }
204
- else {
205
- roots.push(entry);
206
- }
207
- }
208
- // Flatten: parent then its blocked children
209
- const ordered = [];
210
- for (const entry of roots) {
211
- ordered.push({ entry, indent: false });
212
- const kids = childrenByParent.get(entry.feature.id);
213
- if (kids) {
214
- for (const kid of kids) {
215
- ordered.push({ entry: kid, indent: true });
216
- }
217
- childrenByParent.delete(entry.feature.id);
218
- }
318
+ const entries = features.map((feature, i) => ({
319
+ feature,
320
+ run: runs[i],
321
+ phases: timings[i],
322
+ }));
323
+ const groups = groupByRepo(entries, repos);
324
+ const total = countFeatures(groups);
325
+ if (total === 0) {
326
+ messages.newline();
327
+ messages.info('No features found');
328
+ messages.newline();
329
+ return;
219
330
  }
220
- // Append orphaned blocked children (parent not in current list)
221
- for (const kids of childrenByParent.values()) {
222
- for (const kid of kids) {
223
- ordered.push({ entry: kid, indent: false });
331
+ const lines = [];
332
+ lines.push('');
333
+ lines.push(` ${fmt.heading(`Features (${total})`)}`);
334
+ lines.push('');
335
+ // Column headers
336
+ const h1 = colors.muted('NAME'.padEnd(FIRST_COL_WIDTH));
337
+ const h2 = colors.muted('STATUS'.padEnd(STATUS_WIDTH));
338
+ const h3 = colors.muted('R P M ↑'.padEnd(GATES_WIDTH));
339
+ const h4 = colors.muted('ELAPSED'.padEnd(ELAPSED_WIDTH));
340
+ const h5 = colors.muted('DONE');
341
+ lines.push(` ${h1} ${h2} ${h3} ${h4} ${h5}`);
342
+ for (const group of groups) {
343
+ lines.push('');
344
+ lines.push(` ${fmt.heading(group.repoName)} ${colors.muted(group.repoPath)}`);
345
+ const flatRows = flattenTree(group.roots, []);
346
+ for (const { entry, parentIsLast, isLast } of flatRows) {
347
+ const treePrefix = buildTreePrefix(parentIsLast, isLast);
348
+ lines.push(renderFeatureRow(entry, treePrefix));
224
349
  }
225
350
  }
226
- const rows = ordered.map(({ entry: { feature, run, phases }, indent }) => {
227
- const repo = path.basename(feature.repositoryPath);
228
- const prefix = indent ? `${colors.muted('└')} ` : '';
229
- return [
230
- prefix + feature.id.slice(0, 8),
231
- prefix + truncate(feature.name, indent ? 28 : 30),
232
- formatStatus(feature, run),
233
- colors.muted(repo),
234
- formatGates(feature),
235
- formatElapsed(run, phases),
236
- formatDone(run),
237
- ];
238
- });
239
- renderListView({
240
- title: 'Features',
241
- columns: [
242
- { label: 'ID', width: 10 },
243
- { label: 'Name', width: 32 },
244
- { label: 'Status', width: 21 },
245
- { label: 'Repo', width: 20 },
246
- { label: 'R P M ↑', width: 10 },
247
- { label: 'Elapsed', width: 10 },
248
- { label: 'Done', width: 12 },
249
- ],
250
- rows,
251
- emptyMessage: 'No features found',
252
- });
351
+ lines.push('');
352
+ console.log(lines.join('\n'));
253
353
  }
254
354
  catch (error) {
255
355
  const err = error instanceof Error ? error : new Error(String(error));