@damper/cli 0.9.20 → 0.10.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/dist/commands/release.js +4 -30
- package/dist/commands/setup.js +3 -11
- package/dist/commands/start.js +33 -115
- package/dist/index.js +4 -16
- package/dist/services/claude.d.ts +8 -9
- package/dist/services/claude.js +49 -85
- package/dist/services/config.js +13 -3
- package/dist/services/damper-api.d.ts +1 -0
- package/dist/services/git.d.ts +4 -0
- package/dist/services/git.js +11 -0
- package/dist/ui/format.d.ts +1 -1
- package/dist/ui/format.js +5 -5
- package/dist/ui/task-picker.d.ts +0 -3
- package/dist/ui/task-picker.js +6 -36
- package/package.json +2 -3
- package/dist/commands/cleanup.d.ts +0 -1
- package/dist/commands/cleanup.js +0 -203
- package/dist/commands/status.d.ts +0 -1
- package/dist/commands/status.js +0 -94
- package/dist/services/context-bootstrap.d.ts +0 -30
- package/dist/services/context-bootstrap.js +0 -100
- package/dist/services/state.d.ts +0 -22
- package/dist/services/state.js +0 -102
- package/dist/services/worktree.d.ts +0 -40
- package/dist/services/worktree.js +0 -469
- package/dist/templates/CLAUDE_APPEND.md.d.ts +0 -7
- package/dist/templates/CLAUDE_APPEND.md.js +0 -35
- package/dist/templates/TASK_CONTEXT.md.d.ts +0 -17
- package/dist/templates/TASK_CONTEXT.md.js +0 -149
|
@@ -1,469 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { execa } from 'execa';
|
|
4
|
-
import pc from 'picocolors';
|
|
5
|
-
import { addWorktree, removeWorktree, getWorktreeByTaskId } from './state.js';
|
|
6
|
-
/**
|
|
7
|
-
* Convert a task title to a safe slug for branch/directory names
|
|
8
|
-
*/
|
|
9
|
-
function slugify(text) {
|
|
10
|
-
return text
|
|
11
|
-
.toLowerCase()
|
|
12
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
13
|
-
.replace(/^-+|-+$/g, '')
|
|
14
|
-
.slice(0, 50);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Get the project name from package.json or directory name
|
|
18
|
-
*/
|
|
19
|
-
function getProjectName(projectRoot) {
|
|
20
|
-
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
21
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
22
|
-
try {
|
|
23
|
-
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
24
|
-
if (pkg.name) {
|
|
25
|
-
// Strip scope if present
|
|
26
|
-
return pkg.name.replace(/^@[^/]+\//, '');
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
// Fall through to directory name
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return path.basename(projectRoot);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Detect monorepo structure by looking for workspaces in package.json
|
|
37
|
-
*/
|
|
38
|
-
function detectMonorepoPackages(projectRoot) {
|
|
39
|
-
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
40
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
45
|
-
const workspaces = pkg.workspaces || [];
|
|
46
|
-
const packages = [];
|
|
47
|
-
for (const pattern of workspaces) {
|
|
48
|
-
// Simple glob expansion for common patterns
|
|
49
|
-
if (pattern.includes('*')) {
|
|
50
|
-
const base = pattern.replace(/\/\*$/, '').replace(/\*$/, '');
|
|
51
|
-
const basePath = path.join(projectRoot, base);
|
|
52
|
-
if (fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()) {
|
|
53
|
-
const dirs = fs.readdirSync(basePath).filter(d => {
|
|
54
|
-
const fullPath = path.join(basePath, d);
|
|
55
|
-
return fs.statSync(fullPath).isDirectory() &&
|
|
56
|
-
fs.existsSync(path.join(fullPath, 'package.json'));
|
|
57
|
-
});
|
|
58
|
-
packages.push(...dirs.map(d => path.join(base, d)));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
// Direct path
|
|
63
|
-
const pkgPath = path.join(projectRoot, pattern);
|
|
64
|
-
if (fs.existsSync(path.join(pkgPath, 'package.json'))) {
|
|
65
|
-
packages.push(pattern);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return packages;
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return [];
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Find all node_modules directories to symlink
|
|
77
|
-
*/
|
|
78
|
-
function findNodeModulesDirs(projectRoot) {
|
|
79
|
-
const dirs = [];
|
|
80
|
-
// Root node_modules
|
|
81
|
-
const rootModules = path.join(projectRoot, 'node_modules');
|
|
82
|
-
if (fs.existsSync(rootModules)) {
|
|
83
|
-
dirs.push('node_modules');
|
|
84
|
-
}
|
|
85
|
-
// Monorepo package node_modules
|
|
86
|
-
const packages = detectMonorepoPackages(projectRoot);
|
|
87
|
-
for (const pkg of packages) {
|
|
88
|
-
const pkgModules = path.join(projectRoot, pkg, 'node_modules');
|
|
89
|
-
if (fs.existsSync(pkgModules)) {
|
|
90
|
-
dirs.push(path.join(pkg, 'node_modules'));
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return dirs;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Find all .env files to copy
|
|
97
|
-
*/
|
|
98
|
-
function findEnvFiles(projectRoot) {
|
|
99
|
-
const envFiles = [];
|
|
100
|
-
// Root .env files
|
|
101
|
-
const rootEnvFiles = ['.env', '.env.local', '.env.development'];
|
|
102
|
-
for (const envFile of rootEnvFiles) {
|
|
103
|
-
if (fs.existsSync(path.join(projectRoot, envFile))) {
|
|
104
|
-
envFiles.push(envFile);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// Monorepo package .env files
|
|
108
|
-
const packages = detectMonorepoPackages(projectRoot);
|
|
109
|
-
for (const pkg of packages) {
|
|
110
|
-
for (const envFile of rootEnvFiles) {
|
|
111
|
-
const envPath = path.join(pkg, envFile);
|
|
112
|
-
if (fs.existsSync(path.join(projectRoot, envPath))) {
|
|
113
|
-
envFiles.push(envPath);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// Common server locations
|
|
118
|
-
const serverDirs = ['server', 'api', 'backend', 'packages/server'];
|
|
119
|
-
for (const serverDir of serverDirs) {
|
|
120
|
-
for (const envFile of rootEnvFiles) {
|
|
121
|
-
const envPath = path.join(serverDir, envFile);
|
|
122
|
-
if (fs.existsSync(path.join(projectRoot, envPath)) && !envFiles.includes(envPath)) {
|
|
123
|
-
envFiles.push(envPath);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return envFiles;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Check if a git branch exists
|
|
131
|
-
*/
|
|
132
|
-
async function branchExists(branchName, projectRoot) {
|
|
133
|
-
try {
|
|
134
|
-
await execa('git', ['rev-parse', '--verify', branchName], {
|
|
135
|
-
cwd: projectRoot,
|
|
136
|
-
stdio: 'pipe',
|
|
137
|
-
});
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Create a new git worktree for a task
|
|
146
|
-
*/
|
|
147
|
-
export async function createWorktree(options) {
|
|
148
|
-
const { taskId, taskTitle, projectRoot } = options;
|
|
149
|
-
// Check if worktree already exists for this task
|
|
150
|
-
const existing = getWorktreeByTaskId(taskId);
|
|
151
|
-
if (existing && fs.existsSync(existing.path)) {
|
|
152
|
-
return {
|
|
153
|
-
path: existing.path,
|
|
154
|
-
branch: existing.branch,
|
|
155
|
-
isNew: false,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
const shortTaskId = taskId.slice(0, 8);
|
|
159
|
-
const slug = slugify(taskTitle);
|
|
160
|
-
// Put worktrees in .worktrees folder inside the project
|
|
161
|
-
const worktreesDir = path.join(projectRoot, '.worktrees');
|
|
162
|
-
const worktreePath = path.join(worktreesDir, `${shortTaskId}-${slug}`);
|
|
163
|
-
const branchName = `feature/${slug}`;
|
|
164
|
-
// Ensure .worktrees is gitignored
|
|
165
|
-
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
166
|
-
if (fs.existsSync(gitignorePath)) {
|
|
167
|
-
const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
|
|
168
|
-
if (!gitignore.includes('.worktrees')) {
|
|
169
|
-
fs.appendFileSync(gitignorePath, '\n# Damper CLI worktrees\n.worktrees/\n');
|
|
170
|
-
console.log(pc.dim('Added .worktrees/ to .gitignore'));
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
fs.writeFileSync(gitignorePath, '# Damper CLI worktrees\n.worktrees/\n');
|
|
175
|
-
console.log(pc.dim('Created .gitignore with .worktrees/'));
|
|
176
|
-
}
|
|
177
|
-
// Check if worktree directory already exists
|
|
178
|
-
if (fs.existsSync(worktreePath)) {
|
|
179
|
-
console.log(pc.dim(`Worktree directory already exists at ${worktreePath}`));
|
|
180
|
-
// Check if it's a valid worktree and has the expected branch
|
|
181
|
-
try {
|
|
182
|
-
const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
183
|
-
cwd: worktreePath,
|
|
184
|
-
stdio: 'pipe',
|
|
185
|
-
});
|
|
186
|
-
const currentBranch = stdout.trim();
|
|
187
|
-
console.log(pc.dim(`Using existing worktree on branch ${currentBranch}`));
|
|
188
|
-
// Save to state if not tracked
|
|
189
|
-
const worktreeState = {
|
|
190
|
-
taskId,
|
|
191
|
-
path: worktreePath,
|
|
192
|
-
branch: currentBranch,
|
|
193
|
-
projectRoot,
|
|
194
|
-
createdAt: new Date().toISOString(),
|
|
195
|
-
};
|
|
196
|
-
addWorktree(worktreeState);
|
|
197
|
-
return {
|
|
198
|
-
path: worktreePath,
|
|
199
|
-
branch: currentBranch,
|
|
200
|
-
isNew: false,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
// Not a valid git worktree, remove it
|
|
205
|
-
console.log(pc.dim(`Removing invalid directory at ${worktreePath}...`));
|
|
206
|
-
fs.rmSync(worktreePath, { recursive: true });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
// Check if branch already exists
|
|
210
|
-
const branchAlreadyExists = await branchExists(branchName, projectRoot);
|
|
211
|
-
// Create worktree
|
|
212
|
-
console.log(pc.dim(`Creating worktree at ${worktreePath}...`));
|
|
213
|
-
if (branchAlreadyExists) {
|
|
214
|
-
// Use existing branch
|
|
215
|
-
console.log(pc.dim(`Using existing branch ${branchName}`));
|
|
216
|
-
await execa('git', ['worktree', 'add', worktreePath, branchName], {
|
|
217
|
-
cwd: projectRoot,
|
|
218
|
-
stdio: 'pipe',
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
// Create new branch
|
|
223
|
-
await execa('git', ['worktree', 'add', worktreePath, '-b', branchName], {
|
|
224
|
-
cwd: projectRoot,
|
|
225
|
-
stdio: 'pipe',
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
// Symlink node_modules
|
|
229
|
-
const nodeModulesDirs = findNodeModulesDirs(projectRoot);
|
|
230
|
-
for (const dir of nodeModulesDirs) {
|
|
231
|
-
const source = path.join(projectRoot, dir);
|
|
232
|
-
const target = path.join(worktreePath, dir);
|
|
233
|
-
// Ensure parent directory exists
|
|
234
|
-
const targetDir = path.dirname(target);
|
|
235
|
-
if (!fs.existsSync(targetDir)) {
|
|
236
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
237
|
-
}
|
|
238
|
-
console.log(pc.dim(`Linking ${dir}...`));
|
|
239
|
-
await fs.promises.symlink(source, target, 'junction');
|
|
240
|
-
}
|
|
241
|
-
// Copy .env files
|
|
242
|
-
const envFiles = findEnvFiles(projectRoot);
|
|
243
|
-
if (envFiles.length > 0) {
|
|
244
|
-
console.log(pc.dim(`Copying ${envFiles.length} .env file(s) to worktree...`));
|
|
245
|
-
for (const envFile of envFiles) {
|
|
246
|
-
const source = path.join(projectRoot, envFile);
|
|
247
|
-
const target = path.join(worktreePath, envFile);
|
|
248
|
-
// Ensure parent directory exists
|
|
249
|
-
const targetDir = path.dirname(target);
|
|
250
|
-
if (!fs.existsSync(targetDir)) {
|
|
251
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
252
|
-
}
|
|
253
|
-
console.log(pc.dim(` ${envFile}`));
|
|
254
|
-
await fs.promises.copyFile(source, target);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// Create .mcp.json with Damper MCP configured (project-level MCP config)
|
|
258
|
-
const mcpConfig = {
|
|
259
|
-
mcpServers: {
|
|
260
|
-
damper: {
|
|
261
|
-
command: 'npx',
|
|
262
|
-
args: ['-y', '@damper/mcp'],
|
|
263
|
-
env: {
|
|
264
|
-
DAMPER_API_KEY: options.apiKey,
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
|
-
fs.writeFileSync(path.join(worktreePath, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
|
|
270
|
-
console.log(pc.dim('Created .mcp.json with Damper MCP'));
|
|
271
|
-
// Create .claude/settings.local.json - copy root permissions and ensure Damper MCP is allowed
|
|
272
|
-
const claudeDir = path.join(worktreePath, '.claude');
|
|
273
|
-
if (!fs.existsSync(claudeDir)) {
|
|
274
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
275
|
-
}
|
|
276
|
-
// Read existing settings from root project
|
|
277
|
-
const rootSettingsPath = path.join(projectRoot, '.claude', 'settings.local.json');
|
|
278
|
-
let claudeSettings = {};
|
|
279
|
-
if (fs.existsSync(rootSettingsPath)) {
|
|
280
|
-
try {
|
|
281
|
-
claudeSettings = JSON.parse(fs.readFileSync(rootSettingsPath, 'utf-8'));
|
|
282
|
-
}
|
|
283
|
-
catch { }
|
|
284
|
-
}
|
|
285
|
-
// Ensure permissions.allow includes damper MCP
|
|
286
|
-
const permissions = (claudeSettings.permissions ?? {});
|
|
287
|
-
const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
|
|
288
|
-
if (!allow.includes('mcp__damper__*')) {
|
|
289
|
-
allow.unshift('mcp__damper__*');
|
|
290
|
-
}
|
|
291
|
-
claudeSettings.permissions = { ...permissions, allow };
|
|
292
|
-
fs.writeFileSync(path.join(claudeDir, 'settings.local.json'), JSON.stringify(claudeSettings, null, 2));
|
|
293
|
-
console.log(pc.dim('Created .claude/settings.local.json with root permissions + MCP'));
|
|
294
|
-
// Save to state
|
|
295
|
-
const worktreeState = {
|
|
296
|
-
taskId,
|
|
297
|
-
path: worktreePath,
|
|
298
|
-
branch: branchName,
|
|
299
|
-
projectRoot,
|
|
300
|
-
createdAt: new Date().toISOString(),
|
|
301
|
-
};
|
|
302
|
-
addWorktree(worktreeState);
|
|
303
|
-
return {
|
|
304
|
-
path: worktreePath,
|
|
305
|
-
branch: branchName,
|
|
306
|
-
isNew: true,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Remove a git worktree and its branch
|
|
311
|
-
*/
|
|
312
|
-
export async function removeWorktreeDir(worktreePath, projectRoot) {
|
|
313
|
-
console.log(pc.dim(`Removing worktree at ${worktreePath}...`));
|
|
314
|
-
// Get branch name before removing worktree
|
|
315
|
-
let branchName;
|
|
316
|
-
try {
|
|
317
|
-
const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
318
|
-
cwd: worktreePath,
|
|
319
|
-
stdio: 'pipe',
|
|
320
|
-
});
|
|
321
|
-
branchName = stdout.trim();
|
|
322
|
-
}
|
|
323
|
-
catch {
|
|
324
|
-
// Ignore - might not be able to get branch
|
|
325
|
-
}
|
|
326
|
-
// Remove symlinks first to avoid issues
|
|
327
|
-
const nodeModulesDirs = findNodeModulesDirs(projectRoot);
|
|
328
|
-
for (const dir of nodeModulesDirs) {
|
|
329
|
-
const target = path.join(worktreePath, dir);
|
|
330
|
-
if (fs.existsSync(target)) {
|
|
331
|
-
try {
|
|
332
|
-
await fs.promises.unlink(target);
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
// Ignore errors
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
// Remove worktree
|
|
340
|
-
await execa('git', ['worktree', 'remove', worktreePath, '--force'], {
|
|
341
|
-
cwd: projectRoot,
|
|
342
|
-
stdio: 'pipe',
|
|
343
|
-
});
|
|
344
|
-
// Delete the branch if it's a feature branch
|
|
345
|
-
if (branchName && branchName.startsWith('feature/')) {
|
|
346
|
-
try {
|
|
347
|
-
await execa('git', ['branch', '-d', branchName], {
|
|
348
|
-
cwd: projectRoot,
|
|
349
|
-
stdio: 'pipe',
|
|
350
|
-
});
|
|
351
|
-
console.log(pc.dim(`Deleted branch: ${branchName}`));
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
// Branch might not be fully merged - try force delete
|
|
355
|
-
try {
|
|
356
|
-
await execa('git', ['branch', '-D', branchName], {
|
|
357
|
-
cwd: projectRoot,
|
|
358
|
-
stdio: 'pipe',
|
|
359
|
-
});
|
|
360
|
-
console.log(pc.dim(`Force deleted branch: ${branchName}`));
|
|
361
|
-
}
|
|
362
|
-
catch {
|
|
363
|
-
console.log(pc.yellow(`Could not delete branch: ${branchName}`));
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
// Get the worktree state to find the task ID
|
|
368
|
-
const state = await import('./state.js');
|
|
369
|
-
const worktree = state.getWorktreeByPath(worktreePath);
|
|
370
|
-
if (worktree) {
|
|
371
|
-
removeWorktree(worktree.taskId);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* List all git worktrees for a project
|
|
376
|
-
*/
|
|
377
|
-
export async function listWorktrees(projectRoot) {
|
|
378
|
-
const { stdout } = await execa('git', ['worktree', 'list', '--porcelain'], {
|
|
379
|
-
cwd: projectRoot,
|
|
380
|
-
stdio: 'pipe',
|
|
381
|
-
});
|
|
382
|
-
const worktrees = [];
|
|
383
|
-
let current = {};
|
|
384
|
-
for (const line of stdout.split('\n')) {
|
|
385
|
-
if (line.startsWith('worktree ')) {
|
|
386
|
-
current.path = line.slice(9);
|
|
387
|
-
}
|
|
388
|
-
else if (line.startsWith('HEAD ')) {
|
|
389
|
-
current.head = line.slice(5);
|
|
390
|
-
}
|
|
391
|
-
else if (line.startsWith('branch ')) {
|
|
392
|
-
current.branch = line.slice(7);
|
|
393
|
-
}
|
|
394
|
-
else if (line === '' && current.path) {
|
|
395
|
-
if (current.path && current.branch && current.head) {
|
|
396
|
-
worktrees.push({
|
|
397
|
-
path: current.path,
|
|
398
|
-
branch: current.branch,
|
|
399
|
-
head: current.head,
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
current = {};
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
// Handle last entry
|
|
406
|
-
if (current.path && current.branch && current.head) {
|
|
407
|
-
worktrees.push({
|
|
408
|
-
path: current.path,
|
|
409
|
-
branch: current.branch,
|
|
410
|
-
head: current.head,
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
return worktrees;
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Check if a path is inside a git worktree
|
|
417
|
-
*/
|
|
418
|
-
export async function isInWorktree(dir) {
|
|
419
|
-
try {
|
|
420
|
-
const { stdout } = await execa('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
421
|
-
cwd: dir,
|
|
422
|
-
stdio: 'pipe',
|
|
423
|
-
});
|
|
424
|
-
return stdout.trim() === 'true';
|
|
425
|
-
}
|
|
426
|
-
catch {
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* Get the git root directory
|
|
432
|
-
*/
|
|
433
|
-
export async function getGitRoot(dir) {
|
|
434
|
-
const { stdout } = await execa('git', ['rev-parse', '--show-toplevel'], {
|
|
435
|
-
cwd: dir,
|
|
436
|
-
stdio: 'pipe',
|
|
437
|
-
});
|
|
438
|
-
return stdout.trim();
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Get the main project root (not a worktree).
|
|
442
|
-
* If we're in a worktree, this returns the main repo's path.
|
|
443
|
-
*/
|
|
444
|
-
export async function getMainProjectRoot(dir) {
|
|
445
|
-
// Get the common git directory (shared by all worktrees)
|
|
446
|
-
const { stdout: gitCommonDir } = await execa('git', ['rev-parse', '--git-common-dir'], {
|
|
447
|
-
cwd: dir,
|
|
448
|
-
stdio: 'pipe',
|
|
449
|
-
});
|
|
450
|
-
const commonDir = gitCommonDir.trim();
|
|
451
|
-
// If it's just '.git', we're in the main repo
|
|
452
|
-
if (commonDir === '.git') {
|
|
453
|
-
return getGitRoot(dir);
|
|
454
|
-
}
|
|
455
|
-
// Otherwise, the common dir is something like '/path/to/main-repo/.git'
|
|
456
|
-
// Strip the '/.git' to get the main repo path
|
|
457
|
-
if (commonDir.endsWith('/.git')) {
|
|
458
|
-
return commonDir.slice(0, -5);
|
|
459
|
-
}
|
|
460
|
-
// Fallback: resolve relative path
|
|
461
|
-
const gitRoot = await getGitRoot(dir);
|
|
462
|
-
const resolvedCommon = path.resolve(gitRoot, commonDir);
|
|
463
|
-
// Strip .git suffix
|
|
464
|
-
if (resolvedCommon.endsWith('.git')) {
|
|
465
|
-
return resolvedCommon.slice(0, -4);
|
|
466
|
-
}
|
|
467
|
-
// Last resort: just return git root
|
|
468
|
-
return gitRoot;
|
|
469
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export function generateClaudeAppend(options) {
|
|
2
|
-
const { taskId, taskTitle, yolo } = options;
|
|
3
|
-
const planSection = yolo
|
|
4
|
-
? `
|
|
5
|
-
**MANDATORY: Plan First, Then Execute**
|
|
6
|
-
Before making ANY code changes, you MUST enter plan mode using the EnterPlanMode tool.
|
|
7
|
-
Read TASK_CONTEXT.md first, then create a plan. Once the plan is ready, execute it without waiting for user approval.
|
|
8
|
-
`
|
|
9
|
-
: `
|
|
10
|
-
**MANDATORY: Plan Mode**
|
|
11
|
-
Before making ANY code changes, you MUST enter plan mode using the EnterPlanMode tool.
|
|
12
|
-
Read TASK_CONTEXT.md first, then create a plan for user approval. Do NOT write code until the plan is approved.
|
|
13
|
-
`;
|
|
14
|
-
return `
|
|
15
|
-
## Current Task: #${taskId} ${taskTitle}
|
|
16
|
-
|
|
17
|
-
**IMPORTANT**: Read TASK_CONTEXT.md for full task details and architecture context.
|
|
18
|
-
If you feel you've lost context, re-read that file.
|
|
19
|
-
${planSection}
|
|
20
|
-
**NEVER commit these files** (they are generated by the CLI and gitignored):
|
|
21
|
-
- \`CLAUDE.md\` changes (this task section is temporary)
|
|
22
|
-
- \`TASK_CONTEXT.md\`
|
|
23
|
-
- \`.mcp.json\`
|
|
24
|
-
- \`.claude/settings.local.json\`
|
|
25
|
-
|
|
26
|
-
**Your responsibilities (via Damper MCP):**
|
|
27
|
-
1. **Do NOT commit or complete tasks without explicit user confirmation** - Always ask the user before running \`git commit\` or calling \`complete_task\`
|
|
28
|
-
2. Use \`add_commit\` after each git commit
|
|
29
|
-
3. Use \`add_note\` ONLY for non-obvious approach decisions (e.g. "Decision: chose X because Y")
|
|
30
|
-
4. When user confirms: call \`complete_task\` with summary and \`reviewInstructions\` (what to test/verify)
|
|
31
|
-
5. If stopping early: call \`abandon_task\` with what remains and blockers
|
|
32
|
-
|
|
33
|
-
The CLI just bootstrapped this environment - YOU handle the task lifecycle.
|
|
34
|
-
`.trim();
|
|
35
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { TaskDetail, Module } from '../services/damper-api.js';
|
|
2
|
-
import type { SectionBlockIndex } from '../services/context-bootstrap.js';
|
|
3
|
-
interface TaskContextOptions {
|
|
4
|
-
task: TaskDetail;
|
|
5
|
-
criticalRules: string[];
|
|
6
|
-
completionChecklist?: string[];
|
|
7
|
-
blockIndices: SectionBlockIndex[];
|
|
8
|
-
templates: Array<{
|
|
9
|
-
name: string;
|
|
10
|
-
description?: string | null;
|
|
11
|
-
filePattern?: string | null;
|
|
12
|
-
}>;
|
|
13
|
-
modules: Module[];
|
|
14
|
-
damperInstructions: string;
|
|
15
|
-
}
|
|
16
|
-
export declare function generateTaskContext(options: TaskContextOptions): string;
|
|
17
|
-
export {};
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
export function generateTaskContext(options) {
|
|
2
|
-
const { task, criticalRules, completionChecklist, blockIndices, templates, modules, damperInstructions } = options;
|
|
3
|
-
const typeIcon = task.type === 'bug' ? 'Bug' : task.type === 'feature' ? 'Feature' : task.type === 'improvement' ? 'Improvement' : 'Task';
|
|
4
|
-
const lines = [];
|
|
5
|
-
// Header
|
|
6
|
-
lines.push(`# Task: ${task.title} (#${task.id})`);
|
|
7
|
-
lines.push('');
|
|
8
|
-
lines.push(`**Type:** ${typeIcon} | **Status:** ${task.status} | **Priority:** ${task.priority || 'none'}`);
|
|
9
|
-
if (task.effort)
|
|
10
|
-
lines.push(`**Effort:** ${task.effort}`);
|
|
11
|
-
if (task.quarter)
|
|
12
|
-
lines.push(`**Quarter:** ${task.quarter}`);
|
|
13
|
-
lines.push('');
|
|
14
|
-
// Critical Rules
|
|
15
|
-
if (criticalRules.length > 0) {
|
|
16
|
-
lines.push('## Critical Rules (DO NOT SKIP)');
|
|
17
|
-
lines.push('');
|
|
18
|
-
for (const rule of criticalRules) {
|
|
19
|
-
lines.push(`- ${rule}`);
|
|
20
|
-
}
|
|
21
|
-
lines.push('');
|
|
22
|
-
}
|
|
23
|
-
// Completion Checklist
|
|
24
|
-
if (completionChecklist && completionChecklist.length > 0) {
|
|
25
|
-
lines.push('## Completion Checklist (required for complete_task)');
|
|
26
|
-
lines.push('');
|
|
27
|
-
lines.push('You MUST verify each item and pass them as `confirmations` when calling `complete_task`:');
|
|
28
|
-
lines.push('');
|
|
29
|
-
for (const item of completionChecklist) {
|
|
30
|
-
lines.push(`- [ ] ${item}`);
|
|
31
|
-
}
|
|
32
|
-
lines.push('');
|
|
33
|
-
}
|
|
34
|
-
// Damper Workflow
|
|
35
|
-
lines.push('## Damper Workflow');
|
|
36
|
-
lines.push('');
|
|
37
|
-
lines.push(damperInstructions);
|
|
38
|
-
lines.push('');
|
|
39
|
-
// CLI & Tooling Info
|
|
40
|
-
lines.push('## About This Session');
|
|
41
|
-
lines.push('');
|
|
42
|
-
lines.push('You were launched by `@damper/cli` (npm package). The CLI:');
|
|
43
|
-
lines.push('- Created this git worktree for isolated work');
|
|
44
|
-
lines.push('- Generated this TASK_CONTEXT.md from Damper API');
|
|
45
|
-
lines.push('- Configured Damper MCP tools for task lifecycle');
|
|
46
|
-
lines.push('');
|
|
47
|
-
lines.push('**Cleanup:** Ask the user for confirmation before committing or completing the task.');
|
|
48
|
-
lines.push('Once confirmed, call `complete_task` or `abandon_task`. The user will run');
|
|
49
|
-
lines.push('`npx @damper/cli cleanup` to remove the worktree and branch - you don\'t need to');
|
|
50
|
-
lines.push('provide manual cleanup instructions.');
|
|
51
|
-
lines.push('');
|
|
52
|
-
lines.push('**Help improve the tooling:** If you encounter friction, bugs, or have ideas,');
|
|
53
|
-
lines.push('use `report_issue` to log them. Your feedback improves tooling for all future tasks.');
|
|
54
|
-
lines.push('');
|
|
55
|
-
// Task Description
|
|
56
|
-
if (task.description) {
|
|
57
|
-
lines.push('## Task Description');
|
|
58
|
-
lines.push('');
|
|
59
|
-
lines.push(task.description);
|
|
60
|
-
lines.push('');
|
|
61
|
-
}
|
|
62
|
-
// Implementation Plan
|
|
63
|
-
if (task.implementationPlan) {
|
|
64
|
-
lines.push('## Implementation Plan');
|
|
65
|
-
lines.push('');
|
|
66
|
-
lines.push(task.implementationPlan);
|
|
67
|
-
lines.push('');
|
|
68
|
-
}
|
|
69
|
-
// Subtasks
|
|
70
|
-
if (task.subtasks && task.subtasks.length > 0) {
|
|
71
|
-
const done = task.subtasks.filter(s => s.done).length;
|
|
72
|
-
lines.push(`## Subtasks (${done}/${task.subtasks.length})`);
|
|
73
|
-
lines.push('');
|
|
74
|
-
for (const subtask of task.subtasks) {
|
|
75
|
-
lines.push(`- [${subtask.done ? 'x' : ' '}] ${subtask.title} (id: ${subtask.id})`);
|
|
76
|
-
}
|
|
77
|
-
lines.push('');
|
|
78
|
-
}
|
|
79
|
-
// Previous Session Notes
|
|
80
|
-
if (task.agentNotes) {
|
|
81
|
-
lines.push('## Previous Notes');
|
|
82
|
-
lines.push('');
|
|
83
|
-
lines.push(task.agentNotes);
|
|
84
|
-
lines.push('');
|
|
85
|
-
}
|
|
86
|
-
// Previous Commits
|
|
87
|
-
if (task.commits && task.commits.length > 0) {
|
|
88
|
-
lines.push(`## Previous Commits (${task.commits.length})`);
|
|
89
|
-
lines.push('');
|
|
90
|
-
for (const commit of task.commits) {
|
|
91
|
-
lines.push(`- ${commit.hash.slice(0, 7)}: ${commit.message}`);
|
|
92
|
-
}
|
|
93
|
-
lines.push('');
|
|
94
|
-
}
|
|
95
|
-
// Project Context Block Index (load-on-demand)
|
|
96
|
-
if (blockIndices.length > 0) {
|
|
97
|
-
lines.push('## Available Architecture Context');
|
|
98
|
-
lines.push('');
|
|
99
|
-
lines.push('**BEFORE starting work**, load the blocks relevant to your task using the MCP tool:');
|
|
100
|
-
lines.push('`get_section_block_content(section, blockIds)` — pass the section name and an array of block IDs from the list below.');
|
|
101
|
-
lines.push('Only load what you need to keep token usage low.');
|
|
102
|
-
lines.push('');
|
|
103
|
-
for (const index of blockIndices) {
|
|
104
|
-
lines.push(`### ${index.section} (${index.totalChars} chars)`);
|
|
105
|
-
lines.push('');
|
|
106
|
-
for (const block of index.blocks) {
|
|
107
|
-
const heading = block.heading ? block.heading.replace(/^#+\s*/, '') : '(intro)';
|
|
108
|
-
lines.push(`- \`${block.id}\`: ${heading} (${block.charCount} chars)`);
|
|
109
|
-
}
|
|
110
|
-
lines.push('');
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// Templates
|
|
114
|
-
if (templates.length > 0) {
|
|
115
|
-
lines.push('## Templates Available');
|
|
116
|
-
lines.push('');
|
|
117
|
-
for (const template of templates) {
|
|
118
|
-
const pattern = template.filePattern ? ` (${template.filePattern})` : '';
|
|
119
|
-
const desc = template.description ? `: ${template.description}` : '';
|
|
120
|
-
lines.push(`- \`${template.name}\`${pattern}${desc}`);
|
|
121
|
-
}
|
|
122
|
-
lines.push('');
|
|
123
|
-
}
|
|
124
|
-
// Modules
|
|
125
|
-
if (modules.length > 0) {
|
|
126
|
-
lines.push('## Module Structure');
|
|
127
|
-
lines.push('');
|
|
128
|
-
for (const mod of modules) {
|
|
129
|
-
const port = mod.port ? ` (port ${mod.port})` : '';
|
|
130
|
-
const deps = mod.dependsOn && mod.dependsOn.length > 0 ? ` → ${mod.dependsOn.join(', ')}` : '';
|
|
131
|
-
lines.push(`- **${mod.name}**: ${mod.path}${port}${deps}`);
|
|
132
|
-
}
|
|
133
|
-
lines.push('');
|
|
134
|
-
}
|
|
135
|
-
// Linked Feedback
|
|
136
|
-
if (task.feedback && task.feedback.length > 0) {
|
|
137
|
-
lines.push('## Linked Feedback (customer requests driving this task)');
|
|
138
|
-
lines.push('');
|
|
139
|
-
for (const fb of task.feedback) {
|
|
140
|
-
lines.push(`- "${fb.title}" (${fb.voterCount} votes)`);
|
|
141
|
-
}
|
|
142
|
-
lines.push('');
|
|
143
|
-
}
|
|
144
|
-
// Footer
|
|
145
|
-
lines.push('---');
|
|
146
|
-
lines.push(`Generated by @damper/cli at ${new Date().toISOString()}.`);
|
|
147
|
-
lines.push('Re-read this file if conversation context is lost.');
|
|
148
|
-
return lines.join('\n');
|
|
149
|
-
}
|