@damper/cli 0.1.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.
@@ -0,0 +1,77 @@
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
+ }
@@ -0,0 +1,34 @@
1
+ export interface WorktreeOptions {
2
+ taskId: string;
3
+ taskTitle: string;
4
+ projectRoot: string;
5
+ }
6
+ export interface WorktreeResult {
7
+ path: string;
8
+ branch: string;
9
+ isNew: boolean;
10
+ }
11
+ /**
12
+ * Create a new git worktree for a task
13
+ */
14
+ export declare function createWorktree(options: WorktreeOptions): Promise<WorktreeResult>;
15
+ /**
16
+ * Remove a git worktree
17
+ */
18
+ export declare function removeWorktreeDir(worktreePath: string, projectRoot: string): Promise<void>;
19
+ /**
20
+ * List all git worktrees for a project
21
+ */
22
+ export declare function listWorktrees(projectRoot: string): Promise<Array<{
23
+ path: string;
24
+ branch: string;
25
+ head: string;
26
+ }>>;
27
+ /**
28
+ * Check if a path is inside a git worktree
29
+ */
30
+ export declare function isInWorktree(dir: string): Promise<boolean>;
31
+ /**
32
+ * Get the git root directory
33
+ */
34
+ export declare function getGitRoot(dir: string): Promise<string>;
@@ -0,0 +1,289 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { execa } from 'execa';
4
+ import pc from 'picocolors';
5
+ import { addWorktree, removeWorktree, getWorktreeByTaskId } from './state.js';
6
+ /**
7
+ * Convert a task title to a safe slug for branch/directory names
8
+ */
9
+ function slugify(text) {
10
+ return text
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/g, '-')
13
+ .replace(/^-+|-+$/g, '')
14
+ .slice(0, 50);
15
+ }
16
+ /**
17
+ * Get the project name from package.json or directory name
18
+ */
19
+ function getProjectName(projectRoot) {
20
+ const packageJsonPath = path.join(projectRoot, 'package.json');
21
+ if (fs.existsSync(packageJsonPath)) {
22
+ try {
23
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
24
+ if (pkg.name) {
25
+ // Strip scope if present
26
+ return pkg.name.replace(/^@[^/]+\//, '');
27
+ }
28
+ }
29
+ catch {
30
+ // Fall through to directory name
31
+ }
32
+ }
33
+ return path.basename(projectRoot);
34
+ }
35
+ /**
36
+ * Detect monorepo structure by looking for workspaces in package.json
37
+ */
38
+ function detectMonorepoPackages(projectRoot) {
39
+ const packageJsonPath = path.join(projectRoot, 'package.json');
40
+ if (!fs.existsSync(packageJsonPath)) {
41
+ return [];
42
+ }
43
+ try {
44
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
45
+ const workspaces = pkg.workspaces || [];
46
+ const packages = [];
47
+ for (const pattern of workspaces) {
48
+ // Simple glob expansion for common patterns
49
+ if (pattern.includes('*')) {
50
+ const base = pattern.replace(/\/\*$/, '').replace(/\*$/, '');
51
+ const basePath = path.join(projectRoot, base);
52
+ if (fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()) {
53
+ const dirs = fs.readdirSync(basePath).filter(d => {
54
+ const fullPath = path.join(basePath, d);
55
+ return fs.statSync(fullPath).isDirectory() &&
56
+ fs.existsSync(path.join(fullPath, 'package.json'));
57
+ });
58
+ packages.push(...dirs.map(d => path.join(base, d)));
59
+ }
60
+ }
61
+ else {
62
+ // Direct path
63
+ const pkgPath = path.join(projectRoot, pattern);
64
+ if (fs.existsSync(path.join(pkgPath, 'package.json'))) {
65
+ packages.push(pattern);
66
+ }
67
+ }
68
+ }
69
+ return packages;
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ /**
76
+ * Find all node_modules directories to symlink
77
+ */
78
+ function findNodeModulesDirs(projectRoot) {
79
+ const dirs = [];
80
+ // Root node_modules
81
+ const rootModules = path.join(projectRoot, 'node_modules');
82
+ if (fs.existsSync(rootModules)) {
83
+ dirs.push('node_modules');
84
+ }
85
+ // Monorepo package node_modules
86
+ const packages = detectMonorepoPackages(projectRoot);
87
+ for (const pkg of packages) {
88
+ const pkgModules = path.join(projectRoot, pkg, 'node_modules');
89
+ if (fs.existsSync(pkgModules)) {
90
+ dirs.push(path.join(pkg, 'node_modules'));
91
+ }
92
+ }
93
+ return dirs;
94
+ }
95
+ /**
96
+ * Find all .env files to copy
97
+ */
98
+ function findEnvFiles(projectRoot) {
99
+ const envFiles = [];
100
+ // Root .env files
101
+ const rootEnvFiles = ['.env', '.env.local', '.env.development'];
102
+ for (const envFile of rootEnvFiles) {
103
+ if (fs.existsSync(path.join(projectRoot, envFile))) {
104
+ envFiles.push(envFile);
105
+ }
106
+ }
107
+ // Monorepo package .env files
108
+ const packages = detectMonorepoPackages(projectRoot);
109
+ for (const pkg of packages) {
110
+ for (const envFile of rootEnvFiles) {
111
+ const envPath = path.join(pkg, envFile);
112
+ if (fs.existsSync(path.join(projectRoot, envPath))) {
113
+ envFiles.push(envPath);
114
+ }
115
+ }
116
+ }
117
+ // Common server locations
118
+ const serverDirs = ['server', 'api', 'backend', 'packages/server'];
119
+ for (const serverDir of serverDirs) {
120
+ for (const envFile of rootEnvFiles) {
121
+ const envPath = path.join(serverDir, envFile);
122
+ if (fs.existsSync(path.join(projectRoot, envPath)) && !envFiles.includes(envPath)) {
123
+ envFiles.push(envPath);
124
+ }
125
+ }
126
+ }
127
+ return envFiles;
128
+ }
129
+ /**
130
+ * Create a new git worktree for a task
131
+ */
132
+ export async function createWorktree(options) {
133
+ const { taskId, taskTitle, projectRoot } = options;
134
+ // Check if worktree already exists for this task
135
+ const existing = getWorktreeByTaskId(taskId);
136
+ if (existing && fs.existsSync(existing.path)) {
137
+ return {
138
+ path: existing.path,
139
+ branch: existing.branch,
140
+ isNew: false,
141
+ };
142
+ }
143
+ const projectName = getProjectName(projectRoot);
144
+ const slug = slugify(taskTitle);
145
+ const worktreePath = path.resolve(projectRoot, '..', `${projectName}-${slug}`);
146
+ const branchName = `feature/${slug}`;
147
+ // Create worktree
148
+ console.log(pc.dim(`Creating worktree at ${worktreePath}...`));
149
+ await execa('git', ['worktree', 'add', worktreePath, '-b', branchName], {
150
+ cwd: projectRoot,
151
+ stdio: 'pipe',
152
+ });
153
+ // Symlink node_modules
154
+ const nodeModulesDirs = findNodeModulesDirs(projectRoot);
155
+ for (const dir of nodeModulesDirs) {
156
+ const source = path.join(projectRoot, dir);
157
+ const target = path.join(worktreePath, dir);
158
+ // Ensure parent directory exists
159
+ const targetDir = path.dirname(target);
160
+ if (!fs.existsSync(targetDir)) {
161
+ fs.mkdirSync(targetDir, { recursive: true });
162
+ }
163
+ console.log(pc.dim(`Linking ${dir}...`));
164
+ await fs.promises.symlink(source, target, 'junction');
165
+ }
166
+ // Copy .env files
167
+ const envFiles = findEnvFiles(projectRoot);
168
+ for (const envFile of envFiles) {
169
+ const source = path.join(projectRoot, envFile);
170
+ const target = path.join(worktreePath, envFile);
171
+ // Ensure parent directory exists
172
+ const targetDir = path.dirname(target);
173
+ if (!fs.existsSync(targetDir)) {
174
+ fs.mkdirSync(targetDir, { recursive: true });
175
+ }
176
+ console.log(pc.dim(`Copying ${envFile}...`));
177
+ await fs.promises.copyFile(source, target);
178
+ }
179
+ // Save to state
180
+ const worktreeState = {
181
+ taskId,
182
+ path: worktreePath,
183
+ branch: branchName,
184
+ projectRoot,
185
+ createdAt: new Date().toISOString(),
186
+ };
187
+ addWorktree(worktreeState);
188
+ return {
189
+ path: worktreePath,
190
+ branch: branchName,
191
+ isNew: true,
192
+ };
193
+ }
194
+ /**
195
+ * Remove a git worktree
196
+ */
197
+ export async function removeWorktreeDir(worktreePath, projectRoot) {
198
+ console.log(pc.dim(`Removing worktree at ${worktreePath}...`));
199
+ // Remove symlinks first to avoid issues
200
+ const nodeModulesDirs = findNodeModulesDirs(projectRoot);
201
+ for (const dir of nodeModulesDirs) {
202
+ const target = path.join(worktreePath, dir);
203
+ if (fs.existsSync(target)) {
204
+ try {
205
+ await fs.promises.unlink(target);
206
+ }
207
+ catch {
208
+ // Ignore errors
209
+ }
210
+ }
211
+ }
212
+ // Remove worktree
213
+ await execa('git', ['worktree', 'remove', worktreePath, '--force'], {
214
+ cwd: projectRoot,
215
+ stdio: 'pipe',
216
+ });
217
+ // Get the worktree state to find the task ID
218
+ const state = await import('./state.js');
219
+ const worktree = state.getWorktreeByPath(worktreePath);
220
+ if (worktree) {
221
+ removeWorktree(worktree.taskId);
222
+ }
223
+ }
224
+ /**
225
+ * List all git worktrees for a project
226
+ */
227
+ export async function listWorktrees(projectRoot) {
228
+ const { stdout } = await execa('git', ['worktree', 'list', '--porcelain'], {
229
+ cwd: projectRoot,
230
+ stdio: 'pipe',
231
+ });
232
+ const worktrees = [];
233
+ let current = {};
234
+ for (const line of stdout.split('\n')) {
235
+ if (line.startsWith('worktree ')) {
236
+ current.path = line.slice(9);
237
+ }
238
+ else if (line.startsWith('HEAD ')) {
239
+ current.head = line.slice(5);
240
+ }
241
+ else if (line.startsWith('branch ')) {
242
+ current.branch = line.slice(7);
243
+ }
244
+ else if (line === '' && current.path) {
245
+ if (current.path && current.branch && current.head) {
246
+ worktrees.push({
247
+ path: current.path,
248
+ branch: current.branch,
249
+ head: current.head,
250
+ });
251
+ }
252
+ current = {};
253
+ }
254
+ }
255
+ // Handle last entry
256
+ if (current.path && current.branch && current.head) {
257
+ worktrees.push({
258
+ path: current.path,
259
+ branch: current.branch,
260
+ head: current.head,
261
+ });
262
+ }
263
+ return worktrees;
264
+ }
265
+ /**
266
+ * Check if a path is inside a git worktree
267
+ */
268
+ export async function isInWorktree(dir) {
269
+ try {
270
+ const { stdout } = await execa('git', ['rev-parse', '--is-inside-work-tree'], {
271
+ cwd: dir,
272
+ stdio: 'pipe',
273
+ });
274
+ return stdout.trim() === 'true';
275
+ }
276
+ catch {
277
+ return false;
278
+ }
279
+ }
280
+ /**
281
+ * Get the git root directory
282
+ */
283
+ export async function getGitRoot(dir) {
284
+ const { stdout } = await execa('git', ['rev-parse', '--show-toplevel'], {
285
+ cwd: dir,
286
+ stdio: 'pipe',
287
+ });
288
+ return stdout.trim();
289
+ }
@@ -0,0 +1,6 @@
1
+ interface ClaudeAppendOptions {
2
+ taskId: string;
3
+ taskTitle: string;
4
+ }
5
+ export declare function generateClaudeAppend(options: ClaudeAppendOptions): string;
6
+ export {};
@@ -0,0 +1,17 @@
1
+ export function generateClaudeAppend(options) {
2
+ const { taskId, taskTitle } = options;
3
+ return `
4
+ ## Current Task: #${taskId} ${taskTitle}
5
+
6
+ **IMPORTANT**: Read TASK_CONTEXT.md for full task details and architecture context.
7
+ If you feel you've lost context, re-read that file.
8
+
9
+ **Your responsibilities (via Damper MCP):**
10
+ 1. Use \`add_commit\` after each git commit
11
+ 2. Use \`add_note\` for important decisions
12
+ 3. When done: call \`complete_task\` with summary
13
+ 4. If stopping early: call \`abandon_task\` with handoff notes
14
+
15
+ The CLI just bootstrapped this environment - YOU handle the task lifecycle.
16
+ `.trim();
17
+ }
@@ -0,0 +1,15 @@
1
+ import type { TaskDetail, ContextSection, Module } from '../services/damper-api.js';
2
+ interface TaskContextOptions {
3
+ task: TaskDetail;
4
+ criticalRules: string[];
5
+ sections: ContextSection[];
6
+ templates: Array<{
7
+ name: string;
8
+ description?: string | null;
9
+ filePattern?: string | null;
10
+ }>;
11
+ modules: Module[];
12
+ damperInstructions: string;
13
+ }
14
+ export declare function generateTaskContext(options: TaskContextOptions): string;
15
+ export {};
@@ -0,0 +1,115 @@
1
+ export function generateTaskContext(options) {
2
+ const { task, criticalRules, sections, templates, modules, damperInstructions } = options;
3
+ const typeIcon = task.type === 'bug' ? 'Bug' : task.type === 'feature' ? 'Feature' : task.type === 'improvement' ? 'Improvement' : 'Task';
4
+ const lines = [];
5
+ // Header
6
+ lines.push(`# Task: ${task.title} (#${task.id})`);
7
+ lines.push('');
8
+ lines.push(`**Type:** ${typeIcon} | **Status:** ${task.status} | **Priority:** ${task.priority || 'none'}`);
9
+ if (task.effort)
10
+ lines.push(`**Effort:** ${task.effort}`);
11
+ if (task.quarter)
12
+ lines.push(`**Quarter:** ${task.quarter}`);
13
+ lines.push('');
14
+ // Critical Rules
15
+ if (criticalRules.length > 0) {
16
+ lines.push('## Critical Rules (DO NOT SKIP)');
17
+ lines.push('');
18
+ for (const rule of criticalRules) {
19
+ lines.push(`- ${rule}`);
20
+ }
21
+ lines.push('');
22
+ }
23
+ // Damper Workflow
24
+ lines.push('## Damper Workflow');
25
+ lines.push('');
26
+ lines.push(damperInstructions);
27
+ lines.push('');
28
+ // Task Description
29
+ if (task.description) {
30
+ lines.push('## Task Description');
31
+ lines.push('');
32
+ lines.push(task.description);
33
+ lines.push('');
34
+ }
35
+ // Implementation Plan
36
+ if (task.implementationPlan) {
37
+ lines.push('## Implementation Plan');
38
+ lines.push('');
39
+ lines.push(task.implementationPlan);
40
+ lines.push('');
41
+ }
42
+ // Subtasks
43
+ if (task.subtasks && task.subtasks.length > 0) {
44
+ const done = task.subtasks.filter(s => s.done).length;
45
+ lines.push(`## Subtasks (${done}/${task.subtasks.length})`);
46
+ lines.push('');
47
+ for (const subtask of task.subtasks) {
48
+ lines.push(`- [${subtask.done ? 'x' : ' '}] ${subtask.title} (id: ${subtask.id})`);
49
+ }
50
+ lines.push('');
51
+ }
52
+ // Previous Session Notes
53
+ if (task.agentNotes) {
54
+ lines.push('## Previous Session Notes (IMPORTANT for continuity)');
55
+ lines.push('');
56
+ lines.push(task.agentNotes);
57
+ lines.push('');
58
+ }
59
+ // Previous Commits
60
+ if (task.commits && task.commits.length > 0) {
61
+ lines.push(`## Previous Commits (${task.commits.length})`);
62
+ lines.push('');
63
+ for (const commit of task.commits) {
64
+ lines.push(`- ${commit.hash.slice(0, 7)}: ${commit.message}`);
65
+ }
66
+ lines.push('');
67
+ }
68
+ // Project Context Sections
69
+ if (sections.length > 0) {
70
+ lines.push('## Relevant Architecture');
71
+ lines.push('');
72
+ for (const section of sections) {
73
+ lines.push(`### ${section.section}`);
74
+ lines.push('');
75
+ lines.push(section.content);
76
+ lines.push('');
77
+ }
78
+ }
79
+ // Templates
80
+ if (templates.length > 0) {
81
+ lines.push('## Templates Available');
82
+ lines.push('');
83
+ for (const template of templates) {
84
+ const pattern = template.filePattern ? ` (${template.filePattern})` : '';
85
+ const desc = template.description ? `: ${template.description}` : '';
86
+ lines.push(`- \`${template.name}\`${pattern}${desc}`);
87
+ }
88
+ lines.push('');
89
+ }
90
+ // Modules
91
+ if (modules.length > 0) {
92
+ lines.push('## Module Structure');
93
+ lines.push('');
94
+ for (const mod of modules) {
95
+ const port = mod.port ? ` (port ${mod.port})` : '';
96
+ const deps = mod.dependsOn && mod.dependsOn.length > 0 ? ` → ${mod.dependsOn.join(', ')}` : '';
97
+ lines.push(`- **${mod.name}**: ${mod.path}${port}${deps}`);
98
+ }
99
+ lines.push('');
100
+ }
101
+ // Linked Feedback
102
+ if (task.feedback && task.feedback.length > 0) {
103
+ lines.push('## Linked Feedback (customer requests driving this task)');
104
+ lines.push('');
105
+ for (const fb of task.feedback) {
106
+ lines.push(`- "${fb.title}" (${fb.voterCount} votes)`);
107
+ }
108
+ lines.push('');
109
+ }
110
+ // Footer
111
+ lines.push('---');
112
+ lines.push(`Generated by @damper/cli at ${new Date().toISOString()}.`);
113
+ lines.push('Re-read this file if conversation context is lost.');
114
+ return lines.join('\n');
115
+ }
@@ -0,0 +1,15 @@
1
+ import type { Task, DamperApi } from '../services/damper-api.js';
2
+ import type { WorktreeState } from '../services/state.js';
3
+ interface TaskPickerOptions {
4
+ api: DamperApi;
5
+ worktrees: WorktreeState[];
6
+ typeFilter?: 'bug' | 'feature' | 'improvement' | 'task';
7
+ statusFilter?: 'planned' | 'in_progress' | 'done' | 'all';
8
+ }
9
+ interface TaskPickerResult {
10
+ task: Task;
11
+ worktree?: WorktreeState;
12
+ isResume: boolean;
13
+ }
14
+ export declare function pickTask(options: TaskPickerOptions): Promise<TaskPickerResult | null>;
15
+ export {};
@@ -0,0 +1,121 @@
1
+ import { select, Separator } from '@inquirer/prompts';
2
+ import pc from 'picocolors';
3
+ function getTypeIcon(type) {
4
+ switch (type) {
5
+ case 'bug': return pc.red('bug');
6
+ case 'feature': return pc.green('feature');
7
+ case 'improvement': return pc.blue('improvement');
8
+ default: return pc.gray('task');
9
+ }
10
+ }
11
+ function getPriorityIcon(priority) {
12
+ switch (priority) {
13
+ case 'high': return pc.red('!');
14
+ case 'medium': return pc.yellow('~');
15
+ default: return pc.dim('-');
16
+ }
17
+ }
18
+ function formatTaskChoice(choice) {
19
+ const { task } = choice;
20
+ const typeIcon = getTypeIcon(task.type);
21
+ const priorityIcon = getPriorityIcon(task.priority);
22
+ if (choice.type === 'in_progress') {
23
+ const worktreeName = choice.worktree.path.split('/').pop() || choice.worktree.path;
24
+ let description = `${pc.dim('#')}${pc.cyan(task.id)} ${task.title} ${pc.dim(`[${typeIcon}]`)} ${priorityIcon}`;
25
+ description += `\n ${pc.dim(`Worktree: ${worktreeName}`)}`;
26
+ if (choice.lastNote) {
27
+ // Truncate long notes
28
+ const note = choice.lastNote.length > 60 ? choice.lastNote.slice(0, 60) + '...' : choice.lastNote;
29
+ description += `\n ${pc.dim(`Last: "${note}"`)}`;
30
+ }
31
+ return description;
32
+ }
33
+ // Available task
34
+ let description = `${pc.dim('#')}${pc.cyan(task.id)} ${task.title} ${pc.dim(`[${typeIcon}]`)} ${priorityIcon}`;
35
+ if (task.quarter) {
36
+ description += ` ${pc.dim(task.quarter)}`;
37
+ }
38
+ if (task.hasImplementationPlan) {
39
+ description += pc.dim(' [plan]');
40
+ }
41
+ if (task.subtaskProgress) {
42
+ description += pc.dim(` [${task.subtaskProgress.done}/${task.subtaskProgress.total}]`);
43
+ }
44
+ return description;
45
+ }
46
+ export async function pickTask(options) {
47
+ const { api, worktrees, typeFilter, statusFilter } = options;
48
+ // Fetch tasks from Damper
49
+ const { tasks, project } = await api.listTasks({
50
+ status: statusFilter || 'all',
51
+ type: typeFilter,
52
+ sort: 'importance',
53
+ });
54
+ // Filter out completed tasks unless specifically requested
55
+ const availableTasks = tasks.filter(t => statusFilter === 'done' || statusFilter === 'all' ||
56
+ (t.status === 'planned' || t.status === 'in_progress'));
57
+ // Match worktrees with tasks
58
+ const inProgressChoices = [];
59
+ const worktreeTaskIds = new Set();
60
+ for (const worktree of worktrees) {
61
+ const task = tasks.find(t => t.id === worktree.taskId);
62
+ if (task) {
63
+ worktreeTaskIds.add(task.id);
64
+ // Get last note for display (we'd need to fetch task detail for this)
65
+ // For now, we'll leave it undefined - the API doesn't include notes in list
66
+ inProgressChoices.push({
67
+ type: 'in_progress',
68
+ task,
69
+ worktree,
70
+ });
71
+ }
72
+ }
73
+ // Available tasks are those not in our worktrees and either planned or in_progress
74
+ const availableChoices = availableTasks
75
+ .filter(t => !worktreeTaskIds.has(t.id) && (t.status === 'planned' || t.status === 'in_progress'))
76
+ .map(task => ({ type: 'available', task }));
77
+ if (inProgressChoices.length === 0 && availableChoices.length === 0) {
78
+ console.log(pc.yellow('\nNo tasks available.'));
79
+ if (typeFilter) {
80
+ console.log(pc.dim(`(filtered by type: ${typeFilter})`));
81
+ }
82
+ console.log(pc.dim('Create new tasks in Damper or check your filters.\n'));
83
+ return null;
84
+ }
85
+ const choices = [];
86
+ if (inProgressChoices.length > 0) {
87
+ choices.push(new Separator(pc.bold('\n--- In Progress (your worktrees) ---')));
88
+ for (const choice of inProgressChoices) {
89
+ choices.push({
90
+ name: formatTaskChoice(choice),
91
+ value: choice,
92
+ });
93
+ }
94
+ }
95
+ if (availableChoices.length > 0) {
96
+ choices.push(new Separator(pc.bold('\n--- Available Tasks ---')));
97
+ for (const choice of availableChoices) {
98
+ choices.push({
99
+ name: formatTaskChoice(choice),
100
+ value: choice,
101
+ });
102
+ }
103
+ }
104
+ console.log(pc.bold(`\nProject: ${project}`));
105
+ const selected = await select({
106
+ message: 'Select a task to work on:',
107
+ choices: choices,
108
+ pageSize: 15,
109
+ });
110
+ if (selected.type === 'in_progress') {
111
+ return {
112
+ task: selected.task,
113
+ worktree: selected.worktree,
114
+ isResume: true,
115
+ };
116
+ }
117
+ return {
118
+ task: selected.task,
119
+ isResume: false,
120
+ };
121
+ }