mintree 0.1.6 → 0.1.7

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.
@@ -8,6 +8,7 @@ import { createRequire } from "module";
8
8
  import { findBranchConventionDoc, findMainRepoRoot, getCurrentBranch, getMintreeDir, pathExists, } from "../lib/git.js";
9
9
  import { resolveClaudeBinary } from "../lib/claude.js";
10
10
  import { tryExec } from "../lib/exec.js";
11
+ import { getLatestVersion, isNewerVersion } from "../lib/version.js";
11
12
  import { ALLOWED_TYPES } from "../lib/branch.js";
12
13
  import { runCreate, runCreateDetached } from "../lib/worktreeCreate.js";
13
14
  import { runRemove, runRemoveByPath } from "../lib/worktreeRemove.js";
@@ -160,10 +161,10 @@ function useTerminalSize() {
160
161
  }, [stdout]);
161
162
  return size;
162
163
  }
163
- function HeaderRow({ repoName, claudeVersion, issueCount, }) {
164
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "green", children: "mintree" }), _jsx(Text, { dimColor: true, children: ` v${mintreeVersion}` }), claudeVersion && _jsx(Text, { dimColor: true, children: ` · claude ${claudeVersion}` }), repoName && _jsx(Text, { dimColor: true, children: ` · ${repoName}` })] }), _jsx(Box, { children: _jsx(Text, { bold: true, backgroundColor: "cyan", color: "black", children: ` Issues (${issueCount}) ` }) })] }));
164
+ function HeaderRow({ repoName, claudeVersion, issueCount, updateAvailable, }) {
165
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "green", children: "mintree" }), _jsx(Text, { dimColor: true, children: ` v${mintreeVersion}` }), updateAvailable && _jsx(Text, { color: "yellow", children: " (*)" }), claudeVersion && _jsx(Text, { dimColor: true, children: ` · claude ${claudeVersion}` }), repoName && _jsx(Text, { dimColor: true, children: ` · ${repoName}` })] }), _jsx(Box, { children: _jsx(Text, { bold: true, backgroundColor: "cyan", color: "black", children: ` Issues (${issueCount}) ` }) })] }));
165
166
  }
166
- function FooterRow({ phase, overlayKind, }) {
167
+ function FooterRow({ phase, overlayKind, latestVersion, }) {
167
168
  if (phase === "error") {
168
169
  return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "q quit" }) }));
169
170
  }
@@ -173,7 +174,7 @@ function FooterRow({ phase, overlayKind, }) {
173
174
  if (overlayKind === "remove") {
174
175
  return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "y/Y" }), _jsx(Text, { dimColor: true, children: " confirm " }), _jsx(Text, { bold: true, children: "n/Esc" }), _jsx(Text, { dimColor: true, children: " cancel" })] }));
175
176
  }
176
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " nav " }), _jsx(Text, { bold: true, children: "\u21B5" }), _jsx(Text, { dimColor: true, children: " work (resume / create) " }), _jsx(Text, { bold: true, children: "w" }), _jsx(Text, { dimColor: true, children: " work (always create) " }), _jsx(Text, { bold: true, children: "d" }), _jsx(Text, { dimColor: true, children: " remove" })] }) }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "r" }), _jsx(Text, { dimColor: true, children: " refresh " }), _jsx(Text, { bold: true, children: "o" }), _jsx(Text, { dimColor: true, children: " open in browser " }), _jsx(Text, { bold: true, children: "PgUp/PgDn" }), _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { bold: true, children: "wheel" }), _jsx(Text, { dimColor: true, children: " scroll detail " }), _jsx(Text, { bold: true, children: "q" }), _jsx(Text, { dimColor: true, children: " quit" })] })] }));
177
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " nav " }), _jsx(Text, { bold: true, children: "\u21B5" }), _jsx(Text, { dimColor: true, children: " work (resume / create) " }), _jsx(Text, { bold: true, children: "w" }), _jsx(Text, { dimColor: true, children: " work (always create) " }), _jsx(Text, { bold: true, children: "d" }), _jsx(Text, { dimColor: true, children: " remove" })] }) }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "r" }), _jsx(Text, { dimColor: true, children: " refresh " }), _jsx(Text, { bold: true, children: "o" }), _jsx(Text, { dimColor: true, children: " open in browser " }), _jsx(Text, { bold: true, children: "PgUp/PgDn" }), _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { bold: true, children: "wheel" }), _jsx(Text, { dimColor: true, children: " scroll detail " }), _jsx(Text, { bold: true, children: "q" }), _jsx(Text, { dimColor: true, children: " quit" })] }), latestVersion && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "(*)" }), _jsx(Text, { dimColor: true, children: ` new version available — v${latestVersion} · npm i -g mintree` })] }))] }));
177
178
  }
178
179
  function RemoveOverlayView({ overlay }) {
179
180
  return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Remove worktree" }), _jsx(Text, { dimColor: true, children: ` for #${overlay.issue.issue.number}` })] }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: overlay.issue.issue.title }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Branch: " }), _jsx(Text, { color: "cyan", children: overlay.branch ?? `(detached) ${overlay.worktreePath}` })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "State: " }), overlay.dirty ? (_jsx(Text, { color: "yellow", children: "dirty (uncommitted changes will be lost)" })) : (_jsx(Text, { color: "green", children: "clean" }))] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "Removing the worktree leaves the branch and the issue's session_id in place. You can re-attach later with `mintree worktree create`." }) }), _jsx(Box, { marginTop: 1, children: overlay.dirty ? (_jsxs(Text, { children: ["This worktree is dirty. Press", " ", _jsx(Text, { bold: true, color: "red", children: "Y" }), " ", "to force-remove, ", _jsx(Text, { bold: true, children: "N" }), "/", _jsx(Text, { bold: true, children: "Esc" }), " to cancel."] })) : (_jsxs(Text, { children: ["Press", " ", _jsx(Text, { bold: true, color: "green", children: "y" }), " ", "to remove, ", _jsx(Text, { bold: true, children: "N" }), "/", _jsx(Text, { bold: true, children: "Esc" }), " to cancel."] })) }), overlay.error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", overlay.error] }) }))] }));
@@ -399,6 +400,8 @@ export default function Dashboard() {
399
400
  const [state, setState] = useState({ phase: "loading" });
400
401
  const [repoName, setRepoName] = useState(null);
401
402
  const [claudeVersion, setClaudeVersion] = useState(null);
403
+ // Set only when the npm registry reports a strictly newer version.
404
+ const [latestVersion, setLatestVersion] = useState(null);
402
405
  const { columns, rows } = useTerminalSize();
403
406
  // Switch to the alt-screen buffer once, synchronously, on the first render
404
407
  // pass. Doing this here (instead of inside a useEffect) is what makes the
@@ -479,6 +482,10 @@ export default function Dashboard() {
479
482
  setClaudeVersion(m && m[1] ? m[1] : v);
480
483
  }
481
484
  }
485
+ const latest = await getLatestVersion("mintree");
486
+ if (latest && isNewerVersion(mintreeVersion, latest)) {
487
+ setLatestVersion(latest);
488
+ }
482
489
  })();
483
490
  }, []);
484
491
  // SGR mouse tracking: enable on mount, disable on unmount, and route
@@ -908,8 +915,8 @@ export default function Dashboard() {
908
915
  const startIdx = Math.max(0, Math.min(Math.max(0, issues.length - listVisibleRows), selectedIndex - Math.floor(listVisibleRows / 2)));
909
916
  const endIdx = Math.min(issues.length, startIdx + listVisibleRows);
910
917
  const slice = issues.slice(startIdx, endIdx);
911
- 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: issues.length }) }), 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: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No open issues assigned to you in this repo." })) : (_jsxs(_Fragment, { children: [slice.map((d, i) => {
918
+ 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: issues.length, 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: issues.length === 0 ? (_jsx(Text, { dimColor: true, children: "No open issues assigned to you in this repo." })) : (_jsxs(_Fragment, { children: [slice.map((d, i) => {
912
919
  const absoluteIdx = startIdx + i;
913
920
  return (_jsx(IssueListRow, { d: d, selected: absoluteIdx === selectedIndex, identifierWidth: identifierWidth, maxTitleWidth: maxTitleWidth }, d.issue.number));
914
- }), startIdx > 0 && _jsxs(Text, { dimColor: true, children: ["\u2191 ", startIdx, " more above"] }), endIdx < issues.length && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", issues.length - endIdx, " 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: [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 })] })] }));
921
+ }), startIdx > 0 && _jsxs(Text, { dimColor: true, children: ["\u2191 ", startIdx, " more above"] }), endIdx < issues.length && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", issues.length - endIdx, " 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: [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 })] })] }));
915
922
  }
@@ -0,0 +1,2 @@
1
+ export declare function getLatestVersion(pkg: string): Promise<string | null>;
2
+ export declare function isNewerVersion(current: string, latest: string): boolean;
@@ -0,0 +1,45 @@
1
+ // Update check: ask the npm registry for the latest published version and
2
+ // compare it against what's running. Best-effort — any failure (offline,
3
+ // timeout, private registry) resolves to null and the dashboard simply
4
+ // doesn't show an update hint.
5
+ const REGISTRY_TIMEOUT_MS = 3000;
6
+ export async function getLatestVersion(pkg) {
7
+ try {
8
+ const controller = new AbortController();
9
+ const timer = setTimeout(() => controller.abort(), REGISTRY_TIMEOUT_MS);
10
+ const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, {
11
+ signal: controller.signal,
12
+ headers: { accept: "application/json" },
13
+ });
14
+ clearTimeout(timer);
15
+ if (!res.ok)
16
+ return null;
17
+ const data = (await res.json());
18
+ return typeof data.version === "string" ? data.version : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ // Returns true when `latest` is strictly newer than `current`. Both are
25
+ // expected as plain `major.minor.patch` strings; anything unparseable is
26
+ // treated as "not newer" so we never nag on a bad comparison.
27
+ export function isNewerVersion(current, latest) {
28
+ const parse = (v) => v
29
+ .trim()
30
+ .split(".")
31
+ .map((n) => parseInt(n, 10));
32
+ const a = parse(current);
33
+ const b = parse(latest);
34
+ for (let i = 0; i < 3; i++) {
35
+ const ca = a[i] ?? 0;
36
+ const cb = b[i] ?? 0;
37
+ if (Number.isNaN(ca) || Number.isNaN(cb))
38
+ return false;
39
+ if (cb > ca)
40
+ return true;
41
+ if (cb < ca)
42
+ return false;
43
+ }
44
+ return false;
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mintree",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Issue-driven git worktrees + Claude Code sessions for repos with an opinionated SDD+TDD flow.",
5
5
  "license": "MIT",
6
6
  "author": "Martin Mineo <mmineo@canarytechnologies.com>",