@damper/cli 0.9.12 → 0.9.13

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.
@@ -4,7 +4,7 @@ import * as os from 'node:os';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { execa } from 'execa';
6
6
  import pc from 'picocolors';
7
- import { shortIdRaw, setTerminalTitle, clearTerminalTitle, TerminalStatusBar } from '../ui/format.js';
7
+ import { shortIdRaw, setTerminalTitle, clearTerminalTitle } from '../ui/format.js';
8
8
  const CLAUDE_SETTINGS_DIR = path.join(os.homedir(), '.claude');
9
9
  const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_SETTINGS_DIR, 'settings.json');
10
10
  /**
@@ -81,6 +81,62 @@ export function configureDamperMcp() {
81
81
  // Write back
82
82
  fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
83
83
  }
84
+ /**
85
+ * Check if tmux is available
86
+ */
87
+ async function isTmuxAvailable() {
88
+ try {
89
+ await execa('tmux', ['-V'], { stdio: 'pipe' });
90
+ return true;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ /**
97
+ * Launch a command inside a tmux session with a persistent status bar.
98
+ * The session auto-exits when the command finishes.
99
+ */
100
+ async function launchInTmux(options) {
101
+ const { label, command, args, cwd, env } = options;
102
+ const sessionName = `damper-${Date.now()}`;
103
+ // Build the full command string to run inside tmux
104
+ // When the command finishes, tmux exits automatically via remain-on-exit off
105
+ const escapedArgs = args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
106
+ const fullCmd = `${command} ${escapedArgs}`;
107
+ // Create a detached tmux session running the command directly
108
+ // Using tmux new-session with the command means the session dies when the command exits
109
+ await execa('tmux', [
110
+ 'new-session', '-d',
111
+ '-s', sessionName,
112
+ '-x', String(process.stdout.columns || 80),
113
+ '-y', String(process.stdout.rows || 24),
114
+ fullCmd,
115
+ ], { cwd, env: { ...process.env, ...env }, stdio: 'pipe' });
116
+ // Configure the status bar
117
+ const statusBarCmds = [
118
+ ['set-option', '-t', sessionName, 'status', 'on'],
119
+ ['set-option', '-t', sessionName, 'status-position', 'top'],
120
+ ['set-option', '-t', sessionName, 'status-style', 'bg=#1e1e2e,fg=#cdd6f4'],
121
+ ['set-option', '-t', sessionName, 'status-left', ` ${label} `],
122
+ ['set-option', '-t', sessionName, 'status-left-length', '100'],
123
+ ['set-option', '-t', sessionName, 'status-right', ''],
124
+ ];
125
+ for (const cmd of statusBarCmds) {
126
+ await execa('tmux', cmd, { stdio: 'pipe' }).catch(() => { });
127
+ }
128
+ // Attach to the session — blocks until Claude exits and tmux session ends
129
+ await new Promise((resolve) => {
130
+ const child = spawn('tmux', ['attach-session', '-t', sessionName], {
131
+ cwd,
132
+ stdio: 'inherit',
133
+ });
134
+ child.on('error', () => resolve());
135
+ child.on('close', () => resolve());
136
+ });
137
+ // Cleanup in case session lingers
138
+ await execa('tmux', ['kill-session', '-t', sessionName], { stdio: 'pipe' }).catch(() => { });
139
+ }
84
140
  /**
85
141
  * Launch Claude Code in a directory
86
142
  */
@@ -136,35 +192,42 @@ export async function launchClaude(options) {
136
192
  args = yolo ? ['--dangerously-skip-permissions', initialPrompt] : [initialPrompt];
137
193
  console.log(pc.dim(`Launching Claude in ${cwd}...`));
138
194
  }
139
- // Set terminal title and bottom status bar
195
+ // Set terminal title (works in tab bar regardless of tmux)
140
196
  const taskLabel = `#${shortIdRaw(taskId)}: ${taskTitle}`;
141
197
  setTerminalTitle(taskLabel);
142
- const statusBar = new TerminalStatusBar(taskLabel);
143
- statusBar.show();
144
- // Launch Claude Code
145
- // Use spawn with stdio: 'inherit' for proper TTY passthrough
146
- // Signals (Ctrl+C, Escape) are handled naturally since child inherits the terminal
147
- await new Promise((resolve) => {
148
- const child = spawn('claude', args, {
198
+ const hasTmux = await isTmuxAvailable();
199
+ if (hasTmux) {
200
+ // Launch inside tmux with a persistent status bar
201
+ await launchInTmux({
202
+ label: taskLabel,
203
+ command: 'claude',
204
+ args,
149
205
  cwd,
150
- stdio: 'inherit',
151
- env: {
152
- ...process.env,
153
- DAMPER_API_KEY: apiKey,
154
- },
206
+ env: { DAMPER_API_KEY: apiKey },
155
207
  });
156
- child.on('error', (err) => {
157
- if (err.code === 'ENOENT') {
158
- console.log(pc.red('\nError: Claude Code CLI not found.'));
159
- console.log(pc.dim('Install it with: npm install -g @anthropic-ai/claude-code\n'));
160
- process.exit(1);
161
- }
162
- resolve();
208
+ }
209
+ else {
210
+ // Fallback: direct spawn (no status bar, just terminal title)
211
+ await new Promise((resolve) => {
212
+ const child = spawn('claude', args, {
213
+ cwd,
214
+ stdio: 'inherit',
215
+ env: {
216
+ ...process.env,
217
+ DAMPER_API_KEY: apiKey,
218
+ },
219
+ });
220
+ child.on('error', (err) => {
221
+ if (err.code === 'ENOENT') {
222
+ console.log(pc.red('\nError: Claude Code CLI not found.'));
223
+ console.log(pc.dim('Install it with: npm install -g @anthropic-ai/claude-code\n'));
224
+ process.exit(1);
225
+ }
226
+ resolve();
227
+ });
228
+ child.on('close', () => resolve());
163
229
  });
164
- child.on('close', () => resolve());
165
- });
166
- // Clean up status bar and terminal title
167
- statusBar.hide();
230
+ }
168
231
  clearTerminalTitle();
169
232
  console.log(pc.dim('\n─────────────────────────────────────────\n'));
170
233
  return { cwd, taskId, apiKey };
@@ -549,18 +612,27 @@ export async function launchClaudeForReview(options) {
549
612
  ].join('\n');
550
613
  const reviewLabel = `Review #${shortIdRaw(taskId)}`;
551
614
  setTerminalTitle(reviewLabel);
552
- const statusBar = new TerminalStatusBar(reviewLabel);
553
- statusBar.show();
554
- await new Promise((resolve) => {
555
- const child = spawn('claude', [prompt], {
615
+ const hasTmux = await isTmuxAvailable();
616
+ if (hasTmux) {
617
+ await launchInTmux({
618
+ label: reviewLabel,
619
+ command: 'claude',
620
+ args: [prompt],
556
621
  cwd,
557
- stdio: 'inherit',
558
- env: { ...process.env, DAMPER_API_KEY: apiKey },
622
+ env: { DAMPER_API_KEY: apiKey },
559
623
  });
560
- child.on('error', () => resolve());
561
- child.on('close', () => resolve());
562
- });
563
- statusBar.hide();
624
+ }
625
+ else {
626
+ await new Promise((resolve) => {
627
+ const child = spawn('claude', [prompt], {
628
+ cwd,
629
+ stdio: 'inherit',
630
+ env: { ...process.env, DAMPER_API_KEY: apiKey },
631
+ });
632
+ child.on('error', () => resolve());
633
+ child.on('close', () => resolve());
634
+ });
635
+ }
564
636
  clearTerminalTitle();
565
637
  }
566
638
  /**
@@ -573,18 +645,27 @@ async function launchClaudeForMerge(options) {
573
645
  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.';
574
646
  const mergeLabel = 'Resolving merge conflicts';
575
647
  setTerminalTitle(mergeLabel);
576
- const statusBar = new TerminalStatusBar(mergeLabel);
577
- statusBar.show();
578
- await new Promise((resolve) => {
579
- const child = spawn('claude', ['--allowedTools', 'Bash,Read,Write,Edit,Glob,Grep', prompt], {
648
+ const hasTmux = await isTmuxAvailable();
649
+ if (hasTmux) {
650
+ await launchInTmux({
651
+ label: mergeLabel,
652
+ command: 'claude',
653
+ args: ['--allowedTools', 'Bash,Read,Write,Edit,Glob,Grep', prompt],
580
654
  cwd,
581
- stdio: 'inherit',
582
- env: { ...process.env, DAMPER_API_KEY: apiKey },
655
+ env: { DAMPER_API_KEY: apiKey },
583
656
  });
584
- child.on('error', () => resolve());
585
- child.on('close', () => resolve());
586
- });
587
- statusBar.hide();
657
+ }
658
+ else {
659
+ await new Promise((resolve) => {
660
+ const child = spawn('claude', ['--allowedTools', 'Bash,Read,Write,Edit,Glob,Grep', prompt], {
661
+ cwd,
662
+ stdio: 'inherit',
663
+ env: { ...process.env, DAMPER_API_KEY: apiKey },
664
+ });
665
+ child.on('error', () => resolve());
666
+ child.on('close', () => resolve());
667
+ });
668
+ }
588
669
  clearTerminalTitle();
589
670
  }
590
671
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damper/cli",
3
- "version": "0.9.12",
3
+ "version": "0.9.13",
4
4
  "description": "CLI tool for orchestrating Damper task workflows with Claude Code",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {