mintree 0.4.1 → 0.4.3

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.
@@ -16,6 +16,7 @@ import { buildCreateMarkers, emitMarkers } from "../lib/markers.js";
16
16
  import { readMetadata } from "../lib/metadata.js";
17
17
  import { createProvider } from "../lib/providers/index.js";
18
18
  import { loadDashboard } from "../lib/dashboard.js";
19
+ import { priorityDisplay } from "../lib/priority.js";
19
20
  const require = createRequire(import.meta.url);
20
21
  const { version: mintreeVersion } = require("../../package.json");
21
22
  export const description = "Interactive dashboard listing open issues assigned to you with worktree + session state";
@@ -220,13 +221,17 @@ function IssueListRow({ d, selected, identifierWidth, rowWidth, }) {
220
221
  // Status-coloured leading dot — same convention as santree. Falls back to
221
222
  // gray when the issue has no project board membership.
222
223
  const dotColor = d.project?.statusColor ?? "gray";
224
+ // Compact priority glyph (Linear only; GitHub rows render a blank). The
225
+ // fixed single-width icon keeps the ids aligned whether or not a row has a
226
+ // priority. See lib/priority.ts.
227
+ const prio = priorityDisplay(d.issue.priority);
223
228
  const title = d.issue.title;
224
229
  // The leading-dot Text and the rest are nested under a single Text so the
225
230
  // selection background paints the whole row in one contiguous block.
226
231
  // `wrap="truncate"` clamps the row to a single line and Ink renders an
227
232
  // ellipsis at the cut. The outer Box has a fixed width so the wrap
228
233
  // behaviour knows where to truncate.
229
- return (_jsx(Box, { width: rowWidth, children: _jsxs(Text, { wrap: "truncate", backgroundColor: selected ? "blue" : undefined, color: selected ? "white" : undefined, children: [" ", _jsx(Text, { color: selected ? "white" : dotColor, children: "\u25CF" }), ` ${idText} ${title}`] }) }));
234
+ return (_jsx(Box, { width: rowWidth, children: _jsxs(Text, { wrap: "truncate", backgroundColor: selected ? "blue" : undefined, color: selected ? "white" : undefined, children: [" ", _jsx(Text, { color: selected ? "white" : dotColor, children: "\u25CF" }), " ", _jsx(Text, { color: selected ? "white" : prio.color, children: prio.icon }), ` ${idText} ${title}`] }) }));
230
235
  }
231
236
  // A project board header — the top level of the grouped issue list. Mirrors
232
237
  // the bold project name + dim count seen in the santree dashboard.
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ export declare const description = "Update mintree to the latest version (npm i -g mintree)";
3
+ export declare const options: z.ZodObject<{
4
+ force: z.ZodDefault<z.ZodBoolean>;
5
+ }, z.core.$strip>;
6
+ type Props = {
7
+ options: z.infer<typeof options>;
8
+ };
9
+ export default function Update({ options: opts }: Props): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Text } from "ink";
4
+ import Spinner from "ink-spinner";
5
+ import { option } from "pastel";
6
+ import { z } from "zod";
7
+ import { createRequire } from "module";
8
+ import { getLatestVersion, isNewerVersion } from "../lib/version.js";
9
+ import { installLatest, PACKAGE_NAME } from "../lib/update.js";
10
+ const require = createRequire(import.meta.url);
11
+ const { version: currentVersion } = require("../../package.json");
12
+ export const description = "Update mintree to the latest version (npm i -g mintree)";
13
+ export const options = z.object({
14
+ force: z
15
+ .boolean()
16
+ .default(false)
17
+ .describe(option({
18
+ description: "Reinstall even when you're already on the latest version",
19
+ alias: "f",
20
+ })),
21
+ });
22
+ export default function Update({ options: opts }) {
23
+ const [phase, setPhase] = useState({ kind: "checking" });
24
+ useEffect(() => {
25
+ let cancelled = false;
26
+ (async () => {
27
+ const latest = await getLatestVersion(PACKAGE_NAME);
28
+ if (cancelled)
29
+ return;
30
+ // Skip the reinstall only when we're provably current and the user
31
+ // didn't force it. A null probe (offline/private registry) falls
32
+ // through to the install so `mt update` still does something useful.
33
+ if (!opts.force && latest && !isNewerVersion(currentVersion, latest)) {
34
+ setPhase({ kind: "uptodate", latest });
35
+ return;
36
+ }
37
+ setPhase({ kind: "installing", latest });
38
+ const result = await installLatest();
39
+ if (cancelled)
40
+ return;
41
+ setPhase({ kind: "done", result, latest });
42
+ })();
43
+ return () => {
44
+ cancelled = true;
45
+ };
46
+ }, [opts.force]);
47
+ if (phase.kind === "checking") {
48
+ return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" Checking for updates... (current v", currentVersion, ")"] })] }));
49
+ }
50
+ if (phase.kind === "uptodate") {
51
+ return (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsxs(Text, { color: "green", children: ["\u2713 mintree is already up to date (v", phase.latest, ")."] }), _jsx(Text, { dimColor: true, children: "Run with --force to reinstall anyway." })] }));
52
+ }
53
+ if (phase.kind === "installing") {
54
+ const target = phase.latest ? `v${phase.latest}` : "latest";
55
+ return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", "Updating mintree from v", currentVersion, " to ", target, "..."] })] }));
56
+ }
57
+ // done
58
+ const { result, latest } = phase;
59
+ if (result.ok) {
60
+ const target = latest ? `v${latest}` : "the latest version";
61
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", children: ["\u2713 mintree updated to ", target, "."] }), _jsx(Text, { dimColor: true, children: "Open a new shell (or re-run your command) to use it." })] }));
62
+ }
63
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "\u2717 Update failed." }), _jsx(Text, { children: result.message }), result.hint ? _jsx(Text, { dimColor: true, children: result.hint }) : null] }));
64
+ }
@@ -3,6 +3,7 @@ import * as path from "path";
3
3
  import { listWorktrees, getWorktreesDir, isDirty, getAheadBehind, } from "./git.js";
4
4
  import { readMetadata } from "./metadata.js";
5
5
  import { fetchPrForBranch } from "./pr.js";
6
+ import { prioritySortRank } from "./priority.js";
6
7
  import { createProvider } from "./providers/index.js";
7
8
  /**
8
9
  * Builds a map from issue id (the canonical string — "100" on GitHub,
@@ -95,6 +96,13 @@ function sortGroupedIssues(issues, configuredUrl) {
95
96
  return a.project.statusOrder - b.project.statusOrder;
96
97
  }
97
98
  }
99
+ // Within a status group, surface higher-priority issues first
100
+ // (Urgent → Low; "no priority" sinks to the bottom). Orphans and
101
+ // GitHub rows have null priority and so fall through to the date sort.
102
+ const pa = prioritySortRank(a.issue.priority);
103
+ const pb = prioritySortRank(b.issue.priority);
104
+ if (pa !== pb)
105
+ return pa - pb;
98
106
  // Newest-first for issues — id is a numeric-or-prefixed string. Numeric
99
107
  // compare falls back to localeCompare for non-numeric ids (Linear's
100
108
  // "FE-123" form).
@@ -137,6 +145,7 @@ function buildOrphanRows(worktreesByIssue, assignedIds, sessionLookup, prByBranc
137
145
  body: "",
138
146
  createdAt: "",
139
147
  updatedAt: "",
148
+ priority: null,
140
149
  },
141
150
  worktree,
142
151
  session: sessionLookup(issueId),
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Issue priority, normalised across providers.
3
+ *
4
+ * Linear exposes a native `priority` field on the 0-4 scale:
5
+ * 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low.
6
+ * GitHub Issues has no native priority concept, so its provider always yields
7
+ * `null` here — the dashboard simply renders no priority glyph for those rows.
8
+ *
9
+ * `ProviderIssue.priority` stores the raw Linear number (or null), and these
10
+ * helpers turn it into a compact dashboard glyph and a sort rank. Keeping the
11
+ * mapping in one module means the render path (dashboard.tsx) and the sort
12
+ * path (dashboard.ts) stay in lock-step.
13
+ */
14
+ export type PriorityValue = number | null | undefined;
15
+ export type PriorityDisplay = {
16
+ /** Human label, e.g. "Urgent". Empty string when there's no priority. */
17
+ label: string;
18
+ /** Single-width glyph for the list row. A space when there's no priority. */
19
+ icon: string;
20
+ /** Ink-renderable colour name for the glyph. */
21
+ color: string;
22
+ };
23
+ /**
24
+ * Maps a raw priority value to its dashboard glyph. Urgent reads as a bold
25
+ * red bang; High/Medium/Low use arrows that step down in weight and colour.
26
+ * "No priority" (0) and null both render as a blank, keeping rows aligned
27
+ * without drawing the eye.
28
+ */
29
+ export declare function priorityDisplay(priority: PriorityValue): PriorityDisplay;
30
+ /**
31
+ * Sort rank for "highest priority first". Urgent (1) sorts before Low (4);
32
+ * "No priority" (0) and null sort last. Used as a tie-break inside a status
33
+ * group before the newest-first fallback.
34
+ */
35
+ export declare function prioritySortRank(priority: PriorityValue): number;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Issue priority, normalised across providers.
3
+ *
4
+ * Linear exposes a native `priority` field on the 0-4 scale:
5
+ * 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low.
6
+ * GitHub Issues has no native priority concept, so its provider always yields
7
+ * `null` here — the dashboard simply renders no priority glyph for those rows.
8
+ *
9
+ * `ProviderIssue.priority` stores the raw Linear number (or null), and these
10
+ * helpers turn it into a compact dashboard glyph and a sort rank. Keeping the
11
+ * mapping in one module means the render path (dashboard.tsx) and the sort
12
+ * path (dashboard.ts) stay in lock-step.
13
+ */
14
+ const NONE = { label: "", icon: " ", color: "gray" };
15
+ /**
16
+ * Maps a raw priority value to its dashboard glyph. Urgent reads as a bold
17
+ * red bang; High/Medium/Low use arrows that step down in weight and colour.
18
+ * "No priority" (0) and null both render as a blank, keeping rows aligned
19
+ * without drawing the eye.
20
+ */
21
+ export function priorityDisplay(priority) {
22
+ switch (priority) {
23
+ case 1:
24
+ return { label: "Urgent", icon: "!", color: "red" };
25
+ case 2:
26
+ return { label: "High", icon: "↑", color: "red" };
27
+ case 3:
28
+ return { label: "Medium", icon: "=", color: "yellow" };
29
+ case 4:
30
+ return { label: "Low", icon: "↓", color: "blue" };
31
+ default:
32
+ return NONE;
33
+ }
34
+ }
35
+ /**
36
+ * Sort rank for "highest priority first". Urgent (1) sorts before Low (4);
37
+ * "No priority" (0) and null sort last. Used as a tie-break inside a status
38
+ * group before the newest-first fallback.
39
+ */
40
+ export function prioritySortRank(priority) {
41
+ if (priority == null || priority === 0)
42
+ return Number.POSITIVE_INFINITY;
43
+ return priority;
44
+ }
@@ -142,6 +142,8 @@ export class GithubProvider {
142
142
  body: raw.body,
143
143
  createdAt: raw.createdAt,
144
144
  updatedAt: raw.updatedAt,
145
+ // GitHub Issues has no native priority field.
146
+ priority: null,
145
147
  }));
146
148
  }
147
149
  catch {
@@ -271,6 +271,7 @@ const BOOTSTRAP_QUERY = /* GraphQL */ `
271
271
  title
272
272
  description
273
273
  url
274
+ priority
274
275
  createdAt
275
276
  updatedAt
276
277
  team {
@@ -329,6 +330,10 @@ function mapIssueToProviderIssue(wi) {
329
330
  body: wi.description ?? "",
330
331
  createdAt: wi.createdAt ?? "",
331
332
  updatedAt: wi.updatedAt ?? "",
333
+ // Linear sends 0 for "No priority"; normalise it (and any missing
334
+ // value) to null so the dashboard treats it the same as GitHub's
335
+ // no-priority rows.
336
+ priority: wi.priority && wi.priority > 0 ? wi.priority : null,
332
337
  };
333
338
  }
334
339
  export class LinearProvider {
@@ -26,6 +26,7 @@ export type ProviderIssue = {
26
26
  body: string;
27
27
  createdAt: string;
28
28
  updatedAt: string;
29
+ priority: number | null;
29
30
  };
30
31
  /**
31
32
  * The issue's membership on a project board (GitHub Projects v2 / Linear
@@ -0,0 +1,16 @@
1
+ export declare const PACKAGE_NAME = "mintree";
2
+ export type UpdateResult = {
3
+ ok: true;
4
+ output: string;
5
+ } | {
6
+ ok: false;
7
+ message: string;
8
+ hint?: string;
9
+ };
10
+ /**
11
+ * Reinstalls `mintree@latest` globally via npm. Returns a discriminated result
12
+ * so the command can render a precise message instead of dumping a raw stack.
13
+ * The common failure — EACCES on a root-owned global prefix — gets a targeted
14
+ * hint pointing at the usual fixes.
15
+ */
16
+ export declare function installLatest(): Promise<UpdateResult>;
@@ -0,0 +1,43 @@
1
+ // Self-update: reinstall the globally-installed mintree from npm. The CLI is
2
+ // distributed via `npm i -g mintree`, so updating is just re-running that
3
+ // install for the `@latest` tag. We shell out to `npm` rather than reuse the
4
+ // registry probe in version.ts because npm owns the global prefix, perms, and
5
+ // bin-linking we can't replicate reliably here.
6
+ import { exec } from "child_process";
7
+ import { promisify } from "util";
8
+ const execAsync = promisify(exec);
9
+ // npm global installs can be slow on a cold cache; give them room before we
10
+ // give up. 2 minutes mirrors what a fresh `npm i -g` typically needs.
11
+ const INSTALL_TIMEOUT_MS = 120_000;
12
+ export const PACKAGE_NAME = "mintree";
13
+ /**
14
+ * Reinstalls `mintree@latest` globally via npm. Returns a discriminated result
15
+ * so the command can render a precise message instead of dumping a raw stack.
16
+ * The common failure — EACCES on a root-owned global prefix — gets a targeted
17
+ * hint pointing at the usual fixes.
18
+ */
19
+ export async function installLatest() {
20
+ try {
21
+ const { stdout, stderr } = await execAsync(`npm install -g ${PACKAGE_NAME}@latest`, {
22
+ timeout: INSTALL_TIMEOUT_MS,
23
+ });
24
+ return { ok: true, output: (stdout || stderr || "").trim() };
25
+ }
26
+ catch (err) {
27
+ const message = err instanceof Error ? err.message : String(err);
28
+ return { ok: false, message, hint: hintForError(message) };
29
+ }
30
+ }
31
+ function hintForError(message) {
32
+ const m = message.toLowerCase();
33
+ if (m.includes("eacces") || m.includes("permission denied")) {
34
+ return "npm couldn't write to its global prefix. Either fix the prefix ownership (npm docs: 'resolving EACCES permissions errors') or re-run with sudo.";
35
+ }
36
+ if (m.includes("command not found") || m.includes("not recognized")) {
37
+ return "npm wasn't found on your PATH. Install Node.js (which bundles npm) and try again.";
38
+ }
39
+ if (m.includes("etimedout") || m.includes("network") || m.includes("enotfound")) {
40
+ return "Looks like a network problem reaching the npm registry. Check your connection and retry.";
41
+ }
42
+ return undefined;
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mintree",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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>",