@lumoai/cli 1.15.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/README.md +13 -13
- package/assets/skill/SKILL.md +118 -0
- package/assets/skill/references/artifacts-figma.md +124 -0
- package/assets/skill/references/docs.md +306 -0
- package/assets/skill/references/memory.md +69 -0
- package/assets/skill/references/milestones.md +244 -0
- package/assets/skill/references/onboarding.md +102 -0
- package/assets/skill/references/sessions.md +157 -0
- package/assets/skill/references/sprints.md +157 -0
- package/assets/skill/references/task-context.md +109 -0
- package/assets/skill/references/tasks.md +205 -0
- package/assets/skill/references/worktree.md +60 -0
- package/dist/cli/src/commands/milestone-show.js +23 -0
- package/dist/cli/src/commands/session-attach.js +76 -37
- package/dist/cli/src/commands/setup.js +50 -22
- package/dist/cli/src/commands/sprint-show.js +32 -3
- 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 +35 -3
- package/dist/cli/src/lib/worktree.js +174 -0
- package/package.json +1 -1
- package/assets/skill.md +0 -1445
|
@@ -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
|
}
|
|
@@ -103,33 +103,57 @@ async function resolveScope(options) {
|
|
|
103
103
|
process.stderr.write(`Unrecognized choice "${answer}". Aborting.\n`);
|
|
104
104
|
return null;
|
|
105
105
|
}
|
|
106
|
+
// Recursively list files under `dir` as paths relative to `base`.
|
|
107
|
+
function listSkillFiles(dir, base = dir) {
|
|
108
|
+
const out = [];
|
|
109
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
110
|
+
const full = path.join(dir, entry.name);
|
|
111
|
+
if (entry.isDirectory())
|
|
112
|
+
out.push(...listSkillFiles(full, base));
|
|
113
|
+
else if (entry.isFile())
|
|
114
|
+
out.push(path.relative(base, full));
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
function filesEqual(src, dst) {
|
|
119
|
+
if (!fs.existsSync(dst))
|
|
120
|
+
return false;
|
|
121
|
+
if (fs.statSync(src).size !== fs.statSync(dst).size)
|
|
122
|
+
return false;
|
|
123
|
+
return fs.readFileSync(src).equals(fs.readFileSync(dst));
|
|
124
|
+
}
|
|
106
125
|
function installSkill(claudeDir, force) {
|
|
107
126
|
const skillDir = path.join(claudeDir, 'skills', 'lumo');
|
|
108
|
-
const
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
127
|
+
const skillDstMain = path.join(skillDir, 'SKILL.md');
|
|
128
|
+
// The skill is a directory (SKILL.md + references/*.md) bundled in
|
|
129
|
+
// cli/assets/skill/, shipped via package.json "files".
|
|
130
|
+
// From dist/cli/src/commands/setup.js → ../../../../assets/skill
|
|
131
|
+
const skillSrcDir = path.resolve(__dirname, '../../../..', 'assets', 'skill');
|
|
132
|
+
if (!fs.existsSync(path.join(skillSrcDir, 'SKILL.md'))) {
|
|
133
|
+
throw new Error(`Bundled skill asset missing at ${skillSrcDir} — reinstall @lumoai/cli.`);
|
|
134
|
+
}
|
|
135
|
+
const relFiles = listSkillFiles(skillSrcDir);
|
|
136
|
+
// Non-force, install already present: only overwrite when something differs,
|
|
137
|
+
// and then only with --force.
|
|
138
|
+
if (fs.existsSync(skillDstMain) && !force) {
|
|
139
|
+
const allMatch = relFiles.every(rel => filesEqual(path.join(skillSrcDir, rel), path.join(skillDir, rel)));
|
|
140
|
+
if (allMatch) {
|
|
141
|
+
process.stdout.write(`✓ skill already up to date: ${skillDir}\n`);
|
|
142
|
+
return;
|
|
126
143
|
}
|
|
127
|
-
process.stdout.write(`⚠ ${
|
|
144
|
+
process.stdout.write(`⚠ ${skillDir} exists and differs from the bundled version.\n` +
|
|
128
145
|
` Re-run with --force to overwrite.\n`);
|
|
129
146
|
return;
|
|
130
147
|
}
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
// Fresh install or --force: replace the managed references/ dir wholesale so
|
|
149
|
+
// stale reference files don't linger, then copy every bundled file.
|
|
150
|
+
fs.rmSync(path.join(skillDir, 'references'), { recursive: true, force: true });
|
|
151
|
+
for (const rel of relFiles) {
|
|
152
|
+
const dst = path.join(skillDir, rel);
|
|
153
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
154
|
+
fs.copyFileSync(path.join(skillSrcDir, rel), dst);
|
|
155
|
+
}
|
|
156
|
+
process.stdout.write(`✓ wrote skill (${relFiles.length} files): ${skillDir}\n`);
|
|
133
157
|
}
|
|
134
158
|
function mergeSettings(claudeDir, agentToken) {
|
|
135
159
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
@@ -172,7 +196,11 @@ function installGitHook(projectRoot) {
|
|
|
172
196
|
printGitHookManualInstructions(coreHooksPath);
|
|
173
197
|
return;
|
|
174
198
|
}
|
|
175
|
-
const hooksDirRaw = gitCapture(projectRoot, [
|
|
199
|
+
const hooksDirRaw = gitCapture(projectRoot, [
|
|
200
|
+
'rev-parse',
|
|
201
|
+
'--git-path',
|
|
202
|
+
'hooks',
|
|
203
|
+
]);
|
|
176
204
|
if (!hooksDirRaw) {
|
|
177
205
|
process.stdout.write('⚠ could not resolve git hooks dir — skipped prepare-commit-msg hook.\n');
|
|
178
206
|
return;
|
|
@@ -10,7 +10,15 @@ const sanitize_1 = require("../lib/sanitize");
|
|
|
10
10
|
function fmtDate(iso) {
|
|
11
11
|
return iso ? iso.slice(0, 10) : '-';
|
|
12
12
|
}
|
|
13
|
-
function
|
|
13
|
+
function blockerLine(label, items) {
|
|
14
|
+
if (items.length === 0)
|
|
15
|
+
return null;
|
|
16
|
+
const rendered = items
|
|
17
|
+
.map(i => `${i.identifier} ${(0, sanitize_1.sanitizeField)(i.title)}`)
|
|
18
|
+
.join(', ');
|
|
19
|
+
return ` ${label.padEnd(11)}${rendered}`;
|
|
20
|
+
}
|
|
21
|
+
function formatSprintShow(s, progress, tasks, risk, topBlockers) {
|
|
14
22
|
const lines = [
|
|
15
23
|
`Sprint: #${s.number} ${(0, sanitize_1.sanitizeField)(s.name)}`,
|
|
16
24
|
`Status: ${s.status}`,
|
|
@@ -22,6 +30,27 @@ function formatSprintShow(s, progress, tasks) {
|
|
|
22
30
|
``,
|
|
23
31
|
`Progress: ${progress.done} / ${progress.total}`,
|
|
24
32
|
];
|
|
33
|
+
if (risk) {
|
|
34
|
+
lines.push(`Health: ${risk.level.toUpperCase()}`);
|
|
35
|
+
for (const r of risk.reasons) {
|
|
36
|
+
lines.push(` - ${(0, sanitize_1.sanitizeField)(r.detail)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (topBlockers) {
|
|
40
|
+
const blockerLines = [
|
|
41
|
+
blockerLine('Overdue:', topBlockers.overdueTaskIds),
|
|
42
|
+
blockerLine('Stalled:', topBlockers.stalledTaskIds),
|
|
43
|
+
blockerLine('Agent fail:', topBlockers.failingAgentTaskIds),
|
|
44
|
+
topBlockers.staleOpenPrNumbers.length > 0
|
|
45
|
+
? ` ${'Stale PRs:'.padEnd(11)}${topBlockers.staleOpenPrNumbers
|
|
46
|
+
.map(n => `#${n}`)
|
|
47
|
+
.join(', ')}`
|
|
48
|
+
: null,
|
|
49
|
+
].filter((l) => l !== null);
|
|
50
|
+
if (blockerLines.length > 0) {
|
|
51
|
+
lines.push('Blockers:', ...blockerLines);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
25
54
|
if (tasks.length > 0) {
|
|
26
55
|
lines.push('', (0, format_1.formatTaskListTable)(tasks));
|
|
27
56
|
}
|
|
@@ -75,7 +104,7 @@ async function sprintShow(identifier, opts) {
|
|
|
75
104
|
console.error(`Error: sprint tasks failed (HTTP ${tasksRes.status})`);
|
|
76
105
|
return 1;
|
|
77
106
|
}
|
|
78
|
-
const { sprint, progress } = (await sprintRes.json());
|
|
107
|
+
const { sprint, progress, risk, topBlockers } = (await sprintRes.json());
|
|
79
108
|
const { tasks } = (await tasksRes.json());
|
|
80
|
-
process.stdout.write(formatSprintShow(sprint, progress, tasks) + '\n');
|
|
109
|
+
process.stdout.write(formatSprintShow(sprint, progress, tasks, risk, topBlockers) + '\n');
|
|
81
110
|
}
|
|
@@ -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
|
|
@@ -186,7 +189,7 @@ program
|
|
|
186
189
|
.description('Install the Lumo Claude Code skill and wire hook handlers into .claude/settings.json. Run via `npx @lumoai/cli setup` for first-time onboarding.')
|
|
187
190
|
.option('--user', 'Install into ~/.claude (applies across all projects for this user)')
|
|
188
191
|
.option('--project', 'Install into ./.claude (applies to the current project only)')
|
|
189
|
-
.option('--force', 'Overwrite
|
|
192
|
+
.option('--force', 'Overwrite existing skill files (SKILL.md + references/) when they differ from the bundled version')
|
|
190
193
|
.option('--agent <token>', 'Coding agent these hooks run under (claude-code, codex, cursor, gemini-cli, github-copilot, windsurf). Baked into every hook command. Defaults to claude-code.')
|
|
191
194
|
.action(wrap(options => (0, setup_1.setup)(options)));
|
|
192
195
|
program
|
|
@@ -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
|