mintree 0.5.11 → 0.5.13
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/README.md +25 -2
- package/dist/commands/dashboard.js +6 -3
- package/dist/lib/dashboard.d.ts +3 -3
- package/dist/lib/dashboard.js +3 -3
- package/dist/lib/metadata.d.ts +1 -0
- package/dist/lib/metadata.js +33 -0
- package/dist/lib/providers/linear.d.ts +8 -3
- package/dist/lib/providers/linear.js +11 -6
- package/dist/lib/providers/types.d.ts +13 -2
- package/dist/lib/worktreeCreate.js +67 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -161,6 +161,29 @@ Three top-level keys in `.mintree/metadata.json` tune how mintree launches Claud
|
|
|
161
161
|
|
|
162
162
|
When omitted, mintree uses a built-in default that asks Claude to orchestrate the selected tickets with minimal intervention — parallelising via subagents unless dependencies force sequential work, creating a worktree per ticket with mintree, using the repo's skills, and moving each ticket to *in progress* on start and closing it when done.
|
|
163
163
|
|
|
164
|
+
### Linking gitignored files into worktrees (optional)
|
|
165
|
+
|
|
166
|
+
Git worktrees don't share **untracked** files: a new worktree is a fresh working directory, so gitignored config like `.env` lives only in your main checkout and is **absent** in every worktree mintree creates. That breaks per-worktree tooling that needs it — e.g. running an E2E suite that reads `.env` for staging credentials.
|
|
167
|
+
|
|
168
|
+
The `linkFiles` top-level key (valid on GitHub and Linear repos) lists repo-root-relative paths that mintree **symlinks** into each new worktree, right after creating it:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"version": 1,
|
|
173
|
+
"provider": "linear",
|
|
174
|
+
"issues": {},
|
|
175
|
+
"linkFiles": [".env"],
|
|
176
|
+
"linear": { "workspaceSlug": "my-team", "teams": [{ "key": "FE" }] }
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- **Symlink, not copy** — one source of truth. Rotate a credential in the main `.env` and every worktree sees it; no re-copying. `worktree remove` deletes the link, never the original file.
|
|
181
|
+
- **Best-effort, never fatal** — an entry that doesn't exist in the repo root is skipped, and so is one whose target is already present in the worktree (e.g. a tracked file). Both show up as `skip` steps in the create log.
|
|
182
|
+
- **Runs before `.mintree/init.sh`** — so the post-create hook (if any) can rely on the linked files being there.
|
|
183
|
+
- **Sandboxed paths** — entries must be repo-root-relative; absolute paths and `..` escapes are dropped on read, so a stray `metadata.json` can't make mintree link something outside the worktree.
|
|
184
|
+
|
|
185
|
+
This applies to `worktree create` (CLI), the dashboard `w` overlay, and the detached-worktree flow alike. For more involved per-worktree setup (installing deps, copying templated files), use `.mintree/init.sh` instead — see [What gets stored where](#what-gets-stored-where).
|
|
186
|
+
|
|
164
187
|
---
|
|
165
188
|
|
|
166
189
|
## Daily flow
|
|
@@ -188,7 +211,7 @@ It has three tabs, switched with `←` / `→`:
|
|
|
188
211
|
| `a` | Orchestrate tab: select / deselect all visible tickets |
|
|
189
212
|
| `w` | Always open the create overlay (type + kebab description) |
|
|
190
213
|
| `d` | Delete the selected worktree (confirmation overlay) |
|
|
191
|
-
| `r` | Manual refresh (auto-refreshes silently every 5 min)
|
|
214
|
+
| `r` | Manual refresh — bypasses the Linear snapshot cache, so a just-assigned ticket shows up immediately (auto-refreshes silently every 5 min) |
|
|
192
215
|
| `o` | Open the issue in your browser |
|
|
193
216
|
| `q`/`Esc`| Quit (or cancel an open overlay) |
|
|
194
217
|
|
|
@@ -276,7 +299,7 @@ The worktree directory is still the **bare, upper-case issue id** (`VAL-68`) reg
|
|
|
276
299
|
│ └── FE-123/ # Linear form: <TEAM-digits>
|
|
277
300
|
├── session-states/ # gitignored
|
|
278
301
|
│ └── 100.json # live state written by Claude hooks (active/waiting/idle/exited)
|
|
279
|
-
└── init.sh # opt-in. Runs in the new worktree post-create (
|
|
302
|
+
└── init.sh # opt-in. Runs in the new worktree post-create (install deps, scaffold, …)
|
|
280
303
|
```
|
|
281
304
|
|
|
282
305
|
The worktree directory is named after the bare issue id (`100`, `FE-123`, `VAL-68`); the branch keeps its full name — `<type>/<issue>-<desc>` for the convention, or Linear's own `<user>/<team>-<n>-<desc>` on Linear repos.
|
|
@@ -669,7 +669,7 @@ export default function Dashboard() {
|
|
|
669
669
|
// Live value for the mouse handler (mounted once) to read without
|
|
670
670
|
// re-binding on every resize.
|
|
671
671
|
const listWidthRef = useRef(0);
|
|
672
|
-
const refresh = async () => {
|
|
672
|
+
const refresh = async (opts) => {
|
|
673
673
|
const root = findMainRepoRoot();
|
|
674
674
|
if (!root) {
|
|
675
675
|
setState({
|
|
@@ -687,7 +687,7 @@ export default function Dashboard() {
|
|
|
687
687
|
});
|
|
688
688
|
return;
|
|
689
689
|
}
|
|
690
|
-
const issues = await loadDashboard(root);
|
|
690
|
+
const issues = await loadDashboard(root, opts);
|
|
691
691
|
if (!issues) {
|
|
692
692
|
const provider = readMetadata(root).provider ?? "github";
|
|
693
693
|
const message = provider === "linear"
|
|
@@ -975,7 +975,10 @@ export default function Dashboard() {
|
|
|
975
975
|
}
|
|
976
976
|
if (input === "r") {
|
|
977
977
|
setState({ ...state, refreshing: true });
|
|
978
|
-
|
|
978
|
+
// Manual refresh bypasses the Linear snapshot cache: the user pressed
|
|
979
|
+
// `r` to see a change they just made externally (e.g. a freshly
|
|
980
|
+
// assigned ticket), so serving cached data would defeat the gesture.
|
|
981
|
+
void refresh({ forceRefresh: true });
|
|
979
982
|
return;
|
|
980
983
|
}
|
|
981
984
|
// Orchestrate tab: Space toggles the ticket under the cursor; `a`
|
package/dist/lib/dashboard.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type AheadBehind } from "./git.js";
|
|
2
2
|
import { type PrInfo } from "./pr.js";
|
|
3
|
-
import type { IssueProjectInfo, ProviderIssue } from "./providers/types.js";
|
|
3
|
+
import type { IssueProjectInfo, LoadOptions, ProviderIssue } from "./providers/types.js";
|
|
4
4
|
export type { PrInfo, PrState } from "./pr.js";
|
|
5
|
-
export type { ProviderIssue, IssueProjectInfo, IssueId } from "./providers/types.js";
|
|
5
|
+
export type { ProviderIssue, IssueProjectInfo, IssueId, LoadOptions } from "./providers/types.js";
|
|
6
6
|
export type WorktreeInfo = {
|
|
7
7
|
path: string;
|
|
8
8
|
branch: string | null;
|
|
@@ -29,4 +29,4 @@ export type DashboardIssue = {
|
|
|
29
29
|
* session snapshot. Designed to be called on dashboard mount and on every
|
|
30
30
|
* `r` refresh — cheap because all the per-worktree probes are local.
|
|
31
31
|
*/
|
|
32
|
-
export declare function loadDashboard(repoRoot: string): Promise<DashboardIssue[] | null>;
|
|
32
|
+
export declare function loadDashboard(repoRoot: string, opts?: LoadOptions): Promise<DashboardIssue[] | null>;
|
package/dist/lib/dashboard.js
CHANGED
|
@@ -171,9 +171,9 @@ function buildOrphanRows(worktreesByIssue, assignedIds, sessionLookup, prByBranc
|
|
|
171
171
|
* session snapshot. Designed to be called on dashboard mount and on every
|
|
172
172
|
* `r` refresh — cheap because all the per-worktree probes are local.
|
|
173
173
|
*/
|
|
174
|
-
export async function loadDashboard(repoRoot) {
|
|
174
|
+
export async function loadDashboard(repoRoot, opts) {
|
|
175
175
|
const provider = createProvider(repoRoot);
|
|
176
|
-
const issues = await provider.listAssignedIssues();
|
|
176
|
+
const issues = await provider.listAssignedIssues(opts);
|
|
177
177
|
if (!issues)
|
|
178
178
|
return null;
|
|
179
179
|
const worktreesByIssue = buildWorktreeIndex(repoRoot);
|
|
@@ -196,7 +196,7 @@ export async function loadDashboard(repoRoot) {
|
|
|
196
196
|
// alongside the per-branch PR probes so neither blocks the other.
|
|
197
197
|
const [, projectByIssue] = await Promise.all([
|
|
198
198
|
Promise.all(prFetches),
|
|
199
|
-
provider.fetchProjectAssignments(),
|
|
199
|
+
provider.fetchProjectAssignments(opts),
|
|
200
200
|
]);
|
|
201
201
|
// Provider signals total failure (vs no projects configured) with null —
|
|
202
202
|
// treat as a partial load failure so the caller's resilient refresh
|
package/dist/lib/metadata.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export type Metadata = {
|
|
|
30
30
|
defaultPermissionMode?: PermissionMode;
|
|
31
31
|
promptTemplate?: string;
|
|
32
32
|
orchestratorPromptTemplate?: string;
|
|
33
|
+
linkFiles?: string[];
|
|
33
34
|
};
|
|
34
35
|
export declare function readMetadata(repoRoot: string): Metadata;
|
|
35
36
|
export declare function writeMetadata(repoRoot: string, data: Metadata): void;
|
package/dist/lib/metadata.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
2
3
|
import { getMetadataPath } from "./git.js";
|
|
3
4
|
import { PERMISSION_MODES } from "./claude.js";
|
|
4
5
|
const EMPTY = { version: 1, issues: {} };
|
|
@@ -16,6 +17,36 @@ function sanitizePromptTemplate(raw) {
|
|
|
16
17
|
function sanitizeOrchestratorPromptTemplate(raw) {
|
|
17
18
|
return typeof raw === "string" && raw.trim().length > 0 ? raw : undefined;
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Keeps only safe, repo-root-relative paths. Drops non-strings, blanks,
|
|
22
|
+
* absolute paths and any entry that escapes the repo root via `..` — a
|
|
23
|
+
* malicious / fat-fingered `metadata.json` must never make mintree symlink
|
|
24
|
+
* something outside the worktree. Normalises and de-dupes the survivors.
|
|
25
|
+
*/
|
|
26
|
+
function sanitizeLinkFiles(raw) {
|
|
27
|
+
if (!Array.isArray(raw))
|
|
28
|
+
return undefined;
|
|
29
|
+
const out = [];
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
for (const v of raw) {
|
|
32
|
+
if (typeof v !== "string")
|
|
33
|
+
continue;
|
|
34
|
+
const trimmed = v.trim();
|
|
35
|
+
if (trimmed.length === 0 || path.isAbsolute(trimmed))
|
|
36
|
+
continue;
|
|
37
|
+
const norm = path.normalize(trimmed);
|
|
38
|
+
if (norm === ".." ||
|
|
39
|
+
norm.startsWith(`..${path.sep}`) ||
|
|
40
|
+
norm.includes(`${path.sep}..${path.sep}`)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (seen.has(norm))
|
|
44
|
+
continue;
|
|
45
|
+
seen.add(norm);
|
|
46
|
+
out.push(norm);
|
|
47
|
+
}
|
|
48
|
+
return out.length > 0 ? out : undefined;
|
|
49
|
+
}
|
|
19
50
|
function sanitizeLinearTeam(raw) {
|
|
20
51
|
if (typeof raw !== "object" || raw === null)
|
|
21
52
|
return undefined;
|
|
@@ -90,6 +121,7 @@ export function readMetadata(repoRoot) {
|
|
|
90
121
|
const defaultPermissionMode = sanitizePermissionMode(parsed.defaultPermissionMode);
|
|
91
122
|
const promptTemplate = sanitizePromptTemplate(parsed.promptTemplate);
|
|
92
123
|
const orchestratorPromptTemplate = sanitizeOrchestratorPromptTemplate(parsed.orchestratorPromptTemplate);
|
|
124
|
+
const linkFiles = sanitizeLinkFiles(parsed.linkFiles);
|
|
93
125
|
return {
|
|
94
126
|
version: 1,
|
|
95
127
|
issues: typeof parsed.issues === "object" && parsed.issues !== null
|
|
@@ -101,6 +133,7 @@ export function readMetadata(repoRoot) {
|
|
|
101
133
|
...(defaultPermissionMode ? { defaultPermissionMode } : {}),
|
|
102
134
|
...(promptTemplate ? { promptTemplate } : {}),
|
|
103
135
|
...(orchestratorPromptTemplate ? { orchestratorPromptTemplate } : {}),
|
|
136
|
+
...(linkFiles ? { linkFiles } : {}),
|
|
104
137
|
};
|
|
105
138
|
}
|
|
106
139
|
catch {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Linear personal API keys (`lin_api_...`) go directly into the
|
|
15
15
|
* Authorization header with no `Bearer` prefix.
|
|
16
16
|
*/
|
|
17
|
-
import type { IssueId, IssueProjectInfo, IssueProvider, ProviderIssue, TransitionResult } from "./types.js";
|
|
17
|
+
import type { IssueId, IssueProjectInfo, IssueProvider, LoadOptions, ProviderIssue, TransitionResult } from "./types.js";
|
|
18
18
|
export declare class LinearProvider implements IssueProvider {
|
|
19
19
|
private readonly repoRoot;
|
|
20
20
|
readonly kind: "linear";
|
|
@@ -26,10 +26,15 @@ export declare class LinearProvider implements IssueProvider {
|
|
|
26
26
|
* and fetchProjectAssignments call this so we never double-fetch within a
|
|
27
27
|
* load. Per-instance promise memoisation handles the back-to-back call;
|
|
28
28
|
* the module-level cache handles refreshes within the TTL.
|
|
29
|
+
*
|
|
30
|
+
* `forceRefresh` skips the module-level cache read so the live GraphQL query
|
|
31
|
+
* runs again (it still writes the result back to the cache). The per-instance
|
|
32
|
+
* promise is kept either way, so the two callers within one load share the
|
|
33
|
+
* single forced fetch instead of issuing two.
|
|
29
34
|
*/
|
|
30
35
|
private loadSnapshot;
|
|
31
|
-
listAssignedIssues(): Promise<ProviderIssue[] | null>;
|
|
32
|
-
fetchProjectAssignments(): Promise<Map<IssueId, IssueProjectInfo> | null>;
|
|
36
|
+
listAssignedIssues(opts?: LoadOptions): Promise<ProviderIssue[] | null>;
|
|
37
|
+
fetchProjectAssignments(opts?: LoadOptions): Promise<Map<IssueId, IssueProjectInfo> | null>;
|
|
33
38
|
transitionIssueToInProgress(issueId: IssueId): Promise<TransitionResult>;
|
|
34
39
|
}
|
|
35
40
|
/**
|
|
@@ -355,8 +355,13 @@ export class LinearProvider {
|
|
|
355
355
|
* and fetchProjectAssignments call this so we never double-fetch within a
|
|
356
356
|
* load. Per-instance promise memoisation handles the back-to-back call;
|
|
357
357
|
* the module-level cache handles refreshes within the TTL.
|
|
358
|
+
*
|
|
359
|
+
* `forceRefresh` skips the module-level cache read so the live GraphQL query
|
|
360
|
+
* runs again (it still writes the result back to the cache). The per-instance
|
|
361
|
+
* promise is kept either way, so the two callers within one load share the
|
|
362
|
+
* single forced fetch instead of issuing two.
|
|
358
363
|
*/
|
|
359
|
-
async loadSnapshot() {
|
|
364
|
+
async loadSnapshot(forceRefresh = false) {
|
|
360
365
|
if (this.snapshotPromise)
|
|
361
366
|
return this.snapshotPromise;
|
|
362
367
|
const cfg = this.getConfig();
|
|
@@ -377,7 +382,7 @@ export class LinearProvider {
|
|
|
377
382
|
}
|
|
378
383
|
const apiUrl = cfg.apiUrl ?? DEFAULT_API_URL;
|
|
379
384
|
const teamKeys = cfg.teams.map((t) => t.key);
|
|
380
|
-
const cached = readSnapshotCache(cfg.workspaceSlug, teamKeys);
|
|
385
|
+
const cached = forceRefresh ? null : readSnapshotCache(cfg.workspaceSlug, teamKeys);
|
|
381
386
|
if (cached)
|
|
382
387
|
return cached;
|
|
383
388
|
this.snapshotPromise = (async () => {
|
|
@@ -394,11 +399,11 @@ export class LinearProvider {
|
|
|
394
399
|
})();
|
|
395
400
|
return this.snapshotPromise;
|
|
396
401
|
}
|
|
397
|
-
async listAssignedIssues() {
|
|
402
|
+
async listAssignedIssues(opts) {
|
|
398
403
|
const cfg = this.getConfig();
|
|
399
404
|
if (!cfg || cfg.teams.length === 0)
|
|
400
405
|
return [];
|
|
401
|
-
const snapshot = await this.loadSnapshot();
|
|
406
|
+
const snapshot = await this.loadSnapshot(opts?.forceRefresh ?? false);
|
|
402
407
|
if ("ok" in snapshot && snapshot.ok === false)
|
|
403
408
|
return null;
|
|
404
409
|
const data = snapshot;
|
|
@@ -417,12 +422,12 @@ export class LinearProvider {
|
|
|
417
422
|
}
|
|
418
423
|
return out;
|
|
419
424
|
}
|
|
420
|
-
async fetchProjectAssignments() {
|
|
425
|
+
async fetchProjectAssignments(opts) {
|
|
421
426
|
const cfg = this.getConfig();
|
|
422
427
|
const result = new Map();
|
|
423
428
|
if (!cfg || cfg.teams.length === 0)
|
|
424
429
|
return result;
|
|
425
|
-
const snapshot = await this.loadSnapshot();
|
|
430
|
+
const snapshot = await this.loadSnapshot(opts?.forceRefresh ?? false);
|
|
426
431
|
if ("ok" in snapshot && snapshot.ok === false)
|
|
427
432
|
return null;
|
|
428
433
|
const data = snapshot;
|
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
* worktree dir names round-trip through the IssueId without re-parsing.
|
|
11
11
|
*/
|
|
12
12
|
export type IssueId = string;
|
|
13
|
+
/**
|
|
14
|
+
* Options shared by the read methods of IssueProvider. `forceRefresh` tells a
|
|
15
|
+
* provider that keeps a snapshot cache (Linear) to bypass it and re-fetch from
|
|
16
|
+
* the source. The dashboard sets it for the manual `r` refresh so a change made
|
|
17
|
+
* seconds ago (e.g. an issue just assigned to the user) shows up immediately,
|
|
18
|
+
* instead of waiting out the cache TTL. Providers without a cache (GitHub)
|
|
19
|
+
* ignore it.
|
|
20
|
+
*/
|
|
21
|
+
export type LoadOptions = {
|
|
22
|
+
forceRefresh?: boolean;
|
|
23
|
+
};
|
|
13
24
|
/**
|
|
14
25
|
* A workflow issue normalised across providers. Shape mirrors what the GH
|
|
15
26
|
* `gh issue list --json` payload exposes minus the GH-specific `number`
|
|
@@ -91,7 +102,7 @@ export interface IssueProvider {
|
|
|
91
102
|
* Linear: the configured workspace/teams). Returns null on transient
|
|
92
103
|
* failure (auth, network) — the dashboard renders an error hint.
|
|
93
104
|
*/
|
|
94
|
-
listAssignedIssues(): Promise<ProviderIssue[] | null>;
|
|
105
|
+
listAssignedIssues(opts?: LoadOptions): Promise<ProviderIssue[] | null>;
|
|
95
106
|
/**
|
|
96
107
|
* Returns project/board membership for the assigned issues (same scope as
|
|
97
108
|
* listAssignedIssues — typically a single round-trip). The dashboard uses
|
|
@@ -104,7 +115,7 @@ export interface IssueProvider {
|
|
|
104
115
|
* auth missing). Distinct from empty so the dashboard can treat
|
|
105
116
|
* null as a partial load failure and keep its last-good state.
|
|
106
117
|
*/
|
|
107
|
-
fetchProjectAssignments(): Promise<Map<IssueId, IssueProjectInfo> | null>;
|
|
118
|
+
fetchProjectAssignments(opts?: LoadOptions): Promise<Map<IssueId, IssueProjectInfo> | null>;
|
|
108
119
|
/**
|
|
109
120
|
* Moves the issue to its project's "In Progress" workflow state. Idempotent
|
|
110
121
|
* by design (returns noop-already when already there) and conservative on
|
|
@@ -49,6 +49,59 @@ function tryRunInitScript(scriptPath, worktreePath, repoRoot) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Symlinks each `metadata.linkFiles` entry from the main repo into the freshly
|
|
54
|
+
* created worktree. Git worktrees don't share untracked files, so gitignored
|
|
55
|
+
* config like `.env` is absent in a new worktree; this links it in so the
|
|
56
|
+
* worktree's tooling finds the same secrets/config as the main checkout.
|
|
57
|
+
*
|
|
58
|
+
* A symlink (not a copy) keeps a single source of truth — rotating a credential
|
|
59
|
+
* in the main `.env` is seen by every worktree, and `worktree remove` deletes
|
|
60
|
+
* only the link. Entries are repo-root-relative (already validated by
|
|
61
|
+
* `sanitizeLinkFiles`). Each entry is best-effort: a missing source or an
|
|
62
|
+
* already-present target is skipped, never fatal.
|
|
63
|
+
*/
|
|
64
|
+
function linkFilesIntoWorktree(repoRoot, worktreePath, linkFiles, pushStep) {
|
|
65
|
+
for (const rel of linkFiles) {
|
|
66
|
+
const source = path.join(repoRoot, rel);
|
|
67
|
+
const target = path.join(worktreePath, rel);
|
|
68
|
+
if (!pathExists(source)) {
|
|
69
|
+
pushStep({ kind: "skip", label: `skipped link ${rel}`, detail: "not present in repo root" });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// lstat (not pathExists) so an existing symlink/file/dir already at the
|
|
73
|
+
// target — e.g. a tracked file git checked out — counts as present and
|
|
74
|
+
// we don't clobber it.
|
|
75
|
+
let targetTaken = false;
|
|
76
|
+
try {
|
|
77
|
+
fs.lstatSync(target);
|
|
78
|
+
targetTaken = true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
targetTaken = false;
|
|
82
|
+
}
|
|
83
|
+
if (targetTaken) {
|
|
84
|
+
pushStep({
|
|
85
|
+
kind: "skip",
|
|
86
|
+
label: `skipped link ${rel}`,
|
|
87
|
+
detail: "already present in worktree",
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
93
|
+
fs.symlinkSync(source, target);
|
|
94
|
+
pushStep({ kind: "ok", label: `linked ${rel}`, detail: `→ ${source}` });
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
pushStep({
|
|
98
|
+
kind: "warn",
|
|
99
|
+
label: `failed to link ${rel}`,
|
|
100
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
52
105
|
/**
|
|
53
106
|
* Stashes a `--prompt` value into a temp file so the shell wrapper can hand
|
|
54
107
|
* it back to `worktree work` via `--prompt-file`. Plain stdout markers can't
|
|
@@ -207,6 +260,13 @@ export async function runCreate(branchArg, opts) {
|
|
|
207
260
|
upsertIssue(root, parsed.issueId, base ? { base_branch: base } : {});
|
|
208
261
|
pushStep({ kind: "ok", label: "metadata updated", detail: `issue ${parsed.issueId}` });
|
|
209
262
|
await nextFrame(progress);
|
|
263
|
+
// Link gitignored config (e.g. .env) before init.sh, so the hook can rely
|
|
264
|
+
// on those files being present.
|
|
265
|
+
const linkFiles = readMetadata(root).linkFiles;
|
|
266
|
+
if (linkFiles && linkFiles.length > 0) {
|
|
267
|
+
linkFilesIntoWorktree(root, worktreePath, linkFiles, pushStep);
|
|
268
|
+
await nextFrame(progress);
|
|
269
|
+
}
|
|
210
270
|
const initShPath = getInitScriptPath(root);
|
|
211
271
|
if (pathExists(initShPath)) {
|
|
212
272
|
progress?.onPending?.("Running .mintree/init.sh...");
|
|
@@ -362,6 +422,13 @@ export async function runCreateDetached(opts) {
|
|
|
362
422
|
upsertIssue(root, opts.issueId, { base_branch: currentBranch });
|
|
363
423
|
pushStep({ kind: "ok", label: "metadata updated", detail: `issue ${opts.issueId}` });
|
|
364
424
|
await nextFrame(progress);
|
|
425
|
+
// Link gitignored config (e.g. .env) before init.sh, so the hook can rely
|
|
426
|
+
// on those files being present.
|
|
427
|
+
const linkFiles = readMetadata(root).linkFiles;
|
|
428
|
+
if (linkFiles && linkFiles.length > 0) {
|
|
429
|
+
linkFilesIntoWorktree(root, worktreePath, linkFiles, pushStep);
|
|
430
|
+
await nextFrame(progress);
|
|
431
|
+
}
|
|
365
432
|
const initShPath = getInitScriptPath(root);
|
|
366
433
|
if (pathExists(initShPath)) {
|
|
367
434
|
progress?.onPending?.("Running .mintree/init.sh...");
|
package/package.json
CHANGED