mintree 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -28
- package/dist/commands/dashboard.js +53 -11
- package/dist/commands/worktree/work.js +6 -5
- package/dist/lib/branch.js +4 -2
- package/dist/lib/dashboard.js +8 -5
- package/dist/lib/session-signal.d.ts +6 -4
- package/dist/lib/session-signal.js +7 -5
- package/dist/lib/worktreeCreate.d.ts +1 -1
- package/dist/lib/worktreeCreate.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
mintree wraps the steps you do manually every time a feature begins:
|
|
6
6
|
|
|
7
|
-
1. Pick an open issue or work item assigned to you (GitHub Issues or
|
|
7
|
+
1. Pick an open issue or work item assigned to you (GitHub Issues or Linear).
|
|
8
8
|
2. Create a git worktree on a branch named after that item, following the project's convention.
|
|
9
9
|
3. Launch Claude Code inside the worktree with a session ID you can resume later.
|
|
10
10
|
4. Live-track which Claude sessions are active, idle, or waiting.
|
|
11
11
|
|
|
12
|
-
It is a smaller, opinionated cousin of [santree](https://github.com/santiagotoscanini/santree) — built on the same TypeScript + Ink + Pastel stack but stripped to the `<type>/<issue>-<desc>` branch convention and the two issue trackers most likely to be used by a small team: GitHub Issues and
|
|
12
|
+
It is a smaller, opinionated cousin of [santree](https://github.com/santiagotoscanini/santree) — built on the same TypeScript + Ink + Pastel stack but stripped to the `<type>/<issue>-<desc>` branch convention and the two issue trackers most likely to be used by a small team: GitHub Issues and Linear.
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -48,7 +48,7 @@ EOF
|
|
|
48
48
|
|
|
49
49
|
`mintree doctor` should report **all required checks pass** before you continue. The most common gaps are:
|
|
50
50
|
|
|
51
|
-
- `gh` not authenticated → `gh auth login` (still needed when using
|
|
51
|
+
- `gh` not authenticated → `gh auth login` (still needed when using Linear — mintree uses `gh` for PR status on worktree branches)
|
|
52
52
|
- Claude Code not installed → `npm install -g @anthropic-ai/claude-code`
|
|
53
53
|
- Shell integration not loaded → re-run the `echo … >> ~/.zshrc` step and start a new shell
|
|
54
54
|
|
|
@@ -64,8 +64,8 @@ cd path/to/repo
|
|
|
64
64
|
# Default: GitHub Issues provider
|
|
65
65
|
mintree init
|
|
66
66
|
|
|
67
|
-
# Or:
|
|
68
|
-
mintree init --provider
|
|
67
|
+
# Or: Linear provider (--team is repeatable, one per team you pull work from)
|
|
68
|
+
mintree init --provider linear --workspace <your-workspace-slug> --team FE --team BE
|
|
69
69
|
|
|
70
70
|
mintree helpers session-signal install # optional: live session state in the dashboard
|
|
71
71
|
```
|
|
@@ -77,40 +77,40 @@ mintree helpers session-signal install # optional: live session state in the d
|
|
|
77
77
|
mintree supports two issue providers, selected per repo via `.mintree/metadata.json`:
|
|
78
78
|
|
|
79
79
|
- **`github`** (default): lists issues assigned to you on the current repo via `gh`. Transitions to "In Progress" on a Projects v2 board when present.
|
|
80
|
-
- **`
|
|
80
|
+
- **`linear`**: lists issues assigned to you across a configured set of [Linear](https://linear.app) teams, via the Linear GraphQL API. Moves the issue to "In Progress" on `w`.
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
`mintree init --provider linear --workspace <slug> --team FE --team BE` scaffolds the metadata for you. If you skip `--team`, or want to tweak it later, edit `.mintree/metadata.json` so `linear.teams` lists at least one team key:
|
|
83
83
|
|
|
84
84
|
```json
|
|
85
85
|
{
|
|
86
86
|
"version": 1,
|
|
87
|
-
"provider": "
|
|
87
|
+
"provider": "linear",
|
|
88
88
|
"issues": {},
|
|
89
|
-
"
|
|
90
|
-
"apiUrl": "https://api.
|
|
89
|
+
"linear": {
|
|
90
|
+
"apiUrl": "https://api.linear.app/graphql",
|
|
91
91
|
"workspaceSlug": "my-team",
|
|
92
|
-
"
|
|
93
|
-
{ "
|
|
94
|
-
{ "
|
|
92
|
+
"teams": [
|
|
93
|
+
{ "key": "FE", "name": "Frontend" },
|
|
94
|
+
{ "key": "BE", "name": "Backend" }
|
|
95
95
|
]
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
The `workspaceSlug` is the URL key of your Linear workspace (`linear.app/<slug>/...`). Each team `key` is the short prefix shown on issue IDs (the `FE` in `FE-123`); `name` is optional. Optional keys: `inProgressStateName` (override the workflow state `w` transitions to) and `protectedStateTypes` (workflow-state types `clean` won't touch).
|
|
101
101
|
|
|
102
|
-
Authenticate by setting `
|
|
102
|
+
Authenticate by setting `LINEAR_API_KEY` in your shell, or by writing the key to `~/.mintree/credentials.json`:
|
|
103
103
|
|
|
104
104
|
```bash
|
|
105
|
-
export
|
|
105
|
+
export LINEAR_API_KEY=lin_api_XXXXXXXXXXXXXX
|
|
106
106
|
# or
|
|
107
107
|
cat > ~/.mintree/credentials.json <<'EOF'
|
|
108
|
-
{ "
|
|
108
|
+
{ "linear": { "apiKey": "lin_api_XXXXXXXXXXXXXX" } }
|
|
109
109
|
EOF
|
|
110
110
|
chmod 600 ~/.mintree/credentials.json
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
`mintree doctor` validates the key
|
|
113
|
+
The key goes straight into the `Authorization` header (no `Bearer` prefix). `mintree doctor` validates the key, resolves the viewer, and pings each configured team when `provider === "linear"`.
|
|
114
114
|
|
|
115
115
|
---
|
|
116
116
|
|
|
@@ -143,10 +143,11 @@ Same building blocks, scriptable from any shell:
|
|
|
143
143
|
```bash
|
|
144
144
|
# Create a worktree, optionally launch Claude with an initial prompt
|
|
145
145
|
mintree worktree create feat/100-validar-patente
|
|
146
|
-
mintree worktree create feat/
|
|
146
|
+
mintree worktree create feat/FE-123-validar-patente --work --prompt "empezar FE-123"
|
|
147
147
|
|
|
148
148
|
# Resume Claude in the worktree you're currently inside
|
|
149
|
-
|
|
149
|
+
# (the worktree dir is the bare issue id)
|
|
150
|
+
cd .mintree/worktrees/FE-123
|
|
150
151
|
mintree worktree work
|
|
151
152
|
|
|
152
153
|
# Inspect / clean up
|
|
@@ -176,11 +177,11 @@ mintree enforces:
|
|
|
176
177
|
`<issue>` is one of:
|
|
177
178
|
|
|
178
179
|
- Bare digits for GitHub Issues (the issue number, no `#`): `42`, `100`, `1234`
|
|
179
|
-
- `<
|
|
180
|
+
- `<TEAM>-<digits>` for Linear issues (the human identifier): `FE-123`, `BE-7`, `DSGN-12`. The prefix is uppercase letters / digits / underscores, matching Linear's team-key constraints.
|
|
180
181
|
|
|
181
182
|
`<desc>` is lowercase kebab-case.
|
|
182
183
|
|
|
183
|
-
Examples: `feat/42-validacion-patente`, `fix/55-selfie-upload-timeout`, `feat/
|
|
184
|
+
Examples: `feat/42-validacion-patente`, `fix/55-selfie-upload-timeout`, `feat/FE-123-readme-update`, `fix/BE-7-modal`.
|
|
184
185
|
|
|
185
186
|
When the dashboard's `w` overlay opens, it suggests a kebab description capped at 5 words. If your repo has a `docs/conventions/git-workflow.md`, `CONTRIBUTING.md`, or `.claude/skills/` directory, mintree mentions it on the overlay so you can verify the suggestion against your project's rules — then edit the description to match.
|
|
186
187
|
|
|
@@ -194,16 +195,18 @@ When the dashboard's `w` overlay opens, it suggests a kebab description capped a
|
|
|
194
195
|
└── .mintree/
|
|
195
196
|
├── metadata.json # gitignored. provider config + <issue-id> → { base_branch?, session_id? }
|
|
196
197
|
├── worktrees/ # gitignored
|
|
197
|
-
│ ├── 100
|
|
198
|
-
│ └──
|
|
198
|
+
│ ├── 100/ # GH form: <digits>
|
|
199
|
+
│ └── FE-123/ # Linear form: <TEAM-digits>
|
|
199
200
|
├── session-states/ # gitignored
|
|
200
201
|
│ └── 100.json # live state written by Claude hooks (active/waiting/idle/exited)
|
|
201
202
|
└── init.sh # opt-in. Runs in the new worktree post-create (copy .env, install deps, …)
|
|
202
203
|
```
|
|
203
204
|
|
|
204
|
-
|
|
205
|
+
The worktree directory is named after the bare issue id (`100`, `FE-123`); the branch keeps the full `<type>/<issue>-<desc>` name.
|
|
205
206
|
|
|
206
|
-
|
|
207
|
+
`metadata.json` is gitignored because the `session_id` is local to your machine — sharing it would only generate noise. The `provider` and `linear.*` keys can be re-derived from a Linear workspace if needed; sharing them would just leak local config preference.
|
|
208
|
+
|
|
209
|
+
Linear authentication lives in `~/.mintree/credentials.json` (user-scoped, not per-repo) or the `LINEAR_API_KEY` env var.
|
|
207
210
|
|
|
208
211
|
---
|
|
209
212
|
|
|
@@ -220,7 +223,7 @@ Plane authentication lives in `~/.mintree/credentials.json` (user-scoped, not pe
|
|
|
220
223
|
- `mintree doctor` is the first stop. It surfaces missing tools, unauthenticated CLIs, missing hooks, and gitignore drift.
|
|
221
224
|
- The shell wrapper exports `MINTREE_SHELL_INTEGRATION=1` — if doctor says it's missing, the wrapper isn't being loaded by your shell init file.
|
|
222
225
|
- If the dashboard ever opens with a stale session state, press `r` to force a refetch (the auto-refresh runs every 5 minutes).
|
|
223
|
-
-
|
|
226
|
+
- Linear-side issues (timeouts, rate limits, unexpected response shapes) can be logged to `~/.mintree/linear-debug.log` by running `MINTREE_DEBUG=1 mintree dashboard`. The log is file-only so it never corrupts the Ink-rendered TUI.
|
|
224
227
|
|
|
225
228
|
---
|
|
226
229
|
|
|
@@ -228,7 +231,7 @@ Plane authentication lives in `~/.mintree/credentials.json` (user-scoped, not pe
|
|
|
228
231
|
|
|
229
232
|
mintree was written for projects that have:
|
|
230
233
|
|
|
231
|
-
- A small set of trackers (GitHub Issues or
|
|
234
|
+
- A small set of trackers (GitHub Issues or Linear) — santree supports both too but is heavier.
|
|
232
235
|
- An established branch convention without the `gh-` prefix santree imposes.
|
|
233
236
|
- Skills (`.claude/skills/`) that own the SDD + TDD flow — mintree intentionally leaves the rich PR-create / PR-review prompts out of scope.
|
|
234
237
|
|
|
@@ -23,11 +23,19 @@ export const description = "Interactive dashboard listing open issues assigned t
|
|
|
23
23
|
function isOrphan(d) {
|
|
24
24
|
return d.orphan === true;
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// Matches an issue against the live numeric filter by substring on the digit
|
|
27
|
+
// portion of its id ("PLA-234" → "234", "BE-34" → "34"). Letters are ignored,
|
|
28
|
+
// so the user filters by ticket number alone. Empty filter matches everything.
|
|
29
|
+
function issueMatchesFilter(d, filter) {
|
|
30
|
+
if (!filter)
|
|
31
|
+
return true;
|
|
32
|
+
return d.issue.id.replace(/\D/g, "").includes(filter);
|
|
33
|
+
}
|
|
34
|
+
function tabIssues(issues, tab, filter = "") {
|
|
35
|
+
return issues.filter((d) => (tab === "issues" ? !isOrphan(d) : isOrphan(d)) && issueMatchesFilter(d, filter));
|
|
28
36
|
}
|
|
29
37
|
function currentSelected(s) {
|
|
30
|
-
const displayed = tabIssues(s.issues, s.activeTab);
|
|
38
|
+
const displayed = tabIssues(s.issues, s.activeTab, s.filter);
|
|
31
39
|
const selectedIndex = s.activeTab === "issues" ? s.issuesIndex : s.worktreesIndex;
|
|
32
40
|
return { displayed, selectedIndex };
|
|
33
41
|
}
|
|
@@ -186,7 +194,7 @@ function FooterRow({ phase, overlayKind, latestVersion, listWidth, }) {
|
|
|
186
194
|
// align under the left (list) pane; ticket-specific actions align under
|
|
187
195
|
// the right (detail) pane. Falls back to a single inline row when no
|
|
188
196
|
// width hint is available (e.g. the error path).
|
|
189
|
-
const common = (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " nav " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " PgUp/PgDn" }), _jsx(Text, { dimColor: true, children: " scroll " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " r" }), _jsx(Text, { dimColor: true, children: " refresh " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " q" }), _jsx(Text, { dimColor: true, children: " quit" })] }));
|
|
197
|
+
const common = (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " nav " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " PgUp/PgDn" }), _jsx(Text, { dimColor: true, children: " scroll " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " r" }), _jsx(Text, { dimColor: true, children: " refresh " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " #" }), _jsx(Text, { dimColor: true, children: " filter " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " q" }), _jsx(Text, { dimColor: true, children: " quit" })] }));
|
|
190
198
|
const ticket = (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "\u21B5" }), _jsx(Text, { dimColor: true, children: " Switch " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " w" }), _jsx(Text, { dimColor: true, children: " Work " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " o" }), _jsx(Text, { dimColor: true, children: " Open " }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { bold: true, children: " d" }), _jsx(Text, { dimColor: true, children: " Remove" })] }));
|
|
191
199
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: listWidth, children: common }), _jsx(Box, { flexGrow: 1, children: ticket })] }), latestVersion && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "(*)" }), _jsx(Text, { dimColor: true, children: ` new version available — v${latestVersion} · npm i -g mintree` })] }))] }));
|
|
192
200
|
}
|
|
@@ -674,14 +682,15 @@ export default function Dashboard() {
|
|
|
674
682
|
const previousOverlay = prevReady?.overlay ?? null;
|
|
675
683
|
const previousToast = prevReady?.toast ?? null;
|
|
676
684
|
const previousScroll = prevReady?.detailScrollOffset ?? 0;
|
|
677
|
-
const
|
|
678
|
-
const
|
|
685
|
+
const filter = prevReady?.filter ?? "";
|
|
686
|
+
const issuesList = tabIssues(issues, "issues", filter);
|
|
687
|
+
const worktreesList = tabIssues(issues, "worktrees", filter);
|
|
679
688
|
const issuesIndex = Math.min(previousIssuesIndex, Math.max(0, issuesList.length - 1));
|
|
680
689
|
const worktreesIndex = Math.min(previousWorktreesIndex, Math.max(0, worktreesList.length - 1));
|
|
681
690
|
// Preserve scroll only when the active tab's selected issue still
|
|
682
691
|
// resolves to the same row — clamping or list churn means the user
|
|
683
692
|
// is now reading something else.
|
|
684
|
-
const prevDisplayed = prevReady ? tabIssues(prevReady.issues, activeTab) : [];
|
|
693
|
+
const prevDisplayed = prevReady ? tabIssues(prevReady.issues, activeTab, filter) : [];
|
|
685
694
|
const nextDisplayed = activeTab === "issues" ? issuesList : worktreesList;
|
|
686
695
|
const prevSelectedId = prevDisplayed[activeTab === "issues" ? previousIssuesIndex : previousWorktreesIndex]?.issue
|
|
687
696
|
.id ?? null;
|
|
@@ -697,6 +706,7 @@ export default function Dashboard() {
|
|
|
697
706
|
refreshing: false,
|
|
698
707
|
overlay: previousOverlay,
|
|
699
708
|
toast: previousToast,
|
|
709
|
+
filter,
|
|
700
710
|
};
|
|
701
711
|
});
|
|
702
712
|
};
|
|
@@ -818,12 +828,42 @@ export default function Dashboard() {
|
|
|
818
828
|
handleOverlayInput(input, key);
|
|
819
829
|
return;
|
|
820
830
|
}
|
|
831
|
+
// Esc clears an active numeric filter before it falls through to quit —
|
|
832
|
+
// so the user can back out of a search without leaving the dashboard.
|
|
833
|
+
if (key.escape && state.phase === "ready" && state.filter) {
|
|
834
|
+
setState({ ...state, filter: "", issuesIndex: 0, worktreesIndex: 0, detailScrollOffset: 0 });
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
821
837
|
if (input === "q" || key.escape || (input === "c" && key.ctrl)) {
|
|
822
838
|
exit();
|
|
823
839
|
return;
|
|
824
840
|
}
|
|
825
841
|
if (state.phase !== "ready")
|
|
826
842
|
return;
|
|
843
|
+
// Numeric filter: typing a digit narrows the list by ticket number
|
|
844
|
+
// (matched on the digits of the id, so "34" hits both PLA-234 and BE-34).
|
|
845
|
+
// Backspace pops a digit; Esc (handled above) clears it. Reset selection
|
|
846
|
+
// to the first match so the cursor stays on a visible row as it narrows.
|
|
847
|
+
if (/^[0-9]$/.test(input)) {
|
|
848
|
+
setState({
|
|
849
|
+
...state,
|
|
850
|
+
filter: state.filter + input,
|
|
851
|
+
issuesIndex: 0,
|
|
852
|
+
worktreesIndex: 0,
|
|
853
|
+
detailScrollOffset: 0,
|
|
854
|
+
});
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if ((key.backspace || key.delete) && state.filter) {
|
|
858
|
+
setState({
|
|
859
|
+
...state,
|
|
860
|
+
filter: state.filter.slice(0, -1),
|
|
861
|
+
issuesIndex: 0,
|
|
862
|
+
worktreesIndex: 0,
|
|
863
|
+
detailScrollOffset: 0,
|
|
864
|
+
});
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
827
867
|
if (key.leftArrow || key.rightArrow) {
|
|
828
868
|
// Two tabs only — either arrow toggles. Per-tab indices are
|
|
829
869
|
// preserved, so the user returns to the row they left.
|
|
@@ -1172,7 +1212,7 @@ export default function Dashboard() {
|
|
|
1172
1212
|
if (state.phase === "error") {
|
|
1173
1213
|
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", state.message] }), state.hint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", state.hint] }) })), _jsx(Box, { marginTop: 1, children: _jsx(FooterRow, { phase: "error" }) })] }));
|
|
1174
1214
|
}
|
|
1175
|
-
const { issues, refreshing, overlay, toast, activeTab } = state;
|
|
1215
|
+
const { issues, refreshing, overlay, toast, activeTab, filter } = state;
|
|
1176
1216
|
const { displayed, selectedIndex } = currentSelected(state);
|
|
1177
1217
|
const selected = displayed[selectedIndex] ?? null;
|
|
1178
1218
|
const issuesTabCount = issues.reduce((n, d) => (isOrphan(d) ? n : n + 1), 0);
|
|
@@ -1224,7 +1264,9 @@ export default function Dashboard() {
|
|
|
1224
1264
|
: displayed.map((d, index) => ({ kind: "issue", d, index }));
|
|
1225
1265
|
const listView = windowListRows(listRows, selectedIndex, listVisibleRows);
|
|
1226
1266
|
const listContentWidth = Math.max(8, listWidth - 4);
|
|
1227
|
-
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Box, { paddingX: 1, paddingTop: 0, flexDirection: "column", children: _jsx(HeaderRow, { repoName: repoName, claudeVersion: claudeVersion, issueCount: issuesTabCount, worktreeCount: worktreesTabCount, activeTab: activeTab, updateAvailable: latestVersion !== null }) }), overlay ? (_jsx(Box, { flexGrow: 1, flexDirection: "column", borderStyle: "round", borderColor: overlay.kind === "remove" ? "yellow" : "cyan", children: overlay.kind === "create" ? (_jsx(CreateOverlayView, { overlay: overlay, onDescChange: onOverlayDescChange, onPromptChange: onOverlayPromptChange })) : (_jsx(RemoveOverlayView, { overlay: overlay })) })) : (_jsxs(Box, { flexGrow: 1, flexDirection: "row", children: [_jsx(Box, { width: listWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: displayed.length === 0 ? (_jsx(Text, { dimColor: true, children:
|
|
1228
|
-
?
|
|
1229
|
-
:
|
|
1267
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Box, { paddingX: 1, paddingTop: 0, flexDirection: "column", children: _jsx(HeaderRow, { repoName: repoName, claudeVersion: claudeVersion, issueCount: issuesTabCount, worktreeCount: worktreesTabCount, activeTab: activeTab, updateAvailable: latestVersion !== null }) }), overlay ? (_jsx(Box, { flexGrow: 1, flexDirection: "column", borderStyle: "round", borderColor: overlay.kind === "remove" ? "yellow" : "cyan", children: overlay.kind === "create" ? (_jsx(CreateOverlayView, { overlay: overlay, onDescChange: onOverlayDescChange, onPromptChange: onOverlayPromptChange })) : (_jsx(RemoveOverlayView, { overlay: overlay })) })) : (_jsxs(Box, { flexGrow: 1, flexDirection: "row", children: [_jsx(Box, { width: listWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: displayed.length === 0 ? (_jsx(Text, { dimColor: true, children: filter
|
|
1268
|
+
? `No tickets match #${filter} — Esc to clear the filter.`
|
|
1269
|
+
: activeTab === "issues"
|
|
1270
|
+
? "No open issues assigned to you in this repo."
|
|
1271
|
+
: "No orphaned worktrees — anything in `.mintree/worktrees/` matches an open issue." })) : (_jsxs(_Fragment, { children: [listView.sticky.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, width: listContentWidth }, `sticky-${i}`))), listView.issuesAbove > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2191 ", listView.issuesAbove, " more above"] })), listView.body.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, width: listContentWidth }, `body-${i}`))), listView.issuesBelow > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", listView.issuesBelow, " more below"] }))] })) }), _jsx(Box, { width: detailWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(DetailPane, { d: selected, contentWidth: detailWidth - 4, contentHeight: detailContentHeight, scrollOffset: state.detailScrollOffset }) })] })), _jsxs(Box, { paddingX: 1, flexDirection: "column", children: [filter && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: `⌕ #${filter}` }), _jsx(Text, { dimColor: true, children: ` · ${displayed.length} match${displayed.length === 1 ? "" : "es"} · Esc clear` })] })), toast && (_jsx(Box, { children: _jsxs(Text, { color: toast.kind === "success" ? "green" : toast.kind === "error" ? "red" : "cyan", children: [toast.kind === "success" ? "✓ " : toast.kind === "error" ? "✗ " : "· ", toast.text] }) })), refreshing && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { dimColor: true, children: " refreshing" })] })), _jsx(FooterRow, { phase: "ready", overlayKind: overlay?.kind, latestVersion: latestVersion, listWidth: listWidth })] })] }));
|
|
1230
1272
|
}
|
|
@@ -67,15 +67,16 @@ function resolve(cwd) {
|
|
|
67
67
|
const worktreeDirName = path.basename(worktreePath);
|
|
68
68
|
// IssueId comes from the worktree dir name, not the branch — that way
|
|
69
69
|
// detached-HEAD worktrees (the "current branch" path from the dashboard)
|
|
70
|
-
// still resolve their session_id.
|
|
71
|
-
//
|
|
72
|
-
// issueId is either bare
|
|
73
|
-
|
|
70
|
+
// still resolve their session_id. The dir is named after the bare issue
|
|
71
|
+
// id for both attached and detached creates; the trailing `-` clause still
|
|
72
|
+
// matches legacy `<issueId>-<desc>` worktrees. issueId is either bare
|
|
73
|
+
// digits (GitHub) or `<TEAM>-\d+` (Linear).
|
|
74
|
+
const issueIdMatch = worktreeDirName.match(/^((?:[A-Z][A-Z0-9_]*-)?\d+)(?:-|$)/);
|
|
74
75
|
if (!issueIdMatch || !issueIdMatch[1]) {
|
|
75
76
|
return {
|
|
76
77
|
ok: false,
|
|
77
78
|
message: `Worktree directory '${worktreeDirName}' doesn't start with an issue id.`,
|
|
78
|
-
hint: "Expected
|
|
79
|
+
hint: "Expected the issue id (e.g. 100 or AUTH-6).",
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
82
|
const issueId = issueIdMatch[1];
|
package/dist/lib/branch.js
CHANGED
|
@@ -29,7 +29,7 @@ export const ALLOWED_TYPES = [
|
|
|
29
29
|
// `<TEAM_PREFIX>-\d+` (Linear). The TEAM_PREFIX is uppercase letters/digits/
|
|
30
30
|
// underscores starting with a letter, mirroring Linear's team-key
|
|
31
31
|
// constraints. The full issueId captures group 2 verbatim so callers can
|
|
32
|
-
//
|
|
32
|
+
// reuse it as the worktree dir name.
|
|
33
33
|
const BRANCH_REGEX = /^([a-z]+)\/((?:[A-Z][A-Z0-9_]*-)?\d+)-([a-z0-9][a-z0-9-]*)$/;
|
|
34
34
|
export function parseBranch(branch) {
|
|
35
35
|
const match = BRANCH_REGEX.exec(branch);
|
|
@@ -57,7 +57,9 @@ export function parseBranch(branch) {
|
|
|
57
57
|
type: type,
|
|
58
58
|
issueId,
|
|
59
59
|
desc,
|
|
60
|
-
|
|
60
|
+
// Worktree dir is the bare issue id (e.g. "100" or "FE-123"). The desc
|
|
61
|
+
// only seeds the branch name, not the directory.
|
|
62
|
+
worktreeDirName: issueId,
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
export function isParseError(result) {
|
package/dist/lib/dashboard.js
CHANGED
|
@@ -16,8 +16,10 @@ import { createProvider } from "./providers/index.js";
|
|
|
16
16
|
function buildWorktreeIndex(repoRoot) {
|
|
17
17
|
const worktreesRoot = path.resolve(getWorktreesDir(repoRoot));
|
|
18
18
|
// Same shape as the BRANCH_REGEX issueId capture: bare digits (GitHub) or
|
|
19
|
-
// `<TEAM>-\d+` (Linear).
|
|
20
|
-
|
|
19
|
+
// `<TEAM>-\d+` (Linear). The dir name is now the bare issue id (`100`,
|
|
20
|
+
// `FE-123`); the trailing `-` clause keeps matching legacy `<id>-<desc>`
|
|
21
|
+
// worktrees still on disk.
|
|
22
|
+
const dirNameRegex = /^((?:[A-Z][A-Z0-9_]*-)?\d+)(?:-|$)/;
|
|
21
23
|
const index = new Map();
|
|
22
24
|
for (const w of listWorktrees(repoRoot)) {
|
|
23
25
|
const wAbs = path.resolve(w.path);
|
|
@@ -119,9 +121,10 @@ function sortGroupedIssues(issues, configuredUrl) {
|
|
|
119
121
|
* Worktrees" at the bottom of the dashboard so the user can find and `d`elete
|
|
120
122
|
* them.
|
|
121
123
|
*
|
|
122
|
-
* The `issue` stub uses the worktree directory name as the title
|
|
123
|
-
*
|
|
124
|
-
* even when there's no live issue to
|
|
124
|
+
* The `issue` stub uses the worktree directory name as the title — the bare
|
|
125
|
+
* issue id (e.g. "FE-123"), or the legacy "<id>-<desc>" suffix for older
|
|
126
|
+
* worktrees — so the row is identifiable even when there's no live issue to
|
|
127
|
+
* fetch a title from.
|
|
125
128
|
*/
|
|
126
129
|
function buildOrphanRows(worktreesByIssue, assignedIds, sessionLookup, prByBranch, metadataSessionId) {
|
|
127
130
|
const orphans = [];
|
|
@@ -16,10 +16,12 @@ export declare function extractRepoAndDir(cwd: string): {
|
|
|
16
16
|
worktreeDir: string;
|
|
17
17
|
} | null;
|
|
18
18
|
/**
|
|
19
|
-
* Pulls the issue id out of a
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* Pulls the issue id out of a worktree directory name. The dir name is the
|
|
20
|
+
* bare issue id (`100`, `FE-123`); the trailing `-` clause still matches
|
|
21
|
+
* legacy `<issue>-<desc>` worktrees on disk. Returns null when the directory
|
|
22
|
+
* name doesn't follow the convention (e.g. a manually-created worktree
|
|
23
|
+
* dropped under .mintree/worktrees/). The id is either bare digits (GitHub)
|
|
24
|
+
* or a `<TEAM>-\d+` Linear identifier.
|
|
23
25
|
*/
|
|
24
26
|
export declare function issueIdFromWorktreeDir(worktreeDir: string): string | null;
|
|
25
27
|
export type StatePayload = {
|
|
@@ -32,13 +32,15 @@ export function extractRepoAndDir(cwd) {
|
|
|
32
32
|
return { repoRoot, worktreeDir };
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* Pulls the issue id out of a
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* Pulls the issue id out of a worktree directory name. The dir name is the
|
|
36
|
+
* bare issue id (`100`, `FE-123`); the trailing `-` clause still matches
|
|
37
|
+
* legacy `<issue>-<desc>` worktrees on disk. Returns null when the directory
|
|
38
|
+
* name doesn't follow the convention (e.g. a manually-created worktree
|
|
39
|
+
* dropped under .mintree/worktrees/). The id is either bare digits (GitHub)
|
|
40
|
+
* or a `<TEAM>-\d+` Linear identifier.
|
|
39
41
|
*/
|
|
40
42
|
export function issueIdFromWorktreeDir(worktreeDir) {
|
|
41
|
-
const m = worktreeDir.match(/^((?:[A-Z][A-Z0-9_]*-)?\d+)
|
|
43
|
+
const m = worktreeDir.match(/^((?:[A-Z][A-Z0-9_]*-)?\d+)(?:-|$)/);
|
|
42
44
|
return m && m[1] ? m[1] : null;
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
@@ -66,7 +66,7 @@ export type CreateDetachedOpts = {
|
|
|
66
66
|
* the `<type>/<issue>-<desc>` convention upfront. They can `git switch -c`
|
|
67
67
|
* later if/when the work warrants a branch.
|
|
68
68
|
*
|
|
69
|
-
* Worktree dir naming follows the same
|
|
69
|
+
* Worktree dir naming follows the same bare-issueId shape as the
|
|
70
70
|
* branch-based flow so `worktree work` can still recover the issueId from
|
|
71
71
|
* the dir name (where it can't read it from the branch).
|
|
72
72
|
*/
|
|
@@ -238,7 +238,7 @@ export async function runCreate(branchArg, opts) {
|
|
|
238
238
|
* the `<type>/<issue>-<desc>` convention upfront. They can `git switch -c`
|
|
239
239
|
* later if/when the work warrants a branch.
|
|
240
240
|
*
|
|
241
|
-
* Worktree dir naming follows the same
|
|
241
|
+
* Worktree dir naming follows the same bare-issueId shape as the
|
|
242
242
|
* branch-based flow so `worktree work` can still recover the issueId from
|
|
243
243
|
* the dir name (where it can't read it from the branch).
|
|
244
244
|
*/
|
|
@@ -280,7 +280,7 @@ export async function runCreateDetached(opts) {
|
|
|
280
280
|
hint: "Switch the main repo to a branch first (`git switch main`) and try again.",
|
|
281
281
|
};
|
|
282
282
|
}
|
|
283
|
-
const worktreeDirName =
|
|
283
|
+
const worktreeDirName = opts.issueId;
|
|
284
284
|
const worktreePath = path.join(getWorktreesDir(root), worktreeDirName);
|
|
285
285
|
if (pathExists(worktreePath)) {
|
|
286
286
|
return {
|
package/package.json
CHANGED