@knpkv/codecommit 0.5.5 → 0.7.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 (92) hide show
  1. package/README.md +35 -9
  2. package/dist/package.json +15 -15
  3. package/dist/src/FilterService.d.ts +84 -0
  4. package/dist/src/FilterService.d.ts.map +1 -0
  5. package/dist/src/FilterService.js +85 -0
  6. package/dist/src/FilterService.js.map +1 -0
  7. package/dist/src/bin.js +135 -48
  8. package/dist/src/bin.js.map +1 -1
  9. package/dist/src/filterPresets.d.ts +24 -0
  10. package/dist/src/filterPresets.d.ts.map +1 -0
  11. package/dist/src/filterPresets.js +46 -0
  12. package/dist/src/filterPresets.js.map +1 -0
  13. package/dist/src/main.d.ts +3 -1
  14. package/dist/src/main.d.ts.map +1 -1
  15. package/dist/src/main.js +17 -5
  16. package/dist/src/main.js.map +1 -1
  17. package/dist/src/tui/App.d.ts.map +1 -1
  18. package/dist/src/tui/App.js +3 -2
  19. package/dist/src/tui/App.js.map +1 -1
  20. package/dist/src/tui/atoms/actions.d.ts +6 -6
  21. package/dist/src/tui/atoms/actions.d.ts.map +1 -1
  22. package/dist/src/tui/atoms/actions.js +30 -23
  23. package/dist/src/tui/atoms/actions.js.map +1 -1
  24. package/dist/src/tui/atoms/app.d.ts +7 -7
  25. package/dist/src/tui/atoms/app.d.ts.map +1 -1
  26. package/dist/src/tui/atoms/app.js +11 -12
  27. package/dist/src/tui/atoms/app.js.map +1 -1
  28. package/dist/src/tui/atoms/runtime.d.ts +3 -3
  29. package/dist/src/tui/atoms/runtime.d.ts.map +1 -1
  30. package/dist/src/tui/atoms/runtime.js +8 -8
  31. package/dist/src/tui/atoms/runtime.js.map +1 -1
  32. package/dist/src/tui/atoms/ui.d.ts +1 -1
  33. package/dist/src/tui/atoms/ui.d.ts.map +1 -1
  34. package/dist/src/tui/atoms/ui.js +1 -1
  35. package/dist/src/tui/atoms/ui.js.map +1 -1
  36. package/dist/src/tui/components/DetailsView.d.ts.map +1 -1
  37. package/dist/src/tui/components/DetailsView.js +8 -7
  38. package/dist/src/tui/components/DetailsView.js.map +1 -1
  39. package/dist/src/tui/components/Footer.js +1 -1
  40. package/dist/src/tui/components/Footer.js.map +1 -1
  41. package/dist/src/tui/components/Header.d.ts.map +1 -1
  42. package/dist/src/tui/components/Header.js +5 -4
  43. package/dist/src/tui/components/Header.js.map +1 -1
  44. package/dist/src/tui/components/ListItemRow.d.ts.map +1 -1
  45. package/dist/src/tui/components/ListItemRow.js +3 -3
  46. package/dist/src/tui/components/ListItemRow.js.map +1 -1
  47. package/dist/src/tui/components/MainList.d.ts.map +1 -1
  48. package/dist/src/tui/components/MainList.js +5 -4
  49. package/dist/src/tui/components/MainList.js.map +1 -1
  50. package/dist/src/tui/components/QuickFilters.d.ts.map +1 -1
  51. package/dist/src/tui/components/QuickFilters.js +3 -2
  52. package/dist/src/tui/components/QuickFilters.js.map +1 -1
  53. package/dist/src/tui/components/SettingsAccountsTab.js +1 -1
  54. package/dist/src/tui/components/SettingsAccountsTab.js.map +1 -1
  55. package/dist/src/tui/components/SettingsThemeTab.js +1 -1
  56. package/dist/src/tui/components/SettingsThemeTab.js.map +1 -1
  57. package/dist/src/tui/components/SettingsView.js +1 -1
  58. package/dist/src/tui/components/SettingsView.js.map +1 -1
  59. package/dist/src/tui/context/theme.js +1 -1
  60. package/dist/src/tui/context/theme.js.map +1 -1
  61. package/dist/src/tui/hooks/useKeyboardNav.d.ts.map +1 -1
  62. package/dist/src/tui/hooks/useKeyboardNav.js +5 -4
  63. package/dist/src/tui/hooks/useKeyboardNav.js.map +1 -1
  64. package/dist/src/tui/hooks/useListNavigation.js +1 -1
  65. package/dist/src/tui/hooks/useListNavigation.js.map +1 -1
  66. package/dist/src/tui/ui/DialogCommand.js +1 -1
  67. package/dist/src/tui/ui/DialogCommand.js.map +1 -1
  68. package/dist/src/tui/ui/DialogCreatePR.d.ts.map +1 -1
  69. package/dist/src/tui/ui/DialogCreatePR.js +5 -4
  70. package/dist/src/tui/ui/DialogCreatePR.js.map +1 -1
  71. package/dist/src/tui/utils/prTemplates.d.ts +2 -2
  72. package/dist/src/tui/utils/prTemplates.d.ts.map +1 -1
  73. package/dist/src/tui/utils/prTemplates.js +35 -36
  74. package/dist/src/tui/utils/prTemplates.js.map +1 -1
  75. package/dist/test/ConfigService.test.js +10 -7
  76. package/dist/test/ConfigService.test.js.map +1 -1
  77. package/dist/test/FilterService.test.d.ts +2 -0
  78. package/dist/test/FilterService.test.d.ts.map +1 -0
  79. package/dist/test/FilterService.test.js +209 -0
  80. package/dist/test/FilterService.test.js.map +1 -0
  81. package/dist/test/bin.test.d.ts +2 -0
  82. package/dist/test/bin.test.d.ts.map +1 -0
  83. package/dist/test/bin.test.js +36 -0
  84. package/dist/test/bin.test.js.map +1 -0
  85. package/dist/test/filterPresets.test.d.ts +2 -0
  86. package/dist/test/filterPresets.test.d.ts.map +1 -0
  87. package/dist/test/filterPresets.test.js +166 -0
  88. package/dist/test/filterPresets.test.js.map +1 -0
  89. package/dist/tsconfig.tsbuildinfo +1 -1
  90. package/package.json +17 -17
  91. package/skills/codecommit/SKILL.md +69 -0
  92. package/skills/codecommit/agents/openai.yaml +4 -0
package/README.md CHANGED
@@ -57,15 +57,16 @@ codecommit web [--port 3000] [--hostname 127.0.0.1]
57
57
  codecommit pr list [options]
58
58
  ```
59
59
 
60
- | Option | Alias | Description | Default |
61
- | ----------- | ----- | ----------------------- | ----------- |
62
- | `--profile` | `-p` | AWS profile | `default` |
63
- | `--region` | `-r` | AWS region | `us-east-1` |
64
- | `--status` | `-s` | PR status (OPEN/CLOSED) | `OPEN` |
65
- | `--all` | `-a` | Show all PRs | `false` |
66
- | `--repo` | | Filter by repository | - |
67
- | `--author` | | Filter by author | - |
68
- | `--json` | | Output as JSON | `false` |
60
+ | Option | Alias | Description | Default |
61
+ | ----------- | ----- | ---------------------------------------------- | ----------- |
62
+ | `--profile` | `-p` | AWS profile (ignored with --filter) | `default` |
63
+ | `--region` | `-r` | AWS region (ignored with --filter) | `us-east-1` |
64
+ | `--status` | `-s` | PR status, OPEN/CLOSED (ignored with --filter) | `OPEN` |
65
+ | `--all` | `-a` | Show all PRs (ignored with --filter) | `false` |
66
+ | `--repo` | | Filter by repository | - |
67
+ | `--author` | | Filter by author | - |
68
+ | `--filter` | | Named preset, OPEN-only (see below) | - |
69
+ | `--json` | | Output as JSON | `false` |
69
70
 
70
71
  ```bash
71
72
  codecommit pr list
@@ -76,6 +77,31 @@ codecommit pr list --author jane
76
77
  codecommit pr list --json
77
78
  ```
78
79
 
80
+ #### Filter presets (`--filter`)
81
+
82
+ When `--filter` is set, the command fans out across **every enabled account**
83
+ in `~/.codecommit/config.json` (set up via `codecommit tui`) and returns the
84
+ merged list, sorted by last-modified-date. Presets operate on **OPEN PRs only**,
85
+ so `--profile`, `--region`, `--status`, and `--all` are all ignored when
86
+ `--filter` is set. Combine with `--json`, `--repo`, or `--author` for further
87
+ narrowing. If any account fails (e.g. an expired SSO session), a
88
+ `⚠ N account(s) failed` summary is printed to stderr and the PRs from the
89
+ accounts that succeeded are still returned.
90
+
91
+ | Preset | Matches |
92
+ | ----------------- | ------------------------------------------------------------------------ |
93
+ | `mine` | Open PRs you authored (matched against `getCallerIdentity` per profile) |
94
+ | `needs-my-review` | Open PRs awaiting your approval (you're in an unsatisfied approval pool) |
95
+ | `stale` | Open PRs with no activity for more than 7 days |
96
+ | `conflicting` | Open PRs with merge conflicts |
97
+
98
+ ```bash
99
+ codecommit pr list --filter mine --json # all my open PRs everywhere
100
+ codecommit pr list --filter needs-my-review # what I need to review
101
+ codecommit pr list --filter stale --repo my-repo # stale PRs in one repo
102
+ codecommit pr list --filter conflicting --json
103
+ ```
104
+
79
105
  Output:
80
106
 
81
107
  ```
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knpkv/codecommit",
3
- "version": "0.5.5",
3
+ "version": "0.7.0",
4
4
  "description": "TUI for browsing AWS CodeCommit PRs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,7 +12,8 @@
12
12
  "access": "public"
13
13
  },
14
14
  "files": [
15
- "dist"
15
+ "dist",
16
+ "skills"
16
17
  ],
17
18
  "scripts": {
18
19
  "start": "bun src/bin.ts",
@@ -25,26 +26,25 @@
25
26
  "docgen": "docgen"
26
27
  },
27
28
  "dependencies": {
28
- "@effect-atom/atom-react": "latest",
29
- "@effect/cli": "latest",
30
- "@effect/platform": "latest",
31
- "@effect/platform-bun": "latest",
32
- "@effect/platform-node": "latest",
29
+ "@effect/atom-react": "4.0.0-beta.87",
30
+ "@effect/platform-bun": "4.0.0-beta.87",
31
+ "@effect/platform-node": "4.0.0-beta.87",
32
+ "@knpkv/agent-skills": "workspace:^",
33
33
  "@knpkv/codecommit-core": "workspace:^",
34
34
  "@knpkv/codecommit-web": "workspace:^",
35
35
  "@opentui/core": "https://pkg.pr.new/anomalyco/opentui/@opentui/core@367a94087821b3b5feedd35bbb57df43b10a286e",
36
36
  "@opentui/react": "https://pkg.pr.new/anomalyco/opentui/@opentui/react@367a94087821b3b5feedd35bbb57df43b10a286e",
37
- "effect": "latest",
38
- "open": "^10.1.0",
39
- "react": "^19.1.0",
37
+ "effect": "4.0.0-beta.87",
38
+ "react": "^19.2.7",
40
39
  "tslib": "^2.8.1"
41
40
  },
42
41
  "devDependencies": {
43
- "@effect/vitest": "latest",
44
- "@storybook/react": "^8.6.15",
45
- "@types/bun": "^1.3.6",
42
+ "@effect/vitest": "4.0.0-beta.87",
43
+ "@storybook/react": "^10.4.6",
44
+ "@types/bun": "^1.3.14",
46
45
  "@types/node": "latest",
47
- "@types/react": "^19.1.8",
48
- "vitest": "latest"
46
+ "@types/react": "^19.2.17",
47
+ "storybook": "^10.4.6",
48
+ "vitest": "^4.1.9"
49
49
  }
50
50
  }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Cross-account `--filter` orchestration for `pr list`.
3
+ *
4
+ * Extracts the fan-out workflow that backs the named filter presets out of the
5
+ * CLI entrypoint (`bin.ts`) so the entrypoint keeps only presentation. The
6
+ * service depends on {@link AwsClient.AwsClient} and
7
+ * {@link ConfigService.ConfigService} and reuses the pure `matchesPreset` /
8
+ * `matchesRepoAuthor` predicates from `./filterPresets.ts`.
9
+ *
10
+ * Behaviour mirrors the original inline block exactly:
11
+ *
12
+ * - {@link FilterService.Service.resolveTargets} loads config, keeps only
13
+ * enabled accounts, and flattens to `{ profile, region }` targets.
14
+ * - {@link FilterService.Service.collect} resolves caller identity once per
15
+ * profile (for the identity-comparing presets), fans out OPEN-only PR fetches
16
+ * across targets (concurrency 4), filters by preset + repo/author, collects
17
+ * per-account failures, and returns PRs sorted by `lastModifiedDate` desc.
18
+ *
19
+ * @category Service
20
+ * @module
21
+ */
22
+ import { AwsClient, ConfigService, type Domain, type Errors } from "@knpkv/codecommit-core";
23
+ import type { AwsProfileName, AwsRegion } from "@knpkv/codecommit-core/Domain.js";
24
+ import { Context, Effect, Layer, type Option } from "effect";
25
+ import { type FilterPreset } from "./filterPresets.js";
26
+ /** A single `{ profile, region }` account/region pair to scan. */
27
+ export interface FilterTarget {
28
+ readonly profile: AwsProfileName;
29
+ readonly region: AwsRegion;
30
+ }
31
+ /** Repo/author narrowing options applied on top of the preset. */
32
+ export interface FilterOptions {
33
+ readonly repo: Option.Option<string>;
34
+ readonly author: Option.Option<string>;
35
+ }
36
+ /**
37
+ * Structured outcome of a cross-account scan.
38
+ *
39
+ * - `prs` — matching PRs, sorted by `lastModifiedDate` desc.
40
+ * - `failures` — `"<profile>/<region>: <message>"` per failed account.
41
+ * - `unresolvedProfiles` — profiles whose caller identity didn't resolve, so
42
+ * identity-comparing presets may have incomplete results for them.
43
+ */
44
+ export interface FilterResult {
45
+ readonly prs: ReadonlyArray<Domain.PullRequest>;
46
+ readonly failures: ReadonlyArray<string>;
47
+ readonly unresolvedProfiles: ReadonlyArray<string>;
48
+ }
49
+ type FilterCollectError = Errors.AwsApiError | Errors.AwsCredentialError | Errors.AwsThrottleError;
50
+ /**
51
+ * Cross-account filter orchestration service.
52
+ *
53
+ * @category models
54
+ */
55
+ export interface FilterServiceShape {
56
+ readonly resolveTargets: Effect.Effect<ReadonlyArray<FilterTarget>, unknown>;
57
+ readonly collect: (preset: FilterPreset, targets: ReadonlyArray<FilterTarget>, opts: FilterOptions, now?: Date) => Effect.Effect<FilterResult, FilterCollectError>;
58
+ }
59
+ /**
60
+ * Cross-account filter orchestration service.
61
+ *
62
+ * @category models
63
+ */
64
+ export declare namespace FilterService {
65
+ interface Service extends FilterServiceShape {
66
+ }
67
+ }
68
+ declare const FilterService_base: Context.ServiceClass<FilterService, "@knpkv/codecommit/FilterService", FilterServiceShape>;
69
+ /**
70
+ * Cross-account filter orchestration service.
71
+ *
72
+ * @category Service
73
+ */
74
+ export declare class FilterService extends FilterService_base {
75
+ }
76
+ /**
77
+ * Live layer. Requires {@link AwsClient.AwsClient} and
78
+ * {@link ConfigService.ConfigService} (wired by the caller).
79
+ *
80
+ * @category Layer
81
+ */
82
+ export declare const FilterServiceLive: Layer.Layer<FilterService, Errors.AwsCredentialError | Errors.AwsThrottleError | Errors.AwsApiError, AwsClient.AwsClient | ConfigService.ConfigService>;
83
+ export {};
84
+ //# sourceMappingURL=FilterService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilterService.d.ts","sourceRoot":"","sources":["../../src/FilterService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAC3F,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAA;AACjF,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,MAAM,EAAU,MAAM,QAAQ,CAAA;AACpE,OAAO,EAAE,KAAK,YAAY,EAAoC,MAAM,oBAAoB,CAAA;AAExF,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;CAC3B;AAED,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CACvC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IAC/C,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACxC,QAAQ,CAAC,kBAAkB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACnD;AAED,KAAK,kBAAkB,GAAG,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,kBAAkB,GAAG,MAAM,CAAC,gBAAgB,CAAA;AAElG;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,CAAA;IAC5E,QAAQ,CAAC,OAAO,EAAE,CAChB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,EACpC,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,IAAI,KACP,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,kBAAkB,CAAC,CAAA;CACrD;AAsFD;;;;GAIG;AACH,MAAM,CAAC,OAAO,WAAW,aAAa,CAAC;IACrC,UAAiB,OAAQ,SAAQ,kBAAkB;KAAG;CACvD;;AAED;;;;GAIG;AACH,qBAAa,aAAc,SAAQ,kBAGG;CAAG;AAEzC;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,yJAAoC,CAAA"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Cross-account `--filter` orchestration for `pr list`.
3
+ *
4
+ * Extracts the fan-out workflow that backs the named filter presets out of the
5
+ * CLI entrypoint (`bin.ts`) so the entrypoint keeps only presentation. The
6
+ * service depends on {@link AwsClient.AwsClient} and
7
+ * {@link ConfigService.ConfigService} and reuses the pure `matchesPreset` /
8
+ * `matchesRepoAuthor` predicates from `./filterPresets.ts`.
9
+ *
10
+ * Behaviour mirrors the original inline block exactly:
11
+ *
12
+ * - {@link FilterService.Service.resolveTargets} loads config, keeps only
13
+ * enabled accounts, and flattens to `{ profile, region }` targets.
14
+ * - {@link FilterService.Service.collect} resolves caller identity once per
15
+ * profile (for the identity-comparing presets), fans out OPEN-only PR fetches
16
+ * across targets (concurrency 4), filters by preset + repo/author, collects
17
+ * per-account failures, and returns PRs sorted by `lastModifiedDate` desc.
18
+ *
19
+ * @category Service
20
+ * @module
21
+ */
22
+ import { AwsClient, ConfigService } from "@knpkv/codecommit-core";
23
+ import { Context, Effect, Layer, Stream } from "effect";
24
+ import { matchesPreset, matchesRepoAuthor } from "./filterPresets.js";
25
+ const make = Effect.gen(function* () {
26
+ const aws = yield* AwsClient.AwsClient;
27
+ const cs = yield* ConfigService.ConfigService;
28
+ const resolveTargets = Effect.gen(function* () {
29
+ const config = yield* cs.load;
30
+ return config.accounts
31
+ .filter((a) => a.enabled)
32
+ .flatMap((a) => a.regions.map((r) => ({ profile: a.profile, region: r })));
33
+ });
34
+ const collect = (preset, targets, opts, now = new Date()) => Effect.gen(function* () {
35
+ // Resolve caller identity once per profile (deduped per profile within this
36
+ // run, not cached across runs) for presets that compare against "me".
37
+ const callerByProfile = new Map();
38
+ // Profiles whose caller-identity didn't resolve (lookup failed or returned
39
+ // no username). For the identity-comparing presets this means their PRs
40
+ // can't be matched, so we surface a warning rather than silently dropping them.
41
+ const unresolvedCallerProfiles = [];
42
+ if (preset === "mine" || preset === "needs-my-review") {
43
+ const uniqueProfiles = [...new Map(targets.map((t) => [t.profile, t])).values()];
44
+ const callers = yield* Effect.forEach(uniqueProfiles, (acct) => Effect.catchIf(Effect.map(aws.getCallerIdentity(acct), (id) => ({
45
+ profile: acct.profile,
46
+ username: id.username
47
+ })), () => true, () => Effect.succeed({ profile: acct.profile, username: null }), () => Effect.succeed({ profile: acct.profile, username: null })), { concurrency: 4 });
48
+ for (const { profile: p, username } of callers) {
49
+ if (username)
50
+ callerByProfile.set(p, username);
51
+ else
52
+ unresolvedCallerProfiles.push(p);
53
+ }
54
+ }
55
+ const collected = yield* Effect.forEach(targets, (acct) => Effect.catchIf(aws.getPullRequests(acct, { status: "OPEN" }).pipe(Stream.filter((pr) => matchesPreset(preset, pr, callerByProfile, now) && matchesRepoAuthor(pr, opts.repo, opts.author)), Stream.runCollect, Effect.map((chunk) => ({ ok: Array.from(chunk), failed: null }))), () => true,
56
+ // Don't silently coalesce auth/permission failures to "no matches" —
57
+ // collect the failure so it can be surfaced after the results.
58
+ (e) => Effect.succeed({
59
+ ok: [],
60
+ failed: `${acct.profile}/${acct.region}: ${e.message}`
61
+ }), (e) => Effect.succeed({
62
+ ok: [],
63
+ failed: `${acct.profile}/${acct.region}: ${e.message}`
64
+ })), { concurrency: 4 });
65
+ const prs = collected.flatMap((r) => r.ok).sort((a, b) => b.lastModifiedDate.getTime() - a.lastModifiedDate.getTime());
66
+ const failures = collected.flatMap((r) => (r.failed === null ? [] : [r.failed]));
67
+ return { prs, failures, unresolvedProfiles: unresolvedCallerProfiles };
68
+ });
69
+ return { resolveTargets, collect };
70
+ });
71
+ /**
72
+ * Cross-account filter orchestration service.
73
+ *
74
+ * @category Service
75
+ */
76
+ export class FilterService extends Context.Service()("@knpkv/codecommit/FilterService") {
77
+ }
78
+ /**
79
+ * Live layer. Requires {@link AwsClient.AwsClient} and
80
+ * {@link ConfigService.ConfigService} (wired by the caller).
81
+ *
82
+ * @category Layer
83
+ */
84
+ export const FilterServiceLive = Layer.effect(FilterService, make);
85
+ //# sourceMappingURL=FilterService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilterService.js","sourceRoot":"","sources":["../../src/FilterService.ts"],"names":[],"mappings":"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"}
package/dist/src/bin.js CHANGED
@@ -1,37 +1,46 @@
1
1
  #!/usr/bin/env bun
2
- import { Args, Command, Options } from "@effect/cli";
3
- import { FileSystem } from "@effect/platform";
4
- import { BunContext, BunRuntime } from "@effect/platform-bun";
2
+ import { BunRuntime, BunServices } from "@effect/platform-bun";
5
3
  import { NodeHttpClient } from "@effect/platform-node";
6
- import { AwsClient, AwsClientConfig } from "@knpkv/codecommit-core";
4
+ import { makeInstallCommand } from "@knpkv/agent-skills";
5
+ import { AwsClient, AwsClientConfig, CacheService, ConfigService } from "@knpkv/codecommit-core";
7
6
  import { makeServer } from "@knpkv/codecommit-web";
8
7
  import { Console, Effect, Layer, Stream } from "effect";
9
- import open from "open";
8
+ import * as FileSystem from "effect/FileSystem";
9
+ import * as Runtime from "effect/Runtime";
10
+ import * as Stdio from "effect/Stdio";
11
+ import { Argument as Args, Command, Flag as Options } from "effect/unstable/cli";
12
+ import * as ChildProcess from "effect/unstable/process/ChildProcess";
10
13
  import pkg from "../package.json";
14
+ import { FILTER_PRESETS, matchesRepoAuthor } from "./filterPresets.js";
15
+ import { FilterService, FilterServiceLive } from "./FilterService.js";
11
16
  // TUI Command
12
- const tui = Command.make("tui", {}, () => Effect.gen(function* () {
13
- yield* Effect.promise(() => import("./main.js"));
14
- }));
17
+ const launchTui = Effect.gen(function* () {
18
+ const { default: program } = yield* Effect.promise(() => import("./main.js"));
19
+ yield* program;
20
+ });
21
+ const tui = Command.make("tui", {}, () => launchTui);
15
22
  // Web Command
16
23
  const web = Command.make("web", {
17
24
  port: Options.integer("port").pipe(Options.withDefault(3000)),
18
- hostname: Options.text("hostname").pipe(Options.withDefault("127.0.0.1"))
25
+ hostname: Options.string("hostname").pipe(Options.withDefault("127.0.0.1"))
19
26
  }, ({ hostname, port }) => Effect.gen(function* () {
20
27
  yield* Effect.logInfo(`Starting web server at http://${hostname}:${port}`);
21
28
  // Open browser
22
- yield* Effect.promise(() => open(`http://${hostname}:${port}`));
29
+ const url = `http://${hostname}:${port}`;
30
+ const exitCode = (command) => Effect.scoped(command.pipe(Effect.flatMap((handle) => handle.exitCode)));
31
+ yield* exitCode(ChildProcess.make("open", [url])).pipe(Effect.catchIf(() => true, () => exitCode(ChildProcess.make("xdg-open", [url]))), Effect.catchIf(() => true, () => exitCode(ChildProcess.make("rundll32.exe", ["url.dll,FileProtocolHandler", url]))), Effect.catchIf(() => true, () => Effect.void));
23
32
  // Run server with configured port/hostname
24
33
  return yield* Layer.launch(makeServer({ port, hostname }));
25
34
  }));
26
35
  // PR Create Command
27
36
  const prCreate = Command.make("create", {
28
- repo: Args.text({ name: "repository" }).pipe(Args.withDescription("Repository name")),
29
- title: Args.text({ name: "title" }).pipe(Args.withDescription("PR title")),
30
- source: Options.text("source").pipe(Options.withAlias("s"), Options.withDescription("Source branch")),
31
- destination: Options.text("destination").pipe(Options.withAlias("d"), Options.withDescription("Destination branch"), Options.withDefault("main")),
32
- description: Options.text("description").pipe(Options.withDescription("PR description"), Options.optional),
33
- profile: Options.text("profile").pipe(Options.withAlias("p"), Options.withDescription("AWS profile"), Options.withDefault("default")),
34
- region: Options.text("region").pipe(Options.withAlias("r"), Options.withDescription("AWS region"), Options.withDefault("us-east-1"))
37
+ repo: Args.string("repository").pipe(Args.withDescription("Repository name")),
38
+ title: Args.string("title").pipe(Args.withDescription("PR title")),
39
+ source: Options.string("source").pipe(Options.withAlias("s"), Options.withDescription("Source branch")),
40
+ destination: Options.string("destination").pipe(Options.withAlias("d"), Options.withDescription("Destination branch"), Options.withDefault("main")),
41
+ description: Options.string("description").pipe(Options.withDescription("PR description"), Options.optional),
42
+ profile: Options.string("profile").pipe(Options.withAlias("p"), Options.withDescription("AWS profile"), Options.withDefault("default")),
43
+ region: Options.string("region").pipe(Options.withAlias("r"), Options.withDescription("AWS region"), Options.withDefault("us-east-1"))
35
44
  }, ({ description, destination, profile, region, repo, source, title }) => Effect.gen(function* () {
36
45
  const aws = yield* AwsClient.AwsClient;
37
46
  yield* Console.log(`Creating PR: ${source} -> ${destination} in ${repo}`);
@@ -46,28 +55,83 @@ const prCreate = Command.make("create", {
46
55
  const link = `https://${region}.console.aws.amazon.com/codesuite/codecommit/repositories/${repo}/pull-requests/${prId}?region=${region}`;
47
56
  yield* Console.log(`Created PR: ${prId}`);
48
57
  yield* Console.log(link);
49
- }).pipe(Effect.provide(Layer.merge(AwsClient.AwsClientLive, NodeHttpClient.layer)))).pipe(Command.withDescription("Create a pull request"));
58
+ }).pipe(Effect.provide(Layer.merge(AwsClient.AwsClientLive, NodeHttpClient.layerFetch)))).pipe(Command.withDescription("Create a pull request"));
59
+ // Filter presets (FILTER_PRESETS, matchesPreset, matchesRepoAuthor) live in
60
+ // ./filterPresets.ts — a side-effect-free module so they can be unit-tested
61
+ // without importing this CLI entrypoint (which boots the TUI/Bun runtime).
50
62
  // PR List Command
51
63
  const prList = Command.make("list", {
52
- profile: Options.text("profile").pipe(Options.withAlias("p"), Options.withDescription("AWS profile"), Options.withDefault("default")),
53
- region: Options.text("region").pipe(Options.withAlias("r"), Options.withDescription("AWS region"), Options.withDefault("us-east-1")),
54
- status: Options.choice("status", ["OPEN", "CLOSED"]).pipe(Options.withAlias("s"), Options.withDescription("Filter by PR status"), Options.withDefault("OPEN")),
55
- all: Options.boolean("all").pipe(Options.withAlias("a"), Options.withDescription("Show all PRs (both OPEN and CLOSED)"), Options.withDefault(false)),
56
- repo: Options.text("repo").pipe(Options.withDescription("Filter by repository name"), Options.optional),
57
- author: Options.text("author").pipe(Options.withDescription("Filter by author"), Options.optional),
64
+ profile: Options.string("profile").pipe(Options.withAlias("p"), Options.withDescription("AWS profile (ignored when --filter is set — presets fan out across all enabled accounts)"), Options.withDefault("default")),
65
+ region: Options.string("region").pipe(Options.withAlias("r"), Options.withDescription("AWS region (ignored when --filter is set — presets fan out across all enabled accounts)"), Options.withDefault("us-east-1")),
66
+ status: Options.choice("status", ["OPEN", "CLOSED"]).pipe(Options.withAlias("s"), Options.withDescription("Filter by PR status (ignored when --filter is set — presets are OPEN-only)"), Options.withDefault("OPEN")),
67
+ all: Options.boolean("all").pipe(Options.withAlias("a"), Options.withDescription("Show all PRs (both OPEN and CLOSED; ignored when --filter is set — presets are OPEN-only)"), Options.withDefault(false)),
68
+ repo: Options.string("repo").pipe(Options.withDescription("Filter by repository name"), Options.optional),
69
+ author: Options.string("author").pipe(Options.withDescription("Filter by author"), Options.optional),
70
+ filter: Options.choice("filter", FILTER_PRESETS).pipe(Options.withDescription("Named preset (fans out across all enabled accounts, OPEN PRs only — ignores --status/--all): " +
71
+ "mine | needs-my-review | stale | conflicting"), Options.optional),
58
72
  json: Options.boolean("json").pipe(Options.withDescription("Output as JSON"), Options.withDefault(false))
59
- }, ({ all, author, json, profile, region, repo, status }) => Effect.gen(function* () {
73
+ }, ({ all, author, filter, json, profile, region, repo, status }) => Effect.gen(function* () {
60
74
  const aws = yield* AwsClient.AwsClient;
75
+ // ── Filter-preset path: fan out across all enabled accounts ──────────────
76
+ if (filter._tag === "Some") {
77
+ const preset = filter.value;
78
+ const fs = yield* FilterService;
79
+ const targets = yield* fs.resolveTargets;
80
+ if (targets.length === 0) {
81
+ yield* Console.log("No enabled accounts in ~/.codecommit/config.json. Enable some with `codecommit tui`.");
82
+ return;
83
+ }
84
+ // Progress/status text goes to stderr so `--json` emits only the JSON document on stdout.
85
+ if (!json)
86
+ yield* Console.error(`Scanning ${targets.length} account(s) with filter '${preset}'...`);
87
+ const { failures, prs, unresolvedProfiles } = yield* fs.collect(preset, targets, { repo, author });
88
+ const unresolvedCallerProfiles = unresolvedProfiles;
89
+ // Warn (on stderr, so `--json` stdout stays clean) when caller identity
90
+ // couldn't be resolved for an identity-comparing preset — those accounts'
91
+ // results may be incomplete because no PR can match an unknown "me".
92
+ const reportWarnings = Effect.gen(function* () {
93
+ for (const p of unresolvedCallerProfiles) {
94
+ yield* Console.error(`⚠ could not resolve caller identity for profile ${p}; '${preset}' results for it may be incomplete`);
95
+ }
96
+ if (failures.length > 0) {
97
+ yield* Console.error(`\n⚠ ${failures.length} account(s) failed:`);
98
+ for (const f of failures)
99
+ yield* Console.error(` ${f}`);
100
+ }
101
+ });
102
+ if (prs.length === 0) {
103
+ if (json)
104
+ yield* Console.log("[]");
105
+ else
106
+ yield* Console.error(`No PRs match filter '${preset}'.`);
107
+ yield* reportWarnings;
108
+ return;
109
+ }
110
+ if (json) {
111
+ yield* Console.log(JSON.stringify(prs, null, 2));
112
+ }
113
+ else {
114
+ yield* Console.log(`\nFound ${prs.length} PR(s) matching filter '${preset}':\n`);
115
+ for (const pr of prs) {
116
+ const flags = [
117
+ pr.isApproved ? "approved" : "",
118
+ pr.isMergeable ? "mergeable" : "conflicts"
119
+ ].filter(Boolean).join(" ");
120
+ yield* Console.log(`${pr.id} ${pr.repositoryName} [${pr.account.profile}/${pr.account.region}]`);
121
+ yield* Console.log(` ${pr.title}`);
122
+ yield* Console.log(` ${pr.sourceBranch} -> ${pr.destinationBranch}`);
123
+ yield* Console.log(` by ${pr.author} ${flags}`);
124
+ yield* Console.log("");
125
+ }
126
+ }
127
+ yield* reportWarnings;
128
+ return;
129
+ }
130
+ // ── Single-account path (original behaviour) ─────────────────────────────
61
131
  const account = { profile: profile, region: region };
62
132
  const statusLabel = all ? "all" : status.toLowerCase();
63
133
  yield* Console.log(`Fetching ${statusLabel} PRs...`);
64
- const filterPRs = (prStream) => prStream.pipe(Stream.filter((pr) => {
65
- if (repo._tag === "Some" && pr.repositoryName !== repo.value)
66
- return false;
67
- if (author._tag === "Some" && pr.author !== author.value)
68
- return false;
69
- return true;
70
- }), Stream.runCollect, Effect.map((chunk) => Array.from(chunk)));
134
+ const filterPRs = (prStream) => prStream.pipe(Stream.filter((pr) => matchesRepoAuthor(pr, repo, author)), Stream.runCollect, Effect.map((chunk) => Array.from(chunk)));
71
135
  let prs;
72
136
  if (all) {
73
137
  const [openPrs, closedPrs] = yield* Effect.all([
@@ -101,7 +165,10 @@ const prList = Command.make("list", {
101
165
  yield* Console.log("");
102
166
  }
103
167
  }
104
- }).pipe(Effect.provide(Layer.merge(AwsClient.AwsClientLive, NodeHttpClient.layer)))).pipe(Command.withDescription("List open pull requests"));
168
+ }).pipe(Effect.provide(
169
+ // FilterService draws AwsClient/ConfigService from the base layers, which
170
+ // are also merged into the output so the single-account path keeps them.
171
+ FilterServiceLive.pipe(Layer.provideMerge(Layer.mergeAll(AwsClient.AwsClientLive, NodeHttpClient.layerFetch, ConfigService.ConfigServiceLive.pipe(Layer.provide(CacheService.EventsHub.Default)))))))).pipe(Command.withDescription("List pull requests (use --filter for cross-account presets)"));
105
172
  // Helper to render comment threads as markdown
106
173
  const renderThread = (thread, indent = 0) => {
107
174
  const prefix = " ".repeat(indent);
@@ -120,11 +187,11 @@ const renderLocation = (loc) => {
120
187
  };
121
188
  // PR Export Command
122
189
  const prExport = Command.make("export", {
123
- prId: Args.text({ name: "pr-id" }).pipe(Args.withDescription("Pull request ID")),
124
- repo: Args.text({ name: "repository" }).pipe(Args.withDescription("Repository name")),
190
+ prId: Args.string("pr-id").pipe(Args.withDescription("Pull request ID")),
191
+ repo: Args.string("repository").pipe(Args.withDescription("Repository name")),
125
192
  output: Options.file("output").pipe(Options.withAlias("o"), Options.withDescription("Output file path"), Options.optional),
126
- profile: Options.text("profile").pipe(Options.withAlias("p"), Options.withDescription("AWS profile"), Options.withDefault("default")),
127
- region: Options.text("region").pipe(Options.withAlias("r"), Options.withDescription("AWS region"), Options.withDefault("us-east-1"))
193
+ profile: Options.string("profile").pipe(Options.withAlias("p"), Options.withDescription("AWS profile"), Options.withDefault("default")),
194
+ region: Options.string("region").pipe(Options.withAlias("r"), Options.withDescription("AWS region"), Options.withDefault("us-east-1"))
128
195
  }, ({ output, prId, profile, region, repo }) => Effect.gen(function* () {
129
196
  const aws = yield* AwsClient.AwsClient;
130
197
  const fs = yield* FileSystem.FileSystem;
@@ -166,14 +233,14 @@ const prExport = Command.make("export", {
166
233
  yield* Console.log("");
167
234
  yield* Console.log(markdown);
168
235
  }
169
- }).pipe(Effect.provide(Layer.merge(AwsClient.AwsClientLive, NodeHttpClient.layer)))).pipe(Command.withDescription("Export PR comments as markdown"));
236
+ }).pipe(Effect.provide(Layer.merge(AwsClient.AwsClientLive, NodeHttpClient.layerFetch)))).pipe(Command.withDescription("Export PR comments as markdown"));
170
237
  // PR Update Command
171
238
  const prUpdate = Command.make("update", {
172
- prId: Args.text({ name: "pr-id" }).pipe(Args.withDescription("Pull request ID")),
173
- title: Options.text("title").pipe(Options.withAlias("t"), Options.withDescription("New PR title"), Options.optional),
174
- description: Options.text("description").pipe(Options.withAlias("d"), Options.withDescription("New PR description"), Options.optional),
175
- profile: Options.text("profile").pipe(Options.withAlias("p"), Options.withDescription("AWS profile"), Options.withDefault("default")),
176
- region: Options.text("region").pipe(Options.withAlias("r"), Options.withDescription("AWS region"), Options.withDefault("us-east-1"))
239
+ prId: Args.string("pr-id").pipe(Args.withDescription("Pull request ID")),
240
+ title: Options.string("title").pipe(Options.withAlias("t"), Options.withDescription("New PR title"), Options.optional),
241
+ description: Options.string("description").pipe(Options.withAlias("d"), Options.withDescription("New PR description"), Options.optional),
242
+ profile: Options.string("profile").pipe(Options.withAlias("p"), Options.withDescription("AWS profile"), Options.withDefault("default")),
243
+ region: Options.string("region").pipe(Options.withAlias("r"), Options.withDescription("AWS region"), Options.withDefault("us-east-1"))
177
244
  }, ({ description, prId, profile, region, title }) => Effect.gen(function* () {
178
245
  const aws = yield* AwsClient.AwsClient;
179
246
  const account = { profile: profile, region: region };
@@ -190,15 +257,35 @@ const prUpdate = Command.make("update", {
190
257
  yield* aws.updatePullRequestDescription({ account, pullRequestId: prId, description: description.value });
191
258
  }
192
259
  yield* Console.log(`Updated PR ${prId}`);
193
- }).pipe(Effect.provide(Layer.merge(AwsClient.AwsClientLive, NodeHttpClient.layer)))).pipe(Command.withDescription("Update PR title or description"));
260
+ }).pipe(Effect.provide(Layer.merge(AwsClient.AwsClientLive, NodeHttpClient.layerFetch)))).pipe(Command.withDescription("Update PR title or description"));
194
261
  // PR Command (parent)
195
262
  const pr = Command.make("pr", {}, () => Console.log("Usage: codecommit pr <command>")).pipe(Command.withSubcommands([prList, prCreate, prExport, prUpdate]), Command.withDescription("Pull request commands"));
263
+ const skillsInstall = makeInstallCommand({
264
+ description: "Install the CodeCommit agent skill",
265
+ name: "install",
266
+ skills: ["codecommit"]
267
+ });
268
+ const skills = Command.make("skills", {}, () => Console.log("Usage: codecommit skills install")).pipe(Command.withSubcommands([skillsInstall]), Command.withDescription("Agent skill commands"));
196
269
  const command = Command.make("codecommit", {}, () =>
197
270
  // Default to TUI if no subcommand
198
- Effect.promise(() => import("./main.js"))).pipe(Command.withSubcommands([tui, web, pr]));
199
- const cli = Command.run(command, {
200
- name: pkg.name,
271
+ launchTui).pipe(Command.withSubcommands([tui, web, pr, skills]));
272
+ const cli = Command.runWith(command, {
201
273
  version: pkg.version
202
274
  });
203
- Effect.suspend(() => cli(process.argv)).pipe(Effect.provide(Layer.mergeAll(BunContext.layer, NodeHttpClient.layer, AwsClientConfig.Default)), BunRuntime.runMain);
275
+ const AppRuntimeLayer = Layer.mergeAll(NodeHttpClient.layerFetch, AwsClientConfig.Default);
276
+ const needsAppRuntime = (args) => args[0] !== "skills";
277
+ const program = Effect.gen(function* () {
278
+ const stdio = yield* Stdio.Stdio;
279
+ const args = yield* stdio.args;
280
+ const runCli = needsAppRuntime(args) ? cli(args).pipe(Effect.provide(AppRuntimeLayer)) : cli(args);
281
+ return yield* runCli;
282
+ });
283
+ // The TUI keeps long-lived resources open through its atom runtime (SQLite
284
+ // repos, the HTTP client, the EventsHub PubSub). When the user quits in-app the
285
+ // main fiber exits cleanly (code 0) and — because OpenTUI holds stdin in raw
286
+ // mode, so Ctrl-C is delivered as a keypress, not a SIGINT — runMain's default
287
+ // teardown never reaches `process.exit`. The process would then hang on those
288
+ // open handles after the UI has already torn down. Always terminate explicitly.
289
+ const forceExitTeardown = (exit) => Runtime.defaultTeardown(exit, (code) => process.exit(code));
290
+ BunRuntime.runMain(Effect.provide(program, BunServices.layer), { teardown: forceExitTeardown });
204
291
  //# sourceMappingURL=bin.js.map