@damper/cli 0.1.0 → 0.1.2
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 +202 -0
- package/dist/commands/cleanup.js +35 -3
- package/dist/commands/setup.js +1 -1
- package/dist/services/worktree.js +12 -9
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# @damper/cli
|
|
2
|
+
|
|
3
|
+
Agent orchestration CLI for working on [Damper](https://usedamper.com) roadmap tasks with Claude Code.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
1. **Picks a task** from your Damper roadmap (interactive or by ID)
|
|
8
|
+
2. **Creates an isolated git worktree** for the task
|
|
9
|
+
3. **Bootstraps context** - fetches task details, project docs, and critical rules from Damper
|
|
10
|
+
4. **Launches Claude Code** with full context ready to go
|
|
11
|
+
|
|
12
|
+
Claude then handles the task lifecycle via Damper MCP - logging commits, adding notes, and completing/abandoning tasks.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Run directly with npx (recommended)
|
|
18
|
+
npx @damper/cli
|
|
19
|
+
|
|
20
|
+
# Or install globally
|
|
21
|
+
npm install -g @damper/cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Prerequisites
|
|
25
|
+
|
|
26
|
+
- [Claude Code CLI](https://claude.ai/code) installed
|
|
27
|
+
- A Damper account with an API key
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# First time? Run setup to configure MCP and API key
|
|
33
|
+
npx @damper/cli setup
|
|
34
|
+
|
|
35
|
+
# Start working on a task
|
|
36
|
+
npx @damper/cli
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
### `npx @damper/cli` (default)
|
|
42
|
+
|
|
43
|
+
Interactive task picker. Shows:
|
|
44
|
+
- **In Progress** - Tasks with existing worktrees you can resume
|
|
45
|
+
- **Available** - Planned tasks from your roadmap
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx @damper/cli # Interactive picker
|
|
49
|
+
npx @damper/cli --task 42 # Start specific task
|
|
50
|
+
npx @damper/cli --type bug # Filter by type
|
|
51
|
+
npx @damper/cli --status planned # Filter by status
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `npx @damper/cli setup`
|
|
55
|
+
|
|
56
|
+
Configure Damper MCP in Claude Code. Run this first if you haven't set up MCP yet.
|
|
57
|
+
|
|
58
|
+
- Checks if Claude CLI is installed
|
|
59
|
+
- Configures `~/.claude/settings.json` with Damper MCP
|
|
60
|
+
- Prompts for API key if not in environment
|
|
61
|
+
|
|
62
|
+
### `npx @damper/cli status`
|
|
63
|
+
|
|
64
|
+
Show all tracked worktrees and their task status.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Tracked Worktrees:
|
|
68
|
+
|
|
69
|
+
myproject (current)
|
|
70
|
+
/Users/me/projects/myproject
|
|
71
|
+
|
|
72
|
+
● #42 [in_progress]
|
|
73
|
+
Path: myproject-oauth-support
|
|
74
|
+
Branch: feature/oauth-support
|
|
75
|
+
Created: 2/5/2025
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `npx @damper/cli cleanup`
|
|
79
|
+
|
|
80
|
+
Remove worktrees for completed or abandoned tasks. Interactive selection with auto-detection of:
|
|
81
|
+
- Completed tasks
|
|
82
|
+
- Abandoned tasks (unlocked in Damper)
|
|
83
|
+
- Missing worktree directories
|
|
84
|
+
|
|
85
|
+
## Workflow
|
|
86
|
+
|
|
87
|
+
### Starting a New Task
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
$ npx @damper/cli
|
|
91
|
+
|
|
92
|
+
? Select a task to work on:
|
|
93
|
+
--- Available Tasks ---
|
|
94
|
+
❯ #42 Add OAuth support [feature] !
|
|
95
|
+
#38 Fix login timeout [bug] ~
|
|
96
|
+
|
|
97
|
+
✓ Task locked in Damper
|
|
98
|
+
✓ Created worktree: ../myproject-oauth-support
|
|
99
|
+
✓ Created TASK_CONTEXT.md
|
|
100
|
+
✓ Updated CLAUDE.md with task section
|
|
101
|
+
|
|
102
|
+
Starting Claude Code for task #42: Add OAuth support
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Resuming Work
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
$ npx @damper/cli
|
|
109
|
+
|
|
110
|
+
? Select a task to work on:
|
|
111
|
+
--- In Progress (your worktrees) ---
|
|
112
|
+
❯ #42 Add OAuth support [feature] !
|
|
113
|
+
Worktree: myproject-oauth-support
|
|
114
|
+
--- Available Tasks ---
|
|
115
|
+
#38 Fix login timeout [bug] ~
|
|
116
|
+
|
|
117
|
+
✓ Resuming: #42 Add OAuth support
|
|
118
|
+
✓ Updated TASK_CONTEXT.md with latest notes
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Generated Files
|
|
122
|
+
|
|
123
|
+
The CLI creates these files in the worktree:
|
|
124
|
+
|
|
125
|
+
**`TASK_CONTEXT.md`** - Full task context for Claude:
|
|
126
|
+
- Critical rules (must-follow patterns)
|
|
127
|
+
- Damper workflow instructions
|
|
128
|
+
- Task description and implementation plan
|
|
129
|
+
- Previous session notes and commits
|
|
130
|
+
- Project architecture docs
|
|
131
|
+
- Available templates and modules
|
|
132
|
+
- Linked customer feedback
|
|
133
|
+
|
|
134
|
+
**`CLAUDE.md`** (appended) - Task-specific section pointing to TASK_CONTEXT.md
|
|
135
|
+
|
|
136
|
+
### Task Lifecycle (handled by Claude via MCP)
|
|
137
|
+
|
|
138
|
+
Once Claude is running, it handles:
|
|
139
|
+
- `add_commit` - Log commits to Damper
|
|
140
|
+
- `add_note` - Record decisions and progress
|
|
141
|
+
- `complete_task` - Mark done with summary
|
|
142
|
+
- `abandon_task` - Hand off with notes for next agent
|
|
143
|
+
|
|
144
|
+
## Environment Variables
|
|
145
|
+
|
|
146
|
+
| Variable | Description |
|
|
147
|
+
|----------|-------------|
|
|
148
|
+
| `DAMPER_API_KEY` | Your Damper API key (or configure via `setup`) |
|
|
149
|
+
| `DAMPER_API_URL` | API URL override (default: `https://api.usedamper.com`) |
|
|
150
|
+
|
|
151
|
+
## How it Works
|
|
152
|
+
|
|
153
|
+
### Worktree Isolation
|
|
154
|
+
|
|
155
|
+
Each task gets its own git worktree:
|
|
156
|
+
```
|
|
157
|
+
myproject/ # Main repo
|
|
158
|
+
../myproject-oauth-support/ # Task #42 worktree
|
|
159
|
+
../myproject-fix-login/ # Task #38 worktree
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The CLI automatically:
|
|
163
|
+
- Creates the worktree with a feature branch
|
|
164
|
+
- Symlinks `node_modules` (detects monorepo structure)
|
|
165
|
+
- Copies `.env` files
|
|
166
|
+
|
|
167
|
+
### Context from Damper
|
|
168
|
+
|
|
169
|
+
The CLI fetches from Damper:
|
|
170
|
+
- Task details (description, plan, subtasks)
|
|
171
|
+
- Project context (architecture, conventions)
|
|
172
|
+
- Critical rules (shown prominently to Claude)
|
|
173
|
+
- Templates (code patterns to follow)
|
|
174
|
+
- Modules (monorepo structure)
|
|
175
|
+
- Previous agent notes (for continuity)
|
|
176
|
+
|
|
177
|
+
This context survives Claude conversation compaction because it's in local files.
|
|
178
|
+
|
|
179
|
+
### State Tracking
|
|
180
|
+
|
|
181
|
+
Worktree-to-task mappings are stored in `~/.damper/worktrees.json`. This enables:
|
|
182
|
+
- Showing "resume" options for in-progress tasks
|
|
183
|
+
- Detecting stale worktrees for cleanup
|
|
184
|
+
- Tracking across multiple projects
|
|
185
|
+
|
|
186
|
+
## Parallel Work
|
|
187
|
+
|
|
188
|
+
Run multiple instances in different terminals:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# Terminal 1
|
|
192
|
+
npx @damper/cli --task 42
|
|
193
|
+
|
|
194
|
+
# Terminal 2
|
|
195
|
+
npx @damper/cli --task 38
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Each task gets its own worktree and Claude session.
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT
|
package/dist/commands/cleanup.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import { confirm, checkbox } from '@inquirer/prompts';
|
|
3
|
+
import { execa } from 'execa';
|
|
3
4
|
import pc from 'picocolors';
|
|
4
5
|
import { getWorktrees, cleanupStaleWorktrees, removeWorktree } from '../services/state.js';
|
|
5
6
|
import { createDamperApi } from '../services/damper-api.js';
|
|
6
7
|
import { removeWorktreeDir } from '../services/worktree.js';
|
|
8
|
+
async function hasUncommittedChanges(worktreePath) {
|
|
9
|
+
try {
|
|
10
|
+
const { stdout } = await execa('git', ['status', '--porcelain'], {
|
|
11
|
+
cwd: worktreePath,
|
|
12
|
+
stdio: 'pipe',
|
|
13
|
+
});
|
|
14
|
+
return stdout.trim().length > 0;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
7
20
|
export async function cleanupCommand() {
|
|
8
21
|
// Clean up stale entries first
|
|
9
22
|
const stale = cleanupStaleWorktrees();
|
|
@@ -37,6 +50,8 @@ export async function cleanupCommand() {
|
|
|
37
50
|
});
|
|
38
51
|
continue;
|
|
39
52
|
}
|
|
53
|
+
// Check for uncommitted changes
|
|
54
|
+
const uncommitted = await hasUncommittedChanges(wt.path);
|
|
40
55
|
if (api) {
|
|
41
56
|
try {
|
|
42
57
|
const task = await api.getTask(wt.taskId);
|
|
@@ -45,6 +60,7 @@ export async function cleanupCommand() {
|
|
|
45
60
|
worktree: wt,
|
|
46
61
|
task,
|
|
47
62
|
reason: 'completed',
|
|
63
|
+
hasUncommittedChanges: uncommitted,
|
|
48
64
|
});
|
|
49
65
|
}
|
|
50
66
|
else if (task.status === 'planned' && !task.lockedBy) {
|
|
@@ -53,6 +69,7 @@ export async function cleanupCommand() {
|
|
|
53
69
|
worktree: wt,
|
|
54
70
|
task,
|
|
55
71
|
reason: 'abandoned',
|
|
72
|
+
hasUncommittedChanges: uncommitted,
|
|
56
73
|
});
|
|
57
74
|
}
|
|
58
75
|
}
|
|
@@ -61,6 +78,7 @@ export async function cleanupCommand() {
|
|
|
61
78
|
candidates.push({
|
|
62
79
|
worktree: wt,
|
|
63
80
|
reason: 'manual',
|
|
81
|
+
hasUncommittedChanges: uncommitted,
|
|
64
82
|
});
|
|
65
83
|
}
|
|
66
84
|
}
|
|
@@ -69,6 +87,7 @@ export async function cleanupCommand() {
|
|
|
69
87
|
candidates.push({
|
|
70
88
|
worktree: wt,
|
|
71
89
|
reason: 'manual',
|
|
90
|
+
hasUncommittedChanges: uncommitted,
|
|
72
91
|
});
|
|
73
92
|
}
|
|
74
93
|
}
|
|
@@ -99,10 +118,15 @@ export async function cleanupCommand() {
|
|
|
99
118
|
reasonBadge = pc.dim('[manual]');
|
|
100
119
|
break;
|
|
101
120
|
}
|
|
121
|
+
// Warning for uncommitted changes
|
|
122
|
+
const uncommittedWarning = c.hasUncommittedChanges
|
|
123
|
+
? pc.red(' ⚠ uncommitted changes')
|
|
124
|
+
: '';
|
|
102
125
|
return {
|
|
103
|
-
name: `${description} ${reasonBadge}\n ${pc.dim(worktreeName)}`,
|
|
126
|
+
name: `${description} ${reasonBadge}${uncommittedWarning}\n ${pc.dim(worktreeName)}`,
|
|
104
127
|
value: c,
|
|
105
|
-
|
|
128
|
+
// Don't auto-check if there are uncommitted changes
|
|
129
|
+
checked: (c.reason === 'completed' || c.reason === 'missing') && !c.hasUncommittedChanges,
|
|
106
130
|
};
|
|
107
131
|
});
|
|
108
132
|
const selected = await checkbox({
|
|
@@ -114,9 +138,17 @@ export async function cleanupCommand() {
|
|
|
114
138
|
console.log(pc.dim('\nNo worktrees selected. Nothing to clean up.\n'));
|
|
115
139
|
return;
|
|
116
140
|
}
|
|
141
|
+
// Check for uncommitted changes in selected worktrees
|
|
142
|
+
const withUncommitted = selected.filter(c => c.hasUncommittedChanges);
|
|
143
|
+
if (withUncommitted.length > 0) {
|
|
144
|
+
console.log(pc.yellow(`\n⚠ Warning: ${withUncommitted.length} worktree(s) have uncommitted changes that will be lost!`));
|
|
145
|
+
}
|
|
117
146
|
// Confirm
|
|
147
|
+
const confirmMessage = withUncommitted.length > 0
|
|
148
|
+
? `Remove ${selected.length} worktree(s)? (${withUncommitted.length} with uncommitted changes)`
|
|
149
|
+
: `Remove ${selected.length} worktree(s)?`;
|
|
118
150
|
const confirmed = await confirm({
|
|
119
|
-
message:
|
|
151
|
+
message: confirmMessage,
|
|
120
152
|
default: false,
|
|
121
153
|
});
|
|
122
154
|
if (!confirmed) {
|
package/dist/commands/setup.js
CHANGED
|
@@ -17,7 +17,7 @@ export async function setupCommand() {
|
|
|
17
17
|
const apiKey = getConfiguredApiKey();
|
|
18
18
|
if (mcpConfigured && apiKey) {
|
|
19
19
|
console.log(pc.green('✓ Damper MCP configured'));
|
|
20
|
-
console.log(pc.dim(` API key: ${
|
|
20
|
+
console.log(pc.dim(` API key: ${'*'.repeat(12)} (${apiKey.length} chars)`));
|
|
21
21
|
// Verify the API key works
|
|
22
22
|
console.log(pc.dim('\nVerifying API key...'));
|
|
23
23
|
try {
|
|
@@ -165,16 +165,19 @@ export async function createWorktree(options) {
|
|
|
165
165
|
}
|
|
166
166
|
// Copy .env files
|
|
167
167
|
const envFiles = findEnvFiles(projectRoot);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
168
|
+
if (envFiles.length > 0) {
|
|
169
|
+
console.log(pc.dim(`Copying ${envFiles.length} .env file(s) to worktree...`));
|
|
170
|
+
for (const envFile of envFiles) {
|
|
171
|
+
const source = path.join(projectRoot, envFile);
|
|
172
|
+
const target = path.join(worktreePath, envFile);
|
|
173
|
+
// Ensure parent directory exists
|
|
174
|
+
const targetDir = path.dirname(target);
|
|
175
|
+
if (!fs.existsSync(targetDir)) {
|
|
176
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
177
|
+
}
|
|
178
|
+
console.log(pc.dim(` ${envFile}`));
|
|
179
|
+
await fs.promises.copyFile(source, target);
|
|
175
180
|
}
|
|
176
|
-
console.log(pc.dim(`Copying ${envFile}...`));
|
|
177
|
-
await fs.promises.copyFile(source, target);
|
|
178
181
|
}
|
|
179
182
|
// Save to state
|
|
180
183
|
const worktreeState = {
|