@damper/cli 0.1.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/cleanup.d.ts +1 -0
- package/dist/commands/cleanup.js +149 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +67 -0
- package/dist/commands/start.d.ts +6 -0
- package/dist/commands/start.js +139 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +74 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +125 -0
- package/dist/services/claude.d.ts +44 -0
- package/dist/services/claude.js +221 -0
- package/dist/services/context-bootstrap.d.ts +18 -0
- package/dist/services/context-bootstrap.js +94 -0
- package/dist/services/damper-api.d.ts +154 -0
- package/dist/services/damper-api.js +151 -0
- package/dist/services/state.d.ts +15 -0
- package/dist/services/state.js +77 -0
- package/dist/services/worktree.d.ts +34 -0
- package/dist/services/worktree.js +289 -0
- package/dist/templates/CLAUDE_APPEND.md.d.ts +6 -0
- package/dist/templates/CLAUDE_APPEND.md.js +17 -0
- package/dist/templates/TASK_CONTEXT.md.d.ts +15 -0
- package/dist/templates/TASK_CONTEXT.md.js +115 -0
- package/dist/ui/task-picker.d.ts +15 -0
- package/dist/ui/task-picker.js +121 -0
- package/dist/utils/config.d.ts +7 -0
- package/dist/utils/config.js +32 -0
- package/package.json +50 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cleanupCommand(): Promise<void>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { confirm, checkbox } from '@inquirer/prompts';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { getWorktrees, cleanupStaleWorktrees, removeWorktree } from '../services/state.js';
|
|
5
|
+
import { createDamperApi } from '../services/damper-api.js';
|
|
6
|
+
import { removeWorktreeDir } from '../services/worktree.js';
|
|
7
|
+
export async function cleanupCommand() {
|
|
8
|
+
// Clean up stale entries first
|
|
9
|
+
const stale = cleanupStaleWorktrees();
|
|
10
|
+
if (stale.length > 0) {
|
|
11
|
+
console.log(pc.dim(`Removed ${stale.length} stale worktree reference(s)`));
|
|
12
|
+
}
|
|
13
|
+
const worktrees = getWorktrees();
|
|
14
|
+
if (worktrees.length === 0) {
|
|
15
|
+
console.log(pc.yellow('\nNo tracked worktrees found.'));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
// Try to connect to Damper for task status
|
|
19
|
+
let api;
|
|
20
|
+
try {
|
|
21
|
+
api = createDamperApi();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
console.log(pc.yellow('\nWarning: Could not connect to Damper API (no API key).'));
|
|
25
|
+
console.log(pc.dim('Will show all worktrees for manual selection.\n'));
|
|
26
|
+
}
|
|
27
|
+
// Identify cleanup candidates
|
|
28
|
+
const candidates = [];
|
|
29
|
+
console.log(pc.dim('\nAnalyzing worktrees...'));
|
|
30
|
+
for (const wt of worktrees) {
|
|
31
|
+
const exists = fs.existsSync(wt.path);
|
|
32
|
+
if (!exists) {
|
|
33
|
+
// Worktree directory is missing
|
|
34
|
+
candidates.push({
|
|
35
|
+
worktree: wt,
|
|
36
|
+
reason: 'missing',
|
|
37
|
+
});
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (api) {
|
|
41
|
+
try {
|
|
42
|
+
const task = await api.getTask(wt.taskId);
|
|
43
|
+
if (task.status === 'done') {
|
|
44
|
+
candidates.push({
|
|
45
|
+
worktree: wt,
|
|
46
|
+
task,
|
|
47
|
+
reason: 'completed',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else if (task.status === 'planned' && !task.lockedBy) {
|
|
51
|
+
// Task was abandoned (planned and not locked)
|
|
52
|
+
candidates.push({
|
|
53
|
+
worktree: wt,
|
|
54
|
+
task,
|
|
55
|
+
reason: 'abandoned',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Could not fetch task - might be deleted
|
|
61
|
+
candidates.push({
|
|
62
|
+
worktree: wt,
|
|
63
|
+
reason: 'manual',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// No API - show all for manual selection
|
|
69
|
+
candidates.push({
|
|
70
|
+
worktree: wt,
|
|
71
|
+
reason: 'manual',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (candidates.length === 0) {
|
|
76
|
+
console.log(pc.green('\n✓ All worktrees are for active tasks. Nothing to clean up.\n'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Show candidates and let user select
|
|
80
|
+
console.log(pc.bold('\nWorktrees available for cleanup:\n'));
|
|
81
|
+
const choices = candidates.map(c => {
|
|
82
|
+
const worktreeName = c.worktree.path.split('/').pop() || c.worktree.path;
|
|
83
|
+
let description = `#${c.worktree.taskId}`;
|
|
84
|
+
if (c.task) {
|
|
85
|
+
description += ` - ${c.task.title}`;
|
|
86
|
+
}
|
|
87
|
+
let reasonBadge = '';
|
|
88
|
+
switch (c.reason) {
|
|
89
|
+
case 'completed':
|
|
90
|
+
reasonBadge = pc.green('[completed]');
|
|
91
|
+
break;
|
|
92
|
+
case 'abandoned':
|
|
93
|
+
reasonBadge = pc.yellow('[abandoned]');
|
|
94
|
+
break;
|
|
95
|
+
case 'missing':
|
|
96
|
+
reasonBadge = pc.red('[missing]');
|
|
97
|
+
break;
|
|
98
|
+
case 'manual':
|
|
99
|
+
reasonBadge = pc.dim('[manual]');
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
name: `${description} ${reasonBadge}\n ${pc.dim(worktreeName)}`,
|
|
104
|
+
value: c,
|
|
105
|
+
checked: c.reason === 'completed' || c.reason === 'missing',
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
const selected = await checkbox({
|
|
109
|
+
message: 'Select worktrees to remove:',
|
|
110
|
+
choices,
|
|
111
|
+
pageSize: 10,
|
|
112
|
+
});
|
|
113
|
+
if (selected.length === 0) {
|
|
114
|
+
console.log(pc.dim('\nNo worktrees selected. Nothing to clean up.\n'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Confirm
|
|
118
|
+
const confirmed = await confirm({
|
|
119
|
+
message: `Remove ${selected.length} worktree(s)?`,
|
|
120
|
+
default: false,
|
|
121
|
+
});
|
|
122
|
+
if (!confirmed) {
|
|
123
|
+
console.log(pc.dim('\nCancelled.\n'));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Remove selected worktrees
|
|
127
|
+
console.log();
|
|
128
|
+
for (const candidate of selected) {
|
|
129
|
+
const { worktree } = candidate;
|
|
130
|
+
const worktreeName = worktree.path.split('/').pop() || worktree.path;
|
|
131
|
+
try {
|
|
132
|
+
if (candidate.reason === 'missing') {
|
|
133
|
+
// Just remove from state
|
|
134
|
+
removeWorktree(worktree.taskId);
|
|
135
|
+
console.log(pc.green(`✓ Removed reference: ${worktreeName}`));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Remove actual worktree
|
|
139
|
+
await removeWorktreeDir(worktree.path, worktree.projectRoot);
|
|
140
|
+
console.log(pc.green(`✓ Removed: ${worktreeName}`));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
const error = err;
|
|
145
|
+
console.log(pc.red(`✗ Failed to remove ${worktreeName}: ${error.message}`));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
console.log();
|
|
149
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function setupCommand(): Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { isDamperMcpConfigured, getConfiguredApiKey, setupDamperMcp, isClaudeInstalled, } from '../services/claude.js';
|
|
3
|
+
import { createDamperApi } from '../services/damper-api.js';
|
|
4
|
+
export async function setupCommand() {
|
|
5
|
+
console.log(pc.bold('\n@damper/cli Setup\n'));
|
|
6
|
+
// Check Claude CLI
|
|
7
|
+
const claudeInstalled = await isClaudeInstalled();
|
|
8
|
+
if (!claudeInstalled) {
|
|
9
|
+
console.log(pc.red('✗ Claude Code CLI not found'));
|
|
10
|
+
console.log(pc.dim(' Install it with: npm install -g @anthropic-ai/claude-code\n'));
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
console.log(pc.green('✓ Claude Code CLI installed'));
|
|
14
|
+
}
|
|
15
|
+
// Check MCP configuration
|
|
16
|
+
const mcpConfigured = isDamperMcpConfigured();
|
|
17
|
+
const apiKey = getConfiguredApiKey();
|
|
18
|
+
if (mcpConfigured && apiKey) {
|
|
19
|
+
console.log(pc.green('✓ Damper MCP configured'));
|
|
20
|
+
console.log(pc.dim(` API key: ${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`));
|
|
21
|
+
// Verify the API key works
|
|
22
|
+
console.log(pc.dim('\nVerifying API key...'));
|
|
23
|
+
try {
|
|
24
|
+
const api = createDamperApi(apiKey);
|
|
25
|
+
const { project } = await api.listTasks({ limit: 1 });
|
|
26
|
+
console.log(pc.green(`✓ Connected to project: ${project}`));
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const error = err;
|
|
30
|
+
console.log(pc.red(`✗ API key verification failed: ${error.message}`));
|
|
31
|
+
console.log(pc.dim(' You may need to reconfigure with a valid key.\n'));
|
|
32
|
+
// Offer to reconfigure
|
|
33
|
+
const key = await setupDamperMcp();
|
|
34
|
+
if (key) {
|
|
35
|
+
console.log(pc.green('\n✓ Setup complete!\n'));
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.log(pc.green('\n✓ All checks passed! You\'re ready to use @damper/cli.\n'));
|
|
40
|
+
console.log(pc.dim('Run `npx @damper/cli` to start working on a task.\n'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Not configured - run setup
|
|
44
|
+
if (!mcpConfigured) {
|
|
45
|
+
console.log(pc.yellow('○ Damper MCP not configured'));
|
|
46
|
+
}
|
|
47
|
+
else if (!apiKey) {
|
|
48
|
+
console.log(pc.yellow('○ Damper MCP configured but missing API key'));
|
|
49
|
+
}
|
|
50
|
+
const key = await setupDamperMcp();
|
|
51
|
+
if (key) {
|
|
52
|
+
// Verify the new key
|
|
53
|
+
console.log(pc.dim('\nVerifying API key...'));
|
|
54
|
+
try {
|
|
55
|
+
const api = createDamperApi(key);
|
|
56
|
+
const { project } = await api.listTasks({ limit: 1 });
|
|
57
|
+
console.log(pc.green(`✓ Connected to project: ${project}`));
|
|
58
|
+
console.log(pc.green('\n✓ Setup complete! You\'re ready to use @damper/cli.\n'));
|
|
59
|
+
console.log(pc.dim('Run `npx @damper/cli` to start working on a task.\n'));
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const error = err;
|
|
63
|
+
console.log(pc.red(`✗ API key verification failed: ${error.message}`));
|
|
64
|
+
console.log(pc.dim(' Please check your API key and try again.\n'));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { createDamperApi } from '../services/damper-api.js';
|
|
4
|
+
import { createWorktree, getGitRoot } from '../services/worktree.js';
|
|
5
|
+
import { bootstrapContext, refreshContext } from '../services/context-bootstrap.js';
|
|
6
|
+
import { pickTask } from '../ui/task-picker.js';
|
|
7
|
+
import { launchClaude, isClaudeInstalled, ensureMcpConfigured } from '../services/claude.js';
|
|
8
|
+
import { getWorktreesForProject, cleanupStaleWorktrees } from '../services/state.js';
|
|
9
|
+
export async function startCommand(options) {
|
|
10
|
+
// Check Claude CLI first
|
|
11
|
+
const claudeInstalled = await isClaudeInstalled();
|
|
12
|
+
if (!claudeInstalled) {
|
|
13
|
+
console.log(pc.red('\nError: Claude Code CLI not found.'));
|
|
14
|
+
console.log(pc.dim('Install it with: npm install -g @anthropic-ai/claude-code\n'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
// Ensure MCP is configured (will prompt for setup if not)
|
|
18
|
+
// This also handles API key setup
|
|
19
|
+
const apiKey = await ensureMcpConfigured();
|
|
20
|
+
// Get project root (git root)
|
|
21
|
+
let projectRoot;
|
|
22
|
+
try {
|
|
23
|
+
projectRoot = await getGitRoot(process.cwd());
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
console.log(pc.red('\nError: Not in a git repository.'));
|
|
27
|
+
console.log(pc.dim('Run this command from within a git repository.\n'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
// Create API client with the configured key
|
|
31
|
+
const api = createDamperApi(apiKey);
|
|
32
|
+
// Clean up stale worktrees
|
|
33
|
+
const stale = cleanupStaleWorktrees();
|
|
34
|
+
if (stale.length > 0) {
|
|
35
|
+
console.log(pc.dim(`Cleaned up ${stale.length} stale worktree reference(s)`));
|
|
36
|
+
}
|
|
37
|
+
// Get existing worktrees for this project
|
|
38
|
+
const worktrees = getWorktreesForProject(projectRoot);
|
|
39
|
+
let taskId;
|
|
40
|
+
let taskTitle;
|
|
41
|
+
let isResume = false;
|
|
42
|
+
let worktreePath;
|
|
43
|
+
if (options.taskId) {
|
|
44
|
+
// Direct task selection
|
|
45
|
+
taskId = options.taskId;
|
|
46
|
+
const task = await api.getTask(taskId);
|
|
47
|
+
taskTitle = task.title;
|
|
48
|
+
// Check if we have an existing worktree for this task
|
|
49
|
+
const existingWorktree = worktrees.find(w => w.taskId === taskId);
|
|
50
|
+
if (existingWorktree && fs.existsSync(existingWorktree.path)) {
|
|
51
|
+
isResume = true;
|
|
52
|
+
worktreePath = existingWorktree.path;
|
|
53
|
+
console.log(pc.cyan(`\nResuming task #${taskId}: ${taskTitle}`));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.log(pc.cyan(`\nStarting task #${taskId}: ${taskTitle}`));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Interactive task picker
|
|
61
|
+
const result = await pickTask({
|
|
62
|
+
api,
|
|
63
|
+
worktrees,
|
|
64
|
+
typeFilter: options.type,
|
|
65
|
+
statusFilter: options.status,
|
|
66
|
+
});
|
|
67
|
+
if (!result) {
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
taskId = result.task.id;
|
|
71
|
+
taskTitle = result.task.title;
|
|
72
|
+
isResume = result.isResume;
|
|
73
|
+
if (result.worktree) {
|
|
74
|
+
worktreePath = result.worktree.path;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (isResume && worktreePath) {
|
|
78
|
+
// Resume existing worktree
|
|
79
|
+
console.log(pc.green(`\n✓ Resuming: #${taskId} ${taskTitle}`));
|
|
80
|
+
console.log(pc.dim(` Worktree: ${worktreePath}`));
|
|
81
|
+
// Refresh context with latest from Damper
|
|
82
|
+
console.log(pc.dim('\nRefreshing context from Damper...'));
|
|
83
|
+
await refreshContext({
|
|
84
|
+
api,
|
|
85
|
+
taskId,
|
|
86
|
+
worktreePath,
|
|
87
|
+
});
|
|
88
|
+
console.log(pc.green('✓ Updated TASK_CONTEXT.md with latest notes'));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// New task - lock it first
|
|
92
|
+
console.log(pc.dim('\nLocking task in Damper...'));
|
|
93
|
+
try {
|
|
94
|
+
await api.startTask(taskId);
|
|
95
|
+
console.log(pc.green('✓ Task locked'));
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const error = err;
|
|
99
|
+
if (error.lockInfo) {
|
|
100
|
+
console.log(pc.yellow(`\n⚠️ Task is locked by "${error.lockInfo.lockedBy}" since ${error.lockInfo.lockedAt}`));
|
|
101
|
+
console.log(pc.dim('Use --force to take over the lock (if the other agent abandoned it).\n'));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
// Create worktree
|
|
107
|
+
console.log(pc.dim('\nSetting up worktree...'));
|
|
108
|
+
const worktreeResult = await createWorktree({
|
|
109
|
+
taskId,
|
|
110
|
+
taskTitle,
|
|
111
|
+
projectRoot,
|
|
112
|
+
});
|
|
113
|
+
worktreePath = worktreeResult.path;
|
|
114
|
+
if (worktreeResult.isNew) {
|
|
115
|
+
console.log(pc.green(`✓ Created worktree: ${worktreePath}`));
|
|
116
|
+
console.log(pc.dim(` Branch: ${worktreeResult.branch}`));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(pc.green(`✓ Using existing worktree: ${worktreePath}`));
|
|
120
|
+
}
|
|
121
|
+
// Bootstrap context
|
|
122
|
+
console.log(pc.dim('\nBootstrapping context from Damper...'));
|
|
123
|
+
const bootstrapResult = await bootstrapContext({
|
|
124
|
+
api,
|
|
125
|
+
taskId,
|
|
126
|
+
worktreePath,
|
|
127
|
+
});
|
|
128
|
+
console.log(pc.green(`✓ Created ${bootstrapResult.taskContextPath}`));
|
|
129
|
+
if (bootstrapResult.claudeMdUpdated) {
|
|
130
|
+
console.log(pc.green('✓ Updated CLAUDE.md with task section'));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Launch Claude
|
|
134
|
+
await launchClaude({
|
|
135
|
+
cwd: worktreePath,
|
|
136
|
+
taskId,
|
|
137
|
+
taskTitle,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function statusCommand(): Promise<void>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { getWorktrees, cleanupStaleWorktrees } from '../services/state.js';
|
|
4
|
+
import { createDamperApi } from '../services/damper-api.js';
|
|
5
|
+
import { getGitRoot } from '../services/worktree.js';
|
|
6
|
+
export async function statusCommand() {
|
|
7
|
+
// Clean up stale entries first
|
|
8
|
+
const stale = cleanupStaleWorktrees();
|
|
9
|
+
if (stale.length > 0) {
|
|
10
|
+
console.log(pc.dim(`Cleaned up ${stale.length} stale worktree reference(s)\n`));
|
|
11
|
+
}
|
|
12
|
+
// Get current project root
|
|
13
|
+
let projectRoot;
|
|
14
|
+
try {
|
|
15
|
+
projectRoot = await getGitRoot(process.cwd());
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Not in a git repo - show all worktrees
|
|
19
|
+
}
|
|
20
|
+
const worktrees = getWorktrees();
|
|
21
|
+
if (worktrees.length === 0) {
|
|
22
|
+
console.log(pc.yellow('\nNo tracked worktrees found.'));
|
|
23
|
+
console.log(pc.dim('Use `npx @damper/cli` to start working on a task.\n'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Try to fetch task statuses from Damper
|
|
27
|
+
let api;
|
|
28
|
+
try {
|
|
29
|
+
api = createDamperApi();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// No API key - show worktrees without status
|
|
33
|
+
}
|
|
34
|
+
console.log(pc.bold('\nTracked Worktrees:\n'));
|
|
35
|
+
// Group by project
|
|
36
|
+
const byProject = new Map();
|
|
37
|
+
for (const wt of worktrees) {
|
|
38
|
+
const project = wt.projectRoot;
|
|
39
|
+
if (!byProject.has(project)) {
|
|
40
|
+
byProject.set(project, []);
|
|
41
|
+
}
|
|
42
|
+
byProject.get(project).push(wt);
|
|
43
|
+
}
|
|
44
|
+
for (const [project, projectWorktrees] of byProject) {
|
|
45
|
+
const isCurrent = projectRoot && project === projectRoot;
|
|
46
|
+
const projectName = project.split('/').pop() || project;
|
|
47
|
+
console.log(pc.bold(`${projectName}${isCurrent ? pc.cyan(' (current)') : ''}`));
|
|
48
|
+
console.log(pc.dim(` ${project}`));
|
|
49
|
+
console.log();
|
|
50
|
+
for (const wt of projectWorktrees) {
|
|
51
|
+
const exists = fs.existsSync(wt.path);
|
|
52
|
+
const worktreeName = wt.path.split('/').pop() || wt.path;
|
|
53
|
+
// Try to get task status
|
|
54
|
+
let taskStatus = '';
|
|
55
|
+
if (api) {
|
|
56
|
+
try {
|
|
57
|
+
const task = await api.getTask(wt.taskId);
|
|
58
|
+
const statusColor = task.status === 'done' ? pc.green : task.status === 'in_progress' ? pc.yellow : pc.dim;
|
|
59
|
+
taskStatus = statusColor(`[${task.status}]`);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
taskStatus = pc.dim('[unknown]');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const statusIcon = exists ? pc.green('●') : pc.red('○');
|
|
66
|
+
console.log(` ${statusIcon} ${pc.cyan(`#${wt.taskId}`)} ${taskStatus}`);
|
|
67
|
+
console.log(` ${pc.dim('Path:')} ${worktreeName}`);
|
|
68
|
+
console.log(` ${pc.dim('Branch:')} ${wt.branch}`);
|
|
69
|
+
console.log(` ${pc.dim('Created:')} ${new Date(wt.createdAt).toLocaleDateString()}`);
|
|
70
|
+
console.log();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
console.log(pc.dim('Use `npx @damper/cli cleanup` to remove worktrees for completed tasks.\n'));
|
|
74
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { startCommand } from './commands/start.js';
|
|
4
|
+
import { statusCommand } from './commands/status.js';
|
|
5
|
+
import { cleanupCommand } from './commands/cleanup.js';
|
|
6
|
+
import { setupCommand } from './commands/setup.js';
|
|
7
|
+
const VERSION = '0.1.0';
|
|
8
|
+
function showHelp() {
|
|
9
|
+
console.log(`
|
|
10
|
+
${pc.bold('@damper/cli')} - Agent orchestration for Damper tasks
|
|
11
|
+
|
|
12
|
+
${pc.bold('Usage:')}
|
|
13
|
+
npx @damper/cli Start working on a task (interactive picker)
|
|
14
|
+
npx @damper/cli setup Configure Damper MCP and API key
|
|
15
|
+
npx @damper/cli status Show all in-progress worktrees
|
|
16
|
+
npx @damper/cli cleanup Remove worktrees for completed tasks
|
|
17
|
+
|
|
18
|
+
${pc.bold('Options:')}
|
|
19
|
+
--task <id> Work on a specific task
|
|
20
|
+
--type <type> Filter by type: bug, feature, improvement, task
|
|
21
|
+
--status <status> Filter by status: planned, in_progress, done, all
|
|
22
|
+
-h, --help Show this help message
|
|
23
|
+
-v, --version Show version
|
|
24
|
+
|
|
25
|
+
${pc.bold('Environment:')}
|
|
26
|
+
DAMPER_API_KEY API key (or configure via 'setup' command)
|
|
27
|
+
|
|
28
|
+
${pc.bold('Examples:')}
|
|
29
|
+
npx @damper/cli setup # First-time setup
|
|
30
|
+
npx @damper/cli # Interactive task picker
|
|
31
|
+
npx @damper/cli --task 42 # Start specific task
|
|
32
|
+
npx @damper/cli --type bug # Show only bugs
|
|
33
|
+
npx @damper/cli status # List all worktrees
|
|
34
|
+
npx @damper/cli cleanup # Clean up completed tasks
|
|
35
|
+
|
|
36
|
+
${pc.bold('Workflow:')}
|
|
37
|
+
1. CLI picks task from Damper roadmap
|
|
38
|
+
2. Creates isolated git worktree
|
|
39
|
+
3. Bootstraps context (TASK_CONTEXT.md)
|
|
40
|
+
4. Launches Claude Code
|
|
41
|
+
5. Claude handles task lifecycle via MCP
|
|
42
|
+
|
|
43
|
+
${pc.dim('More info: https://usedamper.com/docs/cli')}
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
function showVersion() {
|
|
47
|
+
console.log(`@damper/cli v${VERSION}`);
|
|
48
|
+
}
|
|
49
|
+
function parseArgs(args) {
|
|
50
|
+
const result = {
|
|
51
|
+
options: {},
|
|
52
|
+
help: false,
|
|
53
|
+
version: false,
|
|
54
|
+
};
|
|
55
|
+
for (let i = 0; i < args.length; i++) {
|
|
56
|
+
const arg = args[i];
|
|
57
|
+
if (arg === '-h' || arg === '--help') {
|
|
58
|
+
result.help = true;
|
|
59
|
+
}
|
|
60
|
+
else if (arg === '-v' || arg === '--version') {
|
|
61
|
+
result.version = true;
|
|
62
|
+
}
|
|
63
|
+
else if (arg === '--task') {
|
|
64
|
+
result.options.taskId = args[++i];
|
|
65
|
+
}
|
|
66
|
+
else if (arg === '--type') {
|
|
67
|
+
const type = args[++i];
|
|
68
|
+
if (type && ['bug', 'feature', 'improvement', 'task'].includes(type)) {
|
|
69
|
+
result.options.type = type;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (arg === '--status') {
|
|
73
|
+
const status = args[++i];
|
|
74
|
+
if (status && ['planned', 'in_progress', 'done', 'all'].includes(status)) {
|
|
75
|
+
result.options.status = status;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (!arg.startsWith('-') && !result.command) {
|
|
79
|
+
result.command = arg;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
async function main() {
|
|
85
|
+
const args = process.argv.slice(2);
|
|
86
|
+
const { command, options, help, version } = parseArgs(args);
|
|
87
|
+
if (version) {
|
|
88
|
+
showVersion();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (help) {
|
|
92
|
+
showHelp();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
switch (command) {
|
|
97
|
+
case 'setup':
|
|
98
|
+
await setupCommand();
|
|
99
|
+
break;
|
|
100
|
+
case 'status':
|
|
101
|
+
await statusCommand();
|
|
102
|
+
break;
|
|
103
|
+
case 'cleanup':
|
|
104
|
+
await cleanupCommand();
|
|
105
|
+
break;
|
|
106
|
+
case undefined:
|
|
107
|
+
case 'start':
|
|
108
|
+
await startCommand(options);
|
|
109
|
+
break;
|
|
110
|
+
default:
|
|
111
|
+
console.log(pc.red(`Unknown command: ${command}`));
|
|
112
|
+
console.log(pc.dim('Run with --help for usage information.\n'));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
const error = err;
|
|
118
|
+
console.error(pc.red(`\nError: ${error.message}`));
|
|
119
|
+
if (process.env.DEBUG) {
|
|
120
|
+
console.error(error.stack);
|
|
121
|
+
}
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
main();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
interface McpServerConfig {
|
|
2
|
+
command: string;
|
|
3
|
+
args?: string[];
|
|
4
|
+
env?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Check if Damper MCP is configured in Claude settings
|
|
8
|
+
*/
|
|
9
|
+
export declare function isDamperMcpConfigured(): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Get the current API key from MCP config or environment
|
|
12
|
+
*/
|
|
13
|
+
export declare function getConfiguredApiKey(): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Get the recommended MCP configuration
|
|
16
|
+
*/
|
|
17
|
+
export declare function getDamperMcpConfig(apiKey?: string): McpServerConfig;
|
|
18
|
+
/**
|
|
19
|
+
* Configure Damper MCP in Claude settings
|
|
20
|
+
*/
|
|
21
|
+
export declare function configureDamperMcp(apiKey: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Interactive setup for Damper MCP
|
|
24
|
+
* Returns the API key (either existing or newly entered)
|
|
25
|
+
*/
|
|
26
|
+
export declare function setupDamperMcp(): Promise<string | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Ensure MCP is configured, offering to set it up if not
|
|
29
|
+
* Returns the API key to use
|
|
30
|
+
*/
|
|
31
|
+
export declare function ensureMcpConfigured(): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Launch Claude Code in a directory
|
|
34
|
+
*/
|
|
35
|
+
export declare function launchClaude(options: {
|
|
36
|
+
cwd: string;
|
|
37
|
+
taskId: string;
|
|
38
|
+
taskTitle: string;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Check if Claude Code CLI is installed
|
|
42
|
+
*/
|
|
43
|
+
export declare function isClaudeInstalled(): Promise<boolean>;
|
|
44
|
+
export {};
|