@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.
- package/LICENSE +21 -0
- package/README.md +539 -0
- package/af +2 -0
- package/bun-upgrade.ts +130 -0
- package/commands/bun.ts +55 -0
- package/commands/changes.ts +35 -0
- package/commands/e2e.ts +12 -0
- package/commands/help.ts +236 -0
- package/commands/install-extension.ts +133 -0
- package/commands/jira.ts +577 -0
- package/commands/licenses.ts +32 -0
- package/commands/npm.ts +55 -0
- package/commands/scaffold.ts +105 -0
- package/commands/setup.tsx +156 -0
- package/commands/spec.ts +405 -0
- package/commands/stop-hook.ts +90 -0
- package/commands/todo.ts +208 -0
- package/commands/versions.ts +150 -0
- package/commands/watch.ts +344 -0
- package/commands/worktree.ts +424 -0
- package/components/change-select.tsx +71 -0
- package/components/confirm.tsx +41 -0
- package/components/file-conflict.tsx +52 -0
- package/components/input.tsx +53 -0
- package/components/layout.tsx +70 -0
- package/components/messages.tsx +48 -0
- package/components/progress.tsx +71 -0
- package/components/select.tsx +90 -0
- package/components/status-display.tsx +74 -0
- package/components/table.tsx +79 -0
- package/generated/setup-manifest.ts +67 -0
- package/git-worktree.ts +184 -0
- package/main.ts +12 -0
- package/npm-upgrade.ts +117 -0
- package/package.json +83 -0
- package/resources/copy-prompt-reporter.ts +443 -0
- package/router.ts +220 -0
- package/setup/.claude/commands/commit-work.md +47 -0
- package/setup/.claude/commands/complete-work.md +34 -0
- package/setup/.claude/commands/e2e.md +29 -0
- package/setup/.claude/commands/start-work.md +51 -0
- package/setup/.claude/skills/pm/SKILL.md +294 -0
- package/setup/.claude/skills/pm/templates/api-endpoint.md +69 -0
- package/setup/.claude/skills/pm/templates/bug-fix.md +77 -0
- package/setup/.claude/skills/pm/templates/feature.md +87 -0
- package/setup/.claude/skills/pm/templates/ui-component.md +78 -0
- package/utils/change-select-render.tsx +44 -0
- package/utils/claude.ts +9 -0
- package/utils/config.ts +58 -0
- package/utils/env.ts +53 -0
- package/utils/git.ts +120 -0
- package/utils/ink-render.tsx +50 -0
- package/utils/openspec.ts +54 -0
- package/utils/output.ts +104 -0
- package/utils/proposal.ts +160 -0
- package/utils/resources.ts +64 -0
- 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
|
+
}
|