@avantmedia/af 0.0.1

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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +539 -0
  3. package/af +2 -0
  4. package/bun-upgrade.ts +130 -0
  5. package/commands/bun.ts +55 -0
  6. package/commands/changes.ts +35 -0
  7. package/commands/e2e.ts +12 -0
  8. package/commands/help.ts +236 -0
  9. package/commands/install-extension.ts +133 -0
  10. package/commands/jira.ts +577 -0
  11. package/commands/licenses.ts +32 -0
  12. package/commands/npm.ts +55 -0
  13. package/commands/scaffold.ts +105 -0
  14. package/commands/setup.tsx +156 -0
  15. package/commands/spec.ts +405 -0
  16. package/commands/stop-hook.ts +90 -0
  17. package/commands/todo.ts +208 -0
  18. package/commands/versions.ts +150 -0
  19. package/commands/watch.ts +344 -0
  20. package/commands/worktree.ts +424 -0
  21. package/components/change-select.tsx +71 -0
  22. package/components/confirm.tsx +41 -0
  23. package/components/file-conflict.tsx +52 -0
  24. package/components/input.tsx +53 -0
  25. package/components/layout.tsx +70 -0
  26. package/components/messages.tsx +48 -0
  27. package/components/progress.tsx +71 -0
  28. package/components/select.tsx +90 -0
  29. package/components/status-display.tsx +74 -0
  30. package/components/table.tsx +79 -0
  31. package/generated/setup-manifest.ts +67 -0
  32. package/git-worktree.ts +184 -0
  33. package/main.ts +12 -0
  34. package/npm-upgrade.ts +117 -0
  35. package/package.json +83 -0
  36. package/resources/copy-prompt-reporter.ts +443 -0
  37. package/router.ts +220 -0
  38. package/setup/.claude/commands/commit-work.md +47 -0
  39. package/setup/.claude/commands/complete-work.md +34 -0
  40. package/setup/.claude/commands/e2e.md +29 -0
  41. package/setup/.claude/commands/start-work.md +51 -0
  42. package/setup/.claude/skills/pm/SKILL.md +294 -0
  43. package/setup/.claude/skills/pm/templates/api-endpoint.md +69 -0
  44. package/setup/.claude/skills/pm/templates/bug-fix.md +77 -0
  45. package/setup/.claude/skills/pm/templates/feature.md +87 -0
  46. package/setup/.claude/skills/pm/templates/ui-component.md +78 -0
  47. package/utils/change-select-render.tsx +44 -0
  48. package/utils/claude.ts +9 -0
  49. package/utils/config.ts +58 -0
  50. package/utils/env.ts +53 -0
  51. package/utils/git.ts +120 -0
  52. package/utils/ink-render.tsx +50 -0
  53. package/utils/openspec.ts +54 -0
  54. package/utils/output.ts +104 -0
  55. package/utils/proposal.ts +160 -0
  56. package/utils/resources.ts +64 -0
  57. package/utils/setup-files.ts +230 -0
@@ -0,0 +1,344 @@
1
+ import { watch } from 'node:fs';
2
+ import { join, basename } from 'node:path';
3
+ import { readdir, readFile } from 'node:fs/promises';
4
+ import { error, colors } from '../utils/output.ts';
5
+
6
+ interface Task {
7
+ text: string;
8
+ completed: boolean;
9
+ indent: number;
10
+ }
11
+
12
+ interface Section {
13
+ title: string;
14
+ tasks: Task[];
15
+ }
16
+
17
+ interface ChangeTaskData {
18
+ changeId: string;
19
+ sections: Section[];
20
+ totalTasks: number;
21
+ completedTasks: number;
22
+ }
23
+
24
+ async function parseTasksFile(filePath: string): Promise<{
25
+ sections: Section[];
26
+ totalTasks: number;
27
+ completedTasks: number;
28
+ }> {
29
+ try {
30
+ const content = await readFile(filePath, 'utf-8');
31
+ const lines = content.split('\n');
32
+
33
+ const sections: Section[] = [];
34
+ let currentSection: Section | null = null;
35
+ let totalTasks = 0;
36
+ let completedTasks = 0;
37
+
38
+ for (const line of lines) {
39
+ const sectionMatch = line.match(/^## (.+)$/);
40
+ if (sectionMatch) {
41
+ if (currentSection) {
42
+ sections.push(currentSection);
43
+ }
44
+ currentSection = {
45
+ title: sectionMatch[1].trim(),
46
+ tasks: [],
47
+ };
48
+ continue;
49
+ }
50
+
51
+ const taskMatch = line.match(/^(\s*)- \[([ xX])\] (.+)$/);
52
+ if (taskMatch) {
53
+ const [, indentStr, checkbox, text] = taskMatch;
54
+ const indent = indentStr.length;
55
+ const isCompleted = checkbox.toLowerCase() === 'x';
56
+
57
+ const task: Task = {
58
+ text: text.trim(),
59
+ completed: isCompleted,
60
+ indent,
61
+ };
62
+
63
+ if (currentSection) {
64
+ currentSection.tasks.push(task);
65
+ }
66
+
67
+ totalTasks++;
68
+ if (isCompleted) {
69
+ completedTasks++;
70
+ }
71
+ }
72
+ }
73
+
74
+ if (currentSection) {
75
+ sections.push(currentSection);
76
+ }
77
+
78
+ return { sections, totalTasks, completedTasks };
79
+ } catch (_error) {
80
+ return { sections: [], totalTasks: 0, completedTasks: 0 };
81
+ }
82
+ }
83
+
84
+ async function getActiveChanges(): Promise<string[]> {
85
+ try {
86
+ const changesDir = join(process.cwd(), 'openspec', 'changes');
87
+ const entries = await readdir(changesDir, { withFileTypes: true });
88
+
89
+ return entries
90
+ .filter(entry => entry.isDirectory() && entry.name !== 'archive')
91
+ .map(entry => entry.name);
92
+ } catch (_error) {
93
+ return [];
94
+ }
95
+ }
96
+
97
+ function displayChange(changeData: ChangeTaskData): void {
98
+ const { changeId, sections, totalTasks, completedTasks } = changeData;
99
+
100
+ const progressText = `${completedTasks}/${totalTasks} tasks completed`;
101
+ console.log(
102
+ `${colors.blue}┌─ ${changeId}${colors.reset} ${colors.gray}(${progressText})${colors.reset}`,
103
+ );
104
+ console.log(`${colors.blue}│${colors.reset}`);
105
+
106
+ for (const section of sections) {
107
+ if (section.tasks.length === 0) continue;
108
+
109
+ console.log(
110
+ `${colors.blue}│${colors.reset} ${colors.cyan}${section.title}${colors.reset}`,
111
+ );
112
+
113
+ for (const task of section.tasks) {
114
+ const checkbox = task.completed
115
+ ? `${colors.green}☑${colors.reset}`
116
+ : `${colors.gray}☐${colors.reset}`;
117
+ const indent = ' '.repeat(task.indent / 4);
118
+ console.log(`${colors.blue}│${colors.reset} ${indent}${checkbox} ${task.text}`);
119
+ }
120
+
121
+ console.log(`${colors.blue}│${colors.reset}`);
122
+ }
123
+
124
+ console.log(`${colors.blue}└${'─'.repeat(40)}${colors.reset}`);
125
+ console.log('');
126
+ }
127
+
128
+ export function getProjectName(): string {
129
+ const cwd = process.cwd();
130
+ const projectName = basename(cwd);
131
+ return projectName || 'root';
132
+ }
133
+
134
+ export interface AggregateMetrics {
135
+ totalChanges: number;
136
+ totalTasks: number;
137
+ completedTasks: number;
138
+ }
139
+
140
+ export function calculateAggregateMetrics(changes: ChangeTaskData[]): AggregateMetrics {
141
+ return {
142
+ totalChanges: changes.length,
143
+ totalTasks: changes.reduce((sum, change) => sum + change.totalTasks, 0),
144
+ completedTasks: changes.reduce((sum, change) => sum + change.completedTasks, 0),
145
+ };
146
+ }
147
+
148
+ export function renderProgressBar(completed: number, total: number): string {
149
+ if (total === 0) {
150
+ return `${colors.gray}${'░'.repeat(20)}${colors.reset} ${colors.gray}N/A${colors.reset}`;
151
+ }
152
+
153
+ const percentage = Math.round((completed / total) * 100);
154
+ const barWidth = 20;
155
+ const filledWidth = Math.round((completed / total) * barWidth);
156
+
157
+ const filledBar = '█'.repeat(filledWidth);
158
+ const emptyBar = '░'.repeat(barWidth - filledWidth);
159
+
160
+ const barColor = percentage === 100 ? colors.green : colors.green;
161
+ const emptyColor = colors.gray;
162
+
163
+ return `${barColor}${filledBar}${colors.reset}${emptyColor}${emptyBar}${colors.reset} ${colors.gray}${percentage}%${colors.reset}`;
164
+ }
165
+
166
+ function calculateIdleDuration(lastChangeTime: number): number {
167
+ return Date.now() - lastChangeTime;
168
+ }
169
+
170
+ function formatIdleDuration(durationMs: number): string {
171
+ const seconds = Math.floor(durationMs / 1000);
172
+ const minutes = Math.floor(seconds / 60);
173
+ const remainingSeconds = seconds % 60;
174
+
175
+ if (minutes > 0) {
176
+ return `${minutes}m ${remainingSeconds}s`;
177
+ }
178
+ return `${seconds}s`;
179
+ }
180
+
181
+ function displayStatusBar(changes: ChangeTaskData[], lastChangeTime: number): void {
182
+ const projectName = getProjectName();
183
+ const metrics = calculateAggregateMetrics(changes);
184
+ const progressBar = renderProgressBar(metrics.completedTasks, metrics.totalTasks);
185
+
186
+ const changeText = metrics.totalChanges === 1 ? 'change' : 'changes';
187
+ let statusLine = `Project: ${colors.cyan}${projectName}${colors.reset} ${colors.gray}|${colors.reset} ${colors.gray}${metrics.totalChanges}${colors.reset} ${changeText} ${colors.gray}|${colors.reset} ${colors.gray}${metrics.completedTasks}/${metrics.totalTasks}${colors.reset} tasks ${colors.gray}|${colors.reset} ${progressBar}`;
188
+
189
+ // Add idle warning if idle for more than 60 seconds
190
+ const idleDurationMs = calculateIdleDuration(lastChangeTime);
191
+ const idleThresholdMs = 60 * 1000; // 60 seconds
192
+ const idleRedThresholdMs = 30 * 60 * 1000; // 30 minutes
193
+
194
+ if (idleDurationMs > idleThresholdMs) {
195
+ const idleDuration = formatIdleDuration(idleDurationMs);
196
+ // Use red color for 30+ minutes, yellow for 60s-30m
197
+ const warningColor = idleDurationMs > idleRedThresholdMs ? colors.red : colors.yellow;
198
+ const idleWarning = ` ${colors.gray}|${colors.reset} ${warningColor}⚠ IDLE for ${idleDuration}${colors.reset}`;
199
+ statusLine += idleWarning;
200
+ }
201
+
202
+ const borderLength = 100;
203
+ console.log(`${colors.blue}┌${'─'.repeat(borderLength)}${colors.reset}`);
204
+ console.log(`${colors.blue}│${colors.reset} ${statusLine}`);
205
+ console.log(`${colors.blue}└${'─'.repeat(borderLength)}${colors.reset}`);
206
+ }
207
+
208
+ async function displayTodos(lastChangeTime: number): Promise<void> {
209
+ // Clear screen and position cursor at top-left
210
+ process.stdout.write('\x1b[2J\x1b[H');
211
+
212
+ const changes = await getActiveChanges();
213
+
214
+ // Display header with timestamp
215
+ const timestamp = new Date(lastChangeTime).toLocaleTimeString();
216
+ console.log(
217
+ `${colors.cyan}📋 TODO Items (watching for changes...)${colors.reset} ${colors.gray}Last change: ${timestamp}${colors.reset}`,
218
+ );
219
+ console.log(`${colors.gray}Press Ctrl+C to exit${colors.reset}\n`);
220
+
221
+ if (changes.length === 0) {
222
+ console.log('No active changes found.');
223
+ // Display status bar even with 0 changes
224
+ displayStatusBar([], lastChangeTime);
225
+ return;
226
+ }
227
+
228
+ // Collect all change data for status bar
229
+ const allChangesData: ChangeTaskData[] = [];
230
+
231
+ for (const changeId of changes) {
232
+ const tasksPath = join(process.cwd(), 'openspec', 'changes', changeId, 'tasks.md');
233
+ const { sections, totalTasks, completedTasks } = await parseTasksFile(tasksPath);
234
+
235
+ const changeData: ChangeTaskData = {
236
+ changeId,
237
+ sections,
238
+ totalTasks,
239
+ completedTasks,
240
+ };
241
+
242
+ // Add to collection for status bar (even if no tasks)
243
+ allChangesData.push(changeData);
244
+
245
+ // Only display if there are tasks
246
+ if (totalTasks > 0) {
247
+ displayChange(changeData);
248
+ }
249
+ }
250
+
251
+ // Display status bar at the bottom
252
+ displayStatusBar(allChangesData, lastChangeTime);
253
+ }
254
+
255
+ export async function handleWatch(hasArgs: boolean): Promise<number> {
256
+ if (hasArgs) {
257
+ error('Error: watch command does not accept arguments');
258
+ console.error('Usage: af watch');
259
+ return 1;
260
+ }
261
+
262
+ const changesDir = join(process.cwd(), 'openspec', 'changes');
263
+
264
+ // Track last change time
265
+ let lastChangeTime = Date.now();
266
+
267
+ // Display initial state
268
+ await displayTodos(lastChangeTime);
269
+
270
+ // Set up debouncing
271
+ let debounceTimer: NodeJS.Timeout | null = null;
272
+
273
+ // Set up periodic refresh timer for idle state
274
+ let periodicRefreshTimer: NodeJS.Timeout | null = null;
275
+
276
+ const startPeriodicRefresh = () => {
277
+ // Clear existing timer if any
278
+ if (periodicRefreshTimer) {
279
+ clearInterval(periodicRefreshTimer);
280
+ }
281
+
282
+ // Refresh every 10 seconds when idle
283
+ periodicRefreshTimer = setInterval(() => {
284
+ const idleDurationMs = Date.now() - lastChangeTime;
285
+ const idleThresholdMs = 60 * 1000; // 60 seconds
286
+
287
+ // Only refresh if we're in idle state
288
+ if (idleDurationMs > idleThresholdMs) {
289
+ displayTodos(lastChangeTime).catch(err => {
290
+ console.error(`Error refreshing display: ${err}`);
291
+ });
292
+ }
293
+ }, 10000); // 10 seconds
294
+ };
295
+
296
+ // Start periodic refresh
297
+ startPeriodicRefresh();
298
+
299
+ // Set up file watcher with recursive option
300
+ const watcher = watch(changesDir, { recursive: true }, (_eventType, filename) => {
301
+ // Ignore changes in archive directory
302
+ if (filename && filename.includes('archive')) {
303
+ return;
304
+ }
305
+
306
+ // Update last change time
307
+ lastChangeTime = Date.now();
308
+
309
+ // Debounce: wait 100ms after last change before refreshing
310
+ if (debounceTimer) {
311
+ clearTimeout(debounceTimer);
312
+ }
313
+
314
+ debounceTimer = setTimeout(() => {
315
+ displayTodos(lastChangeTime).catch(err => {
316
+ console.error(`Error refreshing display: ${err}`);
317
+ });
318
+ }, 100);
319
+ });
320
+
321
+ // Handle Ctrl+C gracefully
322
+ process.on('SIGINT', () => {
323
+ console.log('\nStopping watch mode...');
324
+ if (debounceTimer) {
325
+ clearTimeout(debounceTimer);
326
+ }
327
+ if (periodicRefreshTimer) {
328
+ clearInterval(periodicRefreshTimer);
329
+ }
330
+ watcher.close();
331
+ process.exit(0);
332
+ });
333
+
334
+ // Handle errors
335
+ watcher.on('error', err => {
336
+ error(`File watching error: ${err.message}`);
337
+ console.error('Attempting to continue...');
338
+ });
339
+
340
+ // Keep process running
341
+ return new Promise(() => {
342
+ // Never resolves - watch runs until interrupted
343
+ });
344
+ }