@lumoai/cli 1.17.0 → 1.18.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.
- package/assets/skill/SKILL.md +8 -1
- package/assets/skill/references/sessions.md +15 -0
- package/assets/skill/references/worktree.md +60 -0
- package/dist/cli/src/commands/session-attach.js +76 -37
- package/dist/cli/src/commands/worktree-add.js +108 -0
- package/dist/cli/src/commands/worktree-list.js +73 -0
- package/dist/cli/src/commands/worktree-rm.js +53 -0
- package/dist/cli/src/index.js +34 -2
- package/dist/cli/src/lib/worktree.js +174 -0
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lumo
|
|
3
|
-
description: 'Use the Lumo CLI to load task context, manage session bindings, and run tasks / projects / milestones / sprints / docs / memory from the terminal. Activate when: the user mentions a Lumo task identifier (LUM-42 etc.), asks to load task background/context, wants to bind/check/detach a Claude Code session''s task, is about to start work on a task, or wants to create/update/list/show/comment on tasks, projects, milestones, sprints, documents, artifacts, Figma links, or memory. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "which task", "what task am I on", "work on LUM", "session wrap", "wrap up session", "进度评论", "卡住检测", "create task", "new task", "file a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "update task", "change task status", "rename task", "reassign task", "mark task as done", "lumo next", "next task", "what should I work on", "推荐下一个任务", "list projects", "what projects", "milestone", "里程碑", "list/create/update/delete/show milestone", "set milestone", "attach/unbind milestone", "tasks in milestone", "search milestones", "find milestone", "milestone health", "at-risk", "overdue", "archive/unarchive milestone", "归档里程碑", "milestone summary", "里程碑复盘", "reorder/move milestone", "排序里程碑", "milestone add/remove", "挂任务到里程碑", "auth login", "log in", "logout", "sign out", "switch account", "whoami", "who am I", "current workspace", "登录", "切换账号", "create/update/list/show/delete doc", "write doc", "写文档", "新建文档", "修改文档", "查看文档", "bind/unbind doc", "把文档关联到任务", "doc scope", "personal/workspace doc", "tag", "add/remove tag", "标签", "share/unshare doc", "分享文档", "doc share-list", "viewer/editor/manager", "doc tree", "doc move", "move/reparent doc", "移动文档", "sprint", "冲刺", "迭代", "create/list/show/update/delete sprint", "start/close sprint", "开始/关闭冲刺", "add to sprint", "active sprints", "sprint summary", "冲刺总结", "把任务挂到冲刺", "sprint health", "sprint risk", "is this sprint at risk", "冲刺风险", "冲刺健康度", "sprint blockers", "冲刺阻塞", "lumo update", "upgrade lumo", "升级 lumo", "new lumo version", "lumo setup", "install lumo skill/hooks", "wire up lumo", "set up lumo", "安装 lumo", "配置 lumo", "task artifact", "artifact add/list/show/update/rm", "spec artifact", "record/attach spec", "attach plan", "记录 spec", "查看 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task/project memory", "retrieval", "取全文", "拉全文", "task slack show", "看 thread", "show slack thread", "task web show", "web 正文", "task figma context", "figma metadata", "task comments list", "list comments", "看评论", "task pr show", "查看 PR", "show pr", "PR 详情", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入/同步 google 文档", "mark blocked", "blocked tag", "标记 blocked", "stuck", "repeatedly failing".'
|
|
3
|
+
description: 'Use the Lumo CLI to load task context, manage session bindings, and run tasks / projects / milestones / sprints / docs / memory from the terminal. Activate when: the user mentions a Lumo task identifier (LUM-42 etc.), asks to load task background/context, wants to bind/check/detach a Claude Code session''s task, is about to start work on a task, or wants to create/update/list/show/comment on tasks, projects, milestones, sprints, documents, artifacts, Figma links, or memory. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "which task", "what task am I on", "work on LUM", "session wrap", "wrap up session", "进度评论", "卡住检测", "create task", "new task", "file a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "update task", "change task status", "rename task", "reassign task", "mark task as done", "lumo next", "next task", "what should I work on", "推荐下一个任务", "list projects", "what projects", "milestone", "里程碑", "list/create/update/delete/show milestone", "set milestone", "attach/unbind milestone", "tasks in milestone", "search milestones", "find milestone", "milestone health", "at-risk", "overdue", "archive/unarchive milestone", "归档里程碑", "milestone summary", "里程碑复盘", "reorder/move milestone", "排序里程碑", "milestone add/remove", "挂任务到里程碑", "auth login", "log in", "logout", "sign out", "switch account", "whoami", "who am I", "current workspace", "登录", "切换账号", "create/update/list/show/delete doc", "write doc", "写文档", "新建文档", "修改文档", "查看文档", "bind/unbind doc", "把文档关联到任务", "doc scope", "personal/workspace doc", "tag", "add/remove tag", "标签", "share/unshare doc", "分享文档", "doc share-list", "viewer/editor/manager", "doc tree", "doc move", "move/reparent doc", "移动文档", "sprint", "冲刺", "迭代", "create/list/show/update/delete sprint", "start/close sprint", "开始/关闭冲刺", "add to sprint", "active sprints", "sprint summary", "冲刺总结", "把任务挂到冲刺", "sprint health", "sprint risk", "is this sprint at risk", "冲刺风险", "冲刺健康度", "sprint blockers", "冲刺阻塞", "lumo update", "upgrade lumo", "升级 lumo", "new lumo version", "lumo setup", "install lumo skill/hooks", "wire up lumo", "set up lumo", "安装 lumo", "配置 lumo", "task artifact", "artifact add/list/show/update/rm", "spec artifact", "record/attach spec", "attach plan", "记录 spec", "查看 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task/project memory", "retrieval", "取全文", "拉全文", "task slack show", "看 thread", "show slack thread", "task web show", "web 正文", "task figma context", "figma metadata", "task comments list", "list comments", "看评论", "task pr show", "查看 PR", "show pr", "PR 详情", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入/同步 google 文档", "mark blocked", "blocked tag", "标记 blocked", "stuck", "repeatedly failing", "worktree", "git worktree", "并行 worktree", "scaffold worktree", "新建 worktree", "node_modules 软链", "worktree 隔离", "lumo worktree add/rm/list".'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Prerequisites
|
|
@@ -30,6 +30,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
30
30
|
| `sprint*` | [references/sprints.md](references/sprints.md) |
|
|
31
31
|
| `task/project memory`, `memory promote/rm` | [references/memory.md](references/memory.md) |
|
|
32
32
|
| `session attach/status/detach/wrap`, auto-bind, Layer-2 review | [references/sessions.md](references/sessions.md) |
|
|
33
|
+
| `worktree add/rm/list` (local dev tooling) | [references/worktree.md](references/worktree.md) |
|
|
33
34
|
|
|
34
35
|
## Command catalog
|
|
35
36
|
|
|
@@ -99,6 +100,12 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
99
100
|
- `lumo session wrap [--yes] [--dry-run]` — end-of-session panel: progress comment + memory review + blocked-tag prompt
|
|
100
101
|
- Auto-bind at session start + Layer-2 project-memory review — see the reference
|
|
101
102
|
|
|
103
|
+
**Worktrees (local dev tooling)** — see [worktree.md](references/worktree.md)
|
|
104
|
+
|
|
105
|
+
- `lumo worktree add <LUM-N> [slug]` — scaffold `.worktrees/<LUM-N>` + node_modules symlink off origin/main; run from the main checkout
|
|
106
|
+
- `lumo worktree rm <LUM-N> --yes` — remove a worktree (keeps the branch unless `--delete-branch`)
|
|
107
|
+
- `lumo worktree list` — list `.worktrees/` worktrees (task id, branch, dirty, node_modules link)
|
|
108
|
+
|
|
102
109
|
## Core workflow
|
|
103
110
|
|
|
104
111
|
Typical flow when a user says "help me with LUM-42":
|
|
@@ -49,6 +49,21 @@ What it does:
|
|
|
49
49
|
|
|
50
50
|
**After attaching, always run `lumo task context <identifier>` to load the task background.**
|
|
51
51
|
|
|
52
|
+
#### Overwriting an existing binding (LUM-266)
|
|
53
|
+
|
|
54
|
+
Re-attaching a session that's already bound to a **different** task no longer silently clobbers the binding. The server returns the current binding instead of overwriting, and the CLI decides what to do:
|
|
55
|
+
|
|
56
|
+
- **On a TTY** (a human running it directly): prompts `已绑定 LUM-X,覆盖为 LUM-Y? [y/N]`. Answering `y`/`yes` re-binds to `LUM-Y`; anything else (including bare Enter) cancels and leaves `LUM-X` bound.
|
|
57
|
+
- **Off a TTY** (the usual agent case — `stdin` is not interactive): the command **refuses** and prints `Session already bound to LUM-X … Re-run with --force …` without overwriting. Exit code is `0` (a safe refusal, not an error).
|
|
58
|
+
- **`--force`**: skips the prompt/refusal and overwrites unconditionally. Re-attaching to the **same** task is always a no-op re-bind (never prompts).
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
lumo session attach LUM-42 # already on LUM-7 → prompts (TTY) / refuses (agent)
|
|
62
|
+
lumo session attach LUM-42 --force # overwrite without confirmation
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Agent guidance:** when `session attach` reports the session is already bound to a different task, **ask the user** whether to switch before re-running with `--force`. Don't auto-`--force` — the existing binding may be intentional (e.g. an auto-bind from the branch name). Alternatively run `lumo session detach` first, then a clean `session attach`.
|
|
66
|
+
|
|
52
67
|
### Parallel sessions
|
|
53
68
|
|
|
54
69
|
Each Claude Code session has its own `CLAUDE_CODE_SESSION_ID`. Two terminals running `claude code` and binding to different tasks will not interfere with each other — the bindings are scoped per session row server-side.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Worktrees
|
|
2
|
+
|
|
3
|
+
Local dev tooling that scaffolds parallel git worktrees. Unlike every other
|
|
4
|
+
`lumo` command, these do **not** touch the Lumo server — they shell out to git
|
|
5
|
+
and the local filesystem. **Run `add` and `rm` from the main checkout**, not from
|
|
6
|
+
inside a worktree (the node_modules symlink source and `.worktrees/` both live
|
|
7
|
+
there); `list` is read-only and runnable from anywhere.
|
|
8
|
+
|
|
9
|
+
## `lumo worktree add <LUM-N> [slug]`
|
|
10
|
+
|
|
11
|
+
Creates `.worktrees/<LUM-N>[-slug]` on branch `lumo/<LUM-N>[-slug]`, branched off
|
|
12
|
+
`origin/main` (after a fetch), and symlinks its `node_modules` to the main
|
|
13
|
+
checkout so jest/tsc work immediately. With no slug, the dir and branch use the
|
|
14
|
+
bare `LUM-N` form. Prints next-step guidance plus the two gotchas it can't fix
|
|
15
|
+
for you (shared prisma client, jest cwd).
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
lumo worktree add LUM-267 worktree-scaffold # → .worktrees/LUM-267-worktree-scaffold
|
|
19
|
+
lumo worktree add LUM-267 # → .worktrees/LUM-267
|
|
20
|
+
lumo worktree add LUM-267 --base main # branch off local main, skip fetch
|
|
21
|
+
lumo worktree add LUM-267 --no-fetch # branch off existing origin/main
|
|
22
|
+
lumo worktree add LUM-267 --verify # run npx jest baseline after setup
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Flags: `--base <ref>` (branch off a ref other than origin/main; skips the
|
|
26
|
+
fetch), `--no-fetch` (skip `git fetch origin main`), `--verify` (run `npx jest`
|
|
27
|
+
in the new worktree). Errors if the target dir already exists; reuses the branch
|
|
28
|
+
if it already exists (adds the worktree without `-b`).
|
|
29
|
+
|
|
30
|
+
**The prisma gotcha it warns about:** the generated client lives in the shared
|
|
31
|
+
(symlinked) `node_modules`, so a `prisma generate` in one worktree clobbers the
|
|
32
|
+
client every parallel worktree depends on. Verify with jest (SWC mocks Prisma);
|
|
33
|
+
do `generate + tsc` atomically once at the end.
|
|
34
|
+
|
|
35
|
+
**The jest gotcha it warns about:** always run jest from the worktree root (`cd`
|
|
36
|
+
in first) — `cli/` has no jest config, and running from the main checkout hits
|
|
37
|
+
the `cli/package.json` haste collision and silently runs the wrong tests.
|
|
38
|
+
|
|
39
|
+
## `lumo worktree rm <LUM-N>`
|
|
40
|
+
|
|
41
|
+
Removes the worktree for a task. Requires `--yes`. Refuses a dirty worktree
|
|
42
|
+
unless `--force`. Keeps the branch by default (it may hold unpushed work / an
|
|
43
|
+
open PR); `--delete-branch` removes it with `git branch -d` (which itself
|
|
44
|
+
refuses an unmerged branch).
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
lumo worktree rm LUM-267 --yes
|
|
48
|
+
lumo worktree rm LUM-267 --yes --force # discard uncommitted changes
|
|
49
|
+
lumo worktree rm LUM-267 --yes --delete-branch # also delete lumo/LUM-267…
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## `lumo worktree list`
|
|
53
|
+
|
|
54
|
+
Lists worktrees under `.worktrees/` with task id, branch, dirty state, and
|
|
55
|
+
whether `node_modules` is symlinked (a `MISSING` link is the first thing to
|
|
56
|
+
check when jest/tsc misbehaves). Read-only; runnable from anywhere in the repo.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
lumo worktree list
|
|
60
|
+
```
|
|
@@ -4,6 +4,7 @@ exports.sessionAttach = sessionAttach;
|
|
|
4
4
|
const config_1 = require("../lib/config");
|
|
5
5
|
const api_1 = require("../lib/api");
|
|
6
6
|
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
+
const line_prompt_1 = require("../lib/line-prompt");
|
|
7
8
|
/**
|
|
8
9
|
* `lumo session attach <identifier>` — bind the currently-running
|
|
9
10
|
* Claude Code session to a task.
|
|
@@ -14,8 +15,15 @@ const sanitize_1 = require("../lib/sanitize");
|
|
|
14
15
|
*
|
|
15
16
|
* The binding lives entirely on the server (`Session.taskId`); subsequent
|
|
16
17
|
* hooks read it back via the session row. The CLI keeps no local sentinel.
|
|
18
|
+
*
|
|
19
|
+
* Re-binding a session that's already attached to a *different* task no
|
|
20
|
+
* longer silently clobbers `Session.taskId` (LUM-266): the server returns
|
|
21
|
+
* the current binding and we confirm before overwriting —
|
|
22
|
+
* - `--force` skips the prompt and overwrites directly;
|
|
23
|
+
* - on a TTY we ask `已绑定 LUM-X,覆盖为 LUM-Y? [y/N]`;
|
|
24
|
+
* - off a TTY (the usual agent case) we refuse and point at `--force`.
|
|
17
25
|
*/
|
|
18
|
-
async function sessionAttach(identifier) {
|
|
26
|
+
async function sessionAttach(identifier, options = {}) {
|
|
19
27
|
if (!identifier) {
|
|
20
28
|
console.error('Error: missing <identifier>. Usage: lumo session attach <LUM-42>');
|
|
21
29
|
return 1;
|
|
@@ -33,48 +41,79 @@ async function sessionAttach(identifier) {
|
|
|
33
41
|
}
|
|
34
42
|
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
35
43
|
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}/bind-task`;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
res = await fetch(url, {
|
|
39
|
-
method: 'POST',
|
|
40
|
-
headers: {
|
|
41
|
-
'Content-Type': 'application/json',
|
|
42
|
-
Authorization: `Bearer ${creds.token}`,
|
|
43
|
-
},
|
|
44
|
-
body: JSON.stringify({ taskIdentifier: identifier }),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
catch (err) {
|
|
48
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
49
|
-
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
50
|
-
return 1;
|
|
51
|
-
}
|
|
52
|
-
if (res.status === 401) {
|
|
53
|
-
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
54
|
-
return 1;
|
|
55
|
-
}
|
|
56
|
-
if (res.status === 404) {
|
|
57
|
-
let message = 'Not found';
|
|
44
|
+
const bind = async (force) => {
|
|
45
|
+
let res;
|
|
58
46
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
res = await fetch(url, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
Authorization: `Bearer ${creds.token}`,
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({ taskIdentifier: identifier, force }),
|
|
54
|
+
});
|
|
62
55
|
}
|
|
63
|
-
catch {
|
|
64
|
-
|
|
56
|
+
catch (err) {
|
|
57
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
58
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
59
|
+
return { ok: false, code: 1 };
|
|
65
60
|
}
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
if (res.status === 401) {
|
|
62
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
63
|
+
return { ok: false, code: 1 };
|
|
64
|
+
}
|
|
65
|
+
if (res.status === 404) {
|
|
66
|
+
let message = 'Not found';
|
|
67
|
+
try {
|
|
68
|
+
const data = (await res.json());
|
|
69
|
+
if (data.error)
|
|
70
|
+
message = data.error;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// fall through
|
|
74
|
+
}
|
|
75
|
+
console.error(`Error: ${(0, sanitize_1.sanitizeField)(message)}`);
|
|
76
|
+
return { ok: false, code: 1 };
|
|
77
|
+
}
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
console.error(`Error: bind-task failed (HTTP ${res.status})`);
|
|
80
|
+
return { ok: false, code: 1 };
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, body: (await res.json()) };
|
|
83
|
+
};
|
|
84
|
+
// First attempt. `--force` overwrites unconditionally; otherwise the server
|
|
85
|
+
// may answer `already-bound` so we can confirm before clobbering.
|
|
86
|
+
const first = await bind(options.force === true);
|
|
87
|
+
if (!first.ok)
|
|
88
|
+
return first.code;
|
|
89
|
+
let body = first.body;
|
|
90
|
+
if (body.status === 'already-bound') {
|
|
91
|
+
// Reached only when not forced (force=true would have overwritten).
|
|
92
|
+
if (!process.stdin.isTTY) {
|
|
93
|
+
console.error(`Session already bound to ${body.currentTaskIdentifier} "${(0, sanitize_1.sanitizeField)(body.currentTaskTitle)}". ` +
|
|
94
|
+
`Not overwriting. Re-run with --force to switch to ${identifier} ` +
|
|
95
|
+
'(or run `lumo session detach` first).');
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
const answer = await (0, line_prompt_1.promptLine)(`已绑定 ${body.currentTaskIdentifier},覆盖为 ${identifier}? [y/N] `);
|
|
99
|
+
if (!/^y(es)?$/i.test(answer)) {
|
|
100
|
+
console.log(`已取消,仍绑定 ${body.currentTaskIdentifier}。`);
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
const second = await bind(true);
|
|
104
|
+
if (!second.ok)
|
|
105
|
+
return second.code;
|
|
106
|
+
body = second.body;
|
|
68
107
|
}
|
|
69
|
-
if (
|
|
70
|
-
|
|
108
|
+
if (body.status === 'already-bound') {
|
|
109
|
+
// Defensive: a forced bind should never report already-bound.
|
|
110
|
+
console.error(`Error: bind did not take effect (still bound to ${body.currentTaskIdentifier}).`);
|
|
71
111
|
return 1;
|
|
72
112
|
}
|
|
73
|
-
|
|
74
|
-
console.log(`
|
|
75
|
-
|
|
76
|
-
if (result.memorySection !== '') {
|
|
113
|
+
console.log(`Attached session ${sessionId} to ${body.taskIdentifier} "${(0, sanitize_1.sanitizeField)(body.taskTitle)}"`);
|
|
114
|
+
console.log(`Re-tagged ${body.retaggedEventCount} previously-untagged event${body.retaggedEventCount === 1 ? '' : 's'} in this session.`);
|
|
115
|
+
if (body.memorySection !== '') {
|
|
77
116
|
console.log('');
|
|
78
|
-
console.log((0, sanitize_1.sanitizeField)(
|
|
117
|
+
console.log((0, sanitize_1.sanitizeField)(body.memorySection));
|
|
79
118
|
}
|
|
80
119
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.worktreeAdd = worktreeAdd;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const worktree_1 = require("../lib/worktree");
|
|
41
|
+
function printGuidance(dir, branch) {
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(`✓ Worktree ready: ${dir}`);
|
|
44
|
+
console.log(` branch: ${branch}`);
|
|
45
|
+
console.log(` node_modules: symlinked to main checkout`);
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log('Next:');
|
|
48
|
+
console.log(` cd ${dir}`);
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log('Gotchas baked into this worktree:');
|
|
51
|
+
console.log(' • prisma: the generated client is SHARED via the node_modules symlink.');
|
|
52
|
+
console.log(" Don't run `prisma generate` mid-work across parallel worktrees;");
|
|
53
|
+
console.log(' verify with jest, and do generate+tsc atomically once at the end.');
|
|
54
|
+
console.log(' • jest: always run it from INSIDE this worktree (cd first), or the');
|
|
55
|
+
console.log(' cli/package.json haste collision silently runs the wrong tests.');
|
|
56
|
+
}
|
|
57
|
+
async function worktreeAdd(rawId, slug, opts) {
|
|
58
|
+
const taskId = (0, worktree_1.normalizeTaskId)(rawId);
|
|
59
|
+
if (!taskId) {
|
|
60
|
+
console.error(`Error: "${rawId}" is not a task id (expected LUM-N or N)`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
if (!(0, worktree_1.isMainCheckout)()) {
|
|
64
|
+
console.error('Error: run `lumo worktree add` from the main checkout, not inside a worktree');
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
const root = (0, worktree_1.getRepoRoot)();
|
|
68
|
+
const branch = (0, worktree_1.deriveBranchName)(taskId, slug);
|
|
69
|
+
const dir = path.join(root, '.worktrees', (0, worktree_1.deriveWorktreeDirName)(taskId, slug));
|
|
70
|
+
if (fs.existsSync(dir)) {
|
|
71
|
+
console.error(`Error: ${dir} already exists — remove it first or pick a different slug`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
75
|
+
const base = opts.base ?? 'origin/main';
|
|
76
|
+
if (!opts.noFetch && !opts.base) {
|
|
77
|
+
console.log('Fetching origin/main …');
|
|
78
|
+
if ((0, worktree_1.runGit)(['fetch', 'origin', 'main'], root).status !== 0) {
|
|
79
|
+
console.error(`Warning: git fetch failed; branching off the existing local ${base} (may be stale)`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const branchExists = (0, worktree_1.runGit)(['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], root)
|
|
83
|
+
.status === 0;
|
|
84
|
+
const addArgs = branchExists
|
|
85
|
+
? ['worktree', 'add', dir, branch]
|
|
86
|
+
: ['worktree', 'add', dir, '-b', branch, base];
|
|
87
|
+
const added = (0, worktree_1.runGit)(addArgs, root);
|
|
88
|
+
if (added.status !== 0) {
|
|
89
|
+
console.error(`Error: ${(added.stderr || '').trim() || 'git worktree add failed'}`);
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
const srcModules = path.join(root, 'node_modules');
|
|
93
|
+
if (fs.existsSync(srcModules)) {
|
|
94
|
+
fs.symlinkSync(srcModules, path.join(dir, 'node_modules'));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log('Warning: main checkout has no node_modules; run `npm install` there, then symlink manually');
|
|
98
|
+
}
|
|
99
|
+
printGuidance(dir, branch);
|
|
100
|
+
if (opts.verify) {
|
|
101
|
+
console.log('Running baseline jest …');
|
|
102
|
+
const j = (0, child_process_1.spawnSync)('npx', ['jest'], { cwd: dir, stdio: 'inherit' });
|
|
103
|
+
if (j.status !== 0) {
|
|
104
|
+
console.error('Warning: baseline jest did not pass cleanly');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.worktreeList = worktreeList;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const worktree_1 = require("../lib/worktree");
|
|
40
|
+
const git_task_1 = require("../lib/git-task");
|
|
41
|
+
function nodeModulesLinked(dir) {
|
|
42
|
+
try {
|
|
43
|
+
return fs.lstatSync(path.join(dir, 'node_modules')).isSymbolicLink();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Pull a LUM-N from a worktree dir basename or its branch, else '—'. */
|
|
50
|
+
function taskIdOf(dirBase, branch) {
|
|
51
|
+
return ((0, git_task_1.matchTaskIdentifier)(dirBase) ??
|
|
52
|
+
(branch ? (0, git_task_1.matchTaskIdentifier)(branch) : null) ??
|
|
53
|
+
'—');
|
|
54
|
+
}
|
|
55
|
+
async function worktreeList() {
|
|
56
|
+
const mainRoot = path.dirname((0, worktree_1.getGitCommonDir)());
|
|
57
|
+
const worktreesDir = path.join(mainRoot, '.worktrees');
|
|
58
|
+
const entries = (0, worktree_1.parseWorktreePorcelain)((0, worktree_1.gitOutput)(['worktree', 'list', '--porcelain'], mainRoot)).filter(e => e.path.startsWith(worktreesDir + path.sep));
|
|
59
|
+
if (entries.length === 0) {
|
|
60
|
+
console.log('(no worktrees under .worktrees/)');
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
for (const e of entries) {
|
|
64
|
+
const base = path.basename(e.path);
|
|
65
|
+
const id = taskIdOf(base, e.branch);
|
|
66
|
+
const dirty = (0, worktree_1.gitOutput)(['status', '--porcelain'], e.path).length > 0
|
|
67
|
+
? 'dirty'
|
|
68
|
+
: 'clean';
|
|
69
|
+
const nm = nodeModulesLinked(e.path) ? 'linked' : 'MISSING';
|
|
70
|
+
console.log(`${id}\t${e.branch ?? '(detached)'}\t${dirty}\tnode_modules:${nm}\t${e.path}`);
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.worktreeRm = worktreeRm;
|
|
4
|
+
const worktree_1 = require("../lib/worktree");
|
|
5
|
+
async function worktreeRm(rawId, opts) {
|
|
6
|
+
const taskId = (0, worktree_1.normalizeTaskId)(rawId);
|
|
7
|
+
if (!taskId) {
|
|
8
|
+
console.error(`Error: "${rawId}" is not a task id (expected LUM-N or N)`);
|
|
9
|
+
return 1;
|
|
10
|
+
}
|
|
11
|
+
if (!(0, worktree_1.isMainCheckout)()) {
|
|
12
|
+
console.error('Error: run `lumo worktree rm` from the main checkout, not inside a worktree');
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
const root = (0, worktree_1.getRepoRoot)();
|
|
16
|
+
const entries = (0, worktree_1.parseWorktreePorcelain)((0, worktree_1.gitOutput)(['worktree', 'list', '--porcelain'], root));
|
|
17
|
+
const target = (0, worktree_1.findWorktreeByTaskId)(entries, taskId);
|
|
18
|
+
if (!target) {
|
|
19
|
+
console.error(`Error: no worktree found for ${taskId}`);
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
if (!opts.yes) {
|
|
23
|
+
console.error(`Error: refusing to remove ${target.path} without --yes (destructive)`);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
const dirty = (0, worktree_1.gitOutput)(['status', '--porcelain'], target.path).length > 0;
|
|
27
|
+
if (dirty && !opts.force) {
|
|
28
|
+
console.error(`Error: ${target.path} has uncommitted changes — pass --force to remove anyway`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
const removeArgs = dirty && opts.force
|
|
32
|
+
? ['worktree', 'remove', '--force', target.path]
|
|
33
|
+
: ['worktree', 'remove', target.path];
|
|
34
|
+
const removed = (0, worktree_1.runGit)(removeArgs, root);
|
|
35
|
+
if (removed.status !== 0) {
|
|
36
|
+
console.error(`Error: ${(removed.stderr || '').trim() || 'git worktree remove failed'}`);
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
console.log(`✓ Removed worktree ${target.path}`);
|
|
40
|
+
if (opts.deleteBranch && target.branch) {
|
|
41
|
+
const del = (0, worktree_1.runGit)(['branch', '-d', target.branch], root);
|
|
42
|
+
if (del.status !== 0) {
|
|
43
|
+
console.error(`Warning: could not delete branch ${target.branch}: ${(del.stderr || '').trim()}`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log(`✓ Deleted branch ${target.branch}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else if (target.branch) {
|
|
50
|
+
console.log(` branch ${target.branch} kept (use --delete-branch to remove)`);
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -111,6 +111,9 @@ const doc_share_list_1 = require("./commands/doc-share-list");
|
|
|
111
111
|
const doc_move_1 = require("./commands/doc-move");
|
|
112
112
|
const update_1 = require("./commands/update");
|
|
113
113
|
const setup_1 = require("./commands/setup");
|
|
114
|
+
const worktree_add_1 = require("./commands/worktree-add");
|
|
115
|
+
const worktree_rm_1 = require("./commands/worktree-rm");
|
|
116
|
+
const worktree_list_1 = require("./commands/worktree-list");
|
|
114
117
|
const update_check_1 = require("./lib/update-check");
|
|
115
118
|
const sanitize_1 = require("./lib/sanitize");
|
|
116
119
|
// Resolve package.json relative to __dirname so this works regardless of how
|
|
@@ -199,8 +202,9 @@ const session = program
|
|
|
199
202
|
.description('Manage per-terminal coding-session context');
|
|
200
203
|
session
|
|
201
204
|
.command('attach <identifier>')
|
|
202
|
-
.description('Attach the currently-running Claude Code session (CLAUDE_CODE_SESSION_ID) to a task. Sets Session.taskId server-side and re-tags untagged hook events.')
|
|
203
|
-
.
|
|
205
|
+
.description('Attach the currently-running Claude Code session (CLAUDE_CODE_SESSION_ID) to a task. Sets Session.taskId server-side and re-tags untagged hook events. If the session is already bound to a different task, confirms before overwriting (use --force to skip).')
|
|
206
|
+
.option('--force', 'Overwrite an existing binding to a different task without confirmation (skips the [y/N] prompt).')
|
|
207
|
+
.action(wrap((identifier, options) => (0, session_attach_1.sessionAttach)(identifier, options)));
|
|
204
208
|
session
|
|
205
209
|
.command('status')
|
|
206
210
|
.description('Show the task currently bound to this Claude Code session (or "no task" if none).')
|
|
@@ -654,6 +658,34 @@ task
|
|
|
654
658
|
.option('--remove-tag <name>', 'Remove tag by name (repeatable). Unknown names are find-or-create, so prefer --remove-tag-id to avoid orphan rows.', collect, [])
|
|
655
659
|
.option('--remove-tag-id <cuid>', 'Remove tag by id (repeatable). Unknown ids are a no-op.', collect, [])
|
|
656
660
|
.action(wrap((identifier, options) => (0, task_update_1.taskUpdate)(identifier, options)));
|
|
661
|
+
const worktree = program
|
|
662
|
+
.command('worktree')
|
|
663
|
+
.description('Scaffold, remove, and list parallel git worktrees under .worktrees/ (node_modules symlink + prisma/jest gotcha guidance). Local dev tooling; run from the main checkout.');
|
|
664
|
+
worktree
|
|
665
|
+
.command('add <identifier> [slug]')
|
|
666
|
+
.description('Create a worktree at .worktrees/<LUM-N>[-slug] on branch lumo/<LUM-N>[-slug] off origin/main, symlink node_modules to the main checkout, and print prisma/jest guidance. Run from the main checkout.')
|
|
667
|
+
.option('--base <ref>', 'Branch off this ref instead of origin/main (skips fetch)')
|
|
668
|
+
.option('--no-fetch', 'Do not `git fetch origin main` before branching')
|
|
669
|
+
.option('--verify', 'Run `npx jest` in the new worktree to confirm a clean baseline')
|
|
670
|
+
.action(wrap((identifier, slug, options) => {
|
|
671
|
+
const o = options;
|
|
672
|
+
return (0, worktree_add_1.worktreeAdd)(identifier, slug, {
|
|
673
|
+
base: o.base,
|
|
674
|
+
noFetch: o.fetch === false,
|
|
675
|
+
verify: o.verify,
|
|
676
|
+
});
|
|
677
|
+
}));
|
|
678
|
+
worktree
|
|
679
|
+
.command('rm <identifier>')
|
|
680
|
+
.description('Remove the worktree for a task (git worktree remove). Requires --yes. Refuses a dirty worktree unless --force. Keeps the branch unless --delete-branch.')
|
|
681
|
+
.option('--yes', 'Confirm removal (required, no interactive prompt)')
|
|
682
|
+
.option('--force', 'Remove even with uncommitted changes')
|
|
683
|
+
.option('--delete-branch', 'Also delete the branch (git branch -d; refuses if unmerged)')
|
|
684
|
+
.action(wrap((identifier, options) => (0, worktree_rm_1.worktreeRm)(identifier, options)));
|
|
685
|
+
worktree
|
|
686
|
+
.command('list')
|
|
687
|
+
.description('List worktrees under .worktrees/: task id, branch, dirty state, and whether node_modules is symlinked.')
|
|
688
|
+
.action(wrap(() => (0, worktree_list_1.worktreeList)()));
|
|
657
689
|
// Claude Code invokes `lumo hook <type>` per tool call, so these handlers
|
|
658
690
|
// must never crash the caller. Two invariants keep us safe:
|
|
659
691
|
// 1. `hookCommand` is stderr-silent and never throws (it catches internally
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.normalizeTaskId = normalizeTaskId;
|
|
37
|
+
exports.slugify = slugify;
|
|
38
|
+
exports.deriveBranchName = deriveBranchName;
|
|
39
|
+
exports.deriveWorktreeDirName = deriveWorktreeDirName;
|
|
40
|
+
exports.parseWorktreePorcelain = parseWorktreePorcelain;
|
|
41
|
+
exports.findWorktreeByTaskId = findWorktreeByTaskId;
|
|
42
|
+
exports.runGit = runGit;
|
|
43
|
+
exports.gitOutput = gitOutput;
|
|
44
|
+
exports.getGitDir = getGitDir;
|
|
45
|
+
exports.getGitCommonDir = getGitCommonDir;
|
|
46
|
+
exports.isMainCheckout = isMainCheckout;
|
|
47
|
+
exports.getRepoRoot = getRepoRoot;
|
|
48
|
+
/**
|
|
49
|
+
* Normalize a user-supplied task reference to canonical `LUM-<n>` form.
|
|
50
|
+
* Accepts `LUM-267`, `lum-267`, and bare `267`. Returns null for anything
|
|
51
|
+
* that is not a recognizable task id.
|
|
52
|
+
*/
|
|
53
|
+
function normalizeTaskId(input) {
|
|
54
|
+
const trimmed = input.trim();
|
|
55
|
+
const prefixed = trimmed.match(/^lum-?(\d+)$/i);
|
|
56
|
+
if (prefixed)
|
|
57
|
+
return `LUM-${prefixed[1]}`;
|
|
58
|
+
const bare = trimmed.match(/^(\d+)$/);
|
|
59
|
+
if (bare)
|
|
60
|
+
return `LUM-${bare[1]}`;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Turn an arbitrary human slug into a branch/dir-safe segment: lowercase,
|
|
65
|
+
* non-alphanumeric runs collapse to a single dash, leading/trailing dashes
|
|
66
|
+
* stripped.
|
|
67
|
+
*/
|
|
68
|
+
function slugify(s) {
|
|
69
|
+
return s
|
|
70
|
+
.trim()
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
73
|
+
.replace(/^-+|-+$/g, '');
|
|
74
|
+
}
|
|
75
|
+
/** `lumo/LUM-267` or `lumo/LUM-267-<slug>`. */
|
|
76
|
+
function deriveBranchName(taskId, slug) {
|
|
77
|
+
const s = slug ? slugify(slug) : '';
|
|
78
|
+
return s ? `lumo/${taskId}-${s}` : `lumo/${taskId}`;
|
|
79
|
+
}
|
|
80
|
+
/** `.worktrees/` dir basename: `LUM-267` or `LUM-267-<slug>`. */
|
|
81
|
+
function deriveWorktreeDirName(taskId, slug) {
|
|
82
|
+
const s = slug ? slugify(slug) : '';
|
|
83
|
+
return s ? `${taskId}-${s}` : taskId;
|
|
84
|
+
}
|
|
85
|
+
const path = __importStar(require("path"));
|
|
86
|
+
/**
|
|
87
|
+
* Parse `git worktree list --porcelain` output into structured entries.
|
|
88
|
+
* Records are separated by blank lines; a detached worktree has a `detached`
|
|
89
|
+
* line instead of `branch`. Tolerates a final record with no trailing blank.
|
|
90
|
+
*/
|
|
91
|
+
function parseWorktreePorcelain(output) {
|
|
92
|
+
const entries = [];
|
|
93
|
+
let cur = null;
|
|
94
|
+
const flush = () => {
|
|
95
|
+
if (cur)
|
|
96
|
+
entries.push(cur);
|
|
97
|
+
cur = null;
|
|
98
|
+
};
|
|
99
|
+
for (const line of output.split('\n')) {
|
|
100
|
+
if (line.startsWith('worktree ')) {
|
|
101
|
+
flush();
|
|
102
|
+
cur = { path: line.slice('worktree '.length), head: '', branch: null };
|
|
103
|
+
}
|
|
104
|
+
else if (!cur) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
else if (line.startsWith('HEAD ')) {
|
|
108
|
+
cur.head = line.slice('HEAD '.length);
|
|
109
|
+
}
|
|
110
|
+
else if (line.startsWith('branch ')) {
|
|
111
|
+
cur.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '');
|
|
112
|
+
}
|
|
113
|
+
else if (line === '') {
|
|
114
|
+
flush();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
flush();
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Find the worktree belonging to a task id. Matches either the branch
|
|
122
|
+
* (`lumo/LUM-267` exactly or `lumo/LUM-267-*`) or the directory basename
|
|
123
|
+
* (`LUM-267` exactly or `LUM-267-*`).
|
|
124
|
+
*/
|
|
125
|
+
function findWorktreeByTaskId(entries, taskId) {
|
|
126
|
+
return (entries.find(e => {
|
|
127
|
+
const base = path.basename(e.path);
|
|
128
|
+
const branchMatch = e.branch === `lumo/${taskId}` ||
|
|
129
|
+
(e.branch?.startsWith(`lumo/${taskId}-`) ?? false);
|
|
130
|
+
const dirMatch = base === taskId || base.startsWith(`${taskId}-`);
|
|
131
|
+
return branchMatch || dirMatch;
|
|
132
|
+
}) ?? null);
|
|
133
|
+
}
|
|
134
|
+
const child_process_1 = require("child_process");
|
|
135
|
+
/** Low-level git runner — returns the raw spawn result, never throws on exit. */
|
|
136
|
+
function runGit(args, cwd) {
|
|
137
|
+
// 120s is a safety net against a genuine hang (e.g. an index.lock deadlock),
|
|
138
|
+
// NOT a tight bound: this wrapper runs legitimately-slow ops like `git fetch`
|
|
139
|
+
// and `git worktree add`, which must not be killed prematurely. On timeout
|
|
140
|
+
// spawnSync populates `r.error`, which gitOutput already rethrows.
|
|
141
|
+
return (0, child_process_1.spawnSync)('git', args, { cwd, encoding: 'utf8', timeout: 120_000 });
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Run git and return trimmed stdout; THROWS with stderr on non-zero exit or
|
|
145
|
+
* spawn error. NOTE: unlike the same-named `gitOutput` in `cli/src/lib/git-task.ts`
|
|
146
|
+
* (which swallows failures and returns `''`), this one surfaces failures by
|
|
147
|
+
* throwing — callers must handle/expect the throw.
|
|
148
|
+
*/
|
|
149
|
+
function gitOutput(args, cwd) {
|
|
150
|
+
const r = runGit(args, cwd);
|
|
151
|
+
if (r.error)
|
|
152
|
+
throw r.error;
|
|
153
|
+
if (r.status !== 0) {
|
|
154
|
+
throw new Error((r.stderr || '').trim() || `git ${args.join(' ')} failed`);
|
|
155
|
+
}
|
|
156
|
+
return (r.stdout ?? '').trim();
|
|
157
|
+
}
|
|
158
|
+
/** Absolute path to the worktree-local `.git` (file or dir). */
|
|
159
|
+
function getGitDir(cwd) {
|
|
160
|
+
return gitOutput(['rev-parse', '--absolute-git-dir'], cwd);
|
|
161
|
+
}
|
|
162
|
+
/** Absolute path to the shared `.git` (same as git-dir in the main checkout). */
|
|
163
|
+
function getGitCommonDir(cwd) {
|
|
164
|
+
const common = gitOutput(['rev-parse', '--git-common-dir'], cwd);
|
|
165
|
+
return path.resolve(cwd ?? process.cwd(), common);
|
|
166
|
+
}
|
|
167
|
+
/** True only in the main checkout (git-dir === common-dir). */
|
|
168
|
+
function isMainCheckout(cwd) {
|
|
169
|
+
return getGitDir(cwd) === getGitCommonDir(cwd);
|
|
170
|
+
}
|
|
171
|
+
/** Absolute repo root of the current working tree. */
|
|
172
|
+
function getRepoRoot(cwd) {
|
|
173
|
+
return gitOutput(['rev-parse', '--show-toplevel'], cwd);
|
|
174
|
+
}
|