mintree 0.1.3 → 0.1.5
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 +79 -28
- package/dist/commands/worktree/work.js +21 -20
- package/dist/lib/claude.d.ts +1 -0
- package/dist/lib/claude.js +11 -0
- package/dist/lib/dashboard.d.ts +1 -1
- package/dist/lib/dashboard.js +13 -10
- package/dist/lib/worktreeCreate.d.ts +20 -0
- package/dist/lib/worktreeCreate.js +117 -1
- package/dist/lib/worktreeRemove.d.ts +7 -0
- package/dist/lib/worktreeRemove.js +68 -0
- package/package.json +1 -1
|
@@ -5,12 +5,12 @@ import TextInput from "ink-text-input";
|
|
|
5
5
|
import Spinner from "ink-spinner";
|
|
6
6
|
import { execSync } from "child_process";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
|
-
import { findBranchConventionDoc, findMainRepoRoot, getMintreeDir, pathExists, } from "../lib/git.js";
|
|
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
11
|
import { ALLOWED_TYPES } from "../lib/branch.js";
|
|
12
|
-
import { runCreate } from "../lib/worktreeCreate.js";
|
|
13
|
-
import { runRemove } from "../lib/worktreeRemove.js";
|
|
12
|
+
import { runCreate, runCreateDetached } from "../lib/worktreeCreate.js";
|
|
13
|
+
import { runRemove, runRemoveByPath } from "../lib/worktreeRemove.js";
|
|
14
14
|
import { buildCreateMarkers, emitMarkers } from "../lib/markers.js";
|
|
15
15
|
import { loadDashboard, } from "../lib/dashboard.js";
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
@@ -105,6 +105,17 @@ function kebabize(title) {
|
|
|
105
105
|
.replace(/-+/g, "-")
|
|
106
106
|
.replace(/^-+|-+$/g, "");
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Default prompt seeded into the overlay's Prompt field when the user opens
|
|
110
|
+
* `w` for an issue. Single-line on purpose — `ink-text-input` is one-line,
|
|
111
|
+
* so multi-line templates render weirdly when the user tabs in to edit.
|
|
112
|
+
* Points the agent at `gh issue view` for the full body rather than dumping
|
|
113
|
+
* the body inline (issue bodies can be long and contain markdown that
|
|
114
|
+
* doesn't survive argv). User can clear or rewrite freely before Enter.
|
|
115
|
+
*/
|
|
116
|
+
function defaultPromptForIssue(number, title) {
|
|
117
|
+
return `Empezá a trabajar el issue #${number} (${title}). Usá \`gh issue view ${number}\` para leer el contexto completo y seguí las convenciones del repo.`;
|
|
118
|
+
}
|
|
108
119
|
/**
|
|
109
120
|
* Sanitises whatever the user typed into the desc field on every keystroke.
|
|
110
121
|
* Same rules as kebabize but without the word cap — this is for live input.
|
|
@@ -160,7 +171,7 @@ function FooterRow({ phase, overlayKind, }) {
|
|
|
160
171
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "q quit" }) }));
|
|
161
172
|
}
|
|
162
173
|
if (overlayKind === "create") {
|
|
163
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Tab" }), _jsx(Text, { dimColor: true, children: " switch field " }), _jsx(Text, { bold: true, children: "\u2190/\u2192" }), _jsx(Text, { dimColor: true, children: "
|
|
174
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Tab" }), _jsx(Text, { dimColor: true, children: " switch field " }), _jsx(Text, { bold: true, children: "\u2190/\u2192" }), _jsx(Text, { dimColor: true, children: " toggle branch / cycle type " }), _jsx(Text, { bold: true, children: "Enter" }), _jsx(Text, { dimColor: true, children: " create + work" })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Esc" }), _jsx(Text, { dimColor: true, children: " cancel" })] })] }));
|
|
164
175
|
}
|
|
165
176
|
if (overlayKind === "remove") {
|
|
166
177
|
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" })] }));
|
|
@@ -168,12 +179,21 @@ function FooterRow({ phase, overlayKind, }) {
|
|
|
168
179
|
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" })] })] }));
|
|
169
180
|
}
|
|
170
181
|
function RemoveOverlayView({ overlay }) {
|
|
171
|
-
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 })] }), _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] }) }))] }));
|
|
182
|
+
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] }) }))] }));
|
|
172
183
|
}
|
|
173
184
|
function CreateOverlayView({ overlay, onDescChange, onPromptChange, }) {
|
|
174
185
|
const labelWidth = 14;
|
|
175
|
-
const
|
|
176
|
-
|
|
186
|
+
const isNewBranch = overlay.branchMode === "new";
|
|
187
|
+
const detachedDesc = kebabize(overlay.issue.issue.title) || `issue-${overlay.issue.issue.number}`;
|
|
188
|
+
const branchPreview = isNewBranch
|
|
189
|
+
? `${overlay.type}/${overlay.issue.issue.number}-${overlay.desc}`
|
|
190
|
+
: `detached @ ${overlay.currentBranch ?? "(unknown)"}`;
|
|
191
|
+
const dirPreview = isNewBranch
|
|
192
|
+
? `${overlay.issue.issue.number}-${overlay.desc}`
|
|
193
|
+
: `${overlay.issue.issue.number}-${detachedDesc}`;
|
|
194
|
+
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.number}` })] }), _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
|
|
195
|
+
? "new"
|
|
196
|
+
: `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] }) }))] }));
|
|
177
197
|
}
|
|
178
198
|
function stateChar(state) {
|
|
179
199
|
if (!state)
|
|
@@ -296,7 +316,7 @@ function buildDetailLines(d, width) {
|
|
|
296
316
|
lines.push(blank());
|
|
297
317
|
lines.push([{ text: "⌥ Worktree", bold: true }]);
|
|
298
318
|
if (d.worktree) {
|
|
299
|
-
for (const w2 of wrapLine(` branch: ${d.worktree.branch}`, w))
|
|
319
|
+
for (const w2 of wrapLine(` branch: ${d.worktree.branch ?? "(detached HEAD)"}`, w))
|
|
300
320
|
lines.push([{ text: w2, dim: true }]);
|
|
301
321
|
for (const w2 of wrapLine(` path: ${d.worktree.path}`, w))
|
|
302
322
|
lines.push([{ text: w2, dim: true }]);
|
|
@@ -638,6 +658,7 @@ export default function Dashboard() {
|
|
|
638
658
|
kind: "remove",
|
|
639
659
|
issue,
|
|
640
660
|
branch: issue.worktree.branch,
|
|
661
|
+
worktreePath: issue.worktree.path,
|
|
641
662
|
dirty: issue.worktree.dirty,
|
|
642
663
|
error: null,
|
|
643
664
|
},
|
|
@@ -655,10 +676,12 @@ export default function Dashboard() {
|
|
|
655
676
|
overlay: {
|
|
656
677
|
kind: "create",
|
|
657
678
|
issue,
|
|
679
|
+
branchMode: "new",
|
|
680
|
+
currentBranch: root ? getCurrentBranch(root) : null,
|
|
658
681
|
type: "feat",
|
|
659
682
|
desc: kebabize(issue.issue.title) || `issue-${issue.issue.number}`,
|
|
660
|
-
prompt:
|
|
661
|
-
field: "
|
|
683
|
+
prompt: defaultPromptForIssue(issue.issue.number, issue.issue.title),
|
|
684
|
+
field: "branchMode",
|
|
662
685
|
error: null,
|
|
663
686
|
conventionDoc: root ? findBranchConventionDoc(root) : null,
|
|
664
687
|
},
|
|
@@ -678,18 +701,28 @@ export default function Dashboard() {
|
|
|
678
701
|
return;
|
|
679
702
|
}
|
|
680
703
|
// Create overlay from here on.
|
|
704
|
+
// In "current" branch mode we skip type+desc fields entirely — they have
|
|
705
|
+
// no meaning when the worktree is going to be detached. Tab cycles
|
|
706
|
+
// branchMode ⇄ prompt only.
|
|
681
707
|
if (key.tab) {
|
|
682
|
-
const
|
|
683
|
-
? "
|
|
684
|
-
:
|
|
685
|
-
|
|
686
|
-
|
|
708
|
+
const order = overlay.branchMode === "current"
|
|
709
|
+
? ["branchMode", "prompt"]
|
|
710
|
+
: ["branchMode", "type", "desc", "prompt"];
|
|
711
|
+
const i = order.indexOf(overlay.field);
|
|
712
|
+
const nextField = order[(i + 1) % order.length];
|
|
687
713
|
setState({
|
|
688
714
|
...state,
|
|
689
715
|
overlay: { ...overlay, field: nextField },
|
|
690
716
|
});
|
|
691
717
|
return;
|
|
692
718
|
}
|
|
719
|
+
if (overlay.field === "branchMode") {
|
|
720
|
+
if (key.leftArrow || key.rightArrow || input === "h" || input === "l") {
|
|
721
|
+
const next = overlay.branchMode === "new" ? "current" : "new";
|
|
722
|
+
setState({ ...state, overlay: { ...overlay, branchMode: next, error: null } });
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
693
726
|
if (overlay.field === "type") {
|
|
694
727
|
if (key.leftArrow || input === "h") {
|
|
695
728
|
const idx = ALLOWED_TYPES.indexOf(overlay.type);
|
|
@@ -734,20 +767,36 @@ export default function Dashboard() {
|
|
|
734
767
|
async function confirmCreate(overlay) {
|
|
735
768
|
if (state.phase !== "ready")
|
|
736
769
|
return;
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
770
|
+
const prompt = overlay.prompt.trim();
|
|
771
|
+
const issueNumber = overlay.issue.issue.number;
|
|
772
|
+
let result;
|
|
773
|
+
if (overlay.branchMode === "current") {
|
|
774
|
+
// Detached worktree off the main repo's current branch. Desc comes
|
|
775
|
+
// from the issue title (kebabized), not user input — keeping the
|
|
776
|
+
// "current branch" flow as low-friction as possible.
|
|
777
|
+
const descKebab = kebabize(overlay.issue.issue.title) || `issue-${issueNumber}`;
|
|
778
|
+
result = runCreateDetached({
|
|
779
|
+
issueId: String(issueNumber),
|
|
780
|
+
descKebab,
|
|
781
|
+
work: true,
|
|
782
|
+
...(prompt.length > 0 ? { prompt } : {}),
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
const desc = overlay.desc.trim();
|
|
787
|
+
if (!desc) {
|
|
788
|
+
setState({
|
|
789
|
+
...state,
|
|
790
|
+
overlay: { ...overlay, error: "Description is required." },
|
|
791
|
+
});
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const branch = `${overlay.type}/${issueNumber}-${desc}`;
|
|
795
|
+
result = runCreate(branch, {
|
|
796
|
+
work: true,
|
|
797
|
+
...(prompt.length > 0 ? { prompt } : {}),
|
|
742
798
|
});
|
|
743
|
-
return;
|
|
744
799
|
}
|
|
745
|
-
const branch = `${overlay.type}/${overlay.issue.issue.number}-${desc}`;
|
|
746
|
-
const prompt = overlay.prompt.trim();
|
|
747
|
-
const result = runCreate(branch, {
|
|
748
|
-
work: true,
|
|
749
|
-
...(prompt.length > 0 ? { prompt } : {}),
|
|
750
|
-
});
|
|
751
800
|
if (!result.ok) {
|
|
752
801
|
setState({
|
|
753
802
|
...state,
|
|
@@ -766,7 +815,9 @@ export default function Dashboard() {
|
|
|
766
815
|
async function confirmRemove(overlay, force) {
|
|
767
816
|
if (state.phase !== "ready")
|
|
768
817
|
return;
|
|
769
|
-
const result =
|
|
818
|
+
const result = overlay.branch
|
|
819
|
+
? runRemove(overlay.branch, force)
|
|
820
|
+
: runRemoveByPath(overlay.worktreePath, force);
|
|
770
821
|
if (!result.ok) {
|
|
771
822
|
setState({
|
|
772
823
|
...state,
|
|
@@ -7,7 +7,6 @@ import { z } from "zod";
|
|
|
7
7
|
import { randomUUID } from "crypto";
|
|
8
8
|
import { readFileSync, unlinkSync } from "fs";
|
|
9
9
|
import * as path from "path";
|
|
10
|
-
import { parseBranch, isParseError } from "../../lib/branch.js";
|
|
11
10
|
import { findMainRepoRoot, getMintreeDir, getWorktreesDir, getCurrentBranch, pathExists, } from "../../lib/git.js";
|
|
12
11
|
import { getSessionId, setSessionId } from "../../lib/metadata.js";
|
|
13
12
|
import { launchClaude, PERMISSION_MODES } from "../../lib/claude.js";
|
|
@@ -61,28 +60,28 @@ function resolve(cwd) {
|
|
|
61
60
|
hint: "Run `mintree worktree work` from inside `.mintree/worktrees/<issue>-<desc>`.",
|
|
62
61
|
};
|
|
63
62
|
}
|
|
64
|
-
const branch = getCurrentBranch(cwdAbs);
|
|
65
|
-
if (!branch) {
|
|
66
|
-
return {
|
|
67
|
-
ok: false,
|
|
68
|
-
message: "Could not determine the current branch (detached HEAD?)",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
const parsed = parseBranch(branch);
|
|
72
|
-
if (isParseError(parsed)) {
|
|
73
|
-
return {
|
|
74
|
-
ok: false,
|
|
75
|
-
message: `Branch '${branch}' does not match the mintree convention.`,
|
|
76
|
-
hint: parsed.hint,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
63
|
// The worktree path that git knows about is the *root* of this checkout.
|
|
80
64
|
// Walk up from cwd until we land directly under .mintree/worktrees/<name>.
|
|
81
65
|
const segmentBeneathWorktreesDir = cwdAbs.slice(worktreesDir.length + 1).split(path.sep)[0];
|
|
82
66
|
const worktreePath = segmentBeneathWorktreesDir
|
|
83
67
|
? path.join(worktreesDir, segmentBeneathWorktreesDir)
|
|
84
68
|
: cwdAbs;
|
|
85
|
-
const
|
|
69
|
+
const worktreeDirName = path.basename(worktreePath);
|
|
70
|
+
// IssueId comes from the worktree dir name, not the branch — that way
|
|
71
|
+
// detached-HEAD worktrees (the "current branch" path from the dashboard)
|
|
72
|
+
// still resolve their session_id. Convention guarantees the dir is named
|
|
73
|
+
// `<issueId>-<desc>` for both attached and detached creates.
|
|
74
|
+
const issueIdMatch = worktreeDirName.match(/^(\d+)-/);
|
|
75
|
+
if (!issueIdMatch || !issueIdMatch[1]) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
message: `Worktree directory '${worktreeDirName}' doesn't start with an issue number.`,
|
|
79
|
+
hint: "Expected `<issue>-<desc>` (e.g. 100-claude-md-inicial).",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const issueId = issueIdMatch[1];
|
|
83
|
+
const branch = getCurrentBranch(cwdAbs); // null = detached HEAD, that's fine
|
|
84
|
+
const existing = getSessionId(repoRoot, issueId);
|
|
86
85
|
let sessionId;
|
|
87
86
|
let resume;
|
|
88
87
|
if (existing) {
|
|
@@ -91,7 +90,7 @@ function resolve(cwd) {
|
|
|
91
90
|
}
|
|
92
91
|
else {
|
|
93
92
|
sessionId = randomUUID();
|
|
94
|
-
setSessionId(repoRoot,
|
|
93
|
+
setSessionId(repoRoot, issueId, sessionId);
|
|
95
94
|
resume = false;
|
|
96
95
|
}
|
|
97
96
|
return {
|
|
@@ -99,8 +98,9 @@ function resolve(cwd) {
|
|
|
99
98
|
data: {
|
|
100
99
|
repoRoot,
|
|
101
100
|
worktreePath,
|
|
101
|
+
worktreeDirName,
|
|
102
102
|
branch,
|
|
103
|
-
issueId
|
|
103
|
+
issueId,
|
|
104
104
|
sessionId,
|
|
105
105
|
resume,
|
|
106
106
|
},
|
|
@@ -156,6 +156,7 @@ export default function Work({ options }) {
|
|
|
156
156
|
resume: resolved.resume,
|
|
157
157
|
prompt: effectivePrompt,
|
|
158
158
|
cwd: resolved.worktreePath,
|
|
159
|
+
remoteControlName: resolved.worktreeDirName,
|
|
159
160
|
});
|
|
160
161
|
child.on("error", (err) => {
|
|
161
162
|
setState({
|
|
@@ -183,7 +184,7 @@ export default function Work({ options }) {
|
|
|
183
184
|
const { resolved } = state;
|
|
184
185
|
const sessionShort = resolved.sessionId.slice(0, 8);
|
|
185
186
|
const action = resolved.resume ? "resuming" : "starting";
|
|
186
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "mintree worktree work" }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", resolved.branch] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "session: " }), _jsxs(Text, { children: [sessionShort, "\u2026"] }), _jsxs(Text, { dimColor: true, children: [" (", action, ")"] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "permission-mode: " }), _jsx(Text, { children: options.permissionMode })] }), options.prompt && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "initial prompt: " }), _jsxs(Text, { children: ["\"", truncate(options.prompt, 60), "\""] })] })), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "cwd: " }), _jsx(Text, { dimColor: true, children: resolved.worktreePath })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude..." }) })] }));
|
|
187
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "mintree worktree work" }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", resolved.branch ?? `detached @ ${resolved.worktreeDirName}`] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "session: " }), _jsxs(Text, { children: [sessionShort, "\u2026"] }), _jsxs(Text, { dimColor: true, children: [" (", action, ")"] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "permission-mode: " }), _jsx(Text, { children: options.permissionMode })] }), options.prompt && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "initial prompt: " }), _jsxs(Text, { children: ["\"", truncate(options.prompt, 60), "\""] })] })), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "cwd: " }), _jsx(Text, { dimColor: true, children: resolved.worktreePath })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude..." }) })] }));
|
|
187
188
|
}
|
|
188
189
|
function truncate(s, max) {
|
|
189
190
|
if (s.length <= max)
|
package/dist/lib/claude.d.ts
CHANGED
package/dist/lib/claude.js
CHANGED
|
@@ -60,6 +60,17 @@ export function launchClaude(options) {
|
|
|
60
60
|
else {
|
|
61
61
|
args.push("--session-id", options.sessionId);
|
|
62
62
|
}
|
|
63
|
+
// Always launch with Remote Control. Mintree's whole premise is being able
|
|
64
|
+
// to resume sessions from other devices, so the flag is non-optional —
|
|
65
|
+
// global `remoteControlAtStartup` becomes irrelevant for mintree-launched
|
|
66
|
+
// sessions. The optional name = worktree dir, so the RC UI can identify
|
|
67
|
+
// the session at a glance instead of showing a UUID.
|
|
68
|
+
if (options.remoteControlName) {
|
|
69
|
+
args.push("--remote-control", options.remoteControlName);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
args.push("--remote-control");
|
|
73
|
+
}
|
|
63
74
|
if (options.prompt && options.prompt.length > 0) {
|
|
64
75
|
args.push("--", promptArg(options.prompt));
|
|
65
76
|
}
|
package/dist/lib/dashboard.d.ts
CHANGED
package/dist/lib/dashboard.js
CHANGED
|
@@ -26,21 +26,21 @@ export async function fetchAssignedIssues() {
|
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Builds a map from issue id (number, as string) to the matching mintree
|
|
29
|
-
* worktree
|
|
30
|
-
* the
|
|
31
|
-
*
|
|
29
|
+
* worktree. IssueId comes from the worktree dir name (`<issue>-<desc>`)
|
|
30
|
+
* rather than the branch, so detached worktrees (created via the dashboard's
|
|
31
|
+
* "current branch" mode) are included alongside the regular branch-based
|
|
32
|
+
* ones. Worktrees outside `.mintree/worktrees/` are skipped.
|
|
32
33
|
*/
|
|
33
34
|
function buildWorktreeIndex(repoRoot) {
|
|
34
35
|
const worktreesRoot = path.resolve(getWorktreesDir(repoRoot));
|
|
35
|
-
const
|
|
36
|
+
const dirNameRegex = /^(\d+)-/;
|
|
36
37
|
const index = new Map();
|
|
37
38
|
for (const w of listWorktrees(repoRoot)) {
|
|
38
|
-
if (!w.branch)
|
|
39
|
-
continue;
|
|
40
39
|
const wAbs = path.resolve(w.path);
|
|
41
40
|
if (wAbs !== worktreesRoot && !wAbs.startsWith(worktreesRoot + path.sep))
|
|
42
41
|
continue;
|
|
43
|
-
const
|
|
42
|
+
const dirName = path.basename(wAbs);
|
|
43
|
+
const m = dirName.match(dirNameRegex);
|
|
44
44
|
const issueId = m && m[1] ? m[1] : null;
|
|
45
45
|
if (!issueId)
|
|
46
46
|
continue;
|
|
@@ -115,9 +115,12 @@ export async function loadDashboard(repoRoot) {
|
|
|
115
115
|
const metadata = readMetadata(repoRoot);
|
|
116
116
|
// Fetch PRs in parallel for branches that actually have a worktree —
|
|
117
117
|
// issues without one wouldn't have a branch on this user's repo, so we
|
|
118
|
-
// skip the per-issue gh call for them.
|
|
118
|
+
// skip the per-issue gh call for them. Detached worktrees (branch=null)
|
|
119
|
+
// have no PR by definition.
|
|
119
120
|
const prByBranch = new Map();
|
|
120
|
-
const prFetches = Array.from(worktreesByIssue.values())
|
|
121
|
+
const prFetches = Array.from(worktreesByIssue.values())
|
|
122
|
+
.filter((w) => w.branch !== null)
|
|
123
|
+
.map(async (w) => {
|
|
121
124
|
const pr = await fetchPrForBranch(w.branch);
|
|
122
125
|
if (pr)
|
|
123
126
|
prByBranch.set(w.branch, pr);
|
|
@@ -128,7 +131,7 @@ export async function loadDashboard(repoRoot) {
|
|
|
128
131
|
const worktreeRaw = worktreesByIssue.get(issueId) ?? null;
|
|
129
132
|
const sessionId = metadata.issues[issueId]?.session_id;
|
|
130
133
|
const worktree = worktreeRaw ? { ...worktreeRaw, sessionId } : null;
|
|
131
|
-
const pr = worktree ? (prByBranch.get(worktree.branch) ?? null) : null;
|
|
134
|
+
const pr = worktree && worktree.branch ? (prByBranch.get(worktree.branch) ?? null) : null;
|
|
132
135
|
return {
|
|
133
136
|
issue,
|
|
134
137
|
worktree,
|
|
@@ -34,3 +34,23 @@ export type CreateResult = {
|
|
|
34
34
|
* for the work hand-off when relevant.
|
|
35
35
|
*/
|
|
36
36
|
export declare function runCreate(branchArg: string, opts: CreateOpts): CreateResult;
|
|
37
|
+
export type CreateDetachedOpts = {
|
|
38
|
+
issueId: string;
|
|
39
|
+
descKebab: string;
|
|
40
|
+
work: boolean;
|
|
41
|
+
prompt?: string;
|
|
42
|
+
permissionMode?: PermissionMode;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Variant of `runCreate` that doesn't create a new branch — the worktree is
|
|
46
|
+
* checked out in detached HEAD at the tip of the main repo's current branch.
|
|
47
|
+
* Used by the dashboard's "current branch" overlay mode: lets the user spin
|
|
48
|
+
* up a worktree off whatever they're on (typically `main`) without forcing
|
|
49
|
+
* the `<type>/<issue>-<desc>` convention upfront. They can `git switch -c`
|
|
50
|
+
* later if/when the work warrants a branch.
|
|
51
|
+
*
|
|
52
|
+
* Worktree dir naming follows the same `<issueId>-<desc>` shape as the
|
|
53
|
+
* branch-based flow so `worktree work` can still recover the issueId from
|
|
54
|
+
* the dir name (where it can't read it from the branch).
|
|
55
|
+
*/
|
|
56
|
+
export declare function runCreateDetached(opts: CreateDetachedOpts): CreateResult;
|
|
@@ -3,7 +3,7 @@ import * as os from "os";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
5
|
import { parseBranch, isParseError } from "./branch.js";
|
|
6
|
-
import { findMainRepoRoot, getMintreeDir, getWorktreesDir, getInitScriptPath, getDefaultBranch, branchExists, worktreeForBranch, addWorktree, pathExists, isExecutable, } from "./git.js";
|
|
6
|
+
import { findMainRepoRoot, getMintreeDir, getWorktreesDir, getInitScriptPath, getDefaultBranch, getCurrentBranch, branchExists, worktreeForBranch, addWorktree, pathExists, isExecutable, } from "./git.js";
|
|
7
7
|
import { upsertIssue } from "./metadata.js";
|
|
8
8
|
function tryRunInitScript(scriptPath, worktreePath, repoRoot) {
|
|
9
9
|
if (!pathExists(scriptPath))
|
|
@@ -182,3 +182,119 @@ export function runCreate(branchArg, opts) {
|
|
|
182
182
|
permissionMode: opts.permissionMode,
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Variant of `runCreate` that doesn't create a new branch — the worktree is
|
|
187
|
+
* checked out in detached HEAD at the tip of the main repo's current branch.
|
|
188
|
+
* Used by the dashboard's "current branch" overlay mode: lets the user spin
|
|
189
|
+
* up a worktree off whatever they're on (typically `main`) without forcing
|
|
190
|
+
* the `<type>/<issue>-<desc>` convention upfront. They can `git switch -c`
|
|
191
|
+
* later if/when the work warrants a branch.
|
|
192
|
+
*
|
|
193
|
+
* Worktree dir naming follows the same `<issueId>-<desc>` shape as the
|
|
194
|
+
* branch-based flow so `worktree work` can still recover the issueId from
|
|
195
|
+
* the dir name (where it can't read it from the branch).
|
|
196
|
+
*/
|
|
197
|
+
export function runCreateDetached(opts) {
|
|
198
|
+
const root = findMainRepoRoot();
|
|
199
|
+
if (!root) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
message: "Not in a git repository.",
|
|
203
|
+
hint: "Run `git init` and then `mintree init`.",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (!pathExists(getMintreeDir(root))) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
message: ".mintree/ not found in this repo.",
|
|
210
|
+
hint: "Run `mintree init` first.",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (!/^\d+$/.test(opts.issueId)) {
|
|
214
|
+
return { ok: false, message: `Invalid issueId: ${opts.issueId}` };
|
|
215
|
+
}
|
|
216
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(opts.descKebab)) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
message: `Invalid desc: ${opts.descKebab}`,
|
|
220
|
+
hint: "Expected kebab-case starting with [a-z0-9].",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const currentBranch = getCurrentBranch(root);
|
|
224
|
+
if (!currentBranch) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
message: "Main repo is in detached HEAD — can't determine current branch to fork from.",
|
|
228
|
+
hint: "Switch the main repo to a branch first (`git switch main`) and try again.",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const worktreeDirName = `${opts.issueId}-${opts.descKebab}`;
|
|
232
|
+
const worktreePath = path.join(getWorktreesDir(root), worktreeDirName);
|
|
233
|
+
if (pathExists(worktreePath)) {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
message: `Worktree directory already exists: ${worktreePath}`,
|
|
237
|
+
hint: "Remove it first or pick a different description.",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const steps = [];
|
|
241
|
+
steps.push({
|
|
242
|
+
kind: "ok",
|
|
243
|
+
label: "detached worktree",
|
|
244
|
+
detail: `issue=${opts.issueId}, base=${currentBranch}`,
|
|
245
|
+
});
|
|
246
|
+
try {
|
|
247
|
+
execSync(`git worktree add --detach '${worktreePath.replace(/'/g, `'\\''`)}' '${currentBranch.replace(/'/g, `'\\''`)}'`, { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
const stderr = err && typeof err === "object" && "stderr" in err
|
|
251
|
+
? String(err.stderr).trim()
|
|
252
|
+
: err instanceof Error
|
|
253
|
+
? err.message
|
|
254
|
+
: String(err);
|
|
255
|
+
return { ok: false, message: `git worktree add --detach failed: ${stderr}` };
|
|
256
|
+
}
|
|
257
|
+
steps.push({
|
|
258
|
+
kind: "ok",
|
|
259
|
+
label: "checked out detached HEAD",
|
|
260
|
+
detail: `at tip of ${currentBranch}`,
|
|
261
|
+
});
|
|
262
|
+
steps.push({ kind: "ok", label: "worktree created", detail: worktreePath });
|
|
263
|
+
upsertIssue(root, opts.issueId, { base_branch: currentBranch });
|
|
264
|
+
steps.push({ kind: "ok", label: "metadata updated", detail: `issue ${opts.issueId}` });
|
|
265
|
+
const initShPath = getInitScriptPath(root);
|
|
266
|
+
const initResult = tryRunInitScript(initShPath, worktreePath, root);
|
|
267
|
+
if (initResult.ran) {
|
|
268
|
+
steps.push({ kind: "ok", label: "ran .mintree/init.sh", detail: worktreePath });
|
|
269
|
+
}
|
|
270
|
+
else if (initResult.error) {
|
|
271
|
+
steps.push({ kind: "warn", label: "init.sh failed", detail: initResult.error });
|
|
272
|
+
}
|
|
273
|
+
else if (!pathExists(initShPath)) {
|
|
274
|
+
steps.push({ kind: "skip", label: "no init.sh (skipping post-create hook)" });
|
|
275
|
+
}
|
|
276
|
+
let promptFile;
|
|
277
|
+
if (opts.work && opts.prompt && opts.prompt.length > 0) {
|
|
278
|
+
try {
|
|
279
|
+
promptFile = writePromptFile(opts.prompt);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
steps.push({
|
|
283
|
+
kind: "warn",
|
|
284
|
+
label: "failed to stage --prompt for hand-off",
|
|
285
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
ok: true,
|
|
291
|
+
steps,
|
|
292
|
+
worktreePath,
|
|
293
|
+
branch: `detached @ ${currentBranch}`,
|
|
294
|
+
issueId: opts.issueId,
|
|
295
|
+
base: currentBranch,
|
|
296
|
+
work: opts.work,
|
|
297
|
+
promptFile,
|
|
298
|
+
permissionMode: opts.permissionMode,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
@@ -19,3 +19,10 @@ export type RemoveResult = {
|
|
|
19
19
|
* resume the same Claude session.
|
|
20
20
|
*/
|
|
21
21
|
export declare function runRemove(branchArg: string, force: boolean): RemoveResult;
|
|
22
|
+
/**
|
|
23
|
+
* Path-keyed counterpart to `runRemove`, used for worktrees that don't have
|
|
24
|
+
* a parseable branch (detached HEAD ones created via the dashboard's
|
|
25
|
+
* "current branch" mode). Same dirty/force/prune semantics as runRemove —
|
|
26
|
+
* just skips the `parseBranch` step and reports the worktree by its path.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runRemoveByPath(worktreePath: string, force: boolean): RemoveResult;
|
|
@@ -82,3 +82,71 @@ export function runRemove(branchArg, force) {
|
|
|
82
82
|
wasDirty: dirty,
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Path-keyed counterpart to `runRemove`, used for worktrees that don't have
|
|
87
|
+
* a parseable branch (detached HEAD ones created via the dashboard's
|
|
88
|
+
* "current branch" mode). Same dirty/force/prune semantics as runRemove —
|
|
89
|
+
* just skips the `parseBranch` step and reports the worktree by its path.
|
|
90
|
+
*/
|
|
91
|
+
export function runRemoveByPath(worktreePath, force) {
|
|
92
|
+
const root = findMainRepoRoot();
|
|
93
|
+
if (!root) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
message: "Not in a git repository.",
|
|
97
|
+
hint: "Run `git init` and then `mintree init`.",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (!pathExists(getMintreeDir(root))) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
message: ".mintree/ not found in this repo.",
|
|
104
|
+
hint: "Run `mintree init` first.",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const label = worktreePath.split("/").pop() ?? worktreePath;
|
|
108
|
+
if (!pathExists(worktreePath)) {
|
|
109
|
+
try {
|
|
110
|
+
pruneWorktrees(root);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
message: `git worktree prune failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
branch: label,
|
|
121
|
+
worktreePath,
|
|
122
|
+
variant: "pruned-orphan",
|
|
123
|
+
wasDirty: false,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const dirty = isDirty(worktreePath);
|
|
127
|
+
if (dirty && !force) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
message: `Worktree at ${worktreePath} has uncommitted changes.`,
|
|
131
|
+
hint: "Commit/stash first, or pass --force to discard them.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
removeWorktree({ repoRoot: root, worktreePath, force });
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const stderr = err && typeof err === "object" && "stderr" in err
|
|
139
|
+
? String(err.stderr).trim()
|
|
140
|
+
: err instanceof Error
|
|
141
|
+
? err.message
|
|
142
|
+
: String(err);
|
|
143
|
+
return { ok: false, message: `git worktree remove failed: ${stderr}` };
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
branch: label,
|
|
148
|
+
worktreePath,
|
|
149
|
+
variant: "removed",
|
|
150
|
+
wasDirty: dirty,
|
|
151
|
+
};
|
|
152
|
+
}
|
package/package.json
CHANGED