mintree 0.1.11 → 0.2.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/README.md +74 -17
- package/dist/commands/dashboard.js +100 -46
- package/dist/commands/doctor.js +62 -19
- package/dist/commands/init.d.ts +14 -1
- package/dist/commands/init.js +72 -13
- package/dist/commands/worktree/clean.js +2 -19
- package/dist/commands/worktree/create.js +3 -2
- package/dist/commands/worktree/list.js +10 -22
- package/dist/commands/worktree/work.js +5 -4
- package/dist/lib/branch.d.ts +7 -4
- package/dist/lib/branch.js +15 -7
- package/dist/lib/dashboard.d.ts +5 -42
- package/dist/lib/dashboard.js +33 -189
- package/dist/lib/gh.d.ts +16 -0
- package/dist/lib/{github.js → gh.js} +9 -0
- package/dist/lib/metadata.d.ts +15 -0
- package/dist/lib/metadata.js +51 -0
- package/dist/lib/pr.d.ts +26 -0
- package/dist/lib/pr.js +49 -0
- package/dist/lib/providers/github.d.ts +33 -0
- package/dist/lib/providers/github.js +381 -0
- package/dist/lib/providers/index.d.ts +27 -0
- package/dist/lib/providers/index.js +83 -0
- package/dist/lib/providers/plane.d.ts +61 -0
- package/dist/lib/providers/plane.js +749 -0
- package/dist/lib/providers/types.d.ts +113 -0
- package/dist/lib/providers/types.js +12 -0
- package/dist/lib/session-signal.d.ts +3 -2
- package/dist/lib/session-signal.js +4 -3
- package/dist/lib/worktreeCreate.js +4 -1
- package/package.json +1 -1
- package/dist/lib/github.d.ts +0 -7
- package/dist/lib/githubProject.d.ts +0 -55
- package/dist/lib/githubProject.js +0 -277
package/dist/commands/doctor.js
CHANGED
|
@@ -6,8 +6,10 @@ import * as fs from "fs";
|
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
8
|
import { tryExec, getPath } from "../lib/exec.js";
|
|
9
|
-
import { ghCliAvailable, getGhUserLogin, getRepoFullName } from "../lib/
|
|
10
|
-
import { getGhTokenScopes, hasProjectScope } from "../lib/
|
|
9
|
+
import { ghCliAvailable, getGhUserLogin, getRepoFullName } from "../lib/gh.js";
|
|
10
|
+
import { getGhTokenScopes, hasProjectScope } from "../lib/providers/github.js";
|
|
11
|
+
import { checkPlaneSetup } from "../lib/providers/plane.js";
|
|
12
|
+
import { readMetadata } from "../lib/metadata.js";
|
|
11
13
|
import { resolveClaudeBinary } from "../lib/claude.js";
|
|
12
14
|
import { findMainRepoRoot, getMintreeDir, getInitScriptPath, isGitIgnored, isExecutable, pathExists, } from "../lib/git.js";
|
|
13
15
|
const require = createRequire(import.meta.url);
|
|
@@ -49,13 +51,17 @@ async function checkClaude() {
|
|
|
49
51
|
path: resolved,
|
|
50
52
|
};
|
|
51
53
|
}
|
|
52
|
-
async function checkGh() {
|
|
54
|
+
async function checkGh(provider) {
|
|
55
|
+
// When provider=plane, gh is only used for PR detection on worktree
|
|
56
|
+
// branches — still useful, but not strictly required for the issue flow.
|
|
57
|
+
const description = provider === "plane" ? "GitHub CLI (for PR status on worktrees)" : "GitHub CLI for issues + PRs";
|
|
58
|
+
const required = provider !== "plane";
|
|
53
59
|
const binPath = await getPath("gh");
|
|
54
60
|
if (!binPath) {
|
|
55
61
|
return {
|
|
56
62
|
name: "gh",
|
|
57
|
-
description
|
|
58
|
-
required
|
|
63
|
+
description,
|
|
64
|
+
required,
|
|
59
65
|
installed: false,
|
|
60
66
|
hint: "Install: brew install gh && gh auth login",
|
|
61
67
|
};
|
|
@@ -65,8 +71,8 @@ async function checkGh() {
|
|
|
65
71
|
if (!login) {
|
|
66
72
|
return {
|
|
67
73
|
name: "gh",
|
|
68
|
-
description
|
|
69
|
-
required
|
|
74
|
+
description,
|
|
75
|
+
required,
|
|
70
76
|
installed: true,
|
|
71
77
|
version: ver || "unknown",
|
|
72
78
|
path: binPath,
|
|
@@ -75,8 +81,8 @@ async function checkGh() {
|
|
|
75
81
|
}
|
|
76
82
|
return {
|
|
77
83
|
name: "gh",
|
|
78
|
-
description
|
|
79
|
-
required
|
|
84
|
+
description,
|
|
85
|
+
required,
|
|
80
86
|
installed: true,
|
|
81
87
|
version: ver || "unknown",
|
|
82
88
|
path: binPath,
|
|
@@ -263,6 +269,11 @@ function ProjectScopeRow({ status }) {
|
|
|
263
269
|
}
|
|
264
270
|
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] })] })] }));
|
|
265
271
|
}
|
|
272
|
+
function PlaneRow({ status }) {
|
|
273
|
+
const ok = status.configured && status.hasApiKey && status.authOk && status.projects.length > 0 && status.projects.every((p) => p.ok);
|
|
274
|
+
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: "Plane" }), _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.projects.length > 0 ? (status.projects.map((p) => (_jsxs(Text, { dimColor: true, children: [p.ok ? "✓" : "✗", " project ", p.identifier, " (", p.id.slice(0, 8), "\u2026)", p.error ? ` — ${p.error}` : ""] }, p.id)))) : (_jsx(Text, { dimColor: true, children: "No projects configured" })), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })] }));
|
|
276
|
+
}
|
|
266
277
|
function GithubIssuesRow({ gh }) {
|
|
267
278
|
// Required only when we're inside a git repo. Outside one, the row is
|
|
268
279
|
// purely informational (auth check) so doctor can stay green when run
|
|
@@ -299,15 +310,25 @@ export default function Doctor() {
|
|
|
299
310
|
const [tools, setTools] = useState(null);
|
|
300
311
|
const [gh, setGh] = useState(null);
|
|
301
312
|
const [projectScope, setProjectScope] = useState(null);
|
|
313
|
+
const [plane, setPlane] = useState(null);
|
|
302
314
|
const [rc, setRc] = useState(null);
|
|
303
315
|
const [hooks, setHooks] = useState(null);
|
|
304
316
|
const [setup, setSetup] = useState(null);
|
|
305
317
|
const [shell, setShell] = useState(null);
|
|
318
|
+
// Provider drives which integration rows appear + tweaks the gh row's
|
|
319
|
+
// description/required flag. Read once on mount; doctor doesn't react to
|
|
320
|
+
// metadata changes mid-run.
|
|
321
|
+
const [provider, setProvider] = useState(null);
|
|
306
322
|
useEffect(() => {
|
|
307
323
|
(async () => {
|
|
324
|
+
const root = findMainRepoRoot();
|
|
325
|
+
const resolvedProvider = root
|
|
326
|
+
? (readMetadata(root).provider ?? "github")
|
|
327
|
+
: "github";
|
|
328
|
+
setProvider(resolvedProvider);
|
|
308
329
|
const toolResults = await Promise.all([
|
|
309
330
|
checkTool("git", "Version control", true, "git --version | head -1", "Install: brew install git"),
|
|
310
|
-
checkGh(),
|
|
331
|
+
checkGh(resolvedProvider),
|
|
311
332
|
checkClaude(),
|
|
312
333
|
checkTool("tmux", "Open worktrees in separate windows", false, "tmux -V", "Install: brew install tmux"),
|
|
313
334
|
]);
|
|
@@ -327,28 +348,50 @@ export default function Doctor() {
|
|
|
327
348
|
version: process.version,
|
|
328
349
|
};
|
|
329
350
|
toolResults.unshift(nodeRow);
|
|
330
|
-
|
|
331
|
-
|
|
351
|
+
// GH-specific probes only matter when provider=github. For plane we
|
|
352
|
+
// still need *some* value in state so the loading guard resolves, but
|
|
353
|
+
// the row is hidden — populate with a default and skip the network.
|
|
354
|
+
const ghRes = resolvedProvider === "github"
|
|
355
|
+
? await checkGithubIssues()
|
|
356
|
+
: { authenticated: false, inGitRepo: false };
|
|
357
|
+
const projectScopeRes = resolvedProvider === "github"
|
|
358
|
+
? await checkProjectScope()
|
|
359
|
+
: { scopes: null, hasScope: false };
|
|
360
|
+
// Plane probes only run when provider=plane. Always set state so the
|
|
361
|
+
// loading guard resolves.
|
|
362
|
+
const planeRes = resolvedProvider === "plane" && root
|
|
363
|
+
? await checkPlaneSetup(root)
|
|
364
|
+
: { configured: false, hasApiKey: false, authOk: false, projects: [] };
|
|
332
365
|
setTools(toolResults);
|
|
333
366
|
setGh(ghRes);
|
|
334
367
|
setProjectScope(projectScopeRes);
|
|
368
|
+
setPlane(planeRes);
|
|
335
369
|
setRc(checkRemoteControl());
|
|
336
370
|
setHooks(checkSessionSignalHooks());
|
|
337
371
|
setSetup(checkMintreeSetup());
|
|
338
372
|
setShell(checkShellIntegration());
|
|
339
373
|
})();
|
|
340
374
|
}, []);
|
|
341
|
-
const loading = !tools || !gh || !projectScope || !rc || !hooks || !setup || !shell;
|
|
375
|
+
const loading = !tools || !gh || !projectScope || !plane || !rc || !hooks || !setup || !shell || !provider;
|
|
342
376
|
if (loading) {
|
|
343
377
|
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Checking system requirements..." })] }));
|
|
344
378
|
}
|
|
345
379
|
const requiredMissing = tools.filter((t) => t.required && (!t.installed || t.hint));
|
|
346
380
|
const optionalMissing = tools.filter((t) => !t.required && !t.installed);
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
381
|
+
// Provider-specific OK check: when provider=github, the GH integration row
|
|
382
|
+
// must pass (auth + repo); when provider=plane, the Plane row must pass
|
|
383
|
+
// (api key + auth + at least one reachable project).
|
|
384
|
+
const providerOk = provider === "plane"
|
|
385
|
+
? plane.configured &&
|
|
386
|
+
plane.hasApiKey &&
|
|
387
|
+
plane.authOk &&
|
|
388
|
+
plane.projects.length > 0 &&
|
|
389
|
+
plane.projects.every((p) => p.ok)
|
|
390
|
+
: gh.inGitRepo
|
|
391
|
+
? gh.authenticated && !!gh.repoName
|
|
392
|
+
: true;
|
|
350
393
|
const shellOk = shell.configured;
|
|
351
|
-
const allRequired = requiredMissing.length === 0 &&
|
|
352
|
-
const requiredFailing = requiredMissing.length + (
|
|
353
|
-
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" }) }), _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"] }))] })) })] }));
|
|
394
|
+
const allRequired = requiredMissing.length === 0 && providerOk && shellOk;
|
|
395
|
+
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 === "plane" ? (_jsx(PlaneRow, { status: plane })) : (_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"] }))] })) })] }));
|
|
354
397
|
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1,2 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
export declare const description = "Initialize the current repo for mintree (creates .mintree/, updates .gitignore)";
|
|
2
|
-
export
|
|
3
|
+
export declare const options: z.ZodObject<{
|
|
4
|
+
provider: z.ZodDefault<z.ZodEnum<{
|
|
5
|
+
github: "github";
|
|
6
|
+
plane: "plane";
|
|
7
|
+
}>>;
|
|
8
|
+
workspace: z.ZodOptional<z.ZodString>;
|
|
9
|
+
apiUrl: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
type Props = {
|
|
12
|
+
options: z.infer<typeof options>;
|
|
13
|
+
};
|
|
14
|
+
export default function Init({ options: opts }: Props): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
package/dist/commands/init.js
CHANGED
|
@@ -3,12 +3,47 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
5
|
import * as fs from "fs";
|
|
6
|
+
import { option } from "pastel";
|
|
7
|
+
import { z } from "zod";
|
|
6
8
|
import { findMainRepoRoot, getMintreeDir, getMetadataPath, getWorktreesDir, getSessionStatesDir, ensureGitignoreEntries, isGitTracked, } from "../lib/git.js";
|
|
7
9
|
export const description = "Initialize the current repo for mintree (creates .mintree/, updates .gitignore)";
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
export const options = z.object({
|
|
11
|
+
provider: z
|
|
12
|
+
.enum(["github", "plane"])
|
|
13
|
+
.default("github")
|
|
14
|
+
.describe(option({
|
|
15
|
+
description: "Issue provider to scaffold for (default: github)",
|
|
16
|
+
})),
|
|
17
|
+
workspace: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe(option({
|
|
21
|
+
description: "Plane workspace slug (required when --provider plane)",
|
|
22
|
+
})),
|
|
23
|
+
apiUrl: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe(option({
|
|
27
|
+
description: "Plane API URL (default: https://api.plane.so; override for self-hosted)",
|
|
28
|
+
})),
|
|
29
|
+
});
|
|
30
|
+
function buildMetadataTemplate(opts) {
|
|
31
|
+
const base = {
|
|
32
|
+
version: 1,
|
|
33
|
+
provider: opts.provider,
|
|
34
|
+
issues: {},
|
|
35
|
+
};
|
|
36
|
+
if (opts.provider === "plane") {
|
|
37
|
+
base["plane"] = {
|
|
38
|
+
apiUrl: opts.apiUrl ?? "https://api.plane.so",
|
|
39
|
+
workspaceSlug: opts.workspace ?? "FILL-IN-WORKSPACE-SLUG",
|
|
40
|
+
// Empty by default — user fills in their projects before mintree
|
|
41
|
+
// can list assigned work items. Doctor will surface this gap.
|
|
42
|
+
projects: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return base;
|
|
46
|
+
}
|
|
12
47
|
function ensureDir(p, label, steps) {
|
|
13
48
|
if (fs.existsSync(p)) {
|
|
14
49
|
steps.push({ kind: "exists", label });
|
|
@@ -18,15 +53,16 @@ function ensureDir(p, label, steps) {
|
|
|
18
53
|
steps.push({ kind: "created", label });
|
|
19
54
|
}
|
|
20
55
|
}
|
|
21
|
-
function ensureMetadata(metadataPath, steps) {
|
|
56
|
+
function ensureMetadata(metadataPath, opts, steps) {
|
|
22
57
|
if (fs.existsSync(metadataPath)) {
|
|
23
58
|
steps.push({ kind: "exists", label: ".mintree/metadata.json" });
|
|
24
59
|
return;
|
|
25
60
|
}
|
|
26
|
-
|
|
61
|
+
const template = buildMetadataTemplate(opts);
|
|
62
|
+
fs.writeFileSync(metadataPath, JSON.stringify(template, null, 2) + "\n");
|
|
27
63
|
steps.push({ kind: "created", label: ".mintree/metadata.json" });
|
|
28
64
|
}
|
|
29
|
-
function runInit() {
|
|
65
|
+
function runInit(opts) {
|
|
30
66
|
const root = findMainRepoRoot();
|
|
31
67
|
if (!root) {
|
|
32
68
|
return {
|
|
@@ -35,6 +71,10 @@ function runInit() {
|
|
|
35
71
|
hint: "Run `git init` first, then re-run `mintree init`.",
|
|
36
72
|
};
|
|
37
73
|
}
|
|
74
|
+
if (opts.provider === "plane" && (!opts.workspace || opts.workspace.length === 0)) {
|
|
75
|
+
// Allow it to proceed with a FILL-IN placeholder so the user gets a
|
|
76
|
+
// working scaffold to edit, but flag it loudly via a warn step below.
|
|
77
|
+
}
|
|
38
78
|
const steps = [];
|
|
39
79
|
const mintreeDir = getMintreeDir(root);
|
|
40
80
|
const worktreesDir = getWorktreesDir(root);
|
|
@@ -43,7 +83,7 @@ function runInit() {
|
|
|
43
83
|
ensureDir(mintreeDir, ".mintree/", steps);
|
|
44
84
|
ensureDir(worktreesDir, ".mintree/worktrees/", steps);
|
|
45
85
|
ensureDir(sessionStatesDir, ".mintree/session-states/", steps);
|
|
46
|
-
ensureMetadata(metadataPath, steps);
|
|
86
|
+
ensureMetadata(metadataPath, opts, steps);
|
|
47
87
|
// metadata.json holds the per-issue session_id, which is local-only by
|
|
48
88
|
// nature (each dev gets their own UUIDs from `claude`). Versioning it
|
|
49
89
|
// would only generate noise + merge conflicts, so it's gitignored along
|
|
@@ -71,7 +111,22 @@ function runInit() {
|
|
|
71
111
|
hint: "Run: git rm --cached .mintree/metadata.json && git commit -m 'chore: untrack mintree metadata'",
|
|
72
112
|
});
|
|
73
113
|
}
|
|
74
|
-
|
|
114
|
+
// Plane scaffolds are intentionally incomplete — projects[] is empty and
|
|
115
|
+
// workspaceSlug may be a placeholder. Tell the user exactly what to fix
|
|
116
|
+
// before doctor will pass.
|
|
117
|
+
if (opts.provider === "plane") {
|
|
118
|
+
const needs = [];
|
|
119
|
+
if (!opts.workspace || opts.workspace.length === 0) {
|
|
120
|
+
needs.push("workspaceSlug");
|
|
121
|
+
}
|
|
122
|
+
needs.push("projects[] (add at least one { id, identifier, name? })");
|
|
123
|
+
steps.push({
|
|
124
|
+
kind: "warn",
|
|
125
|
+
label: "Plane scaffold needs manual edits",
|
|
126
|
+
hint: `Edit ${metadataPath} and fill in: ${needs.join(", ")}`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return { ok: true, repoRoot: root, provider: opts.provider, steps };
|
|
75
130
|
}
|
|
76
131
|
function StepIcon({ kind }) {
|
|
77
132
|
switch (kind) {
|
|
@@ -99,13 +154,17 @@ function stepDetail(kind) {
|
|
|
99
154
|
return null;
|
|
100
155
|
}
|
|
101
156
|
}
|
|
102
|
-
export default function Init() {
|
|
157
|
+
export default function Init({ options: opts }) {
|
|
103
158
|
const [result, setResult] = useState(null);
|
|
104
159
|
useEffect(() => {
|
|
105
160
|
// Defer one tick so the initial render with the spinner gets to paint.
|
|
106
161
|
setTimeout(() => {
|
|
107
162
|
try {
|
|
108
|
-
setResult(runInit(
|
|
163
|
+
setResult(runInit({
|
|
164
|
+
provider: opts.provider,
|
|
165
|
+
workspace: opts.workspace,
|
|
166
|
+
apiUrl: opts.apiUrl,
|
|
167
|
+
}));
|
|
109
168
|
}
|
|
110
169
|
catch (err) {
|
|
111
170
|
setResult({
|
|
@@ -114,7 +173,7 @@ export default function Init() {
|
|
|
114
173
|
});
|
|
115
174
|
}
|
|
116
175
|
}, 0);
|
|
117
|
-
}, []);
|
|
176
|
+
}, [opts.provider, opts.workspace, opts.apiUrl]);
|
|
118
177
|
if (!result) {
|
|
119
178
|
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Initializing mintree..." })] }));
|
|
120
179
|
}
|
|
@@ -122,7 +181,7 @@ export default function Init() {
|
|
|
122
181
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", result.message] }), result.hint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", result.hint] }) }))] }));
|
|
123
182
|
}
|
|
124
183
|
const anyChange = result.steps.some((s) => s.kind === "created" || s.kind === "added");
|
|
125
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "mintree init" }),
|
|
184
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "mintree init" }), _jsx(Text, { dimColor: true, children: ` · ${result.repoRoot}` }), _jsx(Text, { dimColor: true, children: ` · provider=${result.provider}` })] }), result.steps.map((step, i) => {
|
|
126
185
|
const detail = stepDetail(step.kind);
|
|
127
186
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(StepIcon, { kind: step.kind }), _jsx(Text, { children: " " }), _jsx(Text, { children: step.label }), detail && _jsxs(Text, { dimColor: true, children: [" (", detail, ")"] })] }), step.hint && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", step.hint] }) }))] }, i));
|
|
128
187
|
}), _jsx(Box, { marginTop: 1, children: anyChange ? (_jsxs(Text, { color: "green", children: ["mintree initialized. Run ", _jsx(Text, { bold: true, children: "mintree doctor" }), " to verify the rest of your setup."] })) : (_jsx(Text, { color: "cyan", children: "mintree was already initialized \u2014 nothing to do." })) })] }));
|
|
@@ -6,7 +6,7 @@ import { option } from "pastel";
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { findMainRepoRoot, getMintreeDir, getWorktreesDir, listWorktrees, isDirty, removeWorktree, pathExists, } from "../../lib/git.js";
|
|
9
|
-
import {
|
|
9
|
+
import { fetchPrForBranch } from "../../lib/pr.js";
|
|
10
10
|
export const description = "Remove worktrees whose PR is merged or closed";
|
|
11
11
|
export const options = z.object({
|
|
12
12
|
yes: z
|
|
@@ -22,23 +22,6 @@ export const options = z.object({
|
|
|
22
22
|
description: "Include worktrees with uncommitted changes (clean is conservative by default)",
|
|
23
23
|
})),
|
|
24
24
|
});
|
|
25
|
-
function shQuote(value) {
|
|
26
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
27
|
-
}
|
|
28
|
-
async function fetchPr(branch) {
|
|
29
|
-
const out = await tryExec(`gh pr list --head ${shQuote(branch)} --state all --json number,state --limit 1 2>/dev/null`);
|
|
30
|
-
if (!out)
|
|
31
|
-
return null;
|
|
32
|
-
try {
|
|
33
|
-
const arr = JSON.parse(out);
|
|
34
|
-
if (Array.isArray(arr) && arr.length > 0 && arr[0])
|
|
35
|
-
return arr[0];
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
// fall through
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
25
|
async function loadCandidates(force) {
|
|
43
26
|
const root = findMainRepoRoot();
|
|
44
27
|
if (!root) {
|
|
@@ -67,7 +50,7 @@ async function loadCandidates(force) {
|
|
|
67
50
|
if (ours.length === 0) {
|
|
68
51
|
return { phase: "nothing", message: "No mintree worktrees in this repo. Nothing to clean." };
|
|
69
52
|
}
|
|
70
|
-
const prs = await Promise.all(ours.map((w) =>
|
|
53
|
+
const prs = await Promise.all(ours.map((w) => fetchPrForBranch(w.branch, { withUrl: false })));
|
|
71
54
|
const candidates = [];
|
|
72
55
|
for (let i = 0; i < ours.length; i++) {
|
|
73
56
|
const w = ours[i];
|
|
@@ -8,7 +8,7 @@ import { PERMISSION_MODES } from "../../lib/claude.js";
|
|
|
8
8
|
import { runCreate } from "../../lib/worktreeCreate.js";
|
|
9
9
|
import { buildCreateMarkers, emitMarkers } from "../../lib/markers.js";
|
|
10
10
|
import { findMainRepoRoot } from "../../lib/git.js";
|
|
11
|
-
import {
|
|
11
|
+
import { createProvider, describeTransition } from "../../lib/providers/index.js";
|
|
12
12
|
export const description = "Create a worktree for an issue branch";
|
|
13
13
|
export const args = z.tuple([
|
|
14
14
|
z.string().describe(argument({
|
|
@@ -89,7 +89,8 @@ export default function Create({ args, options }) {
|
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
try {
|
|
92
|
-
const
|
|
92
|
+
const provider = createProvider(root);
|
|
93
|
+
const r = await provider.transitionIssueToInProgress(result.issueId);
|
|
93
94
|
if (!cancelled)
|
|
94
95
|
setTransition(r);
|
|
95
96
|
}
|
|
@@ -7,7 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { findMainRepoRoot, getWorktreesDir, listWorktrees, isDirty, getAheadBehind, pathExists, getMintreeDir, } from "../../lib/git.js";
|
|
9
9
|
import { readMetadata } from "../../lib/metadata.js";
|
|
10
|
-
import {
|
|
10
|
+
import { fetchPrForBranch } from "../../lib/pr.js";
|
|
11
11
|
export const description = "List mintree-managed worktrees with dirty/ahead/PR status";
|
|
12
12
|
export const options = z.object({
|
|
13
13
|
pr: z
|
|
@@ -15,31 +15,15 @@ export const options = z.object({
|
|
|
15
15
|
.default(false)
|
|
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 `<PROJ>-\d+` (Plane). Used to surface BACK-100 in the ISSUE column.
|
|
20
|
+
const ISSUE_ID_REGEX = /^[a-z]+\/((?:[A-Z][A-Z0-9_]*-)?\d+)-/;
|
|
19
21
|
function extractIssueId(branch) {
|
|
20
22
|
if (!branch)
|
|
21
23
|
return null;
|
|
22
24
|
const m = branch.match(ISSUE_ID_REGEX);
|
|
23
25
|
return m && m[1] ? m[1] : null;
|
|
24
26
|
}
|
|
25
|
-
async function fetchPrStatus(branch) {
|
|
26
|
-
const out = await tryExec(`gh pr list --head ${shQuote(branch)} --state all --json number,state --limit 1 2>/dev/null`);
|
|
27
|
-
if (!out)
|
|
28
|
-
return undefined;
|
|
29
|
-
try {
|
|
30
|
-
const arr = JSON.parse(out);
|
|
31
|
-
if (Array.isArray(arr) && arr.length > 0 && arr[0]) {
|
|
32
|
-
return { number: arr[0].number, state: arr[0].state };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
// fall through
|
|
37
|
-
}
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
function shQuote(value) {
|
|
41
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
42
|
-
}
|
|
43
27
|
async function load(checkPr) {
|
|
44
28
|
const root = findMainRepoRoot();
|
|
45
29
|
if (!root) {
|
|
@@ -82,9 +66,13 @@ async function load(checkPr) {
|
|
|
82
66
|
};
|
|
83
67
|
});
|
|
84
68
|
if (checkPr) {
|
|
85
|
-
const prResults = await Promise.all(rows.map((r) => r.branch === "(detached)"
|
|
69
|
+
const prResults = await Promise.all(rows.map((r) => r.branch === "(detached)"
|
|
70
|
+
? Promise.resolve(null)
|
|
71
|
+
: fetchPrForBranch(r.branch, { withUrl: false })));
|
|
86
72
|
rows.forEach((r, i) => {
|
|
87
|
-
|
|
73
|
+
const pr = prResults[i];
|
|
74
|
+
if (pr)
|
|
75
|
+
r.pr = pr;
|
|
88
76
|
});
|
|
89
77
|
}
|
|
90
78
|
return { phase: "ready", repoRoot: root, rows, checkedPr: checkPr };
|
|
@@ -68,13 +68,14 @@ function resolve(cwd) {
|
|
|
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
70
|
// still resolve their session_id. Convention guarantees the dir is named
|
|
71
|
-
// `<issueId>-<desc>` for both attached and detached creates
|
|
72
|
-
|
|
71
|
+
// `<issueId>-<desc>` for both attached and detached creates, where
|
|
72
|
+
// issueId is either bare digits (GitHub) or `<PROJ>-\d+` (Plane).
|
|
73
|
+
const issueIdMatch = worktreeDirName.match(/^((?:[A-Z][A-Z0-9_]*-)?\d+)-/);
|
|
73
74
|
if (!issueIdMatch || !issueIdMatch[1]) {
|
|
74
75
|
return {
|
|
75
76
|
ok: false,
|
|
76
|
-
message: `Worktree directory '${worktreeDirName}' doesn't start with an issue
|
|
77
|
-
hint: "Expected `<issue>-<desc>` (e.g. 100-claude-md-inicial).",
|
|
77
|
+
message: `Worktree directory '${worktreeDirName}' doesn't start with an issue id.`,
|
|
78
|
+
hint: "Expected `<issue>-<desc>` (e.g. 100-claude-md-inicial or AUTH-6-legal-endpoint).",
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
81
|
const issueId = issueIdMatch[1];
|
package/dist/lib/branch.d.ts
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* <type>/<issue>-<kebab-desc>
|
|
5
5
|
*
|
|
6
|
-
* `<type>` is one of the 11 conventional prefixes; `<issue>` is
|
|
7
|
-
*
|
|
6
|
+
* `<type>` is one of the 11 conventional prefixes; `<issue>` is either a
|
|
7
|
+
* bare digit run (GitHub issue number — "100") or a project-prefixed Plane
|
|
8
|
+
* identifier ("BACK-100"); `<desc>` is lower-case kebab-case.
|
|
8
9
|
*
|
|
9
|
-
* Examples that PARSE: feat/100-readme-update, fix/55-upload-timeout
|
|
10
|
-
*
|
|
10
|
+
* Examples that PARSE: feat/100-readme-update, fix/55-upload-timeout,
|
|
11
|
+
* feat/BACK-100-readme-update, fix/WEB-7-modal
|
|
12
|
+
* Examples that REJECT: feat/abc-foo, /100-foo, gh-100-foo, feat/100,
|
|
13
|
+
* feat/100-FooBar, feat/back-100-x (lowercase prefix)
|
|
11
14
|
*/
|
|
12
15
|
export declare const ALLOWED_TYPES: readonly ["feat", "fix", "docs", "chore", "refactor", "test", "build", "ci", "perf", "style", "revert"];
|
|
13
16
|
export type BranchType = (typeof ALLOWED_TYPES)[number];
|
package/dist/lib/branch.js
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* <type>/<issue>-<kebab-desc>
|
|
5
5
|
*
|
|
6
|
-
* `<type>` is one of the 11 conventional prefixes; `<issue>` is
|
|
7
|
-
*
|
|
6
|
+
* `<type>` is one of the 11 conventional prefixes; `<issue>` is either a
|
|
7
|
+
* bare digit run (GitHub issue number — "100") or a project-prefixed Plane
|
|
8
|
+
* identifier ("BACK-100"); `<desc>` is lower-case kebab-case.
|
|
8
9
|
*
|
|
9
|
-
* Examples that PARSE: feat/100-readme-update, fix/55-upload-timeout
|
|
10
|
-
*
|
|
10
|
+
* Examples that PARSE: feat/100-readme-update, fix/55-upload-timeout,
|
|
11
|
+
* feat/BACK-100-readme-update, fix/WEB-7-modal
|
|
12
|
+
* Examples that REJECT: feat/abc-foo, /100-foo, gh-100-foo, feat/100,
|
|
13
|
+
* feat/100-FooBar, feat/back-100-x (lowercase prefix)
|
|
11
14
|
*/
|
|
12
15
|
export const ALLOWED_TYPES = [
|
|
13
16
|
"feat",
|
|
@@ -22,20 +25,25 @@ export const ALLOWED_TYPES = [
|
|
|
22
25
|
"style",
|
|
23
26
|
"revert",
|
|
24
27
|
];
|
|
25
|
-
|
|
28
|
+
// `<type>/<issueId>-<desc>` where issueId is either `\d+` (GitHub) or
|
|
29
|
+
// `<PROJ_PREFIX>-\d+` (Plane). The PROJ_PREFIX is uppercase letters/digits/
|
|
30
|
+
// underscores starting with a letter, mirroring Plane's project-identifier
|
|
31
|
+
// constraints. The full issueId captures group 2 verbatim so callers can
|
|
32
|
+
// round-trip it into the worktree dir name.
|
|
33
|
+
const BRANCH_REGEX = /^([a-z]+)\/((?:[A-Z][A-Z0-9_]*-)?\d+)-([a-z0-9][a-z0-9-]*)$/;
|
|
26
34
|
export function parseBranch(branch) {
|
|
27
35
|
const match = BRANCH_REGEX.exec(branch);
|
|
28
36
|
if (!match) {
|
|
29
37
|
return {
|
|
30
38
|
error: `Invalid branch name: ${branch}`,
|
|
31
|
-
hint: "Expected `<type>/<issue>-<kebab-desc>`.
|
|
39
|
+
hint: "Expected `<type>/<issue>-<kebab-desc>`. Examples: feat/100-claude-md-inicial, feat/BACK-100-claude-md-inicial",
|
|
32
40
|
};
|
|
33
41
|
}
|
|
34
42
|
const [, type, issueId, desc] = match;
|
|
35
43
|
if (!type || !issueId || !desc) {
|
|
36
44
|
return {
|
|
37
45
|
error: `Invalid branch name: ${branch}`,
|
|
38
|
-
hint: "Expected `<type>/<issue>-<kebab-desc>`.
|
|
46
|
+
hint: "Expected `<type>/<issue>-<kebab-desc>`. Examples: feat/100-claude-md-inicial, feat/BACK-100-claude-md-inicial",
|
|
39
47
|
};
|
|
40
48
|
}
|
|
41
49
|
if (!ALLOWED_TYPES.includes(type)) {
|
package/dist/lib/dashboard.d.ts
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import { type AheadBehind } from "./git.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
url: string;
|
|
7
|
-
labels: {
|
|
8
|
-
name: string;
|
|
9
|
-
}[];
|
|
10
|
-
body: string;
|
|
11
|
-
createdAt: string;
|
|
12
|
-
updatedAt: string;
|
|
13
|
-
};
|
|
2
|
+
import { type PrInfo } from "./pr.js";
|
|
3
|
+
import type { IssueProjectInfo, ProviderIssue } from "./providers/types.js";
|
|
4
|
+
export type { PrInfo, PrState } from "./pr.js";
|
|
5
|
+
export type { ProviderIssue, IssueProjectInfo, IssueId } from "./providers/types.js";
|
|
14
6
|
export type WorktreeInfo = {
|
|
15
7
|
path: string;
|
|
16
8
|
branch: string | null;
|
|
@@ -24,42 +16,13 @@ export type SessionStateInfo = {
|
|
|
24
16
|
at: string;
|
|
25
17
|
message: string | null;
|
|
26
18
|
};
|
|
27
|
-
export type PrInfo = {
|
|
28
|
-
number: number;
|
|
29
|
-
state: "OPEN" | "CLOSED" | "MERGED";
|
|
30
|
-
url: string;
|
|
31
|
-
};
|
|
32
|
-
/**
|
|
33
|
-
* The issue's membership in a GitHub Projects v2 board, used to group the
|
|
34
|
-
* dashboard list. `status` is the value of the board's single-select Status
|
|
35
|
-
* field (null when the issue is on the board but has no status set).
|
|
36
|
-
* `statusOrder` is the index of that option in the field definition, so the
|
|
37
|
-
* dashboard can order status sub-groups the same way the board's columns are
|
|
38
|
-
* ordered. `statusColor` is an Ink colour derived from the option's own
|
|
39
|
-
* configured colour.
|
|
40
|
-
*/
|
|
41
|
-
export type IssueProjectInfo = {
|
|
42
|
-
projectTitle: string;
|
|
43
|
-
projectUrl: string;
|
|
44
|
-
projectNumber: number;
|
|
45
|
-
status: string | null;
|
|
46
|
-
statusColor: string;
|
|
47
|
-
statusOrder: number;
|
|
48
|
-
};
|
|
49
19
|
export type DashboardIssue = {
|
|
50
|
-
issue:
|
|
20
|
+
issue: ProviderIssue;
|
|
51
21
|
worktree: WorktreeInfo | null;
|
|
52
22
|
session: SessionStateInfo | null;
|
|
53
23
|
pr: PrInfo | null;
|
|
54
24
|
project: IssueProjectInfo | null;
|
|
55
25
|
};
|
|
56
|
-
/**
|
|
57
|
-
* Fetches open issues assigned to the authenticated GitHub user for the
|
|
58
|
-
* current cwd's repo. Returns null when `gh` isn't authenticated, the cwd
|
|
59
|
-
* isn't a GitHub repo, or the API call fails — the caller surfaces the
|
|
60
|
-
* appropriate hint.
|
|
61
|
-
*/
|
|
62
|
-
export declare function fetchAssignedIssues(): Promise<GhIssue[] | null>;
|
|
63
26
|
/**
|
|
64
27
|
* Top-level loader: enriches each assigned issue with its worktree and
|
|
65
28
|
* session snapshot. Designed to be called on dashboard mount and on every
|