mintree 0.2.3 → 0.3.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.
- package/dist/commands/dashboard.js +40 -23
- package/dist/commands/doctor.js +30 -24
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +34 -21
- package/dist/commands/worktree/list.js +1 -1
- package/dist/commands/worktree/work.js +1 -1
- package/dist/lib/branch.d.ts +2 -2
- package/dist/lib/branch.js +4 -4
- package/dist/lib/dashboard.js +5 -7
- package/dist/lib/gh.d.ts +2 -2
- package/dist/lib/gh.js +2 -2
- package/dist/lib/metadata.d.ts +7 -8
- package/dist/lib/metadata.js +16 -18
- package/dist/lib/pr.d.ts +2 -2
- package/dist/lib/pr.js +2 -2
- package/dist/lib/providers/index.js +3 -3
- package/dist/lib/providers/linear.d.ts +55 -0
- package/dist/lib/providers/linear.js +616 -0
- package/dist/lib/providers/types.d.ts +9 -9
- package/dist/lib/providers/types.js +3 -3
- package/dist/lib/session-signal.d.ts +1 -1
- package/dist/lib/session-signal.js +1 -1
- package/dist/lib/worktreeCreate.js +2 -2
- package/package.json +1 -1
- package/dist/lib/providers/plane.d.ts +0 -61
- package/dist/lib/providers/plane.js +0 -749
|
@@ -99,12 +99,12 @@ function kebabize(title) {
|
|
|
99
99
|
* `w` for an issue. Single-line on purpose — `ink-text-input` is one-line,
|
|
100
100
|
* so multi-line templates render weirdly when the user tabs in to edit.
|
|
101
101
|
* Provider-aware: GitHub issues get the `#<n>` + `gh issue view` form;
|
|
102
|
-
*
|
|
103
|
-
*
|
|
102
|
+
* Linear issues (id like `FE-123`) get the bare id + the issue URL, since
|
|
103
|
+
* `gh` can't read Linear and `#` isn't Linear's notation.
|
|
104
104
|
*/
|
|
105
105
|
function defaultPromptForIssue(id, title, url) {
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
106
|
+
const isTeamPrefixed = /^[A-Z][A-Z0-9_]*-\d+$/.test(id);
|
|
107
|
+
if (isTeamPrefixed) {
|
|
108
108
|
return `Empezá a trabajar el ticket ${id} (${title}). Abrí ${url} para leer el contexto completo y seguí las convenciones del repo.`;
|
|
109
109
|
}
|
|
110
110
|
return `Empezá a trabajar el issue #${id} (${title}). Usá \`gh issue view ${id}\` para leer el contexto completo y seguí las convenciones del repo.`;
|
|
@@ -189,8 +189,8 @@ function CreateOverlayView({ overlay, onDescChange, onPromptChange, }) {
|
|
|
189
189
|
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Create worktree" }), _jsx(Text, { dimColor: true, children: ` for ${overlay.issue.issue.id}` })] }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: overlay.issue.issue.title }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "branchMode", children: overlay.field === "branchMode" ? "▸ Branch:" : " Branch:" }) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "< " }), _jsx(Text, { color: overlay.field === "branchMode" ? "cyan" : undefined, bold: overlay.field === "branchMode", children: isNewBranch ? "new" : `current (${overlay.currentBranch ?? "?"})` }), _jsx(Text, { dimColor: true, children: " >" })] }), overlay.field === "branchMode" && _jsx(Text, { dimColor: true, children: " (use ← / → to toggle)" })] }), isNewBranch && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "type", children: overlay.field === "type" ? "▸ Type:" : " Type:" }) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "< " }), _jsx(Text, { color: overlay.field === "type" ? "cyan" : undefined, bold: overlay.field === "type", children: overlay.type }), _jsx(Text, { dimColor: true, children: " >" })] }), overlay.field === "type" && _jsx(Text, { dimColor: true, children: " (use ← / → to cycle)" })] }), _jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "desc", children: overlay.field === "desc" ? "▸ Description:" : " Description:" }) }), _jsx(Box, { children: overlay.field === "desc" ? (_jsx(TextInput, { value: overlay.desc, onChange: onDescChange, placeholder: "kebab-case" })) : (_jsx(Text, { children: overlay.desc || "(empty)" })) })] })] })), _jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "prompt", children: overlay.field === "prompt" ? "▸ Prompt:" : " Prompt:" }) }), _jsx(Box, { children: overlay.field === "prompt" ? (_jsx(TextInput, { value: overlay.prompt, onChange: onPromptChange, placeholder: "(empty = no initial message)" })) : (_jsx(Text, { dimColor: true, children: overlay.prompt || "(empty — Claude starts with no message)" })) })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Checkout:" }) }), _jsx(Text, { color: "green", children: branchPreview })] }), _jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Worktree:" }) }), _jsxs(Text, { dimColor: true, children: [".mintree/worktrees/", dirPreview] })] }), _jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Mode:" }) }), _jsx(Text, { dimColor: true, children: "--work (Claude launches in the new worktree)" })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [isNewBranch ? (_jsxs(Text, { dimColor: true, children: ["Suggestion is a kebab of the title (capped at ", SUGGESTED_DESC_MAX_WORDS, " words). Edit it to match your repo's branch conventions."] })) : (_jsxs(Text, { dimColor: true, children: ["Detached HEAD at the tip of ", overlay.currentBranch ?? "the current branch", ". No new branch is created \u2014 commit on a new one with `git switch -c` when ready."] })), isNewBranch && overlay.conventionDoc && (_jsx(Text, { dimColor: true, children: `This repo has \`${overlay.conventionDoc}\` — review it before creating.` }))] }), overlay.error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", overlay.error] }) })), overlay.pending && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", overlay.pending] })] }))] }));
|
|
190
190
|
}
|
|
191
191
|
function IssueListRow({ d, selected, identifierWidth, rowWidth, }) {
|
|
192
|
-
// Display the issue id raw (e.g. "
|
|
193
|
-
// GitHub convention that reads as noise for
|
|
192
|
+
// Display the issue id raw (e.g. "FE-123", "100"). The `#` prefix is a
|
|
193
|
+
// GitHub convention that reads as noise for Linear's already-prefixed
|
|
194
194
|
// ids, and dropping it across the board keeps the dashboard provider-
|
|
195
195
|
// agnostic.
|
|
196
196
|
const idText = d.issue.id.padEnd(identifierWidth, " ");
|
|
@@ -611,11 +611,11 @@ export default function Dashboard() {
|
|
|
611
611
|
const issues = await loadDashboard(root);
|
|
612
612
|
if (!issues) {
|
|
613
613
|
const provider = readMetadata(root).provider ?? "github";
|
|
614
|
-
const message = provider === "
|
|
615
|
-
? "Could not fetch
|
|
614
|
+
const message = provider === "linear"
|
|
615
|
+
? "Could not fetch issues from Linear."
|
|
616
616
|
: "Could not fetch issues from GitHub.";
|
|
617
|
-
const hint = provider === "
|
|
618
|
-
? "Check `mintree doctor` —
|
|
617
|
+
const hint = provider === "linear"
|
|
618
|
+
? "Check `mintree doctor` — LINEAR_API_KEY must be set and the workspace + teams reachable."
|
|
619
619
|
: "Check `mintree doctor` — gh must be authenticated and the repo must live on GitHub.";
|
|
620
620
|
setState((prev) => {
|
|
621
621
|
// Initial load failure → escalate to the full error screen so the
|
|
@@ -745,11 +745,11 @@ export default function Dashboard() {
|
|
|
745
745
|
}, [state.phase === "ready" && state.overlay ? state.overlay.kind : null]);
|
|
746
746
|
// Auto-refresh every 5 minutes while the dashboard is idle. 30s was too
|
|
747
747
|
// aggressive for an issue list — most of the time nothing has changed,
|
|
748
|
-
// and
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
//
|
|
748
|
+
// and even a single GraphQL refresh isn't worth firing twice a minute.
|
|
749
|
+
// Press `r` for an immediate refresh when something changed externally.
|
|
750
|
+
// Skipped while an overlay is open so we don't yank state from under a
|
|
751
|
+
// confirmation, and while a manual refresh is in flight to avoid
|
|
752
|
+
// stomping on its spinner.
|
|
753
753
|
const stateRef = useRef(state);
|
|
754
754
|
useEffect(() => {
|
|
755
755
|
stateRef.current = state;
|
|
@@ -976,6 +976,26 @@ export default function Dashboard() {
|
|
|
976
976
|
async function confirmCreate(overlay) {
|
|
977
977
|
if (state.phase !== "ready")
|
|
978
978
|
return;
|
|
979
|
+
// Validate first so we don't flash a spinner just to immediately show
|
|
980
|
+
// a sync-fail message.
|
|
981
|
+
if (overlay.branchMode === "new" && !overlay.desc.trim()) {
|
|
982
|
+
setState({
|
|
983
|
+
...state,
|
|
984
|
+
overlay: { ...overlay, error: "Description is required." },
|
|
985
|
+
});
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
// Surface the spinner BEFORE the heavy sync work. runCreate /
|
|
989
|
+
// runCreateDetached both block the event loop (execSync: git fetch +
|
|
990
|
+
// worktree add + optional .mintree/init.sh — easily several seconds on
|
|
991
|
+
// slow remotes or a repo with a real init script). Without the
|
|
992
|
+
// setImmediate yield Ink wouldn't get to paint the spinner until after
|
|
993
|
+
// that work finished, leaving the user staring at a frozen overlay.
|
|
994
|
+
setState({
|
|
995
|
+
...state,
|
|
996
|
+
overlay: { ...overlay, error: null, pending: "Creating worktree..." },
|
|
997
|
+
});
|
|
998
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
979
999
|
const prompt = overlay.prompt.trim();
|
|
980
1000
|
const issueId = overlay.issue.issue.id;
|
|
981
1001
|
let result;
|
|
@@ -993,13 +1013,6 @@ export default function Dashboard() {
|
|
|
993
1013
|
}
|
|
994
1014
|
else {
|
|
995
1015
|
const desc = overlay.desc.trim();
|
|
996
|
-
if (!desc) {
|
|
997
|
-
setState({
|
|
998
|
-
...state,
|
|
999
|
-
overlay: { ...overlay, error: "Description is required." },
|
|
1000
|
-
});
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
1016
|
const branch = `${overlay.type}/${issueId}-${desc}`;
|
|
1004
1017
|
result = runCreate(branch, {
|
|
1005
1018
|
work: true,
|
|
@@ -1009,7 +1022,11 @@ export default function Dashboard() {
|
|
|
1009
1022
|
if (!result.ok) {
|
|
1010
1023
|
setState({
|
|
1011
1024
|
...state,
|
|
1012
|
-
overlay: {
|
|
1025
|
+
overlay: {
|
|
1026
|
+
...overlay,
|
|
1027
|
+
pending: null,
|
|
1028
|
+
error: result.message + (result.hint ? ` — ${result.hint}` : ""),
|
|
1029
|
+
},
|
|
1013
1030
|
});
|
|
1014
1031
|
return;
|
|
1015
1032
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createRequire } from "module";
|
|
|
8
8
|
import { tryExec, getPath } from "../lib/exec.js";
|
|
9
9
|
import { ghCliAvailable, getGhUserLogin, getRepoFullName } from "../lib/gh.js";
|
|
10
10
|
import { getGhTokenScopes, hasProjectScope } from "../lib/providers/github.js";
|
|
11
|
-
import {
|
|
11
|
+
import { checkLinearSetup } from "../lib/providers/linear.js";
|
|
12
12
|
import { readMetadata } from "../lib/metadata.js";
|
|
13
13
|
import { resolveClaudeBinary } from "../lib/claude.js";
|
|
14
14
|
import { findMainRepoRoot, getMintreeDir, getInitScriptPath, isGitIgnored, isExecutable, pathExists, } from "../lib/git.js";
|
|
@@ -52,10 +52,12 @@ async function checkClaude() {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
async function checkGh(provider) {
|
|
55
|
-
// When provider=
|
|
55
|
+
// When provider=linear, gh is only used for PR detection on worktree
|
|
56
56
|
// branches — still useful, but not strictly required for the issue flow.
|
|
57
|
-
const description = provider === "
|
|
58
|
-
|
|
57
|
+
const description = provider === "linear"
|
|
58
|
+
? "GitHub CLI (for PR status on worktrees)"
|
|
59
|
+
: "GitHub CLI for issues + PRs";
|
|
60
|
+
const required = provider !== "linear";
|
|
59
61
|
const binPath = await getPath("gh");
|
|
60
62
|
if (!binPath) {
|
|
61
63
|
return {
|
|
@@ -269,10 +271,14 @@ function ProjectScopeRow({ status }) {
|
|
|
269
271
|
}
|
|
270
272
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: status.hasScope, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "GitHub Project v2 Scope" }), _jsx(Text, { dimColor: true, children: " - lets `w` move the issue to In Progress" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Token scopes: ", status.scopes.join(", ") || "(none)"] }), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })] }));
|
|
271
273
|
}
|
|
272
|
-
function
|
|
273
|
-
const ok = status.configured &&
|
|
274
|
+
function LinearRow({ status }) {
|
|
275
|
+
const ok = status.configured &&
|
|
276
|
+
status.hasApiKey &&
|
|
277
|
+
status.authOk &&
|
|
278
|
+
status.teams.length > 0 &&
|
|
279
|
+
status.teams.every((t) => t.ok);
|
|
274
280
|
const required = status.configured;
|
|
275
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: ok, required: required }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "
|
|
281
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: ok, required: required }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Linear" }), _jsx(Text, { dimColor: true, children: " - issue listing + In Progress transition" })] }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["API key: ", status.hasApiKey ? "loaded" : "missing", status.authOk && status.user ? ` · user: ${status.user}` : ""] }), status.workspaceSlug && (_jsxs(Text, { dimColor: true, children: ["Workspace: ", status.workspaceSlug, status.apiUrl ? ` (${status.apiUrl})` : ""] })), status.teams.length > 0 ? (status.teams.map((t) => (_jsxs(Text, { dimColor: true, children: [t.ok ? "✓" : "✗", " team ", t.key, t.name ? ` (${t.name})` : "", t.error ? ` — ${t.error}` : ""] }, t.key)))) : (_jsx(Text, { dimColor: true, children: "No teams configured" })), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })] }));
|
|
276
282
|
}
|
|
277
283
|
function GithubIssuesRow({ gh }) {
|
|
278
284
|
// Required only when we're inside a git repo. Outside one, the row is
|
|
@@ -310,7 +316,7 @@ export default function Doctor() {
|
|
|
310
316
|
const [tools, setTools] = useState(null);
|
|
311
317
|
const [gh, setGh] = useState(null);
|
|
312
318
|
const [projectScope, setProjectScope] = useState(null);
|
|
313
|
-
const [
|
|
319
|
+
const [linear, setLinear] = useState(null);
|
|
314
320
|
const [rc, setRc] = useState(null);
|
|
315
321
|
const [hooks, setHooks] = useState(null);
|
|
316
322
|
const [setup, setSetup] = useState(null);
|
|
@@ -348,7 +354,7 @@ export default function Doctor() {
|
|
|
348
354
|
version: process.version,
|
|
349
355
|
};
|
|
350
356
|
toolResults.unshift(nodeRow);
|
|
351
|
-
// GH-specific probes only matter when provider=github. For
|
|
357
|
+
// GH-specific probes only matter when provider=github. For linear we
|
|
352
358
|
// still need *some* value in state so the loading guard resolves, but
|
|
353
359
|
// the row is hidden — populate with a default and skip the network.
|
|
354
360
|
const ghRes = resolvedProvider === "github"
|
|
@@ -357,41 +363,41 @@ export default function Doctor() {
|
|
|
357
363
|
const projectScopeRes = resolvedProvider === "github"
|
|
358
364
|
? await checkProjectScope()
|
|
359
365
|
: { scopes: null, hasScope: false };
|
|
360
|
-
//
|
|
366
|
+
// Linear probes only run when provider=linear. Always set state so the
|
|
361
367
|
// loading guard resolves.
|
|
362
|
-
const
|
|
363
|
-
? await
|
|
364
|
-
: { configured: false, hasApiKey: false, authOk: false,
|
|
368
|
+
const linearRes = resolvedProvider === "linear" && root
|
|
369
|
+
? await checkLinearSetup(root)
|
|
370
|
+
: { configured: false, hasApiKey: false, authOk: false, teams: [] };
|
|
365
371
|
setTools(toolResults);
|
|
366
372
|
setGh(ghRes);
|
|
367
373
|
setProjectScope(projectScopeRes);
|
|
368
|
-
|
|
374
|
+
setLinear(linearRes);
|
|
369
375
|
setRc(checkRemoteControl());
|
|
370
376
|
setHooks(checkSessionSignalHooks());
|
|
371
377
|
setSetup(checkMintreeSetup());
|
|
372
378
|
setShell(checkShellIntegration());
|
|
373
379
|
})();
|
|
374
380
|
}, []);
|
|
375
|
-
const loading = !tools || !gh || !projectScope || !
|
|
381
|
+
const loading = !tools || !gh || !projectScope || !linear || !rc || !hooks || !setup || !shell || !provider;
|
|
376
382
|
if (loading) {
|
|
377
383
|
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Checking system requirements..." })] }));
|
|
378
384
|
}
|
|
379
385
|
const requiredMissing = tools.filter((t) => t.required && (!t.installed || t.hint));
|
|
380
386
|
const optionalMissing = tools.filter((t) => !t.required && !t.installed);
|
|
381
387
|
// Provider-specific OK check: when provider=github, the GH integration row
|
|
382
|
-
// must pass (auth + repo); when provider=
|
|
383
|
-
// (api key + auth + at least one reachable
|
|
384
|
-
const providerOk = provider === "
|
|
385
|
-
?
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
388
|
+
// must pass (auth + repo); when provider=linear, the Linear row must pass
|
|
389
|
+
// (api key + auth + at least one reachable team).
|
|
390
|
+
const providerOk = provider === "linear"
|
|
391
|
+
? linear.configured &&
|
|
392
|
+
linear.hasApiKey &&
|
|
393
|
+
linear.authOk &&
|
|
394
|
+
linear.teams.length > 0 &&
|
|
395
|
+
linear.teams.every((t) => t.ok)
|
|
390
396
|
: gh.inGitRepo
|
|
391
397
|
? gh.authenticated && !!gh.repoName
|
|
392
398
|
: true;
|
|
393
399
|
const shellOk = shell.configured;
|
|
394
400
|
const allRequired = requiredMissing.length === 0 && providerOk && shellOk;
|
|
395
401
|
const requiredFailing = requiredMissing.length + (providerOk ? 0 : 1) + (shellOk ? 0 : 1);
|
|
396
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Mintree Doctor" }), _jsxs(Text, { dimColor: true, children: [" v", version] })] }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "CLI Tools" }) }), tools.map((t) => (_jsx(ToolRow, { tool: t }, t.name))), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Integrations" }) }), provider === "
|
|
402
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Mintree Doctor" }), _jsxs(Text, { dimColor: true, children: [" v", version] })] }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "CLI Tools" }) }), tools.map((t) => (_jsx(ToolRow, { tool: t }, t.name))), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Integrations" }) }), provider === "linear" ? (_jsx(LinearRow, { status: linear })) : (_jsxs(_Fragment, { children: [_jsx(GithubIssuesRow, { gh: gh }), _jsx(ProjectScopeRow, { status: projectScope })] })), _jsx(ShellRow, { status: shell }), _jsx(MintreeSetupRow, { status: setup }), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Claude Code" }) }), _jsx(RemoteControlRow, { status: rc }), _jsx(SessionSignalRow, { status: hooks }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: allRequired ? "green" : "yellow", paddingX: 2, children: allRequired ? (_jsx(Text, { color: "green", children: "All required checks pass. mintree is ready to use." })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [requiredFailing, " required item(s) need attention"] }), optionalMissing.length > 0 && (_jsxs(Text, { dimColor: true, children: [optionalMissing.length, " optional item(s) not installed"] }))] })) })] }));
|
|
397
403
|
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -3,9 +3,10 @@ export declare const description = "Initialize the current repo for mintree (cre
|
|
|
3
3
|
export declare const options: z.ZodObject<{
|
|
4
4
|
provider: z.ZodDefault<z.ZodEnum<{
|
|
5
5
|
github: "github";
|
|
6
|
-
|
|
6
|
+
linear: "linear";
|
|
7
7
|
}>>;
|
|
8
8
|
workspace: z.ZodOptional<z.ZodString>;
|
|
9
|
+
team: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
9
10
|
apiUrl: z.ZodOptional<z.ZodString>;
|
|
10
11
|
}, z.core.$strip>;
|
|
11
12
|
type Props = {
|
package/dist/commands/init.js
CHANGED
|
@@ -9,7 +9,7 @@ import { findMainRepoRoot, getMintreeDir, getMetadataPath, getWorktreesDir, getS
|
|
|
9
9
|
export const description = "Initialize the current repo for mintree (creates .mintree/, updates .gitignore)";
|
|
10
10
|
export const options = z.object({
|
|
11
11
|
provider: z
|
|
12
|
-
.enum(["github", "
|
|
12
|
+
.enum(["github", "linear"])
|
|
13
13
|
.default("github")
|
|
14
14
|
.describe(option({
|
|
15
15
|
description: "Issue provider to scaffold for (default: github)",
|
|
@@ -18,13 +18,19 @@ export const options = z.object({
|
|
|
18
18
|
.string()
|
|
19
19
|
.optional()
|
|
20
20
|
.describe(option({
|
|
21
|
-
description: "
|
|
21
|
+
description: "Linear workspace URL key (required when --provider linear)",
|
|
22
|
+
})),
|
|
23
|
+
team: z
|
|
24
|
+
.array(z.string())
|
|
25
|
+
.optional()
|
|
26
|
+
.describe(option({
|
|
27
|
+
description: "Linear team key (repeatable, e.g. --team FE --team BE)",
|
|
22
28
|
})),
|
|
23
29
|
apiUrl: z
|
|
24
30
|
.string()
|
|
25
31
|
.optional()
|
|
26
32
|
.describe(option({
|
|
27
|
-
description: "
|
|
33
|
+
description: "Linear GraphQL endpoint (default: https://api.linear.app/graphql; override only for self-hosted/proxy)",
|
|
28
34
|
})),
|
|
29
35
|
});
|
|
30
36
|
function buildMetadataTemplate(opts) {
|
|
@@ -33,13 +39,15 @@ function buildMetadataTemplate(opts) {
|
|
|
33
39
|
provider: opts.provider,
|
|
34
40
|
issues: {},
|
|
35
41
|
};
|
|
36
|
-
if (opts.provider === "
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
if (opts.provider === "linear") {
|
|
43
|
+
const teams = (opts.team ?? []).map((key) => ({ key }));
|
|
44
|
+
base["linear"] = {
|
|
45
|
+
apiUrl: opts.apiUrl ?? "https://api.linear.app/graphql",
|
|
39
46
|
workspaceSlug: opts.workspace ?? "FILL-IN-WORKSPACE-SLUG",
|
|
40
|
-
// Empty by default — user fills in their
|
|
41
|
-
// can list assigned work items. Doctor will
|
|
42
|
-
|
|
47
|
+
// Empty by default unless --team was passed — user fills in their
|
|
48
|
+
// teams before mintree can list assigned work items. Doctor will
|
|
49
|
+
// surface this gap.
|
|
50
|
+
teams,
|
|
43
51
|
};
|
|
44
52
|
}
|
|
45
53
|
return base;
|
|
@@ -71,7 +79,7 @@ function runInit(opts) {
|
|
|
71
79
|
hint: "Run `git init` first, then re-run `mintree init`.",
|
|
72
80
|
};
|
|
73
81
|
}
|
|
74
|
-
if (opts.provider === "
|
|
82
|
+
if (opts.provider === "linear" && (!opts.workspace || opts.workspace.length === 0)) {
|
|
75
83
|
// Allow it to proceed with a FILL-IN placeholder so the user gets a
|
|
76
84
|
// working scaffold to edit, but flag it loudly via a warn step below.
|
|
77
85
|
}
|
|
@@ -111,20 +119,24 @@ function runInit(opts) {
|
|
|
111
119
|
hint: "Run: git rm --cached .mintree/metadata.json && git commit -m 'chore: untrack mintree metadata'",
|
|
112
120
|
});
|
|
113
121
|
}
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// before doctor will pass.
|
|
117
|
-
if (opts.provider === "
|
|
122
|
+
// Linear scaffolds may be incomplete — workspaceSlug could be a placeholder
|
|
123
|
+
// and teams[] empty if no --team flags were passed. Tell the user exactly
|
|
124
|
+
// what to fix before doctor will pass.
|
|
125
|
+
if (opts.provider === "linear") {
|
|
118
126
|
const needs = [];
|
|
119
127
|
if (!opts.workspace || opts.workspace.length === 0) {
|
|
120
128
|
needs.push("workspaceSlug");
|
|
121
129
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
if (!opts.team || opts.team.length === 0) {
|
|
131
|
+
needs.push("teams[] (add at least one { key, name? })");
|
|
132
|
+
}
|
|
133
|
+
if (needs.length > 0) {
|
|
134
|
+
steps.push({
|
|
135
|
+
kind: "warn",
|
|
136
|
+
label: "Linear scaffold needs manual edits",
|
|
137
|
+
hint: `Edit ${metadataPath} and fill in: ${needs.join(", ")}`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
128
140
|
}
|
|
129
141
|
return { ok: true, repoRoot: root, provider: opts.provider, steps };
|
|
130
142
|
}
|
|
@@ -163,6 +175,7 @@ export default function Init({ options: opts }) {
|
|
|
163
175
|
setResult(runInit({
|
|
164
176
|
provider: opts.provider,
|
|
165
177
|
workspace: opts.workspace,
|
|
178
|
+
team: opts.team,
|
|
166
179
|
apiUrl: opts.apiUrl,
|
|
167
180
|
}));
|
|
168
181
|
}
|
|
@@ -173,7 +186,7 @@ export default function Init({ options: opts }) {
|
|
|
173
186
|
});
|
|
174
187
|
}
|
|
175
188
|
}, 0);
|
|
176
|
-
}, [opts.provider, opts.workspace, opts.apiUrl]);
|
|
189
|
+
}, [opts.provider, opts.workspace, opts.team, opts.apiUrl]);
|
|
177
190
|
if (!result) {
|
|
178
191
|
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Initializing mintree..." })] }));
|
|
179
192
|
}
|
|
@@ -16,7 +16,7 @@ export const options = z.object({
|
|
|
16
16
|
.describe(option({ description: "Look up PR status for each branch via `gh` (slower)" })),
|
|
17
17
|
});
|
|
18
18
|
// Matches the BRANCH_REGEX shape from lib/branch.ts: either `\d+` (GitHub)
|
|
19
|
-
// or `<
|
|
19
|
+
// or `<TEAM>-\d+` (Linear). Used to surface FE-123 in the ISSUE column.
|
|
20
20
|
const ISSUE_ID_REGEX = /^[a-z]+\/((?:[A-Z][A-Z0-9_]*-)?\d+)-/;
|
|
21
21
|
function extractIssueId(branch) {
|
|
22
22
|
if (!branch)
|
|
@@ -69,7 +69,7 @@ function resolve(cwd) {
|
|
|
69
69
|
// detached-HEAD worktrees (the "current branch" path from the dashboard)
|
|
70
70
|
// still resolve their session_id. Convention guarantees the dir is named
|
|
71
71
|
// `<issueId>-<desc>` for both attached and detached creates, where
|
|
72
|
-
// issueId is either bare digits (GitHub) or `<
|
|
72
|
+
// issueId is either bare digits (GitHub) or `<TEAM>-\d+` (Linear).
|
|
73
73
|
const issueIdMatch = worktreeDirName.match(/^((?:[A-Z][A-Z0-9_]*-)?\d+)-/);
|
|
74
74
|
if (!issueIdMatch || !issueIdMatch[1]) {
|
|
75
75
|
return {
|
package/dist/lib/branch.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* <type>/<issue>-<kebab-desc>
|
|
5
5
|
*
|
|
6
6
|
* `<type>` is one of the 11 conventional prefixes; `<issue>` is either a
|
|
7
|
-
* bare digit run (GitHub issue number — "100") or a
|
|
8
|
-
* identifier ("
|
|
7
|
+
* bare digit run (GitHub issue number — "100") or a team-prefixed Linear
|
|
8
|
+
* identifier ("FE-100"); `<desc>` is lower-case kebab-case.
|
|
9
9
|
*
|
|
10
10
|
* Examples that PARSE: feat/100-readme-update, fix/55-upload-timeout,
|
|
11
11
|
* feat/BACK-100-readme-update, fix/WEB-7-modal
|
package/dist/lib/branch.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* <type>/<issue>-<kebab-desc>
|
|
5
5
|
*
|
|
6
6
|
* `<type>` is one of the 11 conventional prefixes; `<issue>` is either a
|
|
7
|
-
* bare digit run (GitHub issue number — "100") or a
|
|
8
|
-
* identifier ("
|
|
7
|
+
* bare digit run (GitHub issue number — "100") or a team-prefixed Linear
|
|
8
|
+
* identifier ("FE-100"); `<desc>` is lower-case kebab-case.
|
|
9
9
|
*
|
|
10
10
|
* Examples that PARSE: feat/100-readme-update, fix/55-upload-timeout,
|
|
11
11
|
* feat/BACK-100-readme-update, fix/WEB-7-modal
|
|
@@ -26,8 +26,8 @@ export const ALLOWED_TYPES = [
|
|
|
26
26
|
"revert",
|
|
27
27
|
];
|
|
28
28
|
// `<type>/<issueId>-<desc>` where issueId is either `\d+` (GitHub) or
|
|
29
|
-
// `<
|
|
30
|
-
// underscores starting with a letter, mirroring
|
|
29
|
+
// `<TEAM_PREFIX>-\d+` (Linear). The TEAM_PREFIX is uppercase letters/digits/
|
|
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
|
// round-trip it into the worktree dir name.
|
|
33
33
|
const BRANCH_REGEX = /^([a-z]+)\/((?:[A-Z][A-Z0-9_]*-)?\d+)-([a-z0-9][a-z0-9-]*)$/;
|
package/dist/lib/dashboard.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fetchPrForBranch } from "./pr.js";
|
|
|
6
6
|
import { createProvider } from "./providers/index.js";
|
|
7
7
|
/**
|
|
8
8
|
* Builds a map from issue id (the canonical string — "100" on GitHub,
|
|
9
|
-
* "
|
|
9
|
+
* "FE-123" on Linear) to the matching mintree worktree.
|
|
10
10
|
* IssueId comes from the worktree dir name (`<issue>-<desc>`) rather than
|
|
11
11
|
* the branch, so detached worktrees (created via the dashboard's "current
|
|
12
12
|
* branch" mode) are included alongside the regular branch-based ones.
|
|
@@ -15,7 +15,7 @@ import { createProvider } from "./providers/index.js";
|
|
|
15
15
|
function buildWorktreeIndex(repoRoot) {
|
|
16
16
|
const worktreesRoot = path.resolve(getWorktreesDir(repoRoot));
|
|
17
17
|
// Same shape as the BRANCH_REGEX issueId capture: bare digits (GitHub) or
|
|
18
|
-
// `<
|
|
18
|
+
// `<TEAM>-\d+` (Linear). Matches `100-foo` and `FE-123-foo` alike.
|
|
19
19
|
const dirNameRegex = /^((?:[A-Z][A-Z0-9_]*-)?\d+)-/;
|
|
20
20
|
const index = new Map();
|
|
21
21
|
for (const w of listWorktrees(repoRoot)) {
|
|
@@ -96,8 +96,8 @@ function sortGroupedIssues(issues, configuredUrl) {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
// Newest-first for issues — id is a numeric-or-prefixed string. Numeric
|
|
99
|
-
// compare falls back to localeCompare for non-numeric ids (
|
|
100
|
-
// "
|
|
99
|
+
// compare falls back to localeCompare for non-numeric ids (Linear's
|
|
100
|
+
// "FE-123" form).
|
|
101
101
|
const an = Number(a.issue.id);
|
|
102
102
|
const bn = Number(b.issue.id);
|
|
103
103
|
if (Number.isFinite(an) && Number.isFinite(bn))
|
|
@@ -123,9 +123,7 @@ function buildOrphanRows(worktreesByIssue, assignedIds, sessionLookup, prByBranc
|
|
|
123
123
|
const dirName = path.basename(w.path);
|
|
124
124
|
// Strip the leading "<issueId>-" — that leaves the kebab description
|
|
125
125
|
// that originally seeded the branch name.
|
|
126
|
-
const desc = dirName.startsWith(`${issueId}-`)
|
|
127
|
-
? dirName.slice(issueId.length + 1)
|
|
128
|
-
: dirName;
|
|
126
|
+
const desc = dirName.startsWith(`${issueId}-`) ? dirName.slice(issueId.length + 1) : dirName;
|
|
129
127
|
const sessionId = metadataSessionId(issueId);
|
|
130
128
|
const worktree = { ...w, sessionId };
|
|
131
129
|
const pr = w.branch ? (prByBranch.get(w.branch) ?? null) : null;
|
package/dist/lib/gh.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thin shell helpers around the `gh` CLI. These are deliberately
|
|
3
|
-
* provider-agnostic — even when mintree's issue provider is
|
|
3
|
+
* provider-agnostic — even when mintree's issue provider is Linear, `gh` is
|
|
4
4
|
* still used to look up PR status for worktree branches, so doctor and a
|
|
5
5
|
* couple of dashboard surfaces need to know whether `gh` is reachable.
|
|
6
6
|
*
|
|
7
7
|
* The GitHub issue / Project v2 logic lives in `providers/github.ts` and
|
|
8
|
-
* uses these helpers internally; the
|
|
8
|
+
* uses these helpers internally; the Linear provider doesn't touch them.
|
|
9
9
|
*/
|
|
10
10
|
export declare function ghCliAvailable(): Promise<boolean>;
|
|
11
11
|
export declare function getGhUserLogin(): Promise<string | null>;
|
package/dist/lib/gh.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thin shell helpers around the `gh` CLI. These are deliberately
|
|
3
|
-
* provider-agnostic — even when mintree's issue provider is
|
|
3
|
+
* provider-agnostic — even when mintree's issue provider is Linear, `gh` is
|
|
4
4
|
* still used to look up PR status for worktree branches, so doctor and a
|
|
5
5
|
* couple of dashboard surfaces need to know whether `gh` is reachable.
|
|
6
6
|
*
|
|
7
7
|
* The GitHub issue / Project v2 logic lives in `providers/github.ts` and
|
|
8
|
-
* uses these helpers internally; the
|
|
8
|
+
* uses these helpers internally; the Linear provider doesn't touch them.
|
|
9
9
|
*/
|
|
10
10
|
import { tryExec } from "./exec.js";
|
|
11
11
|
export async function ghCliAvailable() {
|
package/dist/lib/metadata.d.ts
CHANGED
|
@@ -8,25 +8,24 @@ export type ProjectMeta = {
|
|
|
8
8
|
inProgressOption?: string;
|
|
9
9
|
protectedStatuses?: string[];
|
|
10
10
|
};
|
|
11
|
-
export type ProviderKind = "github" | "
|
|
12
|
-
export type
|
|
13
|
-
|
|
14
|
-
identifier: string;
|
|
11
|
+
export type ProviderKind = "github" | "linear";
|
|
12
|
+
export type LinearTeamRef = {
|
|
13
|
+
key: string;
|
|
15
14
|
name?: string;
|
|
16
15
|
};
|
|
17
|
-
export type
|
|
16
|
+
export type LinearMeta = {
|
|
18
17
|
apiUrl?: string;
|
|
19
18
|
workspaceSlug: string;
|
|
20
|
-
|
|
19
|
+
teams: LinearTeamRef[];
|
|
21
20
|
inProgressStateName?: string;
|
|
22
|
-
|
|
21
|
+
protectedStateTypes?: string[];
|
|
23
22
|
};
|
|
24
23
|
export type Metadata = {
|
|
25
24
|
version: 1;
|
|
26
25
|
provider?: ProviderKind;
|
|
27
26
|
issues: Record<string, IssueMeta>;
|
|
28
27
|
project?: ProjectMeta;
|
|
29
|
-
|
|
28
|
+
linear?: LinearMeta;
|
|
30
29
|
};
|
|
31
30
|
export declare function readMetadata(repoRoot: string): Metadata;
|
|
32
31
|
export declare function writeMetadata(repoRoot: string, data: Metadata): void;
|
package/dist/lib/metadata.js
CHANGED
|
@@ -2,49 +2,47 @@ import * as fs from "fs";
|
|
|
2
2
|
import { getMetadataPath } from "./git.js";
|
|
3
3
|
const EMPTY = { version: 1, issues: {} };
|
|
4
4
|
function sanitizeProvider(raw) {
|
|
5
|
-
if (raw === "github" || raw === "
|
|
5
|
+
if (raw === "github" || raw === "linear")
|
|
6
6
|
return raw;
|
|
7
7
|
return undefined;
|
|
8
8
|
}
|
|
9
|
-
function
|
|
9
|
+
function sanitizeLinearTeam(raw) {
|
|
10
10
|
if (typeof raw !== "object" || raw === null)
|
|
11
11
|
return undefined;
|
|
12
12
|
const r = raw;
|
|
13
|
-
if (typeof r["
|
|
13
|
+
if (typeof r["key"] !== "string" || r["key"].length === 0)
|
|
14
14
|
return undefined;
|
|
15
|
-
|
|
16
|
-
return undefined;
|
|
17
|
-
const out = { id: r["id"], identifier: r["identifier"] };
|
|
15
|
+
const out = { key: r["key"] };
|
|
18
16
|
if (typeof r["name"] === "string" && r["name"].length > 0)
|
|
19
17
|
out.name = r["name"];
|
|
20
18
|
return out;
|
|
21
19
|
}
|
|
22
|
-
function
|
|
20
|
+
function sanitizeLinear(raw) {
|
|
23
21
|
if (typeof raw !== "object" || raw === null)
|
|
24
22
|
return undefined;
|
|
25
23
|
const r = raw;
|
|
26
24
|
if (typeof r["workspaceSlug"] !== "string" || r["workspaceSlug"].length === 0)
|
|
27
25
|
return undefined;
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
for (const
|
|
31
|
-
const sanitized =
|
|
26
|
+
const teamsRaw = Array.isArray(r["teams"]) ? r["teams"] : [];
|
|
27
|
+
const teams = [];
|
|
28
|
+
for (const t of teamsRaw) {
|
|
29
|
+
const sanitized = sanitizeLinearTeam(t);
|
|
32
30
|
if (sanitized)
|
|
33
|
-
|
|
31
|
+
teams.push(sanitized);
|
|
34
32
|
}
|
|
35
33
|
const out = {
|
|
36
34
|
workspaceSlug: r["workspaceSlug"],
|
|
37
|
-
|
|
35
|
+
teams,
|
|
38
36
|
};
|
|
39
37
|
if (typeof r["apiUrl"] === "string" && r["apiUrl"].length > 0)
|
|
40
38
|
out.apiUrl = r["apiUrl"];
|
|
41
39
|
if (typeof r["inProgressStateName"] === "string" && r["inProgressStateName"].length > 0) {
|
|
42
40
|
out.inProgressStateName = r["inProgressStateName"];
|
|
43
41
|
}
|
|
44
|
-
if (Array.isArray(r["
|
|
45
|
-
const arr = r["
|
|
42
|
+
if (Array.isArray(r["protectedStateTypes"])) {
|
|
43
|
+
const arr = r["protectedStateTypes"].filter((v) => typeof v === "string" && v.length > 0);
|
|
46
44
|
if (arr.length > 0)
|
|
47
|
-
out.
|
|
45
|
+
out.protectedStateTypes = arr;
|
|
48
46
|
}
|
|
49
47
|
return out;
|
|
50
48
|
}
|
|
@@ -78,7 +76,7 @@ export function readMetadata(repoRoot) {
|
|
|
78
76
|
return { ...EMPTY, issues: {} };
|
|
79
77
|
const project = sanitizeProject(parsed.project);
|
|
80
78
|
const provider = sanitizeProvider(parsed.provider);
|
|
81
|
-
const
|
|
79
|
+
const linear = sanitizeLinear(parsed.linear);
|
|
82
80
|
return {
|
|
83
81
|
version: 1,
|
|
84
82
|
issues: typeof parsed.issues === "object" && parsed.issues !== null
|
|
@@ -86,7 +84,7 @@ export function readMetadata(repoRoot) {
|
|
|
86
84
|
: {},
|
|
87
85
|
...(provider ? { provider } : {}),
|
|
88
86
|
...(project ? { project } : {}),
|
|
89
|
-
...(
|
|
87
|
+
...(linear ? { linear } : {}),
|
|
90
88
|
};
|
|
91
89
|
}
|
|
92
90
|
catch {
|
package/dist/lib/pr.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* same `gh pr list --head <branch>` shape. Centralising them here avoids
|
|
5
5
|
* three copies of the shell-quote + JSON-parse dance going out of sync.
|
|
6
6
|
*
|
|
7
|
-
* PR detection stays gh-only even when the issue provider is
|
|
8
|
-
* mintree's worktree branches live on GitHub, and
|
|
7
|
+
* PR detection stays gh-only even when the issue provider is Linear —
|
|
8
|
+
* mintree's worktree branches live on GitHub, and Linear has no concept of
|
|
9
9
|
* git PRs. Callers that aren't sure whether `gh` is available pass through
|
|
10
10
|
* `tryExec`-style failures as `null`, so the dashboard degrades to "no PR"
|
|
11
11
|
* rows instead of erroring.
|