@damper/cli 0.6.11 → 0.6.12

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.
@@ -5,6 +5,7 @@ import pc from 'picocolors';
5
5
  import { getWorktrees, cleanupStaleWorktrees, removeWorktree } from '../services/state.js';
6
6
  import { createDamperApi } from '../services/damper-api.js';
7
7
  import { removeWorktreeDir } from '../services/worktree.js';
8
+ import { shortId, shortIdRaw, formatTaskLine } from '../ui/format.js';
8
9
  async function hasUncommittedChanges(worktreePath) {
9
10
  try {
10
11
  const { stdout } = await execa('git', ['status', '--porcelain'], {
@@ -108,10 +109,7 @@ export async function cleanupCommand() {
108
109
  console.log(pc.bold('\nWorktrees available for cleanup:\n'));
109
110
  const choices = candidates.map(c => {
110
111
  const worktreeName = c.worktree.path.split('/').pop() || c.worktree.path;
111
- let description = `#${c.worktree.taskId}`;
112
- if (c.task) {
113
- description += ` - ${c.task.title}`;
114
- }
112
+ let description = c.task ? formatTaskLine(c.task) : shortId(c.worktree.taskId);
115
113
  let reasonBadge = '';
116
114
  switch (c.reason) {
117
115
  case 'completed':
@@ -177,7 +175,7 @@ export async function cleanupCommand() {
177
175
  if (candidate.reason === 'in_progress' && api) {
178
176
  try {
179
177
  await api.abandonTask(worktree.taskId, 'Dropped via CLI cleanup');
180
- console.log(pc.green(`✓ Released task #${worktree.taskId}`));
178
+ console.log(pc.green(`✓ Released task #${shortIdRaw(worktree.taskId)}`));
181
179
  }
182
180
  catch (err) {
183
181
  const error = err;
@@ -4,6 +4,7 @@ import { createDamperApi } from '../services/damper-api.js';
4
4
  import { getWorktrees, removeWorktree } from '../services/state.js';
5
5
  import { removeWorktreeDir, getGitRoot } from '../services/worktree.js';
6
6
  import { getApiKey } from '../services/config.js';
7
+ import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort } from '../ui/format.js';
7
8
  export async function releaseCommand() {
8
9
  // Get project root
9
10
  let projectRoot;
@@ -34,10 +35,14 @@ export async function releaseCommand() {
34
35
  const worktrees = getWorktrees();
35
36
  const choices = tasks.map(task => {
36
37
  const worktree = worktrees.find(w => w.taskId === task.id);
38
+ const priorityIcon = getPriorityIcon(task.priority);
39
+ const typeIcon = getTypeIcon(task.type);
40
+ const id = shortId(task.id);
41
+ const effort = formatEffort(task.effort);
37
42
  const lockedInfo = task.lockedBy ? pc.dim(` (locked by ${task.lockedBy})`) : '';
38
43
  const worktreeInfo = worktree ? pc.dim(` [has worktree]`) : '';
39
44
  return {
40
- name: `#${task.id} ${task.title}${lockedInfo}${worktreeInfo}`,
45
+ name: `${priorityIcon}${typeIcon} ${id} ${task.title}${effort ? ` ${effort}` : ''}${lockedInfo}${worktreeInfo}`,
41
46
  value: { task, worktree },
42
47
  };
43
48
  });
@@ -49,7 +54,7 @@ export async function releaseCommand() {
49
54
  // Confirm
50
55
  console.log();
51
56
  console.log(pc.yellow(`This will:`));
52
- console.log(pc.dim(` • Release the lock on task #${task.id}`));
57
+ console.log(pc.dim(` • Release the lock on task #${shortIdRaw(task.id)}`));
53
58
  console.log(pc.dim(` • Set task status back to "planned"`));
54
59
  if (worktree) {
55
60
  console.log(pc.dim(` • Optionally remove the worktree`));
@@ -66,7 +71,7 @@ export async function releaseCommand() {
66
71
  // Abandon the task in Damper
67
72
  try {
68
73
  await api.abandonTask(task.id, 'Released via CLI');
69
- console.log(pc.green(`\n✓ Task #${task.id} released to planned status`));
74
+ console.log(pc.green(`\n✓ Task #${shortIdRaw(task.id)} released to planned status`));
70
75
  }
71
76
  catch (err) {
72
77
  const error = err;
@@ -7,6 +7,7 @@ import { pickTask } from '../ui/task-picker.js';
7
7
  import { launchClaude, postTaskFlow, isClaudeInstalled, isDamperMcpConfigured, configureDamperMcp } from '../services/claude.js';
8
8
  import { getWorktreesForProject, cleanupStaleWorktrees } from '../services/state.js';
9
9
  import { getApiKey, isProjectConfigured, getProjectConfigPath } from '../services/config.js';
10
+ import { shortIdRaw } from '../ui/format.js';
10
11
  export async function startCommand(options) {
11
12
  // Check Claude CLI first
12
13
  const claudeInstalled = await isClaudeInstalled();
@@ -68,10 +69,10 @@ export async function startCommand(options) {
68
69
  if (existingWorktree && fs.existsSync(existingWorktree.path)) {
69
70
  isResume = true;
70
71
  worktreePath = existingWorktree.path;
71
- console.log(pc.cyan(`\nResuming task #${taskId}: ${taskTitle}`));
72
+ console.log(pc.cyan(`\nResuming task #${shortIdRaw(taskId)}: ${taskTitle}`));
72
73
  }
73
74
  else {
74
- console.log(pc.cyan(`\nStarting task #${taskId}: ${taskTitle}`));
75
+ console.log(pc.cyan(`\nStarting task #${shortIdRaw(taskId)}: ${taskTitle}`));
75
76
  }
76
77
  }
77
78
  else {
@@ -95,7 +96,7 @@ export async function startCommand(options) {
95
96
  }
96
97
  if (isResume && worktreePath) {
97
98
  // Resume existing worktree
98
- console.log(pc.green(`\n✓ Resuming: #${taskId} ${taskTitle}`));
99
+ console.log(pc.green(`\n✓ Resuming: #${shortIdRaw(taskId)} ${taskTitle}`));
99
100
  console.log(pc.dim(` Worktree: ${worktreePath}`));
100
101
  // Refresh context with latest from Damper
101
102
  console.log(pc.dim('\nRefreshing context from Damper...'));
@@ -3,6 +3,7 @@ import pc from 'picocolors';
3
3
  import { getWorktrees, cleanupStaleWorktrees } from '../services/state.js';
4
4
  import { createDamperApi } from '../services/damper-api.js';
5
5
  import { getGitRoot } from '../services/worktree.js';
6
+ import { shortId, getTypeIcon, getPriorityIcon, formatEffort, formatSubtaskProgress, relativeTime, statusColor as getStatusColor, } from '../ui/format.js';
6
7
  export async function statusCommand() {
7
8
  // Clean up stale entries first
8
9
  const stale = cleanupStaleWorktrees();
@@ -49,25 +50,43 @@ export async function statusCommand() {
49
50
  console.log();
50
51
  for (const wt of projectWorktrees) {
51
52
  const exists = fs.existsSync(wt.path);
52
- const shortTaskId = wt.taskId.slice(0, 8);
53
- // Try to get task status and title
54
- let taskStatus = '';
55
- let taskTitle = '';
53
+ const statusIcon = exists ? pc.green('●') : pc.red('○');
54
+ // Try to get task details from Damper
56
55
  if (api) {
57
56
  try {
58
57
  const task = await api.getTask(wt.taskId);
59
- const statusColor = task.status === 'done' ? pc.green : task.status === 'in_progress' ? pc.yellow : pc.dim;
60
- taskStatus = statusColor(`[${task.status}]`);
61
- taskTitle = task.title;
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')} `)}`);
62
72
  }
63
73
  catch {
64
- taskStatus = pc.dim('[unknown]');
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')} `)}`);
65
80
  }
66
81
  }
67
- const statusIcon = exists ? pc.green('●') : pc.red('○');
68
- const titlePart = taskTitle ? ` ${taskTitle}` : '';
69
- console.log(` ${statusIcon} ${pc.cyan(`#${shortTaskId}`)}${titlePart} ${taskStatus}`);
70
- console.log(` ${wt.branch} ${pc.dim('·')} ${pc.dim(`created ${new Date(wt.createdAt).toLocaleDateString()}`)}`);
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
+ }
71
90
  console.log();
72
91
  }
73
92
  }
@@ -4,6 +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 } from '../ui/format.js';
7
8
  const CLAUDE_SETTINGS_DIR = path.join(os.homedir(), '.claude');
8
9
  const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_SETTINGS_DIR, 'settings.json');
9
10
  /**
@@ -85,7 +86,7 @@ export function configureDamperMcp() {
85
86
  */
86
87
  export async function launchClaude(options) {
87
88
  const { cwd, taskId, taskTitle, apiKey, yolo } = options;
88
- console.log(pc.green(`\nStarting Claude Code for task #${taskId}: ${taskTitle}`));
89
+ console.log(pc.green(`\nStarting Claude Code for task #${shortIdRaw(taskId)}: ${taskTitle}`));
89
90
  console.log(pc.dim(`Directory: ${cwd}`));
90
91
  console.log();
91
92
  // Show workflow explanation
@@ -0,0 +1,30 @@
1
+ import type { Task } from '../services/damper-api.js';
2
+ /** Colored short ID: dim `#` + cyan first 8 chars */
3
+ export declare function shortId(id: string): string;
4
+ /** Plain 8-char slice (for use inside other color wrappers) */
5
+ export declare function shortIdRaw(id: string): string;
6
+ /** Type emoji icon */
7
+ export declare function getTypeIcon(type: string): string;
8
+ /** Priority prefix: red/yellow circle + space, or empty */
9
+ export declare function getPriorityIcon(priority: string): string;
10
+ /** Dim effort badge like `▪ L`, or empty string */
11
+ export declare function formatEffort(effort: string | null | undefined): string;
12
+ /** Dim feedback count like `💬 3`, or empty string */
13
+ export declare function formatFeedback(count: number | null | undefined): string;
14
+ /** Dim labels joined with ` · `, or empty string */
15
+ export declare function formatLabels(labels: string[] | null | undefined): string;
16
+ /** 10-char progress bar: green filled, dim empty, with count */
17
+ export declare function formatSubtaskProgress(progress: {
18
+ done: number;
19
+ total: number;
20
+ } | null | undefined): string;
21
+ /** Relative time string from date, no dependencies */
22
+ export declare function relativeTime(date: string | Date | null | undefined): string;
23
+ /** Returns a picocolors function for the given status */
24
+ export declare function statusColor(status: string): (s: string) => string;
25
+ /** Box-drawing section header: `┌─ Label ───────────────┐` */
26
+ export declare function sectionHeader(label: string, width?: number): string;
27
+ /** Compact one-liner: `🔴 ✨ #a1b2c3d4 Title ▪ L` */
28
+ export declare function formatTaskLine(task: Task): string;
29
+ /** Dim metadata second line with segments joined by ` · ` */
30
+ export declare function formatTaskMeta(task: Task): string;
@@ -0,0 +1,139 @@
1
+ import pc from 'picocolors';
2
+ /** Colored short ID: dim `#` + cyan first 8 chars */
3
+ export function shortId(id) {
4
+ return `${pc.dim('#')}${pc.cyan(id.slice(0, 8))}`;
5
+ }
6
+ /** Plain 8-char slice (for use inside other color wrappers) */
7
+ export function shortIdRaw(id) {
8
+ return id.slice(0, 8);
9
+ }
10
+ /** Type emoji icon */
11
+ export function getTypeIcon(type) {
12
+ switch (type) {
13
+ case 'bug': return '\u{1F41B}';
14
+ case 'feature': return '\u{2728}';
15
+ case 'improvement': return '\u{1F4A1}';
16
+ default: return '\u{1F4CC}';
17
+ }
18
+ }
19
+ /** Priority prefix: red/yellow circle + space, or empty */
20
+ export function getPriorityIcon(priority) {
21
+ switch (priority) {
22
+ case 'high': return '\u{1F534} ';
23
+ case 'medium': return '\u{1F7E1} ';
24
+ default: return '';
25
+ }
26
+ }
27
+ /** Dim effort badge like `▪ L`, or empty string */
28
+ export function formatEffort(effort) {
29
+ if (!effort)
30
+ return '';
31
+ return pc.dim(`\u25AA ${effort.toUpperCase()}`);
32
+ }
33
+ /** Dim feedback count like `💬 3`, or empty string */
34
+ export function formatFeedback(count) {
35
+ if (!count)
36
+ return '';
37
+ return pc.dim(`\u{1F4AC} ${count}`);
38
+ }
39
+ /** Dim labels joined with ` · `, or empty string */
40
+ export function formatLabels(labels) {
41
+ if (!labels || labels.length === 0)
42
+ return '';
43
+ return pc.dim(labels.join(' \u00B7 '));
44
+ }
45
+ /** 10-char progress bar: green filled, dim empty, with count */
46
+ export function formatSubtaskProgress(progress) {
47
+ if (!progress || progress.total === 0)
48
+ return '';
49
+ const { done, total } = progress;
50
+ const width = 10;
51
+ const filled = Math.round((done / total) * width);
52
+ const empty = width - filled;
53
+ const bar = pc.green('\u2501'.repeat(filled)) + pc.dim('\u2591'.repeat(empty));
54
+ return `${bar} ${done}/${total}`;
55
+ }
56
+ /** Relative time string from date, no dependencies */
57
+ export function relativeTime(date) {
58
+ if (!date)
59
+ return '';
60
+ const now = Date.now();
61
+ const then = date instanceof Date ? date.getTime() : new Date(date).getTime();
62
+ if (isNaN(then))
63
+ return '';
64
+ const diffMs = now - then;
65
+ if (diffMs < 0)
66
+ return 'just now';
67
+ const seconds = Math.floor(diffMs / 1000);
68
+ if (seconds < 60)
69
+ return 'just now';
70
+ const minutes = Math.floor(seconds / 60);
71
+ if (minutes < 60)
72
+ return `${minutes}m ago`;
73
+ const hours = Math.floor(minutes / 60);
74
+ if (hours < 24)
75
+ return `${hours}h ago`;
76
+ const days = Math.floor(hours / 24);
77
+ if (days < 14)
78
+ return `${days}d ago`;
79
+ const weeks = Math.floor(days / 7);
80
+ if (weeks < 8)
81
+ return `${weeks}w ago`;
82
+ const months = Math.floor(days / 30);
83
+ if (months < 12)
84
+ return `${months}mo ago`;
85
+ const years = Math.floor(days / 365);
86
+ return `${years}y ago`;
87
+ }
88
+ /** Returns a picocolors function for the given status */
89
+ export function statusColor(status) {
90
+ switch (status) {
91
+ case 'done': return pc.green;
92
+ case 'in_progress': return pc.yellow;
93
+ default: return pc.dim;
94
+ }
95
+ }
96
+ /** Box-drawing section header: `┌─ Label ───────────────┐` */
97
+ export function sectionHeader(label, width = 40) {
98
+ const inner = `\u2500 ${label} `;
99
+ const remaining = Math.max(0, width - inner.length - 2); // 2 for ┌ and ┐
100
+ return `\u250C${inner}${'\u2500'.repeat(remaining)}\u2510`;
101
+ }
102
+ /** Compact one-liner: `🔴 ✨ #a1b2c3d4 Title ▪ L` */
103
+ export function formatTaskLine(task) {
104
+ const priority = getPriorityIcon(task.priority);
105
+ const icon = getTypeIcon(task.type);
106
+ const id = shortId(task.id);
107
+ const effort = formatEffort(task.effort);
108
+ const parts = [
109
+ `${priority}${icon} ${id} ${task.title}`,
110
+ ];
111
+ if (effort)
112
+ parts.push(effort);
113
+ return parts.join(' ');
114
+ }
115
+ /** Dim metadata second line with segments joined by ` · ` */
116
+ export function formatTaskMeta(task) {
117
+ const segments = [];
118
+ if (task.quarter) {
119
+ segments.push(pc.dim(task.quarter));
120
+ }
121
+ if (task.hasImplementationPlan) {
122
+ segments.push(pc.dim('has plan'));
123
+ }
124
+ const progress = formatSubtaskProgress(task.subtaskProgress);
125
+ if (progress) {
126
+ segments.push(progress);
127
+ }
128
+ const feedback = formatFeedback(task.feedbackCount);
129
+ if (feedback) {
130
+ segments.push(feedback);
131
+ }
132
+ const labels = formatLabels(task.labels);
133
+ if (labels) {
134
+ segments.push(labels);
135
+ }
136
+ if (segments.length === 0)
137
+ return '';
138
+ return segments.join(` ${pc.dim('\u00B7')} `);
139
+ }
@@ -1,32 +1,20 @@
1
1
  import { select, confirm, input, Separator } from '@inquirer/prompts';
2
2
  import pc from 'picocolors';
3
- function shortId(id) {
4
- return id.slice(0, 8);
5
- }
6
- function getTypeIcon(type) {
7
- switch (type) {
8
- case 'bug': return '🐛';
9
- case 'feature': return '✨';
10
- case 'improvement': return '💡';
11
- default: return '📌';
12
- }
13
- }
14
- function getPriorityIcon(priority) {
15
- switch (priority) {
16
- case 'high': return '🔴 ';
17
- case 'medium': return '🟡 ';
18
- default: return '';
19
- }
20
- }
3
+ import { shortId, shortIdRaw, getTypeIcon, getPriorityIcon, formatEffort, formatTaskMeta, sectionHeader, relativeTime, } from './format.js';
21
4
  function formatTaskChoice(choice) {
22
5
  const { task } = choice;
23
- const typeIcon = getTypeIcon(task.type);
24
6
  const priorityIcon = getPriorityIcon(task.priority);
25
- const id = `${pc.dim('#')}${pc.cyan(shortId(task.id))}`;
7
+ const typeIcon = getTypeIcon(task.type);
8
+ const id = shortId(task.id);
9
+ const effort = formatEffort(task.effort);
10
+ const firstLine = `${priorityIcon}${typeIcon} ${id} ${task.title}${effort ? ` ${effort}` : ''}`;
26
11
  if (choice.type === 'in_progress') {
27
12
  const worktreeName = choice.worktree.path.split('/').pop() || choice.worktree.path;
28
- let description = `${priorityIcon}${typeIcon} ${id} ${task.title}`;
29
- description += `\n ${pc.dim(worktreeName)}`;
13
+ const time = relativeTime(choice.worktree.createdAt);
14
+ const metaParts = [worktreeName];
15
+ if (time)
16
+ metaParts.push(time);
17
+ let description = `${firstLine}\n ${pc.dim(metaParts.join(' \u00B7 '))}`;
30
18
  if (choice.lastNote) {
31
19
  const note = choice.lastNote.length > 60 ? choice.lastNote.slice(0, 60) + '...' : choice.lastNote;
32
20
  description += `\n ${pc.dim(`Last: "${note}"`)}`;
@@ -34,26 +22,14 @@ function formatTaskChoice(choice) {
34
22
  return description;
35
23
  }
36
24
  if (choice.type === 'locked') {
37
- let description = `${priorityIcon}${typeIcon} ${id} ${task.title}`;
38
- description += `\n ${pc.yellow(`locked by ${choice.lockedBy}`)}`;
39
- return description;
25
+ return `${firstLine}\n ${pc.yellow(`locked by ${choice.lockedBy}`)}`;
40
26
  }
41
27
  // Available task
42
- let description = `${priorityIcon}${typeIcon} ${id} ${task.title}`;
43
- const meta = [];
44
- if (task.quarter) {
45
- meta.push(task.quarter);
46
- }
47
- if (task.hasImplementationPlan) {
48
- meta.push('has plan');
49
- }
50
- if (task.subtaskProgress) {
51
- meta.push(`${task.subtaskProgress.done}/${task.subtaskProgress.total} subtasks`);
52
- }
53
- if (meta.length > 0) {
54
- description += `\n ${pc.dim(meta.join(' · '))}`;
28
+ const meta = formatTaskMeta(task);
29
+ if (meta) {
30
+ return `${firstLine}\n ${meta}`;
55
31
  }
56
- return description;
32
+ return firstLine;
57
33
  }
58
34
  export async function pickTask(options) {
59
35
  const { api, worktrees, typeFilter, statusFilter } = options;
@@ -104,7 +80,7 @@ export async function pickTask(options) {
104
80
  }
105
81
  const choices = [];
106
82
  if (inProgressChoices.length > 0) {
107
- choices.push(new Separator(pc.bold(`\n--- In Progress (${inProgressChoices.length}) ---`)));
83
+ choices.push(new Separator(`\n${sectionHeader(`In Progress (${inProgressChoices.length})`)}`));
108
84
  for (const choice of inProgressChoices) {
109
85
  choices.push({
110
86
  name: formatTaskChoice(choice),
@@ -113,7 +89,7 @@ export async function pickTask(options) {
113
89
  }
114
90
  }
115
91
  if (availableChoices.length > 0) {
116
- choices.push(new Separator(pc.bold(`\n--- Available (${availableChoices.length}) ---`)));
92
+ choices.push(new Separator(`\n${sectionHeader(`Available (${availableChoices.length})`)}`));
117
93
  for (const choice of availableChoices) {
118
94
  choices.push({
119
95
  name: formatTaskChoice(choice),
@@ -122,7 +98,7 @@ export async function pickTask(options) {
122
98
  }
123
99
  }
124
100
  if (lockedChoices.length > 0) {
125
- choices.push(new Separator(pc.bold(`\n--- Locked (${lockedChoices.length}) ---`)));
101
+ choices.push(new Separator(`\n${sectionHeader(`Locked (${lockedChoices.length})`)}`));
126
102
  for (const choice of lockedChoices) {
127
103
  choices.push({
128
104
  name: formatTaskChoice(choice),
@@ -197,7 +173,7 @@ async function handleCreateNewTask(api) {
197
173
  });
198
174
  console.log(pc.dim('\nCreating task in Damper...'));
199
175
  const task = await api.createTask(title.trim(), type);
200
- console.log(pc.green(`✓ Created task #${shortId(task.id)}: ${task.title}`));
176
+ console.log(pc.green(`✓ Created task #${shortIdRaw(task.id)}: ${task.title}`));
201
177
  return {
202
178
  task,
203
179
  isResume: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damper/cli",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
4
4
  "description": "CLI tool for orchestrating Damper task workflows with Claude Code",
5
5
  "author": "Damper <hello@usedamper.com>",
6
6
  "repository": {