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.
- package/dist/commands/dashboard.js +6 -1
- package/dist/commands/update.d.ts +10 -0
- package/dist/commands/update.js +64 -0
- package/dist/lib/dashboard.js +9 -0
- package/dist/lib/priority.d.ts +35 -0
- package/dist/lib/priority.js +44 -0
- package/dist/lib/providers/github.js +2 -0
- package/dist/lib/providers/linear.js +5 -0
- package/dist/lib/providers/types.d.ts +1 -0
- package/dist/lib/update.d.ts +16 -0
- package/dist/lib/update.js +43 -0
- package/package.json +1 -1
|
@@ -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" }), `
|
|
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
|
+
}
|
package/dist/lib/dashboard.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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 {
|
|
@@ -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