@masslessai/push-todo 3.0.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,253 @@
1
+ /**
2
+ * Output formatting utilities for Push CLI.
3
+ *
4
+ * Formats tasks and other data for display.
5
+ */
6
+
7
+ import { bold, dim, green, yellow, red, cyan, muted, symbols } from './colors.js';
8
+
9
+ /**
10
+ * Format a duration in seconds to human-readable string.
11
+ *
12
+ * @param {number} seconds
13
+ * @returns {string} e.g., "2h 30m", "5m", "45s"
14
+ */
15
+ export function formatDuration(seconds) {
16
+ if (seconds < 60) {
17
+ return `${seconds}s`;
18
+ } else if (seconds < 3600) {
19
+ const mins = Math.floor(seconds / 60);
20
+ const secs = seconds % 60;
21
+ return secs ? `${mins}m ${secs}s` : `${mins}m`;
22
+ } else {
23
+ const hours = Math.floor(seconds / 3600);
24
+ const mins = Math.floor((seconds % 3600) / 60);
25
+ return mins ? `${hours}h ${mins}m` : `${hours}h`;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Format a date for display.
31
+ *
32
+ * @param {string|Date} date - ISO date string or Date object
33
+ * @returns {string}
34
+ */
35
+ export function formatDate(date) {
36
+ const d = typeof date === 'string' ? new Date(date) : date;
37
+ return d.toLocaleString();
38
+ }
39
+
40
+ /**
41
+ * Format a task for human-readable display.
42
+ *
43
+ * @param {Object} task - Task object
44
+ * @returns {string} Formatted task string
45
+ */
46
+ export function formatTaskForDisplay(task) {
47
+ const lines = [];
48
+
49
+ // Build task header with display number and status indicator
50
+ const displayNum = task.displayNumber || task.display_number;
51
+
52
+ // Determine status prefix
53
+ let statusPrefix = '';
54
+ if (task.isCompleted || task.is_completed) {
55
+ statusPrefix = '✅ '; // Completed
56
+ } else if (task.isBacklog || task.is_backlog) {
57
+ statusPrefix = '📦 '; // Backlog
58
+ }
59
+
60
+ const numPrefix = displayNum ? `#${displayNum} ` : '';
61
+ const summary = task.summary || 'No summary';
62
+
63
+ lines.push(`## Task: ${numPrefix}${statusPrefix}${summary}`);
64
+ lines.push('');
65
+
66
+ // Project hint
67
+ if (task.projectHint || task.project_hint) {
68
+ lines.push(`**Project:** ${task.projectHint || task.project_hint}`);
69
+ lines.push('');
70
+ }
71
+
72
+ // Content
73
+ lines.push('### Content');
74
+ lines.push(task.content || task.normalizedContent || 'No content');
75
+ lines.push('');
76
+
77
+ // Attachments
78
+ const screenshots = task.screenshotAttachments || task.screenshot_attachments || [];
79
+ const links = task.linkAttachments || task.link_attachments || [];
80
+
81
+ if (screenshots.length > 0 || links.length > 0) {
82
+ lines.push('### Attachments');
83
+ lines.push('');
84
+
85
+ if (screenshots.length > 0) {
86
+ lines.push(`#### Screenshots (${screenshots.length})`);
87
+ screenshots.forEach((screenshot, idx) => {
88
+ const filename = screenshot.imageFilename || 'unknown';
89
+ const width = screenshot.width;
90
+ const height = screenshot.height;
91
+ const dimensions = width && height ? `(${width}x${height})` : '';
92
+ lines.push(`${idx + 1}. ${filename} ${dimensions}`);
93
+ });
94
+ lines.push('');
95
+ }
96
+
97
+ if (links.length > 0) {
98
+ lines.push(`#### Links (${links.length})`);
99
+ links.forEach(link => {
100
+ const url = link.url || '';
101
+ const title = link.title || url;
102
+ lines.push(`🔗 [${title}](${url})`);
103
+ });
104
+ lines.push('');
105
+ }
106
+ }
107
+
108
+ // Transcript
109
+ const transcript = task.transcript || task.originalTranscript;
110
+ if (transcript) {
111
+ lines.push('### Original Voice Transcript');
112
+ lines.push(`> ${transcript}`);
113
+ lines.push('');
114
+ }
115
+
116
+ // Metadata
117
+ lines.push(`**Task ID:** \`${task.id || 'unknown'}\``);
118
+ if (displayNum) {
119
+ lines.push(`**Display Number:** #${displayNum}`);
120
+ }
121
+
122
+ // Status
123
+ if (task.isCompleted || task.is_completed) {
124
+ lines.push('**Status:** ✅ Completed');
125
+ // Show session resume hint if session_id is available
126
+ const sessionId = task.executionSessionId || task.execution_session_id;
127
+ if (sessionId) {
128
+ lines.push(`**Session:** Available (\`push-todo resume ${displayNum}\`)`);
129
+ }
130
+ } else if (task.isBacklog || task.is_backlog) {
131
+ lines.push('**Status:** 📦 Backlog');
132
+ } else {
133
+ lines.push('**Status:** Active');
134
+ }
135
+
136
+ const createdAt = task.createdAt || task.created_at;
137
+ lines.push(`**Created:** ${createdAt || 'unknown'}`);
138
+
139
+ return lines.join('\n');
140
+ }
141
+
142
+ /**
143
+ * Format a search result for display.
144
+ *
145
+ * @param {Object} result - Search result object
146
+ * @returns {string}
147
+ */
148
+ export function formatSearchResult(result) {
149
+ const lines = [];
150
+
151
+ const displayNum = result.displayNumber;
152
+ const isCompleted = result.isCompleted;
153
+ const isBacklog = result.isBacklog;
154
+
155
+ // Status indicators
156
+ let status = '';
157
+ if (isCompleted) {
158
+ status = ' [COMPLETED]';
159
+ } else if (isBacklog) {
160
+ status = ' [BACKLOG]';
161
+ }
162
+
163
+ const numPrefix = displayNum ? `#${displayNum}` : '??';
164
+ const title = result.summary || result.title || 'No summary';
165
+
166
+ lines.push(`**${numPrefix}**${status} ${title}`);
167
+
168
+ // Show match context if available
169
+ const matchContext = result.matchContext;
170
+ if (matchContext) {
171
+ lines.push(` > ${matchContext}`);
172
+ }
173
+
174
+ return lines.join('\n');
175
+ }
176
+
177
+ /**
178
+ * Format a task list as a table.
179
+ *
180
+ * @param {Object[]} tasks - Array of tasks
181
+ * @returns {string}
182
+ */
183
+ export function formatTaskTable(tasks) {
184
+ if (tasks.length === 0) {
185
+ return 'No tasks found.';
186
+ }
187
+
188
+ const lines = [];
189
+ lines.push('| # | Task | Status |');
190
+ lines.push('|-----|-------------------------------|-----------|');
191
+
192
+ for (const task of tasks) {
193
+ const num = String(task.displayNumber || task.display_number || '?').padEnd(3);
194
+ let summary = (task.summary || 'No summary').slice(0, 28);
195
+ if (summary.length < 28) {
196
+ summary = summary.padEnd(28);
197
+ } else {
198
+ summary = summary.slice(0, 27) + '…';
199
+ }
200
+
201
+ let status = 'Active';
202
+ if (task.isCompleted || task.is_completed) {
203
+ status = '✅ Done';
204
+ } else if (task.isBacklog || task.is_backlog) {
205
+ status = '📦 Later';
206
+ }
207
+ status = status.padEnd(9);
208
+
209
+ lines.push(`| ${num} | ${summary} | ${status} |`);
210
+ }
211
+
212
+ return lines.join('\n');
213
+ }
214
+
215
+ /**
216
+ * Format batch offer for display.
217
+ *
218
+ * @param {Object[]} tasks - Tasks to offer
219
+ * @returns {string}
220
+ */
221
+ export function formatBatchOffer(tasks) {
222
+ const count = tasks.length;
223
+ const numbers = tasks.map(t => t.displayNumber || t.display_number).join(',');
224
+
225
+ const lines = [];
226
+ lines.push('='.repeat(50));
227
+ lines.push(`BATCH_OFFER: ${count}`);
228
+ lines.push(`BATCH_TASKS: ${numbers}`);
229
+
230
+ for (const task of tasks) {
231
+ const num = task.displayNumber || task.display_number;
232
+ const summary = (task.summary || 'No summary').slice(0, 50);
233
+ lines.push(` #${num} - ${summary}`);
234
+ }
235
+
236
+ lines.push('='.repeat(50));
237
+
238
+ return lines.join('\n');
239
+ }
240
+
241
+ /**
242
+ * Truncate a string to a maximum length.
243
+ *
244
+ * @param {string} str
245
+ * @param {number} maxLength
246
+ * @returns {string}
247
+ */
248
+ export function truncate(str, maxLength) {
249
+ if (!str || str.length <= maxLength) {
250
+ return str || '';
251
+ }
252
+ return str.slice(0, maxLength - 1) + '…';
253
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Git utilities for Push CLI.
3
+ *
4
+ * Provides helpers for git operations like getting remote URLs.
5
+ */
6
+
7
+ import { execSync } from 'child_process';
8
+
9
+ /**
10
+ * Get the normalized git remote URL for the current directory.
11
+ *
12
+ * Normalizes URLs to a consistent format:
13
+ * - git@github.com:user/repo.git → github.com/user/repo
14
+ * - https://github.com/user/repo.git → github.com/user/repo
15
+ *
16
+ * @returns {string|null} Normalized git remote or null if not a git repo
17
+ */
18
+ export function getGitRemote() {
19
+ try {
20
+ const result = execSync('git remote get-url origin', {
21
+ encoding: 'utf8',
22
+ timeout: 5000,
23
+ stdio: ['pipe', 'pipe', 'pipe']
24
+ });
25
+
26
+ let url = result.trim();
27
+ if (!url) {
28
+ return null;
29
+ }
30
+
31
+ // Normalize: remove protocol, convert : to /, remove .git
32
+ // git@github.com:user/repo.git → github.com/user/repo
33
+ // https://github.com/user/repo.git → github.com/user/repo
34
+
35
+ // Remove protocol prefixes
36
+ const prefixes = ['https://', 'http://', 'git@', 'ssh://git@'];
37
+ for (const prefix of prefixes) {
38
+ if (url.startsWith(prefix)) {
39
+ url = url.slice(prefix.length);
40
+ break;
41
+ }
42
+ }
43
+
44
+ // Convert : to / (for git@ style)
45
+ if (url.includes(':') && !url.includes('://')) {
46
+ url = url.replace(':', '/');
47
+ }
48
+
49
+ // Remove .git suffix
50
+ if (url.endsWith('.git')) {
51
+ url = url.slice(0, -4);
52
+ }
53
+
54
+ return url;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Check if the current directory is a git repository.
62
+ *
63
+ * @returns {boolean}
64
+ */
65
+ export function isGitRepo() {
66
+ try {
67
+ execSync('git rev-parse --git-dir', {
68
+ encoding: 'utf8',
69
+ timeout: 5000,
70
+ stdio: ['pipe', 'pipe', 'pipe']
71
+ });
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Get the current git branch name.
80
+ *
81
+ * @returns {string|null}
82
+ */
83
+ export function getCurrentBranch() {
84
+ try {
85
+ const result = execSync('git rev-parse --abbrev-ref HEAD', {
86
+ encoding: 'utf8',
87
+ timeout: 5000,
88
+ stdio: ['pipe', 'pipe', 'pipe']
89
+ });
90
+ return result.trim() || null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get the root directory of the git repository.
98
+ *
99
+ * @returns {string|null}
100
+ */
101
+ export function getGitRoot() {
102
+ try {
103
+ const result = execSync('git rev-parse --show-toplevel', {
104
+ encoding: 'utf8',
105
+ timeout: 5000,
106
+ stdio: ['pipe', 'pipe', 'pipe']
107
+ });
108
+ return result.trim() || null;
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Get recent commit messages for context.
116
+ *
117
+ * @param {number} count - Number of commits to fetch
118
+ * @returns {string[]} Array of commit messages
119
+ */
120
+ export function getRecentCommits(count = 5) {
121
+ try {
122
+ const result = execSync(`git log --oneline -${count}`, {
123
+ encoding: 'utf8',
124
+ timeout: 5000,
125
+ stdio: ['pipe', 'pipe', 'pipe']
126
+ });
127
+ return result.trim().split('\n').filter(Boolean);
128
+ } catch {
129
+ return [];
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Check if there are uncommitted changes.
135
+ *
136
+ * @returns {boolean}
137
+ */
138
+ export function hasUncommittedChanges() {
139
+ try {
140
+ const result = execSync('git status --porcelain', {
141
+ encoding: 'utf8',
142
+ timeout: 5000,
143
+ stdio: ['pipe', 'pipe', 'pipe']
144
+ });
145
+ return result.trim().length > 0;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }