@senad-d/branchme 0.1.1
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/.env.example +5 -0
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +347 -0
- package/SECURITY.md +77 -0
- package/docs/PROJECT_DEFINITION_BRIEF.md +114 -0
- package/docs/SMOKE_TEST.md +41 -0
- package/docs/STRUCTURE.md +82 -0
- package/docs/TUI_CAPTURE.md +209 -0
- package/img/demo.gif +0 -0
- package/img/icon.png +0 -0
- package/img/icon.svg +28 -0
- package/img/image.png +0 -0
- package/package.json +78 -0
- package/src/commands/branchme-command.ts +166 -0
- package/src/constants.ts +27 -0
- package/src/extension.ts +10 -0
- package/src/git.ts +503 -0
- package/src/github.ts +566 -0
- package/src/redaction.ts +22 -0
- package/src/tools/branchme-tools.ts +275 -0
- package/src/types.ts +76 -0
- package/src/ui/branchme-panel.ts +345 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# BranchMe Structure Guide
|
|
2
|
+
|
|
3
|
+
BranchMe is a TypeScript Pi extension package for current-repository git branch, push, and GitHub pull request workflows.
|
|
4
|
+
|
|
5
|
+
## Source layout
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
src/
|
|
9
|
+
├── extension.ts # extension entry point; registers command and tools only
|
|
10
|
+
├── constants.ts # command/tool names, timeouts, GitHub API constants
|
|
11
|
+
├── types.ts # serializable tool details and helper result types
|
|
12
|
+
├── commands/
|
|
13
|
+
│ └── branchme-command.ts # /branchme status/help command; informational only
|
|
14
|
+
├── tools/
|
|
15
|
+
│ └── branchme-tools.ts # branch_status/change_branch/create_branch/push_branch/pull_request registration
|
|
16
|
+
├── git.ts # argv-style git helpers and per-repo workflow queue
|
|
17
|
+
├── github.ts # GitHub repo resolution, env/.env tokens, branch preflight, REST calls, redaction
|
|
18
|
+
└── ui/
|
|
19
|
+
└── branchme-panel.ts # compact /branchme status panel renderer
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Module boundaries
|
|
23
|
+
|
|
24
|
+
1. `src/extension.ts` stays small and only calls `registerBranchMeCommand(pi)` and `registerBranchMeTools(pi)`.
|
|
25
|
+
2. `src/commands/branchme-command.ts` parses `/branchme`, `/branchme help`, `--help`, and `-h`; it never performs git or GitHub mutations and avoids raw stdout in JSON mode.
|
|
26
|
+
3. `src/tools/branchme-tools.ts` owns TypeBox schemas, prompt metadata, tool content, and safe structured details.
|
|
27
|
+
4. `src/git.ts` owns current-repository git behavior: root detection, branch/upstream/status inspection, branch validation, branch creation, existing-local-branch switching, clean-worktree preflight, current-branch push/publish, and the per-repository workflow queue.
|
|
28
|
+
5. `src/github.ts` owns GitHub `owner/repo` parsing, repository boundary checks, `GITHUB_TOKEN`/`GH_TOKEN` process-env and hardened git-root `.env` fallback resolution, PR branch-name syntax validation, GitHub branch visibility/commit preflight, PR REST calls, response validation, and redacted errors.
|
|
29
|
+
6. `src/redaction.ts` owns shared credential redaction for Git and GitHub messages.
|
|
30
|
+
7. `src/types.ts` keeps serializable details shared by helpers and tools.
|
|
31
|
+
8. `src/ui/branchme-panel.ts` renders a compact status panel and clips lines to terminal width.
|
|
32
|
+
|
|
33
|
+
## Pi extension conventions
|
|
34
|
+
|
|
35
|
+
- No long-lived processes, watchers, timers, sockets, or background jobs start in the extension factory.
|
|
36
|
+
- Slash commands are informational; tools perform branch, push, and PR actions.
|
|
37
|
+
- Every tool uses a strict TypeBox object schema with `additionalProperties: false`.
|
|
38
|
+
- Every tool defines a description, `promptSnippet`, and tool-specific `promptGuidelines` that explicitly name the tool.
|
|
39
|
+
- Git commands use `pi.exec("git", args, { cwd, signal, timeout })` with argv arrays; repository mutations run from the verified git root and same-repository mutation/PR windows are serialized per repository.
|
|
40
|
+
- Tool details avoid token values and unbounded raw command/API output.
|
|
41
|
+
- Pi core packages, including `@earendil-works/pi-tui` for key/width utilities, remain in `peerDependencies` with `"*"`.
|
|
42
|
+
|
|
43
|
+
## Security-sensitive areas
|
|
44
|
+
|
|
45
|
+
- `change_branch` mutates local HEAD and working-tree files only through `git switch <branchName>` for existing local branches after a clean-worktree preflight.
|
|
46
|
+
- `create_branch` mutates local branch/HEAD only with `git switch -c`.
|
|
47
|
+
- `push_branch` mutates remote refs only for the current branch and uses an explicit upstream remote/refspec instead of bare `git push` when an upstream exists.
|
|
48
|
+
- `pull_request` requires `headBranch` and `baseBranch` to exist locally, requires `headBranch` to match the GitHub-visible branch commit, queues behind already-started same-repository git mutation windows, makes GitHub REST API calls for the resolved current repository only, and rejects missing, owner-prefixed, or unsafe branch refs before token lookup or the request.
|
|
49
|
+
- `pull_request` reads `GITHUB_TOKEN` or `GH_TOKEN` from process environment first; only when neither process token is set does it read those token keys from a small regular `.env` file in the verified git root as a fallback.
|
|
50
|
+
- BranchMe does not force checkout, stash, stage, commit, directly edit files, read non-token `.env` keys, follow unsafe `.env` file types, depend on GitHub CLI, or collect telemetry.
|
|
51
|
+
|
|
52
|
+
## Documentation
|
|
53
|
+
|
|
54
|
+
- `docs/PROJECT_DEFINITION_BRIEF.md` preserves the approved project definition.
|
|
55
|
+
- `docs/STRUCTURE.md` describes the implemented source layout.
|
|
56
|
+
- `docs/SMOKE_TEST.md` records isolated validation/smoke-test findings.
|
|
57
|
+
- `docs/TUI_CAPTURE.md` stores deterministic text captures of BranchMe TUI/help surfaces for visual regression review.
|
|
58
|
+
|
|
59
|
+
## Tests
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
test/
|
|
63
|
+
├── command.test.mjs # /branchme parsing, help, fallback, panel width
|
|
64
|
+
├── git.test.mjs # git helper command construction and failures
|
|
65
|
+
├── git-integration.test.mjs # isolated real-git branch helper coverage
|
|
66
|
+
├── github.test.mjs # GitHub parsing, token resolution, fetch wrapper, redaction
|
|
67
|
+
├── preparation.test.mjs # package/docs/source metadata checks
|
|
68
|
+
├── tools.test.mjs # extension registration, schemas, prompt metadata, tool behavior
|
|
69
|
+
└── tui-capture.test.mjs # generated text capture for TUI/help visual baselines
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Validation commands:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run typecheck
|
|
76
|
+
npm run format:check
|
|
77
|
+
npm run test
|
|
78
|
+
npm run check:pack
|
|
79
|
+
npm run validate
|
|
80
|
+
npm run smoke:pi:packed # release gate for the installed npm artifact
|
|
81
|
+
pi --no-extensions -e .
|
|
82
|
+
```
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# BranchMe TUI Capture
|
|
2
|
+
|
|
3
|
+
This file is a deterministic text capture of BranchMe user-facing TUI/help surfaces.
|
|
4
|
+
Use it as a visual baseline when improving layout, wording, spacing, or responsive behavior.
|
|
5
|
+
Trailing spaces are shown as `·` so formatting checks can keep the repository whitespace-clean.
|
|
6
|
+
|
|
7
|
+
Generated and verified by `test/tui-capture.test.mjs`.
|
|
8
|
+
Update intentionally with:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
UPDATE_TUI_CAPTURE=1 node --test test/tui-capture.test.mjs
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## /branchme help
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
# BranchMe
|
|
18
|
+
|
|
19
|
+
Current-repository branch workflow tools for Pi.
|
|
20
|
+
|
|
21
|
+
Commands only show info; BranchMe tools perform actions.
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
24
|
+
|
|
25
|
+
1. `branch_status` — inspect repo and branch state.
|
|
26
|
+
2. `change_branch` / `create_branch` — switch to an existing clean branch or create one from `HEAD`.
|
|
27
|
+
3. Commit outside BranchMe.
|
|
28
|
+
4. `push_branch` — push the current branch.
|
|
29
|
+
5. `pull_request` — open a PR after `push_branch` completes and GitHub sees the branches.
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- Run inside a Git repo with `git` available.
|
|
34
|
+
- For PRs: GitHub `origin` and `GITHUB_TOKEN` or `GH_TOKEN` (environment or `.env`).
|
|
35
|
+
- BranchMe never stages or commits.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Panel: Tiny mode: clean branch with token
|
|
39
|
+
|
|
40
|
+
Width: 18
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
BranchMe
|
|
44
|
+
branch: feature/c…
|
|
45
|
+
repo: senad-d/bra…
|
|
46
|
+
q quit
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Panel: Narrow mode: clean branch with token
|
|
50
|
+
|
|
51
|
+
Width: 40
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
╭ BranchMe ──────────────────── Status ╮
|
|
55
|
+
│current repo only • informational │
|
|
56
|
+
│ ↑↓ section • q quit • /branchme help │
|
|
57
|
+
├──────────────────────────────────────┤
|
|
58
|
+
│ STATUS │
|
|
59
|
+
│ Current branch: feature/current │
|
|
60
|
+
│ GitHub repository: senad-d/branchme │
|
|
61
|
+
│ GitHub token: present │
|
|
62
|
+
│ │
|
|
63
|
+
│ │
|
|
64
|
+
│ │
|
|
65
|
+
├──────────────────────────────────────┤
|
|
66
|
+
│ 1/2 • status • current repository on…│
|
|
67
|
+
╰──────────────────────────────────────╯
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Panel: Wide mode: Status selected
|
|
71
|
+
|
|
72
|
+
Width: 80
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
╭ BranchMe ──────────────────────────────────────────────────────────── Status ╮
|
|
76
|
+
│ ↑↓ section • q quit • /branchme help │
|
|
77
|
+
├─────────────────────┬────────────────────────────────────────────────────────┤
|
|
78
|
+
│▶ Status │ STATUS │
|
|
79
|
+
│ Workflow │ Current branch: feature/current │
|
|
80
|
+
│ │ GitHub repository: senad-d/branchme │
|
|
81
|
+
│ │ GitHub token: present │
|
|
82
|
+
│ │ │
|
|
83
|
+
│ │ │
|
|
84
|
+
│ │ │
|
|
85
|
+
├─────────────────────┴────────────────────────────────────────────────────────┤
|
|
86
|
+
│ 1/2 • status • current repository only • tools perform actions │
|
|
87
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Panel: Wide mode: Workflow selected
|
|
91
|
+
|
|
92
|
+
Width: 80
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
╭ BranchMe ────────────────────────────────────────────────────────── Workflow ╮
|
|
96
|
+
│ ↑↓ section • q quit • /branchme help │
|
|
97
|
+
├─────────────────────┬────────────────────────────────────────────────────────┤
|
|
98
|
+
│ Status │ WORKFLOW │
|
|
99
|
+
│▶ Workflow │ branch_status -> inspect │
|
|
100
|
+
│ │ change_branch -> existing local │
|
|
101
|
+
│ │ create_branch -> from HEAD │
|
|
102
|
+
│ │ push_branch -> current branch │
|
|
103
|
+
│ │ pull_request -> after push │
|
|
104
|
+
│ │ │
|
|
105
|
+
├─────────────────────┴────────────────────────────────────────────────────────┤
|
|
106
|
+
│ 2/2 • workflow • inspect → change/create → push → PR │
|
|
107
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Panel: Very wide terminal: panel width capped
|
|
111
|
+
|
|
112
|
+
Width: 112
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
╭ BranchMe ──────────────────────────────────────────────────────────────────────────── Status ╮
|
|
116
|
+
│ ↑↓ section • q quit • /branchme help │
|
|
117
|
+
├──────────────────────┬───────────────────────────────────────────────────────────────────────┤
|
|
118
|
+
│▶ Status │ STATUS │
|
|
119
|
+
│ Workflow │ Current branch: main │
|
|
120
|
+
│ │ GitHub repository: senad-d/BranchMe │
|
|
121
|
+
│ │ GitHub token: not set │
|
|
122
|
+
│ │ │
|
|
123
|
+
│ │ │
|
|
124
|
+
│ │ │
|
|
125
|
+
├──────────────────────┴───────────────────────────────────────────────────────────────────────┤
|
|
126
|
+
│ 1/2 • status • current repository only • tools perform actions │
|
|
127
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Panel: Fallback values: detached HEAD without repository or token
|
|
131
|
+
|
|
132
|
+
Width: 50
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
╭ BranchMe ────────────────────────────── Status ╮
|
|
136
|
+
│current repo only • informational │
|
|
137
|
+
│ ↑↓ section • q quit • /branchme help │
|
|
138
|
+
├────────────────────────────────────────────────┤
|
|
139
|
+
│ STATUS │
|
|
140
|
+
│ Current branch: detached HEAD │
|
|
141
|
+
│ GitHub repository: not resolved │
|
|
142
|
+
│ GitHub token: not set │
|
|
143
|
+
│ │
|
|
144
|
+
│ │
|
|
145
|
+
│ │
|
|
146
|
+
├────────────────────────────────────────────────┤
|
|
147
|
+
│ 1/2 • warning • Unable to resolve a GitHub rep…│
|
|
148
|
+
╰────────────────────────────────────────────────╯
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Panel: Repository warning: boundary mismatch
|
|
152
|
+
|
|
153
|
+
Width: 80
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
╭ BranchMe ──────────────────────────────────────────────────────────── Status ╮
|
|
157
|
+
│ ↑↓ section • q quit • /branchme help │
|
|
158
|
+
├─────────────────────┬────────────────────────────────────────────────────────┤
|
|
159
|
+
│▶ Status │ STATUS │
|
|
160
|
+
│ Workflow │ Current branch: main │
|
|
161
|
+
│ │ GitHub repository: warning: Repository boundary misma…│
|
|
162
|
+
│ │ GitHub token: present │
|
|
163
|
+
│ │ │
|
|
164
|
+
│ │ │
|
|
165
|
+
│ │ │
|
|
166
|
+
├─────────────────────┴────────────────────────────────────────────────────────┤
|
|
167
|
+
│ 1/2 • warning • Repository boundary mismatch: local origin resolves to senad…│
|
|
168
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Panel: Token warning: fallback error
|
|
172
|
+
|
|
173
|
+
Width: 72
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
╭ BranchMe ──────────────────────────────────────────────────── Status ╮
|
|
177
|
+
│ ↑↓ section • q quit • /branchme help │
|
|
178
|
+
├───────────────────┬──────────────────────────────────────────────────┤
|
|
179
|
+
│▶ Status │ STATUS │
|
|
180
|
+
│ Workflow │ Current branch: main │
|
|
181
|
+
│ │ GitHub repository: senad-d/branchme │
|
|
182
|
+
│ │ GitHub token: warning: Unable to read .env…│
|
|
183
|
+
│ │ │
|
|
184
|
+
│ │ │
|
|
185
|
+
│ │ │
|
|
186
|
+
├───────────────────┴──────────────────────────────────────────────────┤
|
|
187
|
+
│ 1/2 • warning • Unable to read .env file for GitHub token fallback: …│
|
|
188
|
+
╰──────────────────────────────────────────────────────────────────────╯
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Panel: Long values: truncation
|
|
192
|
+
|
|
193
|
+
Width: 72
|
|
194
|
+
|
|
195
|
+
```text
|
|
196
|
+
╭ BranchMe ──────────────────────────────────────────────────── Status ╮
|
|
197
|
+
│ ↑↓ section • q quit • /branchme help │
|
|
198
|
+
├───────────────────┬──────────────────────────────────────────────────┤
|
|
199
|
+
│▶ Status │ STATUS │
|
|
200
|
+
│ Workflow │ Current branch: feature/super-long-branch-na…│
|
|
201
|
+
│ │ GitHub repository: very-long-owner-name/very-lo…│
|
|
202
|
+
│ │ GitHub token: present │
|
|
203
|
+
│ │ │
|
|
204
|
+
│ │ │
|
|
205
|
+
│ │ │
|
|
206
|
+
├───────────────────┴──────────────────────────────────────────────────┤
|
|
207
|
+
│ 1/2 • warning • This deliberately long status note is captured to de…│
|
|
208
|
+
╰──────────────────────────────────────────────────────────────────────╯
|
|
209
|
+
```
|
package/img/demo.gif
ADDED
|
Binary file
|
package/img/icon.png
ADDED
|
Binary file
|
package/img/icon.svg
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="160" y1="120" x2="864" y2="904" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop offset="0" stop-color="#111827"/>
|
|
5
|
+
<stop offset="1" stop-color="#0F766E"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
|
|
9
|
+
<!-- App tile -->
|
|
10
|
+
<rect x="96" y="96" width="832" height="832" rx="208" fill="url(#bg)"/>
|
|
11
|
+
|
|
12
|
+
<!-- Main branch shape -->
|
|
13
|
+
<path d="M344 704V320" stroke="#F8FAFC" stroke-width="72" stroke-linecap="round"/>
|
|
14
|
+
<path d="M344 512H512C604 512 680 436 680 344" stroke="#F8FAFC" stroke-width="72" stroke-linecap="round" stroke-linejoin="round"/>
|
|
15
|
+
<path d="M344 512H512C604 512 680 588 680 680" stroke="#F8FAFC" stroke-width="72" stroke-linecap="round" stroke-linejoin="round"/>
|
|
16
|
+
|
|
17
|
+
<!-- Nodes -->
|
|
18
|
+
<circle cx="344" cy="320" r="72" fill="#34D399"/>
|
|
19
|
+
<circle cx="344" cy="704" r="72" fill="#38BDF8"/>
|
|
20
|
+
<circle cx="680" cy="344" r="72" fill="#38BDF8"/>
|
|
21
|
+
<circle cx="680" cy="680" r="72" fill="#34D399"/>
|
|
22
|
+
|
|
23
|
+
<!-- Inner node cutouts for a clean Git-like look -->
|
|
24
|
+
<circle cx="344" cy="320" r="30" fill="#111827" opacity="0.9"/>
|
|
25
|
+
<circle cx="344" cy="704" r="30" fill="#111827" opacity="0.9"/>
|
|
26
|
+
<circle cx="680" cy="344" r="30" fill="#111827" opacity="0.9"/>
|
|
27
|
+
<circle cx="680" cy="680" r="30" fill="#111827" opacity="0.9"/>
|
|
28
|
+
</svg>
|
package/img/image.png
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@senad-d/branchme",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Minimal Pi extension for current-repository git branch, push, and pull request workflows.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Senad Dizdarević <112484166+senad-d@users.noreply.github.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/senad-d/branchme.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/senad-d/branchme/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/senad-d/branchme#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi",
|
|
18
|
+
"pi-package",
|
|
19
|
+
"pi-extension",
|
|
20
|
+
"pi-coding-agent",
|
|
21
|
+
"branchme",
|
|
22
|
+
"git",
|
|
23
|
+
"git-branch",
|
|
24
|
+
"git-workflow",
|
|
25
|
+
"github",
|
|
26
|
+
"github-pr",
|
|
27
|
+
"pull-request",
|
|
28
|
+
"branch-workflow",
|
|
29
|
+
"automation",
|
|
30
|
+
"typescript"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.19.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
".env.example",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"SECURITY.md",
|
|
40
|
+
"CHANGELOG.md",
|
|
41
|
+
"docs/**/*.md",
|
|
42
|
+
"img/**/*",
|
|
43
|
+
"src/**/*.ts",
|
|
44
|
+
"tsconfig.json"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"dev": "pi --no-extensions -e .",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"lint": "npm run typecheck && npm run format:check && npm run check",
|
|
50
|
+
"format:check": "node scripts/check-format.mjs",
|
|
51
|
+
"test": "node --test test/*.test.mjs",
|
|
52
|
+
"smoke:pi": "node scripts/smoke-pi-runtime.mjs",
|
|
53
|
+
"smoke:pi:packed": "node scripts/smoke-pi-installed-package.mjs",
|
|
54
|
+
"check": "node --check scripts/check-format.mjs && node --check scripts/check-package-contents.mjs && node --check scripts/publish-npm.mjs && node --check scripts/smoke-pi-installed-package.mjs && node --check scripts/smoke-pi-runtime.mjs",
|
|
55
|
+
"pack:dry-run": "npm pack --dry-run --json",
|
|
56
|
+
"check:pack": "node scripts/check-package-contents.mjs",
|
|
57
|
+
"validate": "npm run typecheck && npm run format:check && npm run test && npm run smoke:pi && npm run check && npm run check:pack",
|
|
58
|
+
"release:check": "npm run validate && npm run smoke:pi:packed"
|
|
59
|
+
},
|
|
60
|
+
"pi": {
|
|
61
|
+
"extensions": [
|
|
62
|
+
"./src/extension.ts"
|
|
63
|
+
],
|
|
64
|
+
"image": "https://raw.githubusercontent.com/senad-d/BranchMe/main/img/demo.gif"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
68
|
+
"@earendil-works/pi-tui": "*",
|
|
69
|
+
"typebox": "*"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@earendil-works/pi-coding-agent": "^0.80.2",
|
|
73
|
+
"@earendil-works/pi-tui": "^0.80.2",
|
|
74
|
+
"@types/node": "^26.0.1",
|
|
75
|
+
"typebox": "^1.2.18",
|
|
76
|
+
"typescript": "^6.0.3"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { BRANCHME_COMMAND_NAME, EXTENSION_DISPLAY_NAME } from "../constants.ts";
|
|
3
|
+
import { getBranchStatus } from "../git.ts";
|
|
4
|
+
import { redactSecrets, repositoryLabel, resolveGitHubRepository, resolveGitHubToken } from "../github.ts";
|
|
5
|
+
import { BranchMePanel, type BranchMePanelData } from "../ui/branchme-panel.ts";
|
|
6
|
+
|
|
7
|
+
export type BranchMeCommandMode = "panel" | "help" | "unsupported";
|
|
8
|
+
|
|
9
|
+
function firstBranchMeArgumentLine(args: string): string {
|
|
10
|
+
return args.split(/\r?\n/u, 1)[0] ?? "";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseBranchMeArgs(args: string): BranchMeCommandMode {
|
|
14
|
+
const normalized = firstBranchMeArgumentLine(args).trim().toLowerCase();
|
|
15
|
+
if (!normalized) return "panel";
|
|
16
|
+
if (normalized === "help" || normalized === "--help" || normalized === "-h") return "help";
|
|
17
|
+
return "unsupported";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatUnsupportedBranchMeArgument(args: string): string {
|
|
21
|
+
return `Unknown /branchme argument ${JSON.stringify(firstBranchMeArgumentLine(args).trim())}. Use /branchme help.`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getBranchMeHelpText(): string {
|
|
25
|
+
return [
|
|
26
|
+
"# BranchMe",
|
|
27
|
+
"",
|
|
28
|
+
"Current-repository branch workflow tools for Pi.",
|
|
29
|
+
"",
|
|
30
|
+
"Commands only show info; BranchMe tools perform actions.",
|
|
31
|
+
"",
|
|
32
|
+
"## Workflow",
|
|
33
|
+
"",
|
|
34
|
+
"1. `branch_status` — inspect repo and branch state.",
|
|
35
|
+
"2. `change_branch` / `create_branch` — switch to an existing clean branch or create one from `HEAD`.",
|
|
36
|
+
"3. Commit outside BranchMe.",
|
|
37
|
+
"4. `push_branch` — push the current branch.",
|
|
38
|
+
"5. `pull_request` — open a PR after `push_branch` completes and GitHub sees the branches.",
|
|
39
|
+
"",
|
|
40
|
+
"## Requirements",
|
|
41
|
+
"",
|
|
42
|
+
"- Run inside a Git repo with `git` available.",
|
|
43
|
+
"- For PRs: GitHub `origin` and `GITHUB_TOKEN` or `GH_TOKEN` (environment or `.env`).",
|
|
44
|
+
"- BranchMe never stages or commits.",
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function emitCommandMessage(ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error" = "info"): void {
|
|
49
|
+
if (ctx.hasUI) {
|
|
50
|
+
ctx.ui.notify(message, level);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (ctx.mode === "print") {
|
|
55
|
+
console.log(message);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function safeWarningText(error: unknown): string {
|
|
60
|
+
return redactSecrets(error instanceof Error ? error.message : String(error));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function repositoryWarningFromError(error: unknown): string | null {
|
|
64
|
+
const message = safeWarningText(error);
|
|
65
|
+
if (/Unable to resolve a GitHub repository from origin or GITHUB_REPOSITORY/iu.test(message)) return null;
|
|
66
|
+
if (/Not a git repository/iu.test(message)) return null;
|
|
67
|
+
return message;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function tokenWarningFromError(error: unknown): string | null {
|
|
71
|
+
const message = safeWarningText(error);
|
|
72
|
+
if (/GitHub token is required\. Set GITHUB_TOKEN or GH_TOKEN/iu.test(message)) return null;
|
|
73
|
+
return message;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hasPanelWarning(data: BranchMePanelData): boolean {
|
|
77
|
+
return Boolean(data.statusNote || data.repositoryWarning || data.tokenWarning);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function summaryRepository(data: BranchMePanelData): string {
|
|
81
|
+
return data.repositoryWarning ? `warning: ${data.repositoryWarning}` : data.githubRepository ?? "not resolved";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function summaryToken(data: BranchMePanelData): string {
|
|
85
|
+
return data.tokenWarning ? `warning: ${data.tokenWarning}` : data.tokenSource ? `present (${data.tokenSource})` : "not set";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function summaryStatusWarning(data: BranchMePanelData): string {
|
|
89
|
+
return data.statusNote ? `; warning: ${data.statusNote}` : "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function collectPanelData(
|
|
93
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
94
|
+
ctx: ExtensionCommandContext,
|
|
95
|
+
): Promise<BranchMePanelData> {
|
|
96
|
+
const data: BranchMePanelData = {
|
|
97
|
+
currentBranch: null,
|
|
98
|
+
detached: false,
|
|
99
|
+
githubRepository: null,
|
|
100
|
+
tokenSource: null,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let tokenLookupCwd: string | undefined;
|
|
104
|
+
try {
|
|
105
|
+
const status = await getBranchStatus(pi, ctx, ctx.signal);
|
|
106
|
+
data.currentBranch = status.currentBranch;
|
|
107
|
+
data.detached = status.detached;
|
|
108
|
+
tokenLookupCwd = status.repoRoot;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
data.statusNote = safeWarningText(error);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!data.statusNote) {
|
|
114
|
+
try {
|
|
115
|
+
const repository = await resolveGitHubRepository(pi, ctx, ctx.signal);
|
|
116
|
+
data.githubRepository = repositoryLabel(repository);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
data.githubRepository = null;
|
|
119
|
+
data.repositoryWarning = repositoryWarningFromError(error) ?? undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
data.tokenSource = (await resolveGitHubToken(process.env, { cwd: tokenLookupCwd, signal: ctx.signal })).source;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
data.tokenSource = null;
|
|
127
|
+
data.tokenWarning = tokenWarningFromError(error) ?? undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return data;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function showPanel(pi: Pick<ExtensionAPI, "exec">, ctx: ExtensionCommandContext): Promise<void> {
|
|
134
|
+
const data = await collectPanelData(pi, ctx);
|
|
135
|
+
|
|
136
|
+
if (ctx.mode !== "tui") {
|
|
137
|
+
const branch = data.detached ? "detached HEAD" : data.currentBranch ?? "unknown";
|
|
138
|
+
emitCommandMessage(
|
|
139
|
+
ctx,
|
|
140
|
+
`${EXTENSION_DISPLAY_NAME}: branch ${branch}; GitHub repository ${summaryRepository(data)}; token ${summaryToken(data)}${summaryStatusWarning(data)}.`,
|
|
141
|
+
hasPanelWarning(data) ? "warning" : "info",
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => new BranchMePanel(data, theme, () => done(undefined), () => tui.requestRender()));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function registerBranchMeCommand(pi: Pick<ExtensionAPI, "registerCommand" | "exec">): void {
|
|
150
|
+
pi.registerCommand(BRANCHME_COMMAND_NAME, {
|
|
151
|
+
description: "Show BranchMe status and help",
|
|
152
|
+
handler: async (args, ctx) => {
|
|
153
|
+
const mode = parseBranchMeArgs(args);
|
|
154
|
+
if (mode === "help") {
|
|
155
|
+
emitCommandMessage(ctx, getBranchMeHelpText(), "info");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (mode === "unsupported") {
|
|
159
|
+
emitCommandMessage(ctx, formatUnsupportedBranchMeArgument(args), "warning");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await showPanel(pi, ctx);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const EXTENSION_DISPLAY_NAME = "BranchMe";
|
|
2
|
+
export const BRANCHME_COMMAND_NAME = "branchme";
|
|
3
|
+
|
|
4
|
+
export const BRANCH_STATUS_TOOL_NAME = "branch_status";
|
|
5
|
+
export const CREATE_BRANCH_TOOL_NAME = "create_branch";
|
|
6
|
+
export const CHANGE_BRANCH_TOOL_NAME = "change_branch";
|
|
7
|
+
export const PUSH_BRANCH_TOOL_NAME = "push_branch";
|
|
8
|
+
export const PULL_REQUEST_TOOL_NAME = "pull_request";
|
|
9
|
+
|
|
10
|
+
export const BRANCHME_TOOL_NAMES = [
|
|
11
|
+
BRANCH_STATUS_TOOL_NAME,
|
|
12
|
+
CREATE_BRANCH_TOOL_NAME,
|
|
13
|
+
CHANGE_BRANCH_TOOL_NAME,
|
|
14
|
+
PUSH_BRANCH_TOOL_NAME,
|
|
15
|
+
PULL_REQUEST_TOOL_NAME,
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export const GIT_STATUS_TIMEOUT_MS = 5_000;
|
|
19
|
+
export const GIT_MUTATION_TIMEOUT_MS = 30_000;
|
|
20
|
+
export const GIT_PUSH_TIMEOUT_MS = 120_000;
|
|
21
|
+
|
|
22
|
+
export const GITHUB_API_BASE_URL = "https://api.github.com";
|
|
23
|
+
export const GITHUB_API_VERSION = "2022-11-28";
|
|
24
|
+
export const GITHUB_USER_AGENT = "BranchMe Pi extension";
|
|
25
|
+
|
|
26
|
+
export const MAX_SUMMARY_OUTPUT_CHARS = 4_000;
|
|
27
|
+
export const GITHUB_RESPONSE_BODY_LIMIT_BYTES = 64 * 1024;
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { registerBranchMeCommand } from "./commands/branchme-command.ts";
|
|
3
|
+
import { registerBranchMeTools } from "./tools/branchme-tools.ts";
|
|
4
|
+
|
|
5
|
+
export function branchMeExtension(pi: ExtensionAPI): void {
|
|
6
|
+
registerBranchMeCommand(pi);
|
|
7
|
+
registerBranchMeTools(pi);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default branchMeExtension;
|