@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.
@@ -1,203 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import { confirm, checkbox } from '@inquirer/prompts';
3
- import { execa } from 'execa';
4
- import pc from 'picocolors';
5
- import { getWorktrees, cleanupStaleWorktrees, removeWorktree } from '../services/state.js';
6
- import { createDamperApi } from '../services/damper-api.js';
7
- import { removeWorktreeDir } from '../services/worktree.js';
8
- import { shortId, shortIdRaw, formatTaskLine } from '../ui/format.js';
9
- async function hasUncommittedChanges(worktreePath) {
10
- try {
11
- const { stdout } = await execa('git', ['status', '--porcelain'], {
12
- cwd: worktreePath,
13
- stdio: 'pipe',
14
- });
15
- return stdout.trim().length > 0;
16
- }
17
- catch {
18
- return false;
19
- }
20
- }
21
- export async function cleanupCommand() {
22
- // Clean up stale entries first
23
- const stale = cleanupStaleWorktrees();
24
- if (stale.length > 0) {
25
- console.log(pc.dim(`Removed ${stale.length} stale worktree reference(s)`));
26
- }
27
- const worktrees = getWorktrees();
28
- if (worktrees.length === 0) {
29
- console.log(pc.yellow('\nNo tracked worktrees found.'));
30
- return;
31
- }
32
- // Try to connect to Damper for task status
33
- let api;
34
- try {
35
- api = createDamperApi();
36
- }
37
- catch {
38
- console.log(pc.yellow('\nWarning: Could not connect to Damper API (no API key).'));
39
- console.log(pc.dim('Will show all worktrees for manual selection.\n'));
40
- }
41
- // Identify cleanup candidates
42
- const candidates = [];
43
- console.log(pc.dim('\nAnalyzing worktrees...'));
44
- for (const wt of worktrees) {
45
- const exists = fs.existsSync(wt.path);
46
- if (!exists) {
47
- // Worktree directory is missing
48
- candidates.push({
49
- worktree: wt,
50
- reason: 'missing',
51
- });
52
- continue;
53
- }
54
- // Check for uncommitted changes
55
- const uncommitted = await hasUncommittedChanges(wt.path);
56
- if (api) {
57
- try {
58
- const task = await api.getTask(wt.taskId);
59
- if (task.status === 'done') {
60
- candidates.push({
61
- worktree: wt,
62
- task,
63
- reason: 'completed',
64
- hasUncommittedChanges: uncommitted,
65
- });
66
- }
67
- else if (task.status === 'planned' && !task.lockedBy) {
68
- // Task was abandoned (planned and not locked)
69
- candidates.push({
70
- worktree: wt,
71
- task,
72
- reason: 'abandoned',
73
- hasUncommittedChanges: uncommitted,
74
- });
75
- }
76
- else if (task.status === 'in_progress') {
77
- // Task is still in progress - can drop it
78
- candidates.push({
79
- worktree: wt,
80
- task,
81
- reason: 'in_progress',
82
- hasUncommittedChanges: uncommitted,
83
- });
84
- }
85
- }
86
- catch {
87
- // Could not fetch task - might be deleted
88
- candidates.push({
89
- worktree: wt,
90
- reason: 'manual',
91
- hasUncommittedChanges: uncommitted,
92
- });
93
- }
94
- }
95
- else {
96
- // No API - show all for manual selection
97
- candidates.push({
98
- worktree: wt,
99
- reason: 'manual',
100
- hasUncommittedChanges: uncommitted,
101
- });
102
- }
103
- }
104
- if (candidates.length === 0) {
105
- console.log(pc.green('\n✓ No worktrees to clean up.\n'));
106
- return;
107
- }
108
- // Show candidates and let user select
109
- console.log(pc.bold('\nWorktrees available for cleanup:\n'));
110
- const choices = candidates.map(c => {
111
- const worktreeName = c.worktree.path.split('/').pop() || c.worktree.path;
112
- let description = c.task ? formatTaskLine(c.task) : shortId(c.worktree.taskId);
113
- let reasonBadge = '';
114
- switch (c.reason) {
115
- case 'completed':
116
- reasonBadge = pc.green('[completed]');
117
- break;
118
- case 'abandoned':
119
- reasonBadge = pc.yellow('[abandoned]');
120
- break;
121
- case 'in_progress':
122
- reasonBadge = pc.cyan('[in progress - will release]');
123
- break;
124
- case 'missing':
125
- reasonBadge = pc.red('[missing]');
126
- break;
127
- case 'manual':
128
- reasonBadge = pc.dim('[manual]');
129
- break;
130
- }
131
- // Warning for uncommitted changes
132
- const uncommittedWarning = c.hasUncommittedChanges
133
- ? pc.red(' ⚠ uncommitted changes')
134
- : '';
135
- return {
136
- name: `${description} ${reasonBadge}${uncommittedWarning}\n ${pc.dim(worktreeName)}`,
137
- value: c,
138
- // Don't auto-check if there are uncommitted changes
139
- checked: (c.reason === 'completed' || c.reason === 'missing') && !c.hasUncommittedChanges,
140
- };
141
- });
142
- const selected = await checkbox({
143
- message: 'Select worktrees to remove:',
144
- choices,
145
- pageSize: 10,
146
- });
147
- if (selected.length === 0) {
148
- console.log(pc.dim('\nNo worktrees selected. Nothing to clean up.\n'));
149
- return;
150
- }
151
- // Check for uncommitted changes in selected worktrees
152
- const withUncommitted = selected.filter(c => c.hasUncommittedChanges);
153
- if (withUncommitted.length > 0) {
154
- console.log(pc.yellow(`\n⚠ Warning: ${withUncommitted.length} worktree(s) have uncommitted changes that will be lost!`));
155
- }
156
- // Confirm
157
- const confirmMessage = withUncommitted.length > 0
158
- ? `Remove ${selected.length} worktree(s)? (${withUncommitted.length} with uncommitted changes)`
159
- : `Remove ${selected.length} worktree(s)?`;
160
- const confirmed = await confirm({
161
- message: confirmMessage,
162
- default: false,
163
- });
164
- if (!confirmed) {
165
- console.log(pc.dim('\nCancelled.\n'));
166
- return;
167
- }
168
- // Remove selected worktrees
169
- console.log();
170
- for (const candidate of selected) {
171
- const { worktree } = candidate;
172
- const worktreeName = worktree.path.split('/').pop() || worktree.path;
173
- try {
174
- // Release the task first if it's in progress
175
- if (candidate.reason === 'in_progress' && api) {
176
- try {
177
- await api.abandonTask(worktree.taskId, 'Dropped via CLI cleanup');
178
- console.log(pc.green(`✓ Released task #${shortIdRaw(worktree.taskId)}`));
179
- }
180
- catch (err) {
181
- const error = err;
182
- console.log(pc.yellow(`⚠ Could not release task: ${error.message}`));
183
- // Continue with worktree removal anyway
184
- }
185
- }
186
- if (candidate.reason === 'missing') {
187
- // Just remove from state
188
- removeWorktree(worktree.taskId);
189
- console.log(pc.green(`✓ Removed reference: ${worktreeName}`));
190
- }
191
- else {
192
- // Remove actual worktree
193
- await removeWorktreeDir(worktree.path, worktree.projectRoot);
194
- console.log(pc.green(`✓ Removed: ${worktreeName}`));
195
- }
196
- }
197
- catch (err) {
198
- const error = err;
199
- console.log(pc.red(`✗ Failed to remove ${worktreeName}: ${error.message}`));
200
- }
201
- }
202
- console.log();
203
- }
@@ -1 +0,0 @@
1
- export declare function statusCommand(): Promise<void>;
@@ -1,94 +0,0 @@
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
- import { shortId, getTypeIcon, getPriorityIcon, formatEffort, formatSubtaskProgress, relativeTime, statusColor as getStatusColor, } from '../ui/format.js';
7
- export async function statusCommand() {
8
- // Clean up stale entries first
9
- const stale = cleanupStaleWorktrees();
10
- if (stale.length > 0) {
11
- console.log(pc.dim(`Cleaned up ${stale.length} stale worktree reference(s)\n`));
12
- }
13
- // Get current project root
14
- let projectRoot;
15
- try {
16
- projectRoot = await getGitRoot(process.cwd());
17
- }
18
- catch {
19
- // Not in a git repo - show all worktrees
20
- }
21
- const worktrees = getWorktrees();
22
- if (worktrees.length === 0) {
23
- console.log(pc.yellow('\nNo tracked worktrees found.'));
24
- console.log(pc.dim('Use `npx @damper/cli` to start working on a task.\n'));
25
- return;
26
- }
27
- // Try to fetch task statuses from Damper
28
- let api;
29
- try {
30
- api = createDamperApi();
31
- }
32
- catch {
33
- // No API key - show worktrees without status
34
- }
35
- console.log(pc.bold('\nTracked Worktrees:\n'));
36
- // Group by project
37
- const byProject = new Map();
38
- for (const wt of worktrees) {
39
- const project = wt.projectRoot;
40
- if (!byProject.has(project)) {
41
- byProject.set(project, []);
42
- }
43
- byProject.get(project).push(wt);
44
- }
45
- for (const [project, projectWorktrees] of byProject) {
46
- const isCurrent = projectRoot && project === projectRoot;
47
- const projectName = project.split('/').pop() || project;
48
- console.log(pc.bold(`${projectName}${isCurrent ? pc.cyan(' (current)') : ''}`));
49
- console.log(pc.dim(` ${project}`));
50
- console.log();
51
- for (const wt of projectWorktrees) {
52
- const exists = fs.existsSync(wt.path);
53
- const statusIcon = exists ? pc.green('●') : pc.red('○');
54
- // Try to get task details from Damper
55
- if (api) {
56
- try {
57
- const task = await api.getTask(wt.taskId);
58
- const priorityIcon = getPriorityIcon(task.priority);
59
- const typeIcon = getTypeIcon(task.type);
60
- const colorFn = getStatusColor(task.status);
61
- const statusBadge = colorFn(`[${task.status}]`);
62
- const effort = formatEffort(task.effort);
63
- const progress = formatSubtaskProgress(task.subtaskProgress);
64
- const time = relativeTime(wt.createdAt);
65
- console.log(` ${statusIcon} ${priorityIcon}${typeIcon} ${shortId(task.id)} ${task.title} ${statusBadge}${effort ? ` ${effort}` : ''}`);
66
- const metaParts = [wt.branch];
67
- if (time)
68
- metaParts.push(time);
69
- if (progress)
70
- metaParts.push(progress);
71
- console.log(` ${metaParts.join(` ${pc.dim('\u00B7')} `)}`);
72
- }
73
- catch {
74
- console.log(` ${statusIcon} ${shortId(wt.taskId)} ${pc.dim('[unknown]')}`);
75
- const time = relativeTime(wt.createdAt);
76
- const metaParts = [wt.branch];
77
- if (time)
78
- metaParts.push(time);
79
- console.log(` ${metaParts.join(` ${pc.dim('\u00B7')} `)}`);
80
- }
81
- }
82
- else {
83
- console.log(` ${statusIcon} ${shortId(wt.taskId)}`);
84
- const time = relativeTime(wt.createdAt);
85
- const metaParts = [wt.branch];
86
- if (time)
87
- metaParts.push(time);
88
- console.log(` ${metaParts.join(` ${pc.dim('\u00B7')} `)}`);
89
- }
90
- console.log();
91
- }
92
- }
93
- console.log(pc.dim('Use `npx @damper/cli cleanup` to remove worktrees for completed tasks.\n'));
94
- }
@@ -1,30 +0,0 @@
1
- import type { DamperApi } from './damper-api.js';
2
- export interface BootstrapOptions {
3
- api: DamperApi;
4
- taskId: string;
5
- worktreePath: string;
6
- yolo?: boolean;
7
- completionChecklist?: string[];
8
- }
9
- export interface BootstrapResult {
10
- taskContextPath: string;
11
- claudeMdUpdated: boolean;
12
- }
13
- export interface SectionBlockIndex {
14
- section: string;
15
- blocks: Array<{
16
- id: string;
17
- heading: string | null;
18
- level: number;
19
- charCount: number;
20
- }>;
21
- totalChars: number;
22
- }
23
- /**
24
- * Fetch all relevant context from Damper and write local files
25
- */
26
- export declare function bootstrapContext(options: BootstrapOptions): Promise<BootstrapResult>;
27
- /**
28
- * Refresh TASK_CONTEXT.md with latest data from Damper (for resume scenarios)
29
- */
30
- export declare function refreshContext(options: BootstrapOptions): Promise<BootstrapResult>;
@@ -1,100 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
- import pc from 'picocolors';
4
- import { generateTaskContext } from '../templates/TASK_CONTEXT.md.js';
5
- import { generateClaudeAppend } from '../templates/CLAUDE_APPEND.md.js';
6
- const TASK_CONTEXT_FILE = 'TASK_CONTEXT.md';
7
- const CLAUDE_MD_FILE = 'CLAUDE.md';
8
- const TASK_SECTION_MARKER = '## Current Task:';
9
- /**
10
- * Fetch block indices for relevant sections.
11
- * Only fetches the headings/structure - no content.
12
- * The agent will use get_section_block_content to load what it needs.
13
- */
14
- async function fetchBlockIndices(api, relevantSections) {
15
- const indices = [];
16
- for (const sectionName of relevantSections) {
17
- try {
18
- const blockIndex = await api.getSectionBlocks(sectionName);
19
- indices.push(blockIndex);
20
- }
21
- catch {
22
- console.log(pc.dim(` Could not fetch blocks for: ${sectionName}`));
23
- }
24
- }
25
- return indices;
26
- }
27
- /**
28
- * Fetch all relevant context from Damper and write local files
29
- */
30
- export async function bootstrapContext(options) {
31
- const { api, taskId, worktreePath } = options;
32
- console.log(pc.dim('Fetching task details...'));
33
- const task = await api.getTask(taskId);
34
- console.log(pc.dim('Fetching project context...'));
35
- const context = await api.getProjectContext(taskId);
36
- // Fetch block indices (not content) for relevant sections
37
- const relevantSections = context.relevantSections || [];
38
- let blockIndices = [];
39
- if (relevantSections.length > 0) {
40
- console.log(pc.dim(`Fetching block indices for ${relevantSections.length} relevant sections...`));
41
- blockIndices = await fetchBlockIndices(api, relevantSections);
42
- }
43
- // Fetch templates
44
- console.log(pc.dim('Fetching templates...'));
45
- const templatesResult = await api.listTemplates();
46
- const templates = templatesResult.isEmpty ? [] : templatesResult.templates;
47
- // Fetch modules
48
- console.log(pc.dim('Fetching modules...'));
49
- const modulesResult = await api.listModules();
50
- const modules = modulesResult.isEmpty ? [] : modulesResult.modules;
51
- // Get agent instructions
52
- console.log(pc.dim('Fetching Damper workflow instructions...'));
53
- const instructions = await api.getAgentInstructions('section');
54
- // Generate TASK_CONTEXT.md
55
- const taskContext = generateTaskContext({
56
- task,
57
- criticalRules: context.criticalRules || [],
58
- completionChecklist: options.completionChecklist,
59
- blockIndices,
60
- templates,
61
- modules,
62
- damperInstructions: instructions.content,
63
- });
64
- // Write TASK_CONTEXT.md
65
- const taskContextPath = path.join(worktreePath, TASK_CONTEXT_FILE);
66
- console.log(pc.dim(`Writing ${TASK_CONTEXT_FILE}...`));
67
- await fs.promises.writeFile(taskContextPath, taskContext, 'utf-8');
68
- // Update CLAUDE.md with task section
69
- const claudeMdPath = path.join(worktreePath, CLAUDE_MD_FILE);
70
- let claudeMdUpdated = false;
71
- if (fs.existsSync(claudeMdPath)) {
72
- console.log(pc.dim(`Updating ${CLAUDE_MD_FILE}...`));
73
- let claudeMd = await fs.promises.readFile(claudeMdPath, 'utf-8');
74
- // Remove any existing task section
75
- const markerIndex = claudeMd.indexOf(TASK_SECTION_MARKER);
76
- if (markerIndex !== -1) {
77
- claudeMd = claudeMd.slice(0, markerIndex).trimEnd();
78
- }
79
- // Append new task section
80
- const taskSection = generateClaudeAppend({
81
- taskId,
82
- taskTitle: task.title,
83
- yolo: options.yolo,
84
- });
85
- claudeMd = `${claudeMd}\n\n${taskSection}\n`;
86
- await fs.promises.writeFile(claudeMdPath, claudeMd, 'utf-8');
87
- claudeMdUpdated = true;
88
- }
89
- return {
90
- taskContextPath,
91
- claudeMdUpdated,
92
- };
93
- }
94
- /**
95
- * Refresh TASK_CONTEXT.md with latest data from Damper (for resume scenarios)
96
- */
97
- export async function refreshContext(options) {
98
- // For now, just run bootstrap again - it will overwrite with fresh data
99
- return bootstrapContext(options);
100
- }
@@ -1,22 +0,0 @@
1
- export interface WorktreeState {
2
- taskId: string;
3
- path: string;
4
- branch: string;
5
- projectRoot: string;
6
- createdAt: string;
7
- }
8
- export declare function getWorktrees(): WorktreeState[];
9
- export declare function getWorktreeByTaskId(taskId: string): WorktreeState | undefined;
10
- export declare function getWorktreeByPath(worktreePath: string): WorktreeState | undefined;
11
- export declare function addWorktree(worktree: WorktreeState): void;
12
- export declare function removeWorktree(taskId: string): void;
13
- export declare function removeWorktreeByPath(worktreePath: string): void;
14
- export declare function cleanupStaleWorktrees(): WorktreeState[];
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 {};
@@ -1,102 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
- import * as os from 'node:os';
4
- const STATE_DIR = path.join(os.homedir(), '.damper');
5
- const WORKTREES_FILE = path.join(STATE_DIR, 'worktrees.json');
6
- function ensureStateDir() {
7
- if (!fs.existsSync(STATE_DIR)) {
8
- fs.mkdirSync(STATE_DIR, { recursive: true });
9
- }
10
- }
11
- function readState() {
12
- ensureStateDir();
13
- if (!fs.existsSync(WORKTREES_FILE)) {
14
- return { worktrees: [] };
15
- }
16
- try {
17
- const content = fs.readFileSync(WORKTREES_FILE, 'utf-8');
18
- return JSON.parse(content);
19
- }
20
- catch {
21
- return { worktrees: [] };
22
- }
23
- }
24
- function writeState(state) {
25
- ensureStateDir();
26
- fs.writeFileSync(WORKTREES_FILE, JSON.stringify(state, null, 2));
27
- }
28
- export function getWorktrees() {
29
- return readState().worktrees;
30
- }
31
- export function getWorktreeByTaskId(taskId) {
32
- return readState().worktrees.find(w => w.taskId === taskId);
33
- }
34
- export function getWorktreeByPath(worktreePath) {
35
- const normalized = path.resolve(worktreePath);
36
- return readState().worktrees.find(w => path.resolve(w.path) === normalized);
37
- }
38
- export function addWorktree(worktree) {
39
- const state = readState();
40
- // Remove any existing entry for this task or path
41
- state.worktrees = state.worktrees.filter(w => w.taskId !== worktree.taskId && path.resolve(w.path) !== path.resolve(worktree.path));
42
- state.worktrees.push(worktree);
43
- writeState(state);
44
- }
45
- export function removeWorktree(taskId) {
46
- const state = readState();
47
- state.worktrees = state.worktrees.filter(w => w.taskId !== taskId);
48
- writeState(state);
49
- }
50
- export function removeWorktreeByPath(worktreePath) {
51
- const normalized = path.resolve(worktreePath);
52
- const state = readState();
53
- state.worktrees = state.worktrees.filter(w => path.resolve(w.path) !== normalized);
54
- writeState(state);
55
- }
56
- export function cleanupStaleWorktrees() {
57
- const state = readState();
58
- const validWorktrees = [];
59
- const staleWorktrees = [];
60
- for (const w of state.worktrees) {
61
- if (fs.existsSync(w.path)) {
62
- validWorktrees.push(w);
63
- }
64
- else {
65
- staleWorktrees.push(w);
66
- }
67
- }
68
- if (staleWorktrees.length > 0) {
69
- state.worktrees = validWorktrees;
70
- writeState(state);
71
- }
72
- return staleWorktrees;
73
- }
74
- export function getWorktreesForProject(projectRoot) {
75
- const normalized = path.resolve(projectRoot);
76
- return readState().worktrees.filter(w => path.resolve(w.projectRoot) === normalized);
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
- }
@@ -1,40 +0,0 @@
1
- export interface WorktreeOptions {
2
- taskId: string;
3
- taskTitle: string;
4
- projectRoot: string;
5
- apiKey: string;
6
- }
7
- export interface WorktreeResult {
8
- path: string;
9
- branch: string;
10
- isNew: boolean;
11
- }
12
- /**
13
- * Create a new git worktree for a task
14
- */
15
- export declare function createWorktree(options: WorktreeOptions): Promise<WorktreeResult>;
16
- /**
17
- * Remove a git worktree and its branch
18
- */
19
- export declare function removeWorktreeDir(worktreePath: string, projectRoot: string): Promise<void>;
20
- /**
21
- * List all git worktrees for a project
22
- */
23
- export declare function listWorktrees(projectRoot: string): Promise<Array<{
24
- path: string;
25
- branch: string;
26
- head: string;
27
- }>>;
28
- /**
29
- * Check if a path is inside a git worktree
30
- */
31
- export declare function isInWorktree(dir: string): Promise<boolean>;
32
- /**
33
- * Get the git root directory
34
- */
35
- export declare function getGitRoot(dir: string): Promise<string>;
36
- /**
37
- * Get the main project root (not a worktree).
38
- * If we're in a worktree, this returns the main repo's path.
39
- */
40
- export declare function getMainProjectRoot(dir: string): Promise<string>;