@damper/cli 0.9.13 → 0.9.15
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 +77 -1
- package/dist/services/claude.js +22 -0
- package/dist/services/state.d.ts +7 -0
- package/dist/services/state.js +25 -0
- 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 } from '../services/state.js';
|
|
8
|
+
import { getWorktreesForProject, cleanupStaleWorktrees, getPreference, setPreference } 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,6 +39,8 @@ 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();
|
|
42
44
|
// Ensure MCP is configured globally (without key - key comes from env)
|
|
43
45
|
if (!isDamperMcpConfigured()) {
|
|
44
46
|
console.log(pc.dim('Configuring Damper MCP...'));
|
|
@@ -310,3 +312,77 @@ async function handleReviewAndComplete(options) {
|
|
|
310
312
|
}
|
|
311
313
|
console.log();
|
|
312
314
|
}
|
|
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
|
@@ -81,6 +81,25 @@ export function configureDamperMcp() {
|
|
|
81
81
|
// Write back
|
|
82
82
|
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Send a notification when Claude finishes.
|
|
86
|
+
* - BEL: triggers terminal bell (Ghostty: sound + dock badge)
|
|
87
|
+
* - macOS notification: shows a system banner via osascript
|
|
88
|
+
*/
|
|
89
|
+
function notifySessionEnd(label) {
|
|
90
|
+
// Terminal bell — triggers Ghostty sound/badge, works in any terminal
|
|
91
|
+
if (process.stdout.isTTY) {
|
|
92
|
+
process.stdout.write('\x07');
|
|
93
|
+
}
|
|
94
|
+
// macOS desktop notification (fire-and-forget)
|
|
95
|
+
if (process.platform === 'darwin') {
|
|
96
|
+
const escaped = label.replace(/"/g, '\\"');
|
|
97
|
+
spawn('osascript', [
|
|
98
|
+
'-e',
|
|
99
|
+
`display notification "${escaped}" with title "Damper" sound name "Glass"`,
|
|
100
|
+
], { stdio: 'ignore', detached: true }).unref();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
84
103
|
/**
|
|
85
104
|
* Check if tmux is available
|
|
86
105
|
*/
|
|
@@ -229,6 +248,7 @@ export async function launchClaude(options) {
|
|
|
229
248
|
});
|
|
230
249
|
}
|
|
231
250
|
clearTerminalTitle();
|
|
251
|
+
notifySessionEnd(`Claude finished: ${taskLabel}`);
|
|
232
252
|
console.log(pc.dim('\n─────────────────────────────────────────\n'));
|
|
233
253
|
return { cwd, taskId, apiKey };
|
|
234
254
|
}
|
|
@@ -634,6 +654,7 @@ export async function launchClaudeForReview(options) {
|
|
|
634
654
|
});
|
|
635
655
|
}
|
|
636
656
|
clearTerminalTitle();
|
|
657
|
+
notifySessionEnd(`Review finished: ${reviewLabel}`);
|
|
637
658
|
}
|
|
638
659
|
/**
|
|
639
660
|
* Launch Claude to resolve merge conflicts
|
|
@@ -667,6 +688,7 @@ async function launchClaudeForMerge(options) {
|
|
|
667
688
|
});
|
|
668
689
|
}
|
|
669
690
|
clearTerminalTitle();
|
|
691
|
+
notifySessionEnd('Merge conflict resolution finished');
|
|
670
692
|
}
|
|
671
693
|
/**
|
|
672
694
|
* Check if Claude Code CLI is installed
|
package/dist/services/state.d.ts
CHANGED
|
@@ -13,3 +13,10 @@ export declare function removeWorktree(taskId: string): void;
|
|
|
13
13
|
export declare function removeWorktreeByPath(worktreePath: string): void;
|
|
14
14
|
export declare function cleanupStaleWorktrees(): WorktreeState[];
|
|
15
15
|
export declare function getWorktreesForProject(projectRoot: string): WorktreeState[];
|
|
16
|
+
interface Preferences {
|
|
17
|
+
tmuxPromptDismissed?: boolean;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
export declare function getPreference<K extends keyof Preferences>(key: K): Preferences[K];
|
|
21
|
+
export declare function setPreference<K extends keyof Preferences>(key: K, value: Preferences[K]): void;
|
|
22
|
+
export {};
|
package/dist/services/state.js
CHANGED
|
@@ -75,3 +75,28 @@ export function getWorktreesForProject(projectRoot) {
|
|
|
75
75
|
const normalized = path.resolve(projectRoot);
|
|
76
76
|
return readState().worktrees.filter(w => path.resolve(w.projectRoot) === normalized);
|
|
77
77
|
}
|
|
78
|
+
// ── Preferences (persistent user choices) ──
|
|
79
|
+
const PREFS_FILE = path.join(STATE_DIR, 'preferences.json');
|
|
80
|
+
function readPrefs() {
|
|
81
|
+
ensureStateDir();
|
|
82
|
+
if (!fs.existsSync(PREFS_FILE))
|
|
83
|
+
return {};
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(fs.readFileSync(PREFS_FILE, 'utf-8'));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function writePrefs(prefs) {
|
|
92
|
+
ensureStateDir();
|
|
93
|
+
fs.writeFileSync(PREFS_FILE, JSON.stringify(prefs, null, 2));
|
|
94
|
+
}
|
|
95
|
+
export function getPreference(key) {
|
|
96
|
+
return readPrefs()[key];
|
|
97
|
+
}
|
|
98
|
+
export function setPreference(key, value) {
|
|
99
|
+
const prefs = readPrefs();
|
|
100
|
+
prefs[key] = value;
|
|
101
|
+
writePrefs(prefs);
|
|
102
|
+
}
|