@damper/cli 0.9.17 → 0.9.19
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/start.js +1 -77
- package/dist/services/claude.js +32 -125
- package/dist/ui/task-picker.js +39 -3
- package/package.json +1 -1
package/dist/commands/start.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createWorktree, getMainProjectRoot, removeWorktreeDir } from '../servic
|
|
|
5
5
|
import { bootstrapContext, refreshContext } from '../services/context-bootstrap.js';
|
|
6
6
|
import { pickTask } from '../ui/task-picker.js';
|
|
7
7
|
import { launchClaude, launchClaudeForReview, postTaskFlow, isClaudeInstalled, isDamperMcpConfigured, configureDamperMcp } from '../services/claude.js';
|
|
8
|
-
import { getWorktreesForProject, cleanupStaleWorktrees
|
|
8
|
+
import { getWorktreesForProject, cleanupStaleWorktrees } from '../services/state.js';
|
|
9
9
|
import { getApiKey, isProjectConfigured, getProjectConfigPath } from '../services/config.js';
|
|
10
10
|
import { shortIdRaw } from '../ui/format.js';
|
|
11
11
|
export async function startCommand(options) {
|
|
@@ -39,8 +39,6 @@ export async function startCommand(options) {
|
|
|
39
39
|
}
|
|
40
40
|
process.exit(1);
|
|
41
41
|
}
|
|
42
|
-
// Check if tmux is installed (enables persistent task title bar)
|
|
43
|
-
await ensureTmux();
|
|
44
42
|
// Ensure MCP is configured globally (without key - key comes from env)
|
|
45
43
|
if (!isDamperMcpConfigured()) {
|
|
46
44
|
console.log(pc.dim('Configuring Damper MCP...'));
|
|
@@ -312,77 +310,3 @@ async function handleReviewAndComplete(options) {
|
|
|
312
310
|
}
|
|
313
311
|
console.log();
|
|
314
312
|
}
|
|
315
|
-
/**
|
|
316
|
-
* Check if tmux is installed; offer to install or recommend it if not.
|
|
317
|
-
* Skipped if the user previously dismissed the prompt.
|
|
318
|
-
*/
|
|
319
|
-
async function ensureTmux() {
|
|
320
|
-
// Already dismissed?
|
|
321
|
-
if (getPreference('tmuxPromptDismissed'))
|
|
322
|
-
return;
|
|
323
|
-
// Already installed?
|
|
324
|
-
try {
|
|
325
|
-
const { execa } = await import('execa');
|
|
326
|
-
await execa('tmux', ['-V'], { stdio: 'pipe' });
|
|
327
|
-
return; // tmux is available
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
// not installed — continue
|
|
331
|
-
}
|
|
332
|
-
// Check if brew is available
|
|
333
|
-
let hasBrew = false;
|
|
334
|
-
try {
|
|
335
|
-
const { execa } = await import('execa');
|
|
336
|
-
await execa('brew', ['--version'], { stdio: 'pipe' });
|
|
337
|
-
hasBrew = true;
|
|
338
|
-
}
|
|
339
|
-
catch {
|
|
340
|
-
// no brew
|
|
341
|
-
}
|
|
342
|
-
console.log(pc.yellow('\n⚠ tmux not found'));
|
|
343
|
-
console.log(pc.dim(' tmux enables a persistent task title bar while Claude runs.'));
|
|
344
|
-
if (hasBrew) {
|
|
345
|
-
const { select } = await import('@inquirer/prompts');
|
|
346
|
-
const action = await select({
|
|
347
|
-
message: 'Install tmux via Homebrew?',
|
|
348
|
-
choices: [
|
|
349
|
-
{ name: 'Install now (brew install tmux)', value: 'install' },
|
|
350
|
-
{ name: 'Skip for now', value: 'skip' },
|
|
351
|
-
{ name: "Don't ask again", value: 'dismiss' },
|
|
352
|
-
],
|
|
353
|
-
});
|
|
354
|
-
if (action === 'install') {
|
|
355
|
-
console.log(pc.dim('\nInstalling tmux...'));
|
|
356
|
-
try {
|
|
357
|
-
const { execa } = await import('execa');
|
|
358
|
-
await execa('brew', ['install', 'tmux'], { stdio: 'inherit' });
|
|
359
|
-
console.log(pc.green('✓ tmux installed\n'));
|
|
360
|
-
}
|
|
361
|
-
catch {
|
|
362
|
-
console.log(pc.red('Failed to install tmux. You can install it manually: brew install tmux\n'));
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
else if (action === 'dismiss') {
|
|
366
|
-
setPreference('tmuxPromptDismissed', true);
|
|
367
|
-
console.log(pc.dim('You can install it later: brew install tmux\n'));
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
console.log(pc.dim('Continuing without tmux.\n'));
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
else {
|
|
374
|
-
console.log(pc.dim(' Install it with: brew install tmux'));
|
|
375
|
-
console.log(pc.dim(' Or see: https://github.com/tmux/tmux/wiki/Installing\n'));
|
|
376
|
-
const { select } = await import('@inquirer/prompts');
|
|
377
|
-
const action = await select({
|
|
378
|
-
message: 'What would you like to do?',
|
|
379
|
-
choices: [
|
|
380
|
-
{ name: 'Continue without tmux', value: 'skip' },
|
|
381
|
-
{ name: "Don't remind me again", value: 'dismiss' },
|
|
382
|
-
],
|
|
383
|
-
});
|
|
384
|
-
if (action === 'dismiss') {
|
|
385
|
-
setPreference('tmuxPromptDismissed', true);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
package/dist/services/claude.js
CHANGED
|
@@ -104,62 +104,6 @@ function notifySessionEnd(label) {
|
|
|
104
104
|
], { stdio: 'ignore', detached: true }).unref();
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
-
/**
|
|
108
|
-
* Check if tmux is available
|
|
109
|
-
*/
|
|
110
|
-
async function isTmuxAvailable() {
|
|
111
|
-
try {
|
|
112
|
-
await execa('tmux', ['-V'], { stdio: 'pipe' });
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Launch a command inside a tmux session with a persistent status bar.
|
|
121
|
-
* The session auto-exits when the command finishes.
|
|
122
|
-
*/
|
|
123
|
-
async function launchInTmux(options) {
|
|
124
|
-
const { label, command, args, cwd, env } = options;
|
|
125
|
-
const sessionName = `damper-${Date.now()}`;
|
|
126
|
-
// Build the full command string to run inside tmux
|
|
127
|
-
// When the command finishes, tmux exits automatically via remain-on-exit off
|
|
128
|
-
const escapedArgs = args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
|
|
129
|
-
const fullCmd = `${command} ${escapedArgs}`;
|
|
130
|
-
// Create a detached tmux session running the command directly
|
|
131
|
-
// Using tmux new-session with the command means the session dies when the command exits
|
|
132
|
-
await execa('tmux', [
|
|
133
|
-
'new-session', '-d',
|
|
134
|
-
'-s', sessionName,
|
|
135
|
-
'-x', String(process.stdout.columns || 80),
|
|
136
|
-
'-y', String(process.stdout.rows || 24),
|
|
137
|
-
fullCmd,
|
|
138
|
-
], { cwd, env: { ...process.env, ...env }, stdio: 'pipe' });
|
|
139
|
-
// Configure the status bar
|
|
140
|
-
const statusBarCmds = [
|
|
141
|
-
['set-option', '-t', sessionName, 'status', 'on'],
|
|
142
|
-
['set-option', '-t', sessionName, 'status-position', 'top'],
|
|
143
|
-
['set-option', '-t', sessionName, 'status-style', 'bg=#1e1e2e,fg=#cdd6f4'],
|
|
144
|
-
['set-option', '-t', sessionName, 'status-left', ` ${label} `],
|
|
145
|
-
['set-option', '-t', sessionName, 'status-left-length', '100'],
|
|
146
|
-
['set-option', '-t', sessionName, 'status-right', ''],
|
|
147
|
-
];
|
|
148
|
-
for (const cmd of statusBarCmds) {
|
|
149
|
-
await execa('tmux', cmd, { stdio: 'pipe' }).catch(() => { });
|
|
150
|
-
}
|
|
151
|
-
// Attach to the session — blocks until Claude exits and tmux session ends
|
|
152
|
-
await new Promise((resolve) => {
|
|
153
|
-
const child = spawn('tmux', ['attach-session', '-t', sessionName], {
|
|
154
|
-
cwd,
|
|
155
|
-
stdio: 'inherit',
|
|
156
|
-
});
|
|
157
|
-
child.on('error', () => resolve());
|
|
158
|
-
child.on('close', () => resolve());
|
|
159
|
-
});
|
|
160
|
-
// Cleanup in case session lingers
|
|
161
|
-
await execa('tmux', ['kill-session', '-t', sessionName], { stdio: 'pipe' }).catch(() => { });
|
|
162
|
-
}
|
|
163
107
|
/**
|
|
164
108
|
* Launch Claude Code in a directory
|
|
165
109
|
*/
|
|
@@ -215,42 +159,29 @@ export async function launchClaude(options) {
|
|
|
215
159
|
args = yolo ? ['--dangerously-skip-permissions', initialPrompt] : [initialPrompt];
|
|
216
160
|
console.log(pc.dim(`Launching Claude in ${cwd}...`));
|
|
217
161
|
}
|
|
218
|
-
// Set terminal title
|
|
162
|
+
// Set terminal title so task is visible in tab bar
|
|
219
163
|
const taskLabel = `#${shortIdRaw(taskId)}: ${taskTitle}`;
|
|
220
164
|
setTerminalTitle(taskLabel);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
await launchInTmux({
|
|
225
|
-
label: taskLabel,
|
|
226
|
-
command: 'claude',
|
|
227
|
-
args,
|
|
165
|
+
// Launch Claude Code with stdio: 'inherit' for proper TTY passthrough
|
|
166
|
+
await new Promise((resolve) => {
|
|
167
|
+
const child = spawn('claude', args, {
|
|
228
168
|
cwd,
|
|
229
|
-
|
|
169
|
+
stdio: 'inherit',
|
|
170
|
+
env: {
|
|
171
|
+
...process.env,
|
|
172
|
+
DAMPER_API_KEY: apiKey,
|
|
173
|
+
},
|
|
230
174
|
});
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
env: {
|
|
239
|
-
...process.env,
|
|
240
|
-
DAMPER_API_KEY: apiKey,
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
child.on('error', (err) => {
|
|
244
|
-
if (err.code === 'ENOENT') {
|
|
245
|
-
console.log(pc.red('\nError: Claude Code CLI not found.'));
|
|
246
|
-
console.log(pc.dim('Install it with: npm install -g @anthropic-ai/claude-code\n'));
|
|
247
|
-
process.exit(1);
|
|
248
|
-
}
|
|
249
|
-
resolve();
|
|
250
|
-
});
|
|
251
|
-
child.on('close', () => resolve());
|
|
175
|
+
child.on('error', (err) => {
|
|
176
|
+
if (err.code === 'ENOENT') {
|
|
177
|
+
console.log(pc.red('\nError: Claude Code CLI not found.'));
|
|
178
|
+
console.log(pc.dim('Install it with: npm install -g @anthropic-ai/claude-code\n'));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
resolve();
|
|
252
182
|
});
|
|
253
|
-
|
|
183
|
+
child.on('close', () => resolve());
|
|
184
|
+
});
|
|
254
185
|
clearTerminalTitle();
|
|
255
186
|
notifySessionEnd(`Claude finished: ${taskLabel}`);
|
|
256
187
|
console.log(pc.dim('\n─────────────────────────────────────────\n'));
|
|
@@ -636,27 +567,15 @@ export async function launchClaudeForReview(options) {
|
|
|
636
567
|
].join('\n');
|
|
637
568
|
const reviewLabel = `Review #${shortIdRaw(taskId)}`;
|
|
638
569
|
setTerminalTitle(reviewLabel);
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
await launchInTmux({
|
|
642
|
-
label: reviewLabel,
|
|
643
|
-
command: 'claude',
|
|
644
|
-
args: [prompt],
|
|
570
|
+
await new Promise((resolve) => {
|
|
571
|
+
const child = spawn('claude', [prompt], {
|
|
645
572
|
cwd,
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
else {
|
|
650
|
-
await new Promise((resolve) => {
|
|
651
|
-
const child = spawn('claude', [prompt], {
|
|
652
|
-
cwd,
|
|
653
|
-
stdio: 'inherit',
|
|
654
|
-
env: { ...process.env, DAMPER_API_KEY: apiKey },
|
|
655
|
-
});
|
|
656
|
-
child.on('error', () => resolve());
|
|
657
|
-
child.on('close', () => resolve());
|
|
573
|
+
stdio: 'inherit',
|
|
574
|
+
env: { ...process.env, DAMPER_API_KEY: apiKey },
|
|
658
575
|
});
|
|
659
|
-
|
|
576
|
+
child.on('error', () => resolve());
|
|
577
|
+
child.on('close', () => resolve());
|
|
578
|
+
});
|
|
660
579
|
clearTerminalTitle();
|
|
661
580
|
notifySessionEnd(`Review finished: ${reviewLabel}`);
|
|
662
581
|
}
|
|
@@ -670,27 +589,15 @@ async function launchClaudeForMerge(options) {
|
|
|
670
589
|
const prompt = 'IMPORTANT: Your ONLY job is to resolve merge conflicts. Do NOT read TASK_CONTEXT.md or work on any task. Run: git merge origin/main --no-edit. If there are conflicts, resolve them, stage, and commit. Do not use any MCP tools.';
|
|
671
590
|
const mergeLabel = 'Resolving merge conflicts';
|
|
672
591
|
setTerminalTitle(mergeLabel);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
await launchInTmux({
|
|
676
|
-
label: mergeLabel,
|
|
677
|
-
command: 'claude',
|
|
678
|
-
args: ['--allowedTools', 'Bash,Read,Write,Edit,Glob,Grep', prompt],
|
|
592
|
+
await new Promise((resolve) => {
|
|
593
|
+
const child = spawn('claude', ['--allowedTools', 'Bash,Read,Write,Edit,Glob,Grep', prompt], {
|
|
679
594
|
cwd,
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
else {
|
|
684
|
-
await new Promise((resolve) => {
|
|
685
|
-
const child = spawn('claude', ['--allowedTools', 'Bash,Read,Write,Edit,Glob,Grep', prompt], {
|
|
686
|
-
cwd,
|
|
687
|
-
stdio: 'inherit',
|
|
688
|
-
env: { ...process.env, DAMPER_API_KEY: apiKey },
|
|
689
|
-
});
|
|
690
|
-
child.on('error', () => resolve());
|
|
691
|
-
child.on('close', () => resolve());
|
|
595
|
+
stdio: 'inherit',
|
|
596
|
+
env: { ...process.env, DAMPER_API_KEY: apiKey },
|
|
692
597
|
});
|
|
693
|
-
|
|
598
|
+
child.on('error', () => resolve());
|
|
599
|
+
child.on('close', () => resolve());
|
|
600
|
+
});
|
|
694
601
|
clearTerminalTitle();
|
|
695
602
|
notifySessionEnd('Merge conflict resolution finished');
|
|
696
603
|
}
|
package/dist/ui/task-picker.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { search, select, confirm, input, Separator } from '@inquirer/prompts';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
|
+
import * as readline from 'readline';
|
|
3
4
|
import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort, formatProgressCompact, formatDueDate, sectionHeader, relativeTime, getTerminalWidth, padEnd, padStart, } from './format.js';
|
|
4
5
|
// Layout constants (terminal column widths)
|
|
5
6
|
const CURSOR_WIDTH = 2; // inquirer select prefix (❯ or )
|
|
@@ -265,15 +266,50 @@ export async function pickTask(options) {
|
|
|
265
266
|
action,
|
|
266
267
|
};
|
|
267
268
|
}
|
|
269
|
+
function multilineInput(message) {
|
|
270
|
+
return new Promise((resolve) => {
|
|
271
|
+
process.stdout.write(`${pc.bold(pc.green('?'))} ${pc.bold(message)} ${pc.dim('(Enter to skip)')}\n`);
|
|
272
|
+
const lines = [];
|
|
273
|
+
let hintShown = false;
|
|
274
|
+
let hintTimer = null;
|
|
275
|
+
const rl = readline.createInterface({
|
|
276
|
+
input: process.stdin,
|
|
277
|
+
output: process.stdout,
|
|
278
|
+
prompt: '',
|
|
279
|
+
});
|
|
280
|
+
const showHint = () => {
|
|
281
|
+
if (!hintShown) {
|
|
282
|
+
hintShown = true;
|
|
283
|
+
process.stdout.write(pc.dim(' (Press Enter on an empty line to submit)\n'));
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
rl.on('line', (line) => {
|
|
287
|
+
if (hintTimer)
|
|
288
|
+
clearTimeout(hintTimer);
|
|
289
|
+
if (lines.length === 0 && line === '') {
|
|
290
|
+
rl.close();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (line === '' && lines[lines.length - 1] === '') {
|
|
294
|
+
lines.pop(); // remove trailing empty line
|
|
295
|
+
rl.close();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
lines.push(line);
|
|
299
|
+
hintTimer = setTimeout(showHint, 50);
|
|
300
|
+
});
|
|
301
|
+
rl.on('close', () => {
|
|
302
|
+
resolve(lines.join('\n'));
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
268
306
|
async function handleCreateNewTask(api, searchTerm) {
|
|
269
307
|
const title = await input({
|
|
270
308
|
message: 'Task title:',
|
|
271
309
|
default: searchTerm || undefined,
|
|
272
310
|
validate: (value) => value.trim().length > 0 || 'Title is required',
|
|
273
311
|
});
|
|
274
|
-
const description = await
|
|
275
|
-
message: 'Description (optional, press Enter to skip):',
|
|
276
|
-
});
|
|
312
|
+
const description = await multilineInput('Description (optional):');
|
|
277
313
|
const type = await select({
|
|
278
314
|
message: 'Task type:',
|
|
279
315
|
choices: [
|