@damper/cli 0.9.19 → 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 +81 -88
- 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
package/dist/services/claude.js
CHANGED
|
@@ -31,33 +31,52 @@ export function getConfiguredApiKey() {
|
|
|
31
31
|
if (process.env.DAMPER_API_KEY) {
|
|
32
32
|
return process.env.DAMPER_API_KEY;
|
|
33
33
|
}
|
|
34
|
-
// Then check Claude settings (
|
|
34
|
+
// Then check Claude settings (global MCP config)
|
|
35
35
|
if (!fs.existsSync(CLAUDE_SETTINGS_FILE)) {
|
|
36
36
|
return undefined;
|
|
37
37
|
}
|
|
38
38
|
try {
|
|
39
39
|
const settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'));
|
|
40
|
-
|
|
40
|
+
const config = settings.mcpServers?.damper;
|
|
41
|
+
if (!config)
|
|
42
|
+
return undefined;
|
|
43
|
+
// HTTP config: extract from Authorization header
|
|
44
|
+
if ('type' in config && config.type === 'http') {
|
|
45
|
+
const auth = config.headers?.Authorization;
|
|
46
|
+
return auth?.startsWith('Bearer ') ? auth.slice(7) : undefined;
|
|
47
|
+
}
|
|
48
|
+
// Legacy stdio config: extract from env
|
|
49
|
+
if ('env' in config) {
|
|
50
|
+
return config.env?.DAMPER_API_KEY;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
41
53
|
}
|
|
42
54
|
catch {
|
|
43
55
|
return undefined;
|
|
44
56
|
}
|
|
45
57
|
}
|
|
58
|
+
/** MCP endpoint URL (matches server /mcp route) */
|
|
59
|
+
const MCP_URL = process.env.DAMPER_API_URL
|
|
60
|
+
? `${process.env.DAMPER_API_URL}/mcp`
|
|
61
|
+
: 'https://api.usedamper.com/mcp';
|
|
46
62
|
/**
|
|
47
|
-
* Get the recommended MCP configuration
|
|
48
|
-
* Note: API key is passed via environment when launching Claude, not stored in config
|
|
63
|
+
* Get the recommended MCP configuration (hosted Streamable HTTP)
|
|
49
64
|
*/
|
|
50
|
-
export function getDamperMcpConfig() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
export function getDamperMcpConfig(apiKey) {
|
|
66
|
+
const config = {
|
|
67
|
+
type: 'http',
|
|
68
|
+
url: MCP_URL,
|
|
54
69
|
};
|
|
70
|
+
if (apiKey) {
|
|
71
|
+
config.headers = { Authorization: `Bearer ${apiKey}` };
|
|
72
|
+
}
|
|
73
|
+
return config;
|
|
55
74
|
}
|
|
56
75
|
/**
|
|
57
76
|
* Configure Damper MCP in Claude settings (global)
|
|
58
|
-
*
|
|
77
|
+
* With HTTP transport, the API key is stored in the config headers.
|
|
59
78
|
*/
|
|
60
|
-
export function configureDamperMcp() {
|
|
79
|
+
export function configureDamperMcp(apiKey) {
|
|
61
80
|
// Ensure directory exists
|
|
62
81
|
if (!fs.existsSync(CLAUDE_SETTINGS_DIR)) {
|
|
63
82
|
fs.mkdirSync(CLAUDE_SETTINGS_DIR, { recursive: true });
|
|
@@ -73,11 +92,11 @@ export function configureDamperMcp() {
|
|
|
73
92
|
settings = {};
|
|
74
93
|
}
|
|
75
94
|
}
|
|
76
|
-
// Add/update MCP config
|
|
95
|
+
// Add/update MCP config
|
|
77
96
|
if (!settings.mcpServers) {
|
|
78
97
|
settings.mcpServers = {};
|
|
79
98
|
}
|
|
80
|
-
settings.mcpServers.damper = getDamperMcpConfig();
|
|
99
|
+
settings.mcpServers.damper = getDamperMcpConfig(apiKey);
|
|
81
100
|
// Write back
|
|
82
101
|
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
83
102
|
}
|
|
@@ -120,7 +139,7 @@ export async function launchClaude(options) {
|
|
|
120
139
|
if (!isResume) {
|
|
121
140
|
// Show workflow explanation only for new tasks
|
|
122
141
|
console.log(pc.bold('Workflow:'));
|
|
123
|
-
console.log(pc.dim(' 1. Claude
|
|
142
|
+
console.log(pc.dim(' 1. Claude fetches task details from Damper MCP and creates a plan'));
|
|
124
143
|
if (!yolo) {
|
|
125
144
|
console.log(pc.dim(' 2. You approve the plan before Claude makes changes'));
|
|
126
145
|
}
|
|
@@ -130,8 +149,6 @@ export async function launchClaude(options) {
|
|
|
130
149
|
}
|
|
131
150
|
console.log(pc.bold('When finished:'));
|
|
132
151
|
console.log(pc.dim(' • Push your branch and create a PR if needed'));
|
|
133
|
-
console.log(pc.dim(' • Run: npx @damper/cli cleanup'));
|
|
134
|
-
console.log(pc.dim(' (removes worktree and branch)'));
|
|
135
152
|
console.log();
|
|
136
153
|
if (yolo) {
|
|
137
154
|
console.log(pc.yellow('Mode: YOLO (plan then auto-execute)'));
|
|
@@ -144,7 +161,7 @@ export async function launchClaude(options) {
|
|
|
144
161
|
let args;
|
|
145
162
|
if (isResume) {
|
|
146
163
|
// Resume previous conversation so Claude continues where it left off
|
|
147
|
-
const resumePrompt = `
|
|
164
|
+
const resumePrompt = `Continue working on task #${taskId}: ${taskTitle}. Call get_project_context via Damper MCP to refresh context if needed.`;
|
|
148
165
|
args = yolo
|
|
149
166
|
? ['--continue', '--dangerously-skip-permissions', resumePrompt]
|
|
150
167
|
: ['--continue', resumePrompt];
|
|
@@ -153,9 +170,9 @@ export async function launchClaude(options) {
|
|
|
153
170
|
else {
|
|
154
171
|
// New task - enter plan mode first
|
|
155
172
|
const planInstruction = yolo
|
|
156
|
-
? '
|
|
157
|
-
: '
|
|
158
|
-
const initialPrompt = `IMPORTANT: Start by
|
|
173
|
+
? ' Then use the EnterPlanMode tool to create an implementation plan, then execute it without waiting for user approval.'
|
|
174
|
+
: ' Then use the EnterPlanMode tool to create an implementation plan for user approval before making any code changes.';
|
|
175
|
+
const initialPrompt = `IMPORTANT: Start by calling get_project_context via Damper MCP to fetch architecture and conventions, then call start_task to get task details for task #${taskId}: ${taskTitle}.${planInstruction}`;
|
|
159
176
|
args = yolo ? ['--dangerously-skip-permissions', initialPrompt] : [initialPrompt];
|
|
160
177
|
console.log(pc.dim(`Launching Claude in ${cwd}...`));
|
|
161
178
|
}
|
|
@@ -194,8 +211,6 @@ export async function postTaskFlow(options) {
|
|
|
194
211
|
const { cwd, taskId, apiKey, projectRoot, isNewTask } = options;
|
|
195
212
|
const { confirm, select } = await import('@inquirer/prompts');
|
|
196
213
|
const { createDamperApi } = await import('./damper-api.js');
|
|
197
|
-
const { removeWorktreeDir } = await import('./worktree.js');
|
|
198
|
-
const { removeWorktree } = await import('./state.js');
|
|
199
214
|
// Check task status from Damper
|
|
200
215
|
let taskStatus;
|
|
201
216
|
let taskTitle;
|
|
@@ -212,23 +227,6 @@ export async function postTaskFlow(options) {
|
|
|
212
227
|
catch {
|
|
213
228
|
console.log(pc.yellow('Could not fetch task status from Damper.'));
|
|
214
229
|
}
|
|
215
|
-
// Clean up CLI-generated modifications to tracked files before checking git status
|
|
216
|
-
// The CLI appends a "## Current Task:" section to CLAUDE.md which is never meant to be committed
|
|
217
|
-
try {
|
|
218
|
-
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
219
|
-
if (fs.existsSync(claudeMdPath)) {
|
|
220
|
-
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
221
|
-
const marker = '## Current Task:';
|
|
222
|
-
const markerIndex = content.indexOf(marker);
|
|
223
|
-
if (markerIndex !== -1) {
|
|
224
|
-
const restored = content.slice(0, markerIndex).trimEnd() + '\n';
|
|
225
|
-
fs.writeFileSync(claudeMdPath, restored, 'utf-8');
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
// Ignore cleanup errors
|
|
231
|
-
}
|
|
232
230
|
// Check git status
|
|
233
231
|
let hasUnpushedCommits = false;
|
|
234
232
|
let hasUncommittedChanges = false;
|
|
@@ -288,7 +286,6 @@ export async function postTaskFlow(options) {
|
|
|
288
286
|
}
|
|
289
287
|
// Offer actions based on state
|
|
290
288
|
const hasChanges = hasUnpushedCommits || hasUncommittedChanges;
|
|
291
|
-
let worktreeRemoved = false;
|
|
292
289
|
if (hasChanges) {
|
|
293
290
|
if (hasUncommittedChanges) {
|
|
294
291
|
console.log(pc.yellow('⚠ There are uncommitted changes. You may want to commit them first.\n'));
|
|
@@ -316,7 +313,7 @@ export async function postTaskFlow(options) {
|
|
|
316
313
|
console.log(pc.red('Failed to fetch origin/main. Check your network connection.\n'));
|
|
317
314
|
return;
|
|
318
315
|
}
|
|
319
|
-
// Step 1: Merge origin/main INTO the feature branch
|
|
316
|
+
// Step 1: Merge origin/main INTO the feature branch
|
|
320
317
|
console.log(pc.dim('Merging main into feature branch...'));
|
|
321
318
|
let mainMergedIntoFeature = false;
|
|
322
319
|
try {
|
|
@@ -335,7 +332,7 @@ export async function postTaskFlow(options) {
|
|
|
335
332
|
console.log(pc.dim('Cleaning up merge state...'));
|
|
336
333
|
await execa('git', ['reset', '--merge'], { cwd, stdio: 'pipe' }).catch(() => { });
|
|
337
334
|
}
|
|
338
|
-
await launchClaudeForMerge({ cwd, apiKey });
|
|
335
|
+
await launchClaudeForMerge({ cwd, apiKey, taskTitle });
|
|
339
336
|
// Verify merge was completed (origin/main should be ancestor of HEAD)
|
|
340
337
|
try {
|
|
341
338
|
await execa('git', ['merge-base', '--is-ancestor', 'origin/main', 'HEAD'], { cwd, stdio: 'pipe' });
|
|
@@ -391,36 +388,17 @@ export async function postTaskFlow(options) {
|
|
|
391
388
|
}
|
|
392
389
|
}
|
|
393
390
|
}
|
|
394
|
-
//
|
|
395
|
-
if (taskStatus === '
|
|
396
|
-
const statusText = taskStatus === 'done' ? 'completed' : taskStatus === 'in_review' ? 'in review' : 'not started/abandoned';
|
|
397
|
-
const shouldCleanup = await confirm({
|
|
398
|
-
message: `Task is ${statusText}. Remove worktree and branch?`,
|
|
399
|
-
default: taskStatus === 'done',
|
|
400
|
-
});
|
|
401
|
-
if (shouldCleanup) {
|
|
402
|
-
try {
|
|
403
|
-
await removeWorktreeDir(cwd, projectRoot);
|
|
404
|
-
worktreeRemoved = true;
|
|
405
|
-
console.log(pc.green('✓ Worktree and branch removed\n'));
|
|
406
|
-
}
|
|
407
|
-
catch (err) {
|
|
408
|
-
const error = err;
|
|
409
|
-
console.log(pc.red(`Failed to remove worktree: ${error.message}\n`));
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
else if (taskStatus === 'in_progress') {
|
|
391
|
+
// Handle task lock based on status
|
|
392
|
+
if (taskStatus === 'in_progress') {
|
|
414
393
|
// Task not completed - ask whether to keep lock for later resumption
|
|
415
394
|
const exitAction = await select({
|
|
416
395
|
message: 'Task is still in progress. What would you like to do?',
|
|
417
396
|
choices: [
|
|
418
397
|
{ name: 'Keep lock (resume later)', value: 'keep' },
|
|
419
398
|
{ name: 'Release lock (let others pick it up)', value: 'release' },
|
|
420
|
-
{ name: 'Release lock and remove worktree', value: 'release_cleanup' },
|
|
421
399
|
],
|
|
422
400
|
});
|
|
423
|
-
if (exitAction === 'release'
|
|
401
|
+
if (exitAction === 'release') {
|
|
424
402
|
console.log(pc.dim('\nReleasing task lock...'));
|
|
425
403
|
try {
|
|
426
404
|
const api = createDamperApi(apiKey);
|
|
@@ -431,17 +409,6 @@ export async function postTaskFlow(options) {
|
|
|
431
409
|
const error = err;
|
|
432
410
|
console.log(pc.yellow(`Could not release task: ${error.message}`));
|
|
433
411
|
}
|
|
434
|
-
if (exitAction === 'release_cleanup') {
|
|
435
|
-
try {
|
|
436
|
-
await removeWorktreeDir(cwd, projectRoot);
|
|
437
|
-
worktreeRemoved = true;
|
|
438
|
-
console.log(pc.green('✓ Worktree and branch removed\n'));
|
|
439
|
-
}
|
|
440
|
-
catch (err) {
|
|
441
|
-
const error = err;
|
|
442
|
-
console.log(pc.red(`Failed to remove worktree: ${error.message}\n`));
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
412
|
}
|
|
446
413
|
else {
|
|
447
414
|
console.log(pc.green('✓ Lock kept — resume with:'));
|
|
@@ -477,11 +444,9 @@ export async function postTaskFlow(options) {
|
|
|
477
444
|
const statusColor = taskStatus === 'done' ? pc.green : (taskStatus === 'in_progress' || taskStatus === 'in_review') ? pc.yellow : pc.dim;
|
|
478
445
|
const statusLabel = taskStatus || 'unknown';
|
|
479
446
|
console.log(pc.bold(`\n #${shortIdRaw(taskId)} ${taskTitle || 'Unknown task'}`) + ` ${statusColor(statusLabel)}`);
|
|
480
|
-
if (
|
|
481
|
-
console.log(pc.dim(` ${cwd}`));
|
|
447
|
+
if (taskStatus === 'in_progress') {
|
|
482
448
|
console.log(`\n To continue, run:`);
|
|
483
|
-
console.log(pc.cyan(`
|
|
484
|
-
console.log(pc.dim(` or: npx @damper/cli --task ${taskId}`));
|
|
449
|
+
console.log(pc.cyan(` npx @damper/cli --task ${taskId}`));
|
|
485
450
|
}
|
|
486
451
|
}
|
|
487
452
|
console.log();
|
|
@@ -556,12 +521,13 @@ export async function launchClaudeForReview(options) {
|
|
|
556
521
|
`Review task #${taskId} and determine if it is ready to be marked as complete.`,
|
|
557
522
|
'',
|
|
558
523
|
'Steps:',
|
|
559
|
-
'1.
|
|
560
|
-
'2.
|
|
561
|
-
'3.
|
|
562
|
-
'4.
|
|
563
|
-
'5.
|
|
564
|
-
'6. If
|
|
524
|
+
'1. Call get_project_context via Damper MCP to understand the project',
|
|
525
|
+
'2. Call get_task via Damper MCP to get the task details and requirements',
|
|
526
|
+
'3. Check git log and git diff main...HEAD to review what was done',
|
|
527
|
+
'4. Run the project tests (use `bun run test` from the monorepo root, NEVER `bun test`)',
|
|
528
|
+
'5. Check the completion checklist via get_project_settings MCP tool',
|
|
529
|
+
'6. If everything looks good, call complete_task via MCP with a summary and confirmations',
|
|
530
|
+
'7. If NOT ready, explain what is missing or broken — do NOT call complete_task',
|
|
565
531
|
'',
|
|
566
532
|
'IMPORTANT: Do NOT make any code changes. This is a review-only session.',
|
|
567
533
|
].join('\n');
|
|
@@ -582,15 +548,42 @@ export async function launchClaudeForReview(options) {
|
|
|
582
548
|
/**
|
|
583
549
|
* Launch Claude to resolve merge conflicts
|
|
584
550
|
* Uses --allowedTools to restrict Claude to only git/file operations
|
|
585
|
-
* so it doesn't pick up task context and try to work on the task
|
|
551
|
+
* so it doesn't pick up task context and try to work on the task.
|
|
552
|
+
* Runs with --dangerously-skip-permissions for fully autonomous resolution.
|
|
586
553
|
*/
|
|
587
554
|
async function launchClaudeForMerge(options) {
|
|
588
|
-
const { cwd, apiKey } = options;
|
|
589
|
-
const
|
|
555
|
+
const { cwd, apiKey, taskTitle } = options;
|
|
556
|
+
const taskContext = taskTitle
|
|
557
|
+
? `You are merging a feature branch for task "${taskTitle}" into main.`
|
|
558
|
+
: 'You are merging a feature branch into main.';
|
|
559
|
+
const prompt = [
|
|
560
|
+
`MERGE CONFLICT RESOLUTION — Your ONLY job is to resolve merge conflicts.`,
|
|
561
|
+
'',
|
|
562
|
+
taskContext,
|
|
563
|
+
'',
|
|
564
|
+
'Steps:',
|
|
565
|
+
'1. Run: git merge origin/main --no-edit',
|
|
566
|
+
'2. If there are conflicts, identify all conflicted files with: git diff --name-only --diff-filter=U',
|
|
567
|
+
'3. Read each conflicted file and resolve the conflicts:',
|
|
568
|
+
' - For code conflicts: combine both sides logically, preserving the intent of both the feature branch changes and main branch updates',
|
|
569
|
+
' - For package.json / lock files: accept the version that satisfies both sides, preferring the newer version',
|
|
570
|
+
'4. Stage all resolved files with: git add <file>',
|
|
571
|
+
'5. Commit the merge with: git commit --no-edit',
|
|
572
|
+
'6. Verify no conflicts remain with: git diff --name-only --diff-filter=U',
|
|
573
|
+
'',
|
|
574
|
+
'IMPORTANT:',
|
|
575
|
+
'- Do NOT use any MCP tools — only use Bash, Read, Write, Edit, Glob, Grep',
|
|
576
|
+
'- Do NOT make any changes beyond resolving the merge conflicts',
|
|
577
|
+
'- If a conflict is too complex to resolve confidently, leave it and explain what needs manual attention',
|
|
578
|
+
].join('\n');
|
|
590
579
|
const mergeLabel = 'Resolving merge conflicts';
|
|
591
580
|
setTerminalTitle(mergeLabel);
|
|
592
581
|
await new Promise((resolve) => {
|
|
593
|
-
const child = spawn('claude', [
|
|
582
|
+
const child = spawn('claude', [
|
|
583
|
+
'--allowedTools', 'Bash,Read,Write,Edit,Glob,Grep',
|
|
584
|
+
'--dangerously-skip-permissions',
|
|
585
|
+
prompt,
|
|
586
|
+
], {
|
|
594
587
|
cwd,
|
|
595
588
|
stdio: 'inherit',
|
|
596
589
|
env: { ...process.env, DAMPER_API_KEY: apiKey },
|
package/dist/services/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as os from 'node:os';
|
|
4
|
-
import { getGitRoot } from './
|
|
4
|
+
import { getGitRoot } from './git.js';
|
|
5
5
|
const CONFIG_DIR = '.damper';
|
|
6
6
|
const CONFIG_FILE = 'config.json';
|
|
7
7
|
/**
|
|
@@ -66,7 +66,8 @@ export function getApiKey(projectRoot) {
|
|
|
66
66
|
return getGlobalApiKey();
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Get API key from global MCP settings (
|
|
69
|
+
* Get API key from global MCP settings (fallback)
|
|
70
|
+
* Supports both HTTP (headers.Authorization) and legacy stdio (env.DAMPER_API_KEY) configs.
|
|
70
71
|
*/
|
|
71
72
|
function getGlobalApiKey() {
|
|
72
73
|
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
@@ -75,7 +76,16 @@ function getGlobalApiKey() {
|
|
|
75
76
|
}
|
|
76
77
|
try {
|
|
77
78
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
78
|
-
|
|
79
|
+
const config = settings.mcpServers?.damper;
|
|
80
|
+
if (!config)
|
|
81
|
+
return undefined;
|
|
82
|
+
// HTTP config: extract from Authorization header
|
|
83
|
+
if (config.type === 'http') {
|
|
84
|
+
const auth = config.headers?.Authorization;
|
|
85
|
+
return auth?.startsWith('Bearer ') ? auth.slice(7) : undefined;
|
|
86
|
+
}
|
|
87
|
+
// Legacy stdio config: extract from env
|
|
88
|
+
return config.env?.DAMPER_API_KEY;
|
|
79
89
|
}
|
|
80
90
|
catch {
|
|
81
91
|
return undefined;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
/**
|
|
3
|
+
* Get the git root directory for the given path
|
|
4
|
+
*/
|
|
5
|
+
export async function getGitRoot(dir) {
|
|
6
|
+
const { stdout } = await execa('git', ['rev-parse', '--show-toplevel'], {
|
|
7
|
+
cwd: dir,
|
|
8
|
+
stdio: 'pipe',
|
|
9
|
+
});
|
|
10
|
+
return stdout.trim();
|
|
11
|
+
}
|
package/dist/ui/format.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare function padStart(str: string, width: number): string;
|
|
|
9
9
|
export declare function shortId(id: string): string;
|
|
10
10
|
/** Plain 8-char slice (for use inside other color wrappers) */
|
|
11
11
|
export declare function shortIdRaw(id: string): string;
|
|
12
|
-
/** Type
|
|
12
|
+
/** Type indicator — colored circles */
|
|
13
13
|
export declare function getTypeIcon(type: string): string;
|
|
14
14
|
/** Priority prefix: red/yellow circle + space, or empty */
|
|
15
15
|
export declare function getPriorityIcon(priority: string): string;
|
package/dist/ui/format.js
CHANGED
|
@@ -22,13 +22,13 @@ export function shortId(id) {
|
|
|
22
22
|
export function shortIdRaw(id) {
|
|
23
23
|
return id.slice(0, 8);
|
|
24
24
|
}
|
|
25
|
-
/** Type
|
|
25
|
+
/** Type indicator — colored circles */
|
|
26
26
|
export function getTypeIcon(type) {
|
|
27
27
|
switch (type) {
|
|
28
|
-
case 'bug': return '\u{
|
|
29
|
-
case 'feature': return '\u{
|
|
30
|
-
case 'improvement': return '\u{
|
|
31
|
-
default: return '\u{
|
|
28
|
+
case 'bug': return '\u{1F534}';
|
|
29
|
+
case 'feature': return '\u{1F535}';
|
|
30
|
+
case 'improvement': return '\u{1F7E3}';
|
|
31
|
+
default: return '\u{26AA}';
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
/** Priority prefix: red/yellow circle + space, or empty */
|
package/dist/ui/task-picker.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { Task, DamperApi } from '../services/damper-api.js';
|
|
2
|
-
import type { WorktreeState } from '../services/state.js';
|
|
3
2
|
interface TaskPickerOptions {
|
|
4
3
|
api: DamperApi;
|
|
5
|
-
worktrees: WorktreeState[];
|
|
6
4
|
typeFilter?: 'bug' | 'feature' | 'improvement' | 'task';
|
|
7
5
|
statusFilter?: 'planned' | 'in_progress' | 'in_review' | 'done' | 'all';
|
|
8
6
|
}
|
|
9
7
|
interface TaskPickerResult {
|
|
10
8
|
task: Task;
|
|
11
|
-
worktree?: WorktreeState;
|
|
12
9
|
isResume: boolean;
|
|
13
10
|
forceTakeover?: boolean;
|
|
14
11
|
isNewTask?: boolean;
|
package/dist/ui/task-picker.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { search, select, confirm, input, Separator } from '@inquirer/prompts';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import * as readline from 'readline';
|
|
4
|
-
import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort, formatProgressCompact, formatDueDate, sectionHeader,
|
|
4
|
+
import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort, formatProgressCompact, formatDueDate, sectionHeader, getTerminalWidth, padEnd, padStart, } from './format.js';
|
|
5
5
|
// Layout constants (terminal column widths)
|
|
6
6
|
const CURSOR_WIDTH = 2; // inquirer select prefix (❯ or )
|
|
7
7
|
const PRIORITY_WIDTH = 3; // emoji(2) + space(1), or 3 spaces
|
|
@@ -88,18 +88,7 @@ function formatTaskChoice(choice, titleWidth, layout) {
|
|
|
88
88
|
const firstLine = formatTableRow(choice.task, titleWidth, layout);
|
|
89
89
|
const indent = ' '.repeat(INDENT_WIDTH);
|
|
90
90
|
if (choice.type === 'in_progress') {
|
|
91
|
-
|
|
92
|
-
const time = relativeTime(choice.worktree.createdAt);
|
|
93
|
-
const metaParts = [worktreeName];
|
|
94
|
-
if (time)
|
|
95
|
-
metaParts.push(time);
|
|
96
|
-
let description = `${firstLine}\n${indent}${pc.dim(metaParts.join(' \u00B7 '))}`;
|
|
97
|
-
if (choice.lastNote) {
|
|
98
|
-
const maxNoteWidth = titleWidth;
|
|
99
|
-
const note = choice.lastNote.length > maxNoteWidth ? choice.lastNote.slice(0, maxNoteWidth - 3) + '...' : choice.lastNote;
|
|
100
|
-
description += `\n${indent}${pc.dim(`Last: "${note}"`)}`;
|
|
101
|
-
}
|
|
102
|
-
return description;
|
|
91
|
+
return firstLine;
|
|
103
92
|
}
|
|
104
93
|
if (choice.type === 'locked') {
|
|
105
94
|
return `${firstLine}\n${indent}${pc.yellow(`locked by ${choice.lockedBy}`)}`;
|
|
@@ -118,7 +107,7 @@ function formatTaskChoice(choice, titleWidth, layout) {
|
|
|
118
107
|
return firstLine;
|
|
119
108
|
}
|
|
120
109
|
export async function pickTask(options) {
|
|
121
|
-
const { api,
|
|
110
|
+
const { api, typeFilter, statusFilter } = options;
|
|
122
111
|
// Fetch all tasks from Damper (paginated)
|
|
123
112
|
const { tasks, project } = await api.listAllTasks({
|
|
124
113
|
status: statusFilter || 'all',
|
|
@@ -128,33 +117,15 @@ export async function pickTask(options) {
|
|
|
128
117
|
// Filter out completed tasks unless specifically requested
|
|
129
118
|
const availableTasks = tasks.filter(t => statusFilter === 'done' || statusFilter === 'all' ||
|
|
130
119
|
(t.status === 'planned' || t.status === 'in_progress' || t.status === 'in_review'));
|
|
131
|
-
//
|
|
120
|
+
// Categorize tasks
|
|
132
121
|
const inProgressChoices = [];
|
|
133
|
-
const worktreeTaskIds = new Set();
|
|
134
|
-
for (const worktree of worktrees) {
|
|
135
|
-
const task = tasks.find(t => t.id === worktree.taskId);
|
|
136
|
-
if (task) {
|
|
137
|
-
worktreeTaskIds.add(task.id);
|
|
138
|
-
inProgressChoices.push({
|
|
139
|
-
type: 'in_progress',
|
|
140
|
-
task,
|
|
141
|
-
worktree,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// Separate available tasks from locked tasks
|
|
146
122
|
const availableChoices = [];
|
|
147
123
|
const lockedChoices = [];
|
|
148
124
|
for (const task of availableTasks) {
|
|
149
|
-
if (worktreeTaskIds.has(task.id))
|
|
150
|
-
continue;
|
|
151
125
|
const taskAny = task;
|
|
152
126
|
if (task.status === 'in_progress' && taskAny.lockedBy) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
task,
|
|
156
|
-
lockedBy: taskAny.lockedBy,
|
|
157
|
-
});
|
|
127
|
+
// Show as "in progress" (resumable) or "locked" depending on who locked it
|
|
128
|
+
inProgressChoices.push({ type: 'in_progress', task });
|
|
158
129
|
}
|
|
159
130
|
else if (task.status === 'planned' || task.status === 'in_progress' || task.status === 'in_review') {
|
|
160
131
|
availableChoices.push({ type: 'available', task });
|
|
@@ -229,7 +200,6 @@ export async function pickTask(options) {
|
|
|
229
200
|
});
|
|
230
201
|
return {
|
|
231
202
|
task: selected.task,
|
|
232
|
-
worktree: selected.worktree,
|
|
233
203
|
isResume: true,
|
|
234
204
|
action,
|
|
235
205
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@damper/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "CLI tool for orchestrating Damper task workflows with Claude Code",
|
|
5
5
|
"author": "Damper <hello@usedamper.com>",
|
|
6
6
|
"repository": {
|
|
@@ -44,8 +44,7 @@
|
|
|
44
44
|
"cli",
|
|
45
45
|
"claude",
|
|
46
46
|
"ai",
|
|
47
|
-
"task-management"
|
|
48
|
-
"worktree"
|
|
47
|
+
"task-management"
|
|
49
48
|
],
|
|
50
49
|
"license": "MIT"
|
|
51
50
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function cleanupCommand(): Promise<void>;
|