@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,90 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { getStopHookConfig } from '../utils/config.ts';
3
+ import { info } from '../utils/output.ts';
4
+
5
+ /**
6
+ * Get changed files from git.
7
+ * Combines staged, unstaged, and untracked files.
8
+ */
9
+ function getChangedFiles(): string[] {
10
+ const files = new Set<string>();
11
+
12
+ // Staged changes
13
+ const staged = spawnSync('git', ['diff', '--name-only', '--cached'], {
14
+ encoding: 'utf-8',
15
+ stdio: ['pipe', 'pipe', 'pipe'],
16
+ });
17
+ if (staged.status === 0 && staged.stdout) {
18
+ for (const file of staged.stdout.trim().split('\n')) {
19
+ if (file) files.add(file);
20
+ }
21
+ }
22
+
23
+ // Unstaged changes
24
+ const unstaged = spawnSync('git', ['diff', '--name-only'], {
25
+ encoding: 'utf-8',
26
+ stdio: ['pipe', 'pipe', 'pipe'],
27
+ });
28
+ if (unstaged.status === 0 && unstaged.stdout) {
29
+ for (const file of unstaged.stdout.trim().split('\n')) {
30
+ if (file) files.add(file);
31
+ }
32
+ }
33
+
34
+ // Untracked files
35
+ const untracked = spawnSync('git', ['ls-files', '--others', '--exclude-standard'], {
36
+ encoding: 'utf-8',
37
+ stdio: ['pipe', 'pipe', 'pipe'],
38
+ });
39
+ if (untracked.status === 0 && untracked.stdout) {
40
+ for (const file of untracked.stdout.trim().split('\n')) {
41
+ if (file) files.add(file);
42
+ }
43
+ }
44
+
45
+ return Array.from(files);
46
+ }
47
+
48
+ /**
49
+ * Filter out files that match any of the ignored path prefixes.
50
+ */
51
+ function filterIgnoredPaths(files: string[], ignoredPaths: string[]): string[] {
52
+ return files.filter(file => !ignoredPaths.some(prefix => file.startsWith(prefix)));
53
+ }
54
+
55
+ /**
56
+ * Handle the 'stop-hook' command.
57
+ * Conditionally runs e2e tests based on changed files.
58
+ *
59
+ * @returns Exit code: 0 (success/skipped), 2 (e2e failed)
60
+ */
61
+ export async function handleStopHook(): Promise<number> {
62
+ const config = getStopHookConfig();
63
+
64
+ const changedFiles = getChangedFiles();
65
+ const relevantFiles = filterIgnoredPaths(changedFiles, config.ignoredPaths);
66
+
67
+ if (relevantFiles.length === 0) {
68
+ info('No relevant file changes detected, skipping e2e tests');
69
+ return 0;
70
+ }
71
+
72
+ info(`Running e2e tests (${relevantFiles.length} relevant file(s) changed)`);
73
+ console.log(relevantFiles.join('\n'));
74
+
75
+ // Parse the command into parts
76
+ const parts = config.command.split(/\s+/);
77
+ const cmd = parts[0];
78
+ const args = parts.slice(1);
79
+
80
+ const result = spawnSync(cmd, args, {
81
+ stdio: 'inherit',
82
+ shell: true,
83
+ });
84
+
85
+ if (result.status !== 0) {
86
+ return 2;
87
+ }
88
+
89
+ return 0;
90
+ }
@@ -0,0 +1,208 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { error, colors } from '../utils/output.ts';
4
+
5
+ /**
6
+ * Represents a single task from tasks.md
7
+ */
8
+ interface Task {
9
+ text: string;
10
+ completed: boolean;
11
+ indent: number;
12
+ }
13
+
14
+ /**
15
+ * Represents a section in tasks.md (e.g., "Implementation Tasks")
16
+ */
17
+ interface Section {
18
+ title: string;
19
+ tasks: Task[];
20
+ }
21
+
22
+ /**
23
+ * Represents all task data for a single change
24
+ */
25
+ interface ChangeTaskData {
26
+ changeId: string;
27
+ sections: Section[];
28
+ totalTasks: number;
29
+ completedTasks: number;
30
+ }
31
+
32
+ /**
33
+ * Parse a tasks.md file and extract structured task data.
34
+ *
35
+ * @param filePath - Path to the tasks.md file
36
+ * @returns Parsed task data with sections and completion counts
37
+ */
38
+ async function parseTasksFile(filePath: string): Promise<{
39
+ sections: Section[];
40
+ totalTasks: number;
41
+ completedTasks: number;
42
+ }> {
43
+ try {
44
+ const content = await readFile(filePath, 'utf-8');
45
+ const lines = content.split('\n');
46
+
47
+ const sections: Section[] = [];
48
+ let currentSection: Section | null = null;
49
+ let totalTasks = 0;
50
+ let completedTasks = 0;
51
+
52
+ for (const line of lines) {
53
+ // Match section headers (## Header)
54
+ const sectionMatch = line.match(/^## (.+)$/);
55
+ if (sectionMatch) {
56
+ if (currentSection) {
57
+ sections.push(currentSection);
58
+ }
59
+ currentSection = {
60
+ title: sectionMatch[1].trim(),
61
+ tasks: [],
62
+ };
63
+ continue;
64
+ }
65
+
66
+ // Match task items (- [ ] or - [x])
67
+ const taskMatch = line.match(/^(\s*)- \[([ xX])\] (.+)$/);
68
+ if (taskMatch) {
69
+ const [, indentStr, checkbox, text] = taskMatch;
70
+ const indent = indentStr.length;
71
+ const isCompleted = checkbox.toLowerCase() === 'x';
72
+
73
+ const task: Task = {
74
+ text: text.trim(),
75
+ completed: isCompleted,
76
+ indent,
77
+ };
78
+
79
+ if (currentSection) {
80
+ currentSection.tasks.push(task);
81
+ }
82
+
83
+ totalTasks++;
84
+ if (isCompleted) {
85
+ completedTasks++;
86
+ }
87
+ }
88
+ }
89
+
90
+ // Push the last section
91
+ if (currentSection) {
92
+ sections.push(currentSection);
93
+ }
94
+
95
+ return { sections, totalTasks, completedTasks };
96
+ } catch (_error) {
97
+ // If file doesn't exist or can't be read, return empty data
98
+ return { sections: [], totalTasks: 0, completedTasks: 0 };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Get all active change directories from openspec/changes/.
104
+ * Excludes the 'archive' directory.
105
+ *
106
+ * @returns Array of change directory names
107
+ */
108
+ async function getActiveChanges(): Promise<string[]> {
109
+ try {
110
+ const changesDir = join(process.cwd(), 'openspec', 'changes');
111
+ const entries = await readdir(changesDir, { withFileTypes: true });
112
+
113
+ return entries
114
+ .filter(entry => entry.isDirectory() && entry.name !== 'archive')
115
+ .map(entry => entry.name);
116
+ } catch (_error) {
117
+ // Directory doesn't exist or can't be read
118
+ return [];
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Format and display a single change's tasks with visual formatting.
124
+ *
125
+ * @param changeData - The change task data to display
126
+ */
127
+ function displayChange(changeData: ChangeTaskData): void {
128
+ const { changeId, sections, totalTasks, completedTasks } = changeData;
129
+
130
+ // Display change header with progress
131
+ const progressText = `${completedTasks}/${totalTasks} tasks completed`;
132
+ console.log(
133
+ `${colors.blue}┌─ ${changeId}${colors.reset} ${colors.gray}(${progressText})${colors.reset}`,
134
+ );
135
+ console.log(`${colors.blue}│${colors.reset}`);
136
+
137
+ // Display each section and its tasks
138
+ for (const section of sections) {
139
+ if (section.tasks.length === 0) continue;
140
+
141
+ console.log(
142
+ `${colors.blue}│${colors.reset} ${colors.cyan}${section.title}${colors.reset}`,
143
+ );
144
+
145
+ for (const task of section.tasks) {
146
+ const checkbox = task.completed
147
+ ? `${colors.green}☑${colors.reset}`
148
+ : `${colors.gray}☐${colors.reset}`;
149
+ const indent = ' '.repeat(task.indent / 4); // Convert spaces to visual indent
150
+ console.log(`${colors.blue}│${colors.reset} ${indent}${checkbox} ${task.text}`);
151
+ }
152
+
153
+ console.log(`${colors.blue}│${colors.reset}`);
154
+ }
155
+
156
+ // Display bottom border
157
+ console.log(`${colors.blue}└${'─'.repeat(40)}${colors.reset}`);
158
+ console.log('');
159
+ }
160
+
161
+ /**
162
+ * Handle the 'todo' command.
163
+ * Displays all TODO items from active OpenSpec changes.
164
+ *
165
+ * @param hasArgs - Whether any arguments were provided to the command
166
+ * @returns Exit code (0 for success, 1 for error)
167
+ */
168
+ export async function handleTodo(hasArgs: boolean): Promise<number> {
169
+ // Reject if arguments were provided
170
+ if (hasArgs) {
171
+ error('Error: todo command does not accept arguments');
172
+ console.error('Usage: af todo');
173
+ return 1;
174
+ }
175
+
176
+ // Get all active changes
177
+ const changes = await getActiveChanges();
178
+
179
+ if (changes.length === 0) {
180
+ console.log('No active changes found.');
181
+ return 0;
182
+ }
183
+
184
+ // Display header
185
+ console.log(`\n${colors.cyan}📋 TODO Items${colors.reset}\n`);
186
+
187
+ // Process each change
188
+ for (const changeId of changes) {
189
+ const tasksPath = join(process.cwd(), 'openspec', 'changes', changeId, 'tasks.md');
190
+ const { sections, totalTasks, completedTasks } = await parseTasksFile(tasksPath);
191
+
192
+ if (totalTasks === 0) {
193
+ // Skip changes with no tasks
194
+ continue;
195
+ }
196
+
197
+ const changeData: ChangeTaskData = {
198
+ changeId,
199
+ sections,
200
+ totalTasks,
201
+ completedTasks,
202
+ };
203
+
204
+ displayChange(changeData);
205
+ }
206
+
207
+ return 0;
208
+ }
@@ -0,0 +1,150 @@
1
+ import {
2
+ getCurrentHeadCommit,
3
+ hasUncommittedChanges,
4
+ isGitRepository,
5
+ listWorktrees,
6
+ pushWorktree,
7
+ resetWorktree,
8
+ } from '../git-worktree.ts';
9
+ import { info, success, error } from '../utils/output.ts';
10
+
11
+ /**
12
+ * Force-pushes all version worktrees (matching /v\d+/ pattern) to their remote repositories.
13
+ *
14
+ * @returns Exit code (0 for success, 1 for error)
15
+ */
16
+ export async function handleVersionsPush(): Promise<number> {
17
+ try {
18
+ // 1. Validate we're in a git repository
19
+ if (!isGitRepository()) {
20
+ error('Error: Not in a git repository');
21
+ return 1;
22
+ }
23
+
24
+ // 2. Enumerate worktrees and filter by /v\d+/ pattern
25
+ const allWorktrees = listWorktrees();
26
+ const versionPattern = /^v\d+$/;
27
+ const matchingWorktrees = allWorktrees.filter(wt => versionPattern.test(wt.branch));
28
+
29
+ // Handle case where no matching worktrees are found
30
+ if (matchingWorktrees.length === 0) {
31
+ info('No worktrees with branches matching /v\\d+/ pattern found.');
32
+ return 0;
33
+ }
34
+
35
+ // 3. Push each worktree
36
+ for (const worktree of matchingWorktrees) {
37
+ info(`Pushing worktree ${worktree.branch}...`);
38
+ try {
39
+ pushWorktree(worktree.path);
40
+ } catch (err) {
41
+ if (err instanceof Error) {
42
+ error(err.message);
43
+ } else {
44
+ error(`Failed to push worktree '${worktree.branch}'`);
45
+ }
46
+ return 1;
47
+ }
48
+ }
49
+
50
+ // 4. Display success summary
51
+ const branchNames = matchingWorktrees.map(wt => wt.branch).join(', ');
52
+ success(`Successfully pushed ${matchingWorktrees.length} worktree(s): ${branchNames}`);
53
+ return 0;
54
+ } catch (err) {
55
+ if (err instanceof Error) {
56
+ error(`Error: ${err.message}`);
57
+ } else {
58
+ error('An unexpected error occurred');
59
+ }
60
+ return 1;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Resets all version worktrees (matching /v\d+/ pattern) to the current branch HEAD.
66
+ *
67
+ * @returns Exit code (0 for success, 1 for error)
68
+ */
69
+ export async function handleVersionsReset(): Promise<number> {
70
+ try {
71
+ // 1. Validate we're in a git repository
72
+ if (!isGitRepository()) {
73
+ error('Error: Not in a git repository');
74
+ return 1;
75
+ }
76
+
77
+ // 2. Get current HEAD commit
78
+ const targetRevision = getCurrentHeadCommit();
79
+
80
+ // 3. Enumerate worktrees and filter by /v\d+/ pattern
81
+ const allWorktrees = listWorktrees();
82
+ const versionPattern = /^v\d+$/;
83
+ const matchingWorktrees = allWorktrees.filter(wt => versionPattern.test(wt.branch));
84
+
85
+ // Handle case where no matching worktrees are found
86
+ if (matchingWorktrees.length === 0) {
87
+ info('No worktrees with branches matching /v\\d+/ pattern found.');
88
+ return 0;
89
+ }
90
+
91
+ // 4. Check each worktree for uncommitted changes
92
+ const worktreesWithChanges: string[] = [];
93
+ for (const worktree of matchingWorktrees) {
94
+ try {
95
+ if (hasUncommittedChanges(worktree.path)) {
96
+ worktreesWithChanges.push(worktree.branch);
97
+ }
98
+ } catch (err) {
99
+ if (err instanceof Error) {
100
+ error(err.message);
101
+ } else {
102
+ error(`Failed to check worktree '${worktree.branch}'`);
103
+ }
104
+ return 1;
105
+ }
106
+ }
107
+
108
+ // 5. If any worktree has uncommitted changes, report and exit
109
+ if (worktreesWithChanges.length > 0) {
110
+ error('Error: Cannot reset worktrees with uncommitted changes');
111
+ for (const worktree of matchingWorktrees) {
112
+ if (worktreesWithChanges.includes(worktree.branch)) {
113
+ console.error(
114
+ `Worktree '${worktree.branch}' at ${worktree.path} has uncommitted changes`,
115
+ );
116
+ }
117
+ }
118
+ console.error('\nPlease commit or stash changes and try again.');
119
+ console.error('You can check the status by running: git status');
120
+ return 1;
121
+ }
122
+
123
+ // 6. Reset each worktree
124
+ for (const worktree of matchingWorktrees) {
125
+ info(`Resetting worktree ${worktree.branch}...`);
126
+ try {
127
+ resetWorktree(worktree.path, targetRevision);
128
+ } catch (err) {
129
+ if (err instanceof Error) {
130
+ error(err.message);
131
+ } else {
132
+ error(`Failed to reset worktree '${worktree.branch}'`);
133
+ }
134
+ return 1;
135
+ }
136
+ }
137
+
138
+ // 7. Display success summary
139
+ const branchNames = matchingWorktrees.map(wt => wt.branch).join(', ');
140
+ success(`Successfully reset ${matchingWorktrees.length} worktree(s): ${branchNames}`);
141
+ return 0;
142
+ } catch (err) {
143
+ if (err instanceof Error) {
144
+ error(`Error: ${err.message}`);
145
+ } else {
146
+ error('An unexpected error occurred');
147
+ }
148
+ return 1;
149
+ }
150
+ }