mintree 0.2.4 → 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.
@@ -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
- * Plane work items (id like `DSGN-1`) get the bare id + the issue URL,
103
- * since `gh` can't read Plane and `#` isn't Plane's notation.
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 isPlane = /^[A-Z][A-Z0-9_]*-\d+$/.test(id);
107
- if (isPlane) {
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. "AUTH-6", "100"). The `#` prefix is a
193
- // GitHub convention that reads as noise for Plane's already-prefixed
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 === "plane"
615
- ? "Could not fetch work items from Plane."
614
+ const message = provider === "linear"
615
+ ? "Could not fetch issues from Linear."
616
616
  : "Could not fetch issues from GitHub.";
617
- const hint = provider === "plane"
618
- ? "Check `mintree doctor` — PLANE_API_KEY must be set and the workspace + projects reachable."
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 with multiple Plane projects configured each refresh fires N×2
749
- // API calls. Press `r` for an immediate refresh when something
750
- // changed externally. Skipped while an overlay is open so we don't yank
751
- // state from under a confirmation, and while a manual refresh is in
752
- // flight to avoid stomping on its spinner.
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;
@@ -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 { checkPlaneSetup } from "../lib/providers/plane.js";
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=plane, gh is only used for PR detection on worktree
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 === "plane" ? "GitHub CLI (for PR status on worktrees)" : "GitHub CLI for issues + PRs";
58
- const required = provider !== "plane";
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 PlaneRow({ status }) {
273
- const ok = status.configured && status.hasApiKey && status.authOk && status.projects.length > 0 && status.projects.every((p) => p.ok);
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: "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] })] })] }));
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 [plane, setPlane] = useState(null);
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 plane we
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
- // Plane probes only run when provider=plane. Always set state so the
366
+ // Linear probes only run when provider=linear. Always set state so the
361
367
  // loading guard resolves.
362
- const planeRes = resolvedProvider === "plane" && root
363
- ? await checkPlaneSetup(root)
364
- : { configured: false, hasApiKey: false, authOk: false, projects: [] };
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
- setPlane(planeRes);
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 || !plane || !rc || !hooks || !setup || !shell || !provider;
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=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)
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 === "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"] }))] })) })] }));
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
  }
@@ -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
- plane: "plane";
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 = {
@@ -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", "plane"])
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: "Plane workspace slug (required when --provider plane)",
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: "Plane API URL (default: https://api.plane.so; override for self-hosted)",
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 === "plane") {
37
- base["plane"] = {
38
- apiUrl: opts.apiUrl ?? "https://api.plane.so",
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 projects before mintree
41
- // can list assigned work items. Doctor will surface this gap.
42
- projects: [],
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 === "plane" && (!opts.workspace || opts.workspace.length === 0)) {
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
- // 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") {
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
- 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
- });
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 `<PROJ>-\d+` (Plane). Used to surface BACK-100 in the ISSUE column.
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 `<PROJ>-\d+` (Plane).
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 {
@@ -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 project-prefixed Plane
8
- * identifier ("BACK-100"); `<desc>` is lower-case kebab-case.
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
@@ -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 project-prefixed Plane
8
- * identifier ("BACK-100"); `<desc>` is lower-case kebab-case.
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
- // `<PROJ_PREFIX>-\d+` (Plane). The PROJ_PREFIX is uppercase letters/digits/
30
- // underscores starting with a letter, mirroring Plane's project-identifier
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-]*)$/;
@@ -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
- * "BACK-100" on Plane once that lands) to the matching mintree worktree.
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
- // `<PROJ>-\d+` (Plane). Matches `100-foo` and `BACK-100-foo` alike.
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 (Plane's
100
- // "BACK-100" form).
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 Plane, `gh` 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 Plane provider doesn't touch them.
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 Plane, `gh` 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 Plane provider doesn't touch them.
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() {
@@ -8,25 +8,24 @@ export type ProjectMeta = {
8
8
  inProgressOption?: string;
9
9
  protectedStatuses?: string[];
10
10
  };
11
- export type ProviderKind = "github" | "plane";
12
- export type PlaneProjectRef = {
13
- id: string;
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 PlaneMeta = {
16
+ export type LinearMeta = {
18
17
  apiUrl?: string;
19
18
  workspaceSlug: string;
20
- projects: PlaneProjectRef[];
19
+ teams: LinearTeamRef[];
21
20
  inProgressStateName?: string;
22
- protectedStateGroups?: string[];
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
- plane?: PlaneMeta;
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;
@@ -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 === "plane")
5
+ if (raw === "github" || raw === "linear")
6
6
  return raw;
7
7
  return undefined;
8
8
  }
9
- function sanitizePlaneProject(raw) {
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["id"] !== "string" || r["id"].length === 0)
13
+ if (typeof r["key"] !== "string" || r["key"].length === 0)
14
14
  return undefined;
15
- if (typeof r["identifier"] !== "string" || r["identifier"].length === 0)
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 sanitizePlane(raw) {
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 projectsRaw = Array.isArray(r["projects"]) ? r["projects"] : [];
29
- const projects = [];
30
- for (const p of projectsRaw) {
31
- const sanitized = sanitizePlaneProject(p);
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
- projects.push(sanitized);
31
+ teams.push(sanitized);
34
32
  }
35
33
  const out = {
36
34
  workspaceSlug: r["workspaceSlug"],
37
- projects,
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["protectedStateGroups"])) {
45
- const arr = r["protectedStateGroups"].filter((v) => typeof v === "string" && v.length > 0);
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.protectedStateGroups = arr;
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 plane = sanitizePlane(parsed.plane);
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
- ...(plane ? { plane } : {}),
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 Plane
8
- * mintree's worktree branches live on GitHub, and Plane has no concept of
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.
package/dist/lib/pr.js 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 Plane
8
- * mintree's worktree branches live on GitHub, and Plane has no concept of
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.
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { readMetadata } from "../metadata.js";
9
9
  import { GithubProvider } from "./github.js";
10
- import { PlaneProvider } from "./plane.js";
10
+ import { LinearProvider } from "./linear.js";
11
11
  /**
12
12
  * Returns the IssueProvider for this repo. Reads metadata.provider — when
13
13
  * omitted (i.e. repos initialised before the provider field existed) we
@@ -19,8 +19,8 @@ export function createProvider(repoRoot) {
19
19
  switch (kind) {
20
20
  case "github":
21
21
  return new GithubProvider(repoRoot);
22
- case "plane":
23
- return new PlaneProvider(repoRoot);
22
+ case "linear":
23
+ return new LinearProvider(repoRoot);
24
24
  }
25
25
  }
26
26
  /**