@shepai/cli 1.142.0 → 1.142.1-pr455.16bd4d2
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/src/presentation/cli/commands/feat/ls.command.d.ts +44 -2
- package/dist/src/presentation/cli/commands/feat/ls.command.d.ts.map +1 -1
- package/dist/src/presentation/cli/commands/feat/ls.command.js +182 -82
- package/dist/src/presentation/web/app/api/sessions/route.d.ts.map +1 -1
- package/dist/src/presentation/web/app/api/sessions/route.js +45 -13
- package/dist/src/presentation/web/components/common/feature-node/feature-node.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/feature-node/feature-node.js +1 -1
- package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.d.ts +3 -1
- package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.js +13 -5
- package/dist/src/presentation/web/components/common/repository-node/repository-node.d.ts.map +1 -1
- package/dist/src/presentation/web/components/common/repository-node/repository-node.js +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/fallback-build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +3 -3
- package/web/.next/required-server-files.js +3 -3
- package/web/.next/required-server-files.json +3 -3
- package/web/.next/server/app/(dashboard)/@drawer/adopt/page/server-reference-manifest.json +28 -28
- package/web/.next/server/app/(dashboard)/@drawer/adopt/page.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/adopt/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/adopt/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/create/page/server-reference-manifest.json +28 -28
- package/web/.next/server/app/(dashboard)/@drawer/create/page.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/create/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/create/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page/server-reference-manifest.json +36 -36
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page/server-reference-manifest.json +36 -36
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page/server-reference-manifest.json +26 -26
- package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page.js +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/create/page/server-reference-manifest.json +28 -28
- package/web/.next/server/app/(dashboard)/create/page.js +1 -1
- package/web/.next/server/app/(dashboard)/create/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/create/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page/server-reference-manifest.json +36 -36
- package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page.js +1 -1
- package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/feature/[featureId]/page/server-reference-manifest.json +36 -36
- package/web/.next/server/app/(dashboard)/feature/[featureId]/page.js +1 -1
- package/web/.next/server/app/(dashboard)/feature/[featureId]/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/feature/[featureId]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/page/server-reference-manifest.json +26 -26
- package/web/.next/server/app/(dashboard)/page.js +1 -1
- package/web/.next/server/app/(dashboard)/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page/server-reference-manifest.json +26 -26
- package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page.js +1 -1
- package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page.js.nft.json +1 -1
- package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/server/app/_not-found/page/server-reference-manifest.json +3 -3
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/api/sessions/route.js +3 -2
- package/web/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/web/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/skills/page/server-reference-manifest.json +8 -8
- package/web/.next/server/app/skills/page.js +1 -1
- package/web/.next/server/app/skills/page.js.nft.json +1 -1
- package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/tools/page/server-reference-manifest.json +8 -8
- package/web/.next/server/app/tools/page.js +1 -1
- package/web/.next/server/app/tools/page.js.nft.json +1 -1
- package/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/version/page/server-reference-manifest.json +3 -3
- package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_ff60e4a5.js +3 -0
- package/web/.next/server/chunks/403f9_next_dist_esm_build_templates_app-route_ff60e4a5.js.map +1 -0
- package/web/.next/server/chunks/[externals]__448264a3._.js +3 -0
- package/web/.next/server/chunks/[externals]__448264a3._.js.map +1 -0
- package/web/.next/server/chunks/[root-of-the-server]__a402b567._.js +1 -1
- package/web/.next/server/chunks/ssr/744ca_web_components_common_control-center-drawer_create-drawer-client_tsx_5e26fc0a._.js +1 -1
- package/web/.next/server/chunks/ssr/744ca_web_components_common_control-center-drawer_create-drawer-client_tsx_5e26fc0a._.js.map +1 -1
- package/web/.next/server/chunks/ssr/{[root-of-the-server]__6ec59045._.js → [root-of-the-server]__0b150ddf._.js} +2 -2
- package/web/.next/server/chunks/ssr/{[root-of-the-server]__6ec59045._.js.map → [root-of-the-server]__0b150ddf._.js.map} +1 -1
- package/web/.next/server/chunks/ssr/{[root-of-the-server]__a5f9c6e5._.js → [root-of-the-server]__2138fa7e._.js} +3 -3
- package/web/.next/server/chunks/ssr/{[root-of-the-server]__a5f9c6e5._.js.map → [root-of-the-server]__2138fa7e._.js.map} +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__29580090._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__29580090._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__357d99f9._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__3ef34e4c._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__43f51aa6._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__43f51aa6._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__815546bd._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__815546bd._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__aad040c0._.js +2 -2
- package/web/.next/server/chunks/ssr/[root-of-the-server]__aad040c0._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__c094882b._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__c094882b._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__d48c5b11._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__d48c5b11._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__dac5dbf1._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__dac5dbf1._.js.map +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__fae8b355._.js +1 -1
- package/web/.next/server/chunks/ssr/[root-of-the-server]__fae8b355._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_0c5f56e3._.js +2 -2
- package/web/.next/server/chunks/ssr/_0c5f56e3._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_1b719e7f._.js +1 -1
- package/web/.next/server/chunks/ssr/_1b719e7f._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_37e8548b._.js +1 -1
- package/web/.next/server/chunks/ssr/_37e8548b._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_55d763e2._.js +1 -1
- package/web/.next/server/chunks/ssr/_55d763e2._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_6256a985._.js +1 -1
- package/web/.next/server/chunks/ssr/_6256a985._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_64bdfc6f._.js +2 -2
- package/web/.next/server/chunks/ssr/_64bdfc6f._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_7dca1882._.js +1 -1
- package/web/.next/server/chunks/ssr/_7dca1882._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_a9f57758._.js +1 -1
- package/web/.next/server/chunks/ssr/_b71645b4._.js +1 -1
- package/web/.next/server/chunks/ssr/_b71645b4._.js.map +1 -1
- package/web/.next/server/chunks/ssr/{_7e5c4d9c._.js → _c2b8e868._.js} +2 -2
- package/web/.next/server/chunks/ssr/{_7e5c4d9c._.js.map → _c2b8e868._.js.map} +1 -1
- package/web/.next/server/chunks/ssr/{_1c6fbecf._.js → _c904beb8._.js} +2 -2
- package/web/.next/server/chunks/ssr/{_1c6fbecf._.js.map → _c904beb8._.js.map} +1 -1
- package/web/.next/server/chunks/ssr/_d8575088._.js +1 -1
- package/web/.next/server/chunks/ssr/_d8575088._.js.map +1 -1
- package/web/.next/server/chunks/ssr/_f39a1adb._.js +1 -1
- package/web/.next/server/chunks/ssr/_f39a1adb._.js.map +1 -1
- package/web/.next/server/chunks/ssr/b1a17_presentation_web_components_features_settings_settings-page-client_tsx_6ed9d5f8._.js +1 -1
- package/web/.next/server/chunks/ssr/b1a17_presentation_web_components_features_settings_settings-page-client_tsx_6ed9d5f8._.js.map +1 -1
- package/web/.next/server/chunks/ssr/{src_presentation_web_7bda2f00._.js → src_presentation_web_8207e252._.js} +2 -2
- package/web/.next/server/chunks/ssr/{src_presentation_web_7bda2f00._.js.map → src_presentation_web_8207e252._.js.map} +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_skills_page_actions_1b176e3c.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_skills_page_actions_1b176e3c.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_tools_page_actions_bd9f0dda.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_tools_page_actions_bd9f0dda.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_app_actions_open-ide_ts_baaca5d5._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_e599bb8c._.js.map +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_7ac3562e._.js +1 -1
- package/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_7ac3562e._.js.map +1 -1
- package/web/.next/server/pages/500.html +2 -2
- package/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/server/server-reference-manifest.json +44 -44
- package/web/.next/static/chunks/{13978ecd00a81172.js → 0a6be91d8d53e3fd.js} +1 -1
- package/web/.next/static/chunks/{74eef53714ab461d.js → 12e3ff09fc17f052.js} +1 -1
- package/web/.next/static/chunks/1330778ac8908e69.js +1 -0
- package/web/.next/static/chunks/{2eec9756f5220e22.js → 3aa226c0e32ce07c.js} +1 -1
- package/web/.next/static/chunks/{e599050b1f6466fe.js → 65085b062e182aa4.js} +1 -1
- package/web/.next/static/chunks/{4d7e25ef4b8e2f62.js → 6eb17d2ce8a9aa4f.js} +1 -1
- package/web/.next/static/chunks/{21ddf1d59c98adf0.js → 80b1513ba8f15d69.js} +1 -1
- package/web/.next/static/chunks/{73889aad2b3356b1.js → 9a16838aae04976b.js} +2 -2
- package/web/.next/static/chunks/{4adda8dd11458a46.js → 9c2e38efcd95fe72.js} +2 -2
- package/web/.next/static/chunks/{ea9969f79b2162ec.js → a7f51b1734e55a52.js} +1 -1
- package/web/.next/static/chunks/b0a40414ed362492.js +1 -0
- package/web/.next/static/chunks/b88100de43de46a4.js +1 -0
- package/web/.next/server/chunks/[root-of-the-server]__a62bd945._.js +0 -3
- package/web/.next/server/chunks/[root-of-the-server]__a62bd945._.js.map +0 -1
- package/web/.next/static/chunks/3f18dfdd07d48ecd.js +0 -1
- package/web/.next/static/chunks/6ad8d8682f4836c9.js +0 -1
- package/web/.next/static/chunks/a6a6a8387bae31f0.js +0 -1
- /package/web/.next/static/{G2BdtwIuunZEcXQ0FjJJF → rDu9PslAKyILjx18WKTCI}/_buildManifest.js +0 -0
- /package/web/.next/static/{G2BdtwIuunZEcXQ0FjJJF → rDu9PslAKyILjx18WKTCI}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{G2BdtwIuunZEcXQ0FjJJF → rDu9PslAKyILjx18WKTCI}/_ssgManifest.js +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Feature List Command
|
|
3
3
|
*
|
|
4
|
-
* Lists features in a
|
|
5
|
-
*
|
|
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;
|
|
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
|
|
5
|
-
*
|
|
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,
|
|
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
|
-
/**
|
|
164
|
-
function
|
|
165
|
-
if (!
|
|
166
|
-
return
|
|
167
|
-
|
|
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
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/app/api/sessions/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/app/api/sessions/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAM3C,eAAO,MAAM,OAAO,kBAAkB,CAAC;AA2VvC;;;;;GAKG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO;;;;YA9VpC,MAAM;mBACC,MAAM;iBACR,MAAM,GAAG,IAAI;sBACR,MAAM;wBACJ,MAAM,GAAG,IAAI;uBACd,MAAM,GAAG,IAAI;mBACjB,MAAM,GAAG,IAAI;qBACX,MAAM;kBACT,MAAM;;IAoXjB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { readdir, stat } from 'node:fs/promises';
|
|
@@ -20,28 +21,58 @@ function cursorEncodePath(p) {
|
|
|
20
21
|
return p.replace(/^\//, '').replace(/\./g, '').replace(/[/\\]/g, '-');
|
|
21
22
|
}
|
|
22
23
|
// ── Claude Code session scanner ───────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Collect .jsonl session files from a single Claude project directory.
|
|
26
|
+
*/
|
|
27
|
+
async function collectJsonlFiles(projectDir) {
|
|
28
|
+
let entries;
|
|
27
29
|
try {
|
|
28
|
-
|
|
29
|
-
files = entries.filter((e) => e.endsWith('.jsonl'));
|
|
30
|
+
entries = await readdir(projectDir);
|
|
30
31
|
}
|
|
31
32
|
catch {
|
|
32
33
|
return [];
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
const fileInfos = await Promise.allSettled(
|
|
35
|
+
const jsonlFiles = entries.filter((e) => e.endsWith('.jsonl'));
|
|
36
|
+
const fileInfos = await Promise.allSettled(jsonlFiles.map(async (name) => {
|
|
36
37
|
const filePath = join(projectDir, name);
|
|
37
38
|
const s = await stat(filePath);
|
|
38
39
|
return { name, filePath, mtime: s.mtime.getTime() };
|
|
39
40
|
}));
|
|
40
|
-
|
|
41
|
+
return fileInfos
|
|
41
42
|
.filter((r) => r.status === 'fulfilled')
|
|
42
|
-
.map((r) => r.value)
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
.map((r) => r.value);
|
|
44
|
+
}
|
|
45
|
+
async function scanClaudeSessions(repositoryPath, limit, includeWorktrees = false) {
|
|
46
|
+
const dirName = claudeEncodePath(repositoryPath);
|
|
47
|
+
const projectsRoot = join(homedir(), '.claude', 'projects');
|
|
48
|
+
// Collect files from the exact directory
|
|
49
|
+
const primaryDir = join(projectsRoot, dirName);
|
|
50
|
+
let allFiles = await collectJsonlFiles(primaryDir);
|
|
51
|
+
// When includeWorktrees is set, also scan:
|
|
52
|
+
// 1. Directories whose name starts with the encoded repo path (git worktrees, .worktrees)
|
|
53
|
+
// 2. Shep worktree directories (~/.shep/repos/<hash>/wt/*) which use a hash of the repo path
|
|
54
|
+
if (includeWorktrees) {
|
|
55
|
+
try {
|
|
56
|
+
const allDirs = await readdir(projectsRoot);
|
|
57
|
+
// Match git-style worktrees (same prefix as repo path)
|
|
58
|
+
const prefixMatches = allDirs.filter((d) => d !== dirName && d.startsWith(dirName));
|
|
59
|
+
// Match shep worktrees: compute repo hash → find dirs starting with encoded shep path
|
|
60
|
+
const normalizedRepoPath = repositoryPath.replace(/\\/g, '/');
|
|
61
|
+
const repoHash = createHash('sha256').update(normalizedRepoPath).digest('hex').slice(0, 16);
|
|
62
|
+
const shepHome = join(homedir(), '.shep').replace(/\\/g, '/');
|
|
63
|
+
const shepWorktreePrefix = claudeEncodePath(join(shepHome, 'repos', repoHash));
|
|
64
|
+
const shepMatches = allDirs.filter((d) => d.startsWith(shepWorktreePrefix) && !prefixMatches.includes(d) && d !== dirName);
|
|
65
|
+
const worktreeDirs = [...prefixMatches, ...shepMatches];
|
|
66
|
+
const worktreeResults = await Promise.all(worktreeDirs.map((d) => collectJsonlFiles(join(projectsRoot, d))));
|
|
67
|
+
for (const files of worktreeResults) {
|
|
68
|
+
allFiles = allFiles.concat(files);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// projectsRoot doesn't exist — no sessions at all
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const valid = allFiles.sort((a, b) => b.mtime - a.mtime).slice(0, limit);
|
|
45
76
|
// Parse each file
|
|
46
77
|
const results = await Promise.allSettled(valid.map(async (fi) => parseClaudeSession(fi.filePath, fi.name, fi.mtime, repositoryPath)));
|
|
47
78
|
return results
|
|
@@ -238,13 +269,14 @@ export async function GET(request) {
|
|
|
238
269
|
const url = new URL(request.url);
|
|
239
270
|
const repositoryPath = url.searchParams.get('repositoryPath');
|
|
240
271
|
const limit = parseInt(url.searchParams.get('limit') ?? '10', 10);
|
|
272
|
+
const includeWorktrees = url.searchParams.get('includeWorktrees') === 'true';
|
|
241
273
|
if (!repositoryPath?.trim()) {
|
|
242
274
|
return NextResponse.json({ error: 'repositoryPath is required' }, { status: 400 });
|
|
243
275
|
}
|
|
244
276
|
try {
|
|
245
277
|
// Scan all providers in parallel
|
|
246
278
|
const [claudeSessions, cursorSessions] = await Promise.all([
|
|
247
|
-
scanClaudeSessions(repositoryPath, limit),
|
|
279
|
+
scanClaudeSessions(repositoryPath, limit, includeWorktrees),
|
|
248
280
|
scanCursorSessions(repositoryPath, limit),
|
|
249
281
|
]);
|
|
250
282
|
// Merge and sort by mtime descending, apply limit
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feature-node.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/feature-node/feature-node.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AA6CnE,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,
|
|
1
|
+
{"version":3,"file":"feature-node.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/feature-node/feature-node.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AA6CnE,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,2CAgeA"}
|
|
@@ -112,7 +112,7 @@ export function FeatureNode({ data, selected, }) {
|
|
|
112
112
|
? 'cursor-pointer opacity-80 transition-opacity hover:opacity-100'
|
|
113
113
|
: 'cursor-default'), children: data.deployment.status === DeploymentState.Booting ? (_jsx(Loader2, { className: "h-3 w-3 animate-spin text-blue-500" })) : (_jsx(Globe, { className: "h-3 w-3 text-green-600" })) }) }), _jsx(TooltipContent, { side: "top", children: data.deployment.status === DeploymentState.Booting
|
|
114
114
|
? 'Deploying...'
|
|
115
|
-
: (data.deployment.url ?? 'Live') })] }) })) : null, data.fastMode ? (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { "data-testid": "feature-node-fast-mode-badge", children: _jsx(Zap, { className: "h-3 w-3 text-amber-500" }) }) }), _jsx(TooltipContent, { side: "top", children: "Fast Mode" })] }) })) : null, data.repositoryPath ? (_jsx(FeatureSessionsDropdown, { repositoryPath: data.repositoryPath })) : null] }), data.state === 'deleting' ? (_jsxs("div", { className: "flex items-center gap-1.5 text-xs", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 shrink-0 animate-spin text-gray-400" }), _jsx("span", { className: "text-muted-foreground", children: "Deleting\u2026" })] })) : data.state === 'creating' ? (_jsxs("div", { className: "flex items-center gap-1.5 text-xs", children: [_jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 animate-spin text-teal-600 dark:text-teal-400" }), _jsx("span", { className: "font-medium text-teal-600 dark:text-teal-400", children: getBadgeText(data) })] })) : data.state === 'running' ? (_jsxs("div", { className: "flex items-center gap-1.5 text-xs", children: [_jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 animate-spin text-teal-600 dark:text-teal-400" }), _jsx("span", { className: "font-medium text-teal-600 dark:text-teal-400", children: getBadgeText(data) })] })) : data.state === 'action-required' ? (_jsxs(Button, { variant: "default", size: "xs", "aria-label": getActionRequiredLabel(data), "data-testid": "feature-node-approve-button",
|
|
115
|
+
: (data.deployment.url ?? 'Live') })] }) })) : null, data.fastMode ? (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { "data-testid": "feature-node-fast-mode-badge", children: _jsx(Zap, { className: "h-3 w-3 text-amber-500" }) }) }), _jsx(TooltipContent, { side: "top", children: "Fast Mode" })] }) })) : null, (data.worktreePath ?? data.repositoryPath) ? (_jsx(FeatureSessionsDropdown, { repositoryPath: data.worktreePath ?? data.repositoryPath })) : null] }), data.state === 'deleting' ? (_jsxs("div", { className: "flex items-center gap-1.5 text-xs", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 shrink-0 animate-spin text-gray-400" }), _jsx("span", { className: "text-muted-foreground", children: "Deleting\u2026" })] })) : data.state === 'creating' ? (_jsxs("div", { className: "flex items-center gap-1.5 text-xs", children: [_jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 animate-spin text-teal-600 dark:text-teal-400" }), _jsx("span", { className: "font-medium text-teal-600 dark:text-teal-400", children: getBadgeText(data) })] })) : data.state === 'running' ? (_jsxs("div", { className: "flex items-center gap-1.5 text-xs", children: [_jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 animate-spin text-teal-600 dark:text-teal-400" }), _jsx("span", { className: "font-medium text-teal-600 dark:text-teal-400", children: getBadgeText(data) })] })) : data.state === 'action-required' ? (_jsxs(Button, { variant: "default", size: "xs", "aria-label": getActionRequiredLabel(data), "data-testid": "feature-node-approve-button",
|
|
116
116
|
// eslint-disable-next-line @typescript-eslint/no-empty-function -- click bubbles to card's onNodeClick
|
|
117
117
|
onClick: () => { }, className: "nodrag dark:bg-primary dark:text-primary-foreground dark:hover:bg-primary/90 cursor-pointer bg-neutral-900 text-[11px] text-white hover:bg-neutral-800", children: [_jsx(Eye, { className: "h-3 w-3" }), getActionRequiredLabel(data)] })) : data.state === 'error' && data.onRetry ? (_jsxs(Button, { variant: "outline", size: "xs", "aria-label": "Retry", "data-testid": "feature-node-retry-button", onClick: (e) => {
|
|
118
118
|
e.stopPropagation();
|
package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.d.ts
CHANGED
|
@@ -13,9 +13,11 @@ export interface SessionSummary {
|
|
|
13
13
|
interface FeatureSessionsDropdownProps {
|
|
14
14
|
repositoryPath: string;
|
|
15
15
|
className?: string;
|
|
16
|
+
/** When true, also scan worktree session directories (used by repo nodes). */
|
|
17
|
+
includeWorktrees?: boolean;
|
|
16
18
|
/** Callback to create a feature from a session. Only shown on repo nodes. */
|
|
17
19
|
onCreateFromSession?: (session: SessionSummary, sessionFilePath: string) => void;
|
|
18
20
|
}
|
|
19
|
-
export declare function FeatureSessionsDropdown({ repositoryPath, className, onCreateFromSession, }: FeatureSessionsDropdownProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare function FeatureSessionsDropdown({ repositoryPath, className, includeWorktrees, onCreateFromSession, }: FeatureSessionsDropdownProps): import("react/jsx-runtime").JSX.Element;
|
|
20
22
|
export {};
|
|
21
23
|
//# sourceMappingURL=feature-sessions-dropdown.d.ts.map
|
package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feature-sessions-dropdown.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/feature-node/feature-sessions-dropdown.tsx"],"names":[],"mappings":"AA8BA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,4BAA4B;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;CAClF;AAmDD,wBAAgB,uBAAuB,CAAC,EACtC,cAAc,EACd,SAAS,EACT,mBAAmB,GACpB,EAAE,4BAA4B,
|
|
1
|
+
{"version":3,"file":"feature-sessions-dropdown.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/feature-node/feature-sessions-dropdown.tsx"],"names":[],"mappings":"AA8BA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,4BAA4B;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,6EAA6E;IAC7E,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;CAClF;AAmDD,wBAAgB,uBAAuB,CAAC,EACtC,cAAc,EACd,SAAS,EACT,gBAAgB,EAChB,mBAAmB,GACpB,EAAE,4BAA4B,2CAmK9B"}
|
package/dist/src/presentation/web/components/common/feature-node/feature-sessions-dropdown.js
CHANGED
|
@@ -56,7 +56,7 @@ async function copyToClipboard(text) {
|
|
|
56
56
|
function stopNodeEvent(e) {
|
|
57
57
|
e.stopPropagation();
|
|
58
58
|
}
|
|
59
|
-
export function FeatureSessionsDropdown({ repositoryPath, className, onCreateFromSession, }) {
|
|
59
|
+
export function FeatureSessionsDropdown({ repositoryPath, className, includeWorktrees, onCreateFromSession, }) {
|
|
60
60
|
const [sessions, setSessions] = useState([]);
|
|
61
61
|
const [loading, setLoading] = useState(false);
|
|
62
62
|
const [fetched, setFetched] = useState(false);
|
|
@@ -77,7 +77,11 @@ export function FeatureSessionsDropdown({ repositoryPath, className, onCreateFro
|
|
|
77
77
|
// Populates count badge + active indicator, and pre-loads the dropdown.
|
|
78
78
|
useEffect(() => {
|
|
79
79
|
let cancelled = false;
|
|
80
|
-
const params = new URLSearchParams({
|
|
80
|
+
const params = new URLSearchParams({
|
|
81
|
+
repositoryPath,
|
|
82
|
+
limit: '10',
|
|
83
|
+
...(includeWorktrees && { includeWorktrees: 'true' }),
|
|
84
|
+
});
|
|
81
85
|
fetch(`/api/sessions?${params.toString()}`)
|
|
82
86
|
.then((res) => (res.ok ? res.json() : null))
|
|
83
87
|
.then((data) => {
|
|
@@ -91,14 +95,18 @@ export function FeatureSessionsDropdown({ repositoryPath, className, onCreateFro
|
|
|
91
95
|
return () => {
|
|
92
96
|
cancelled = true;
|
|
93
97
|
};
|
|
94
|
-
}, [repositoryPath]);
|
|
98
|
+
}, [repositoryPath, includeWorktrees]);
|
|
95
99
|
// Re-fetch on dropdown open if not already loaded (e.g. path changed)
|
|
96
100
|
const doFetch = useCallback(async () => {
|
|
97
101
|
if (fetched)
|
|
98
102
|
return;
|
|
99
103
|
setLoading(true);
|
|
100
104
|
try {
|
|
101
|
-
const params = new URLSearchParams({
|
|
105
|
+
const params = new URLSearchParams({
|
|
106
|
+
repositoryPath,
|
|
107
|
+
limit: '10',
|
|
108
|
+
...(includeWorktrees && { includeWorktrees: 'true' }),
|
|
109
|
+
});
|
|
102
110
|
const res = await fetch(`/api/sessions?${params.toString()}`);
|
|
103
111
|
if (res.ok) {
|
|
104
112
|
const data = (await res.json());
|
|
@@ -113,7 +121,7 @@ export function FeatureSessionsDropdown({ repositoryPath, className, onCreateFro
|
|
|
113
121
|
setLoading(false);
|
|
114
122
|
setFetched(true);
|
|
115
123
|
}
|
|
116
|
-
}, [repositoryPath, fetched]);
|
|
124
|
+
}, [repositoryPath, fetched, includeWorktrees]);
|
|
117
125
|
const handleOpenChange = useCallback((open) => {
|
|
118
126
|
if (open)
|
|
119
127
|
void doFetch();
|
package/dist/src/presentation/web/components/common/repository-node/repository-node.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repository-node.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/repository-node/repository-node.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAOnE,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,
|
|
1
|
+
{"version":3,"file":"repository-node.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/components/common/repository-node/repository-node.tsx"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAOnE,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,2CA0aA"}
|
|
@@ -71,7 +71,7 @@ export function RepositoryNode({ data, selected, }) {
|
|
|
71
71
|
e.stopPropagation();
|
|
72
72
|
data.onClick?.();
|
|
73
73
|
}
|
|
74
|
-
}, className: cn('nodrag bg-card flex w-[26rem] cursor-pointer flex-col overflow-hidden rounded-xl border shadow-sm transition-[border-color,box-shadow] duration-200 dark:bg-neutral-800/80', selected && 'border-blue-400 dark:border-amber-500/60'), children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [_jsx(Github, { className: "text-muted-foreground h-5 w-5 shrink-0" }), _jsx("span", { "data-testid": "repository-node-name", className: "min-w-0 truncate text-sm font-medium", children: data.name }), _jsxs("div", { className: cn('flex shrink-0 items-center gap-2', (data.repositoryPath ?? data.onAdd) && 'ml-auto'), onClick: (e) => e.stopPropagation(), children: [data.repositoryPath ? (_jsxs(_Fragment, { children: [_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { className: "flex items-center", children: _jsx(ActionButton, { label: "Open in IDE", onClick: actions.openInIde, loading: actions.ideLoading, error: !!actions.ideError, icon: Code2, iconOnly: true, variant: "ghost", size: "icon-xs" }) }) }), _jsx(TooltipContent, { children: "Open in IDE" })] }) }), _jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { className: "flex items-center", children: _jsx(ActionButton, { label: "Open in Shell", onClick: actions.openInShell, loading: actions.shellLoading, error: !!actions.shellError, icon: Terminal, iconOnly: true, variant: "ghost", size: "icon-xs" }) }) }), _jsx(TooltipContent, { children: "Open in Shell" })] }) }), _jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { className: "flex items-center", children: _jsx(ActionButton, { label: "Open Folder", onClick: actions.openFolder, loading: actions.folderLoading, error: !!actions.folderError, icon: FolderOpen, iconOnly: true, variant: "ghost", size: "icon-xs" }) }) }), _jsx(TooltipContent, { children: "Open Folder" })] }) }), _jsx(FeatureSessionsDropdown, { repositoryPath: data.repositoryPath, onCreateFromSession: handleCreateFromSession })] })) : null, data.onAdd ? _jsx("div", { className: "ml-1.5" }) : null, data.onAdd ? (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { "aria-label": "New feature", "data-testid": "repository-node-add-button", onClick: (e) => {
|
|
74
|
+
}, className: cn('nodrag bg-card flex w-[26rem] cursor-pointer flex-col overflow-hidden rounded-xl border shadow-sm transition-[border-color,box-shadow] duration-200 dark:bg-neutral-800/80', selected && 'border-blue-400 dark:border-amber-500/60'), children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [_jsx(Github, { className: "text-muted-foreground h-5 w-5 shrink-0" }), _jsx("span", { "data-testid": "repository-node-name", className: "min-w-0 truncate text-sm font-medium", children: data.name }), _jsxs("div", { className: cn('flex shrink-0 items-center gap-2', (data.repositoryPath ?? data.onAdd) && 'ml-auto'), onClick: (e) => e.stopPropagation(), children: [data.repositoryPath ? (_jsxs(_Fragment, { children: [_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { className: "flex items-center", children: _jsx(ActionButton, { label: "Open in IDE", onClick: actions.openInIde, loading: actions.ideLoading, error: !!actions.ideError, icon: Code2, iconOnly: true, variant: "ghost", size: "icon-xs" }) }) }), _jsx(TooltipContent, { children: "Open in IDE" })] }) }), _jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { className: "flex items-center", children: _jsx(ActionButton, { label: "Open in Shell", onClick: actions.openInShell, loading: actions.shellLoading, error: !!actions.shellError, icon: Terminal, iconOnly: true, variant: "ghost", size: "icon-xs" }) }) }), _jsx(TooltipContent, { children: "Open in Shell" })] }) }), _jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { className: "flex items-center", children: _jsx(ActionButton, { label: "Open Folder", onClick: actions.openFolder, loading: actions.folderLoading, error: !!actions.folderError, icon: FolderOpen, iconOnly: true, variant: "ghost", size: "icon-xs" }) }) }), _jsx(TooltipContent, { children: "Open Folder" })] }) }), _jsx(FeatureSessionsDropdown, { repositoryPath: data.repositoryPath, includeWorktrees: true, onCreateFromSession: handleCreateFromSession })] })) : null, data.onAdd ? _jsx("div", { className: "ml-1.5" }) : null, data.onAdd ? (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("button", { "aria-label": "New feature", "data-testid": "repository-node-add-button", onClick: (e) => {
|
|
75
75
|
e.stopPropagation();
|
|
76
76
|
data.onAdd?.();
|
|
77
77
|
}, className: cn('flex h-6 shrink-0 cursor-pointer items-center gap-0.5 rounded bg-blue-500 px-1.5 text-[11px] font-bold text-white transition-colors hover:bg-blue-600 dark:bg-amber-500 dark:hover:bg-amber-400', data.pulseAdd && 'animate-pulse-cta'), children: [_jsx(Plus, { className: "h-3 w-3" }), _jsx("span", { className: "translate-y-px", children: "New" })] }) }), _jsx(TooltipContent, { side: "top", children: "New feature" })] }) })) : null] })] }), data.branch ? (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "repository-node-git-info", className: "text-muted-foreground border-border/50 border-t px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-3 text-xs", children: [_jsxs("span", { className: "flex items-center gap-1 truncate", "data-testid": "repository-node-branch", children: [_jsx(GitBranch, { className: "h-3 w-3 shrink-0" }), _jsx("span", { className: "truncate", children: data.branch })] }), data.behindCount != null && data.behindCount > 0 ? (_jsxs("span", { className: "flex shrink-0 items-center gap-1 whitespace-nowrap text-amber-500", "data-testid": "repository-node-behind", children: [_jsx(ArrowDown, { className: "h-3 w-3 shrink-0" }), data.behindCount, " behind"] })) : null] }) }), data.commitMessage ? (_jsx("div", { "data-testid": "repository-node-commit-info", className: "text-muted-foreground border-border/50 border-t px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-2 text-xs", children: [_jsx(GitCommitHorizontal, { className: "h-3 w-3 shrink-0" }), _jsx("span", { className: "min-w-0 truncate", "data-testid": "repository-node-commit-message", children: data.commitMessage }), data.committer ? (_jsxs("span", { className: "text-muted-foreground/70 ml-auto flex shrink-0 items-center gap-1", "data-testid": "repository-node-committer", children: [_jsx(User, { className: "h-3 w-3 shrink-0" }), _jsx("span", { children: data.committer })] })) : null] }) })) : null] })) : data.gitInfoStatus === 'not-a-repo' ? (
|