@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,54 @@
1
+ import { execSync } from 'node:child_process';
2
+
3
+ /**
4
+ * Represents an ongoing change from OpenSpec
5
+ */
6
+ export interface OngoingChange {
7
+ /** The change ID (e.g., "add-user-auth") */
8
+ id: string;
9
+ /** The task progress status (e.g., "3/8 tasks" or "✓ Complete") */
10
+ status: string;
11
+ }
12
+
13
+ /**
14
+ * Parse the output of `openspec list --changes` to extract ongoing changes.
15
+ *
16
+ * @param output - The raw output from `openspec list --changes`
17
+ * @returns Array of ongoing changes with their IDs and statuses
18
+ */
19
+ export function parseOpenspecListOutput(output: string): OngoingChange[] {
20
+ const changes: OngoingChange[] = [];
21
+ const lines = output.split('\n');
22
+
23
+ for (const line of lines) {
24
+ const trimmed = line.trim();
25
+ // Skip empty lines and the "Changes:" header
26
+ if (!trimmed || trimmed === 'Changes:') continue;
27
+
28
+ // Parse lines like " add-user-auth 3/8 tasks" or " my-change ✓ Complete"
29
+ // The ID is the first word, status is everything after
30
+ const match = trimmed.match(/^(\S+)\s+(.+)$/);
31
+ if (match) {
32
+ changes.push({
33
+ id: match[1],
34
+ status: match[2].trim(),
35
+ });
36
+ }
37
+ }
38
+
39
+ return changes;
40
+ }
41
+
42
+ /**
43
+ * List all ongoing changes from OpenSpec.
44
+ *
45
+ * @returns Array of ongoing changes, or empty array if command fails
46
+ */
47
+ export function listOngoingChanges(): OngoingChange[] {
48
+ try {
49
+ const output = execSync('openspec list --changes', { encoding: 'utf-8' });
50
+ return parseOpenspecListOutput(output);
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Terminal output utilities with Ink-based components for consistent CLI formatting.
3
+ * Provides backward-compatible wrapper functions that use chalk (from Ink's dependency).
4
+ *
5
+ * For interactive UI, import and use Ink components directly via render().
6
+ * These functions are optimized for static, one-off messages.
7
+ */
8
+
9
+ import chalk from 'chalk';
10
+
11
+ /**
12
+ * ANSI color codes for terminal output.
13
+ * Kept for backward compatibility but deprecated in favor of chalk.
14
+ * @deprecated Use chalk directly or Ink components for better control
15
+ */
16
+ export const colors = {
17
+ reset: '\x1b[0m',
18
+ red: '\x1b[31m',
19
+ green: '\x1b[32m',
20
+ yellow: '\x1b[33m',
21
+ blue: '\x1b[34m',
22
+ cyan: '\x1b[36m',
23
+ gray: '\x1b[90m',
24
+ };
25
+
26
+ /**
27
+ * Display a success message in green.
28
+ * @param message - The success message to display
29
+ */
30
+ export function success(message: string): void {
31
+ console.log(chalk.green(message));
32
+ }
33
+
34
+ /**
35
+ * Display an error message in red.
36
+ * @param message - The error message to display
37
+ */
38
+ export function error(message: string): void {
39
+ console.error(chalk.red(message));
40
+ }
41
+
42
+ /**
43
+ * Display an informational message in cyan.
44
+ * @param message - The info message to display
45
+ */
46
+ export function info(message: string): void {
47
+ console.log(chalk.cyan(message));
48
+ }
49
+
50
+ /**
51
+ * Display a warning message in yellow.
52
+ * @param message - The warning message to display
53
+ */
54
+ export function warn(message: string): void {
55
+ console.log(chalk.yellow(message));
56
+ }
57
+
58
+ /**
59
+ * Display a header message in blue with emphasis.
60
+ * Useful for section titles or major status updates.
61
+ * @param message - The header text to display
62
+ */
63
+ export function header(message: string): void {
64
+ console.log();
65
+ console.log(chalk.blue(message));
66
+ }
67
+
68
+ /**
69
+ * Display a section message in bold (using color for emphasis).
70
+ * Useful for sub-sections or grouped content.
71
+ * @param message - The section text to display
72
+ */
73
+ export function section(message: string): void {
74
+ console.log();
75
+ console.log(chalk.cyan(message));
76
+ }
77
+
78
+ /**
79
+ * Display a list item with an optional symbol prefix.
80
+ * @param message - The list item text to display
81
+ * @param symbol - Optional symbol to prefix (default: '•')
82
+ */
83
+ export function listItem(message: string, symbol: string = '•'): void {
84
+ console.log(` ${chalk.gray(symbol)} ${message}`);
85
+ }
86
+
87
+ /**
88
+ * Create a clickable hyperlink using OSC 8 escape sequences.
89
+ * In terminals that support OSC 8 (iTerm2, GNOME Terminal, Windows Terminal, etc.),
90
+ * the text will be displayed as a clickable link. In unsupported terminals,
91
+ * the text is displayed without the URL (graceful degradation).
92
+ *
93
+ * @param text - The visible text to display
94
+ * @param url - The URL to open when clicked
95
+ * @returns A string with OSC 8 escape sequences
96
+ */
97
+ export function link(text: string, url: string): string {
98
+ // OSC 8 format: \x1b]8;;URL\x07TEXT\x1b]8;;\x07
99
+ // - \x1b]8;; starts the hyperlink with URL
100
+ // - \x07 (BEL) terminates the URL
101
+ // - TEXT is displayed
102
+ // - \x1b]8;;\x07 closes the hyperlink
103
+ return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
104
+ }
@@ -0,0 +1,160 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { error as logError } from './output.ts';
4
+
5
+ /**
6
+ * Extract the proposal title from the first line of a proposal.md file.
7
+ * Strips leading '#', whitespace, and optional "Proposal: " prefix.
8
+ *
9
+ * @param proposalPath - Path to the proposal.md file
10
+ * @returns The extracted title, or null if extraction fails
11
+ */
12
+ export function extractProposalTitle(proposalPath: string): string | null {
13
+ try {
14
+ const content = readFileSync(proposalPath, 'utf-8');
15
+ const firstLine = content.split('\n')[0];
16
+
17
+ if (!firstLine) {
18
+ return null;
19
+ }
20
+
21
+ // Remove leading '#' and whitespace
22
+ let title = firstLine.replace(/^#+\s*/, '');
23
+
24
+ // Remove "Proposal: " prefix if present (case-insensitive)
25
+ title = title.replace(/^Proposal:\s*/i, '');
26
+
27
+ // Trim any remaining whitespace
28
+ title = title.trim();
29
+
30
+ return title || null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Get the most recently created change directory in openspec/changes.
38
+ * This is useful for determining which proposal was just created.
39
+ *
40
+ * @param changesDir - Optional path to changes directory (defaults to 'openspec/changes')
41
+ * @returns The change ID of the most recent change, or null if none found
42
+ */
43
+ export function getLatestChangeId(changesDir: string = 'openspec/changes'): string | null {
44
+ try {
45
+ const entries = readdirSync(changesDir);
46
+
47
+ let latestTime = 0;
48
+ let latestId: string | null = null;
49
+
50
+ for (const entry of entries) {
51
+ if (entry === 'archive') {
52
+ continue;
53
+ }
54
+
55
+ const fullPath = join(changesDir, entry);
56
+ const stats = statSync(fullPath);
57
+
58
+ if (stats.isDirectory() && stats.mtimeMs > latestTime) {
59
+ latestTime = stats.mtimeMs;
60
+ latestId = entry;
61
+ }
62
+ }
63
+
64
+ return latestId;
65
+ } catch (error) {
66
+ logError(`Error getting latest change ID: ${error}`);
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Represents an active change with its title and task progress.
73
+ */
74
+ export interface ActiveChange {
75
+ id: string;
76
+ title: string | null;
77
+ completedTasks: number;
78
+ totalTasks: number;
79
+ }
80
+
81
+ /**
82
+ * Parse a tasks.md file and count completed and total tasks.
83
+ * Tasks are markdown checkbox lines: `- [ ]` (uncompleted) or `- [x]` (completed).
84
+ *
85
+ * @param tasksPath - Path to the tasks.md file
86
+ * @returns Object with completed and total task counts
87
+ */
88
+ function countTasks(tasksPath: string): { completed: number; total: number } {
89
+ try {
90
+ const content = readFileSync(tasksPath, 'utf-8');
91
+ const lines = content.split('\n');
92
+
93
+ let completed = 0;
94
+ let total = 0;
95
+
96
+ for (const line of lines) {
97
+ const trimmed = line.trim();
98
+ if (trimmed.startsWith('- [x]') || trimmed.startsWith('- [X]')) {
99
+ completed++;
100
+ total++;
101
+ } else if (trimmed.startsWith('- [ ]')) {
102
+ total++;
103
+ }
104
+ }
105
+
106
+ return { completed, total };
107
+ } catch {
108
+ return { completed: 0, total: 0 };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Get all active changes with their titles and task progress.
114
+ *
115
+ * @param changesDir - Optional path to changes directory (defaults to 'openspec/changes')
116
+ * @returns Array of active changes sorted alphabetically by ID
117
+ */
118
+ export function getActiveChanges(changesDir: string = 'openspec/changes'): ActiveChange[] {
119
+ try {
120
+ if (!existsSync(changesDir)) {
121
+ return [];
122
+ }
123
+
124
+ const entries = readdirSync(changesDir);
125
+ const changes: ActiveChange[] = [];
126
+
127
+ for (const entry of entries) {
128
+ if (entry === 'archive') {
129
+ continue;
130
+ }
131
+
132
+ const fullPath = join(changesDir, entry);
133
+ const stats = statSync(fullPath);
134
+
135
+ if (!stats.isDirectory()) {
136
+ continue;
137
+ }
138
+
139
+ const proposalPath = join(fullPath, 'proposal.md');
140
+ const tasksPath = join(fullPath, 'tasks.md');
141
+
142
+ const title = extractProposalTitle(proposalPath);
143
+ const { completed, total } = countTasks(tasksPath);
144
+
145
+ changes.push({
146
+ id: entry,
147
+ title,
148
+ completedTasks: completed,
149
+ totalTasks: total,
150
+ });
151
+ }
152
+
153
+ // Sort alphabetically by ID
154
+ changes.sort((a, b) => a.id.localeCompare(b.id));
155
+
156
+ return changes;
157
+ } catch {
158
+ return [];
159
+ }
160
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Resource file utilities for extracting bundled resource files.
3
+ * Handles both development mode (files on disk) and compiled mode (embedded in binary).
4
+ */
5
+
6
+ import { mkdirSync, writeFileSync, copyFileSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { RESOURCE_FILES, isCompiled, type ResourceFile } from '../generated/setup-manifest.ts';
9
+
10
+ /**
11
+ * Get the project root directory where resources/ folder is located.
12
+ * In development mode, this is relative to this source file.
13
+ * In compiled mode, files are embedded and this isn't used.
14
+ */
15
+ function getProjectRoot(): string {
16
+ // import.meta.dirname gives us the directory of this file (utils/)
17
+ // We need to go up one level to get to the project root
18
+ return dirname(import.meta.dirname);
19
+ }
20
+
21
+ /**
22
+ * Get a resource file entry by name.
23
+ */
24
+ export function getResource(name: string): ResourceFile | undefined {
25
+ return RESOURCE_FILES.find(r => r.name === name);
26
+ }
27
+
28
+ /**
29
+ * Extract a resource file to a target path.
30
+ * Works in both development mode (copies from disk) and compiled mode (extracts from binary).
31
+ *
32
+ * @param name - The resource file name (e.g., "copy-prompt-reporter.ts")
33
+ * @param targetPath - The absolute path to write the file to
34
+ * @throws Error if the resource is not found
35
+ */
36
+ export async function extractResource(name: string, targetPath: string): Promise<void> {
37
+ const resource = getResource(name);
38
+ if (!resource) {
39
+ throw new Error(`Resource not found: ${name}`);
40
+ }
41
+
42
+ // Ensure target directory exists
43
+ mkdirSync(dirname(targetPath), { recursive: true });
44
+
45
+ if (isCompiled()) {
46
+ // In compiled mode, use Bun.file() to read from $bunfs virtual filesystem
47
+ const content = await Bun.file(resource.embeddedPath).bytes();
48
+ writeFileSync(targetPath, content);
49
+ } else {
50
+ // In development mode, copy from source directory relative to project root
51
+ const sourcePath = join(getProjectRoot(), 'resources', resource.name);
52
+ copyFileSync(sourcePath, targetPath);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * List all available resource files.
58
+ */
59
+ export function listResources(): string[] {
60
+ return RESOURCE_FILES.map(r => r.name);
61
+ }
62
+
63
+ // Re-export for convenience
64
+ export { isCompiled };
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Setup file utilities for copying bundled configuration files.
3
+ * Handles both development mode (files on disk) and compiled mode (embedded in binary).
4
+ */
5
+
6
+ import { existsSync, mkdirSync, writeFileSync, copyFileSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+ import { SETUP_FILES, isCompiled, type SetupFile } from '../generated/setup-manifest.ts';
10
+
11
+ export type ConflictResolution = 'overwrite' | 'skip' | 'overwrite-all' | 'skip-all';
12
+
13
+ export interface SetupResult {
14
+ copied: string[];
15
+ skipped: string[];
16
+ errors: Array<{ path: string; error: string }>;
17
+ }
18
+
19
+ export interface FileInfo {
20
+ file: SetupFile;
21
+ targetPath: string;
22
+ exists: boolean;
23
+ /** OpenCode target path for command files (null for skills) */
24
+ openCodePath?: string;
25
+ openCodeExists?: boolean;
26
+ }
27
+
28
+ /**
29
+ * Get the Claude target directory (user's home directory).
30
+ */
31
+ export function getTargetDir(): string {
32
+ return homedir();
33
+ }
34
+
35
+ /**
36
+ * Get the OpenCode config directory.
37
+ */
38
+ export function getOpenCodeDir(): string {
39
+ return join(homedir(), '.config', 'opencode');
40
+ }
41
+
42
+ /**
43
+ * Check if a file is a command file (not a skill).
44
+ */
45
+ function isCommandFile(relativePath: string): boolean {
46
+ return relativePath.includes('.claude/commands/');
47
+ }
48
+
49
+ /**
50
+ * Get OpenCode target path for a Claude command file.
51
+ * Returns null for non-command files (skills are shared via ~/.claude/skills/).
52
+ */
53
+ export function getOpenCodeCommandPath(claudePath: string): string | null {
54
+ // Only transform command files
55
+ if (!claudePath.includes('/commands/')) return null;
56
+
57
+ // ~/.claude/commands/foo.md → ~/.config/opencode/command/foo.md
58
+ return claudePath
59
+ .replace(join(homedir(), '.claude'), join(homedir(), '.config', 'opencode'))
60
+ .replace('/commands/', '/command/');
61
+ }
62
+
63
+ /**
64
+ * List all files that would be copied, with conflict info.
65
+ * Includes OpenCode target paths for command files.
66
+ */
67
+ export function listSetupFiles(): FileInfo[] {
68
+ const targetDir = getTargetDir();
69
+
70
+ return SETUP_FILES.map(file => {
71
+ const targetPath = join(targetDir, file.relativePath);
72
+ const openCodePath = isCommandFile(file.relativePath)
73
+ ? getOpenCodeCommandPath(targetPath)
74
+ : undefined;
75
+
76
+ return {
77
+ file,
78
+ targetPath,
79
+ exists: existsSync(targetPath),
80
+ openCodePath: openCodePath ?? undefined,
81
+ openCodeExists: openCodePath ? existsSync(openCodePath) : undefined,
82
+ };
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Get the project root directory where setup/ folder is located.
88
+ * In development mode, this is relative to this source file.
89
+ * In compiled mode, files are embedded and this isn't used.
90
+ */
91
+ function getProjectRoot(): string {
92
+ // import.meta.dirname gives us the directory of this file (utils/)
93
+ // We need to go up one level to get to the project root
94
+ return dirname(import.meta.dirname);
95
+ }
96
+
97
+ /**
98
+ * Copy a single file from embedded source to target.
99
+ */
100
+ export async function copySetupFile(file: SetupFile, targetPath: string): Promise<void> {
101
+ // Ensure target directory exists
102
+ mkdirSync(dirname(targetPath), { recursive: true });
103
+
104
+ if (isCompiled()) {
105
+ // In compiled mode, use Bun.file() to read from $bunfs virtual filesystem
106
+ const content = await Bun.file(file.embeddedPath).bytes();
107
+ writeFileSync(targetPath, content);
108
+ } else {
109
+ // In development mode, copy from source directory relative to project root
110
+ const sourcePath = join(getProjectRoot(), 'setup', file.relativePath);
111
+ copyFileSync(sourcePath, targetPath);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Perform the full setup operation.
117
+ * Copies files to both Claude and OpenCode directories.
118
+ * @param resolveConflict - Callback for conflict resolution
119
+ */
120
+ export async function performSetup(
121
+ resolveConflict: (targetPath: string) => Promise<ConflictResolution>,
122
+ ): Promise<SetupResult> {
123
+ const result: SetupResult = {
124
+ copied: [],
125
+ skipped: [],
126
+ errors: [],
127
+ };
128
+
129
+ let skipAll = false;
130
+ let overwriteAll = false;
131
+
132
+ const files = listSetupFiles();
133
+
134
+ for (const { file, targetPath, exists, openCodePath, openCodeExists } of files) {
135
+ // Handle Claude target
136
+ try {
137
+ if (exists && !overwriteAll && !skipAll) {
138
+ const resolution = await resolveConflict(targetPath);
139
+
140
+ if (resolution === 'skip-all') {
141
+ skipAll = true;
142
+ result.skipped.push(targetPath);
143
+ // Also skip OpenCode path
144
+ if (openCodePath) {
145
+ result.skipped.push(openCodePath);
146
+ }
147
+ continue;
148
+ }
149
+ if (resolution === 'overwrite-all') {
150
+ overwriteAll = true;
151
+ }
152
+ if (resolution === 'skip') {
153
+ result.skipped.push(targetPath);
154
+ // Also skip OpenCode path
155
+ if (openCodePath) {
156
+ result.skipped.push(openCodePath);
157
+ }
158
+ continue;
159
+ }
160
+ } else if (exists && skipAll) {
161
+ result.skipped.push(targetPath);
162
+ if (openCodePath) {
163
+ result.skipped.push(openCodePath);
164
+ }
165
+ continue;
166
+ }
167
+
168
+ await copySetupFile(file, targetPath);
169
+ result.copied.push(targetPath);
170
+ } catch (err) {
171
+ result.errors.push({
172
+ path: targetPath,
173
+ error: err instanceof Error ? err.message : String(err),
174
+ });
175
+ }
176
+
177
+ // Handle OpenCode target (commands only)
178
+ if (openCodePath) {
179
+ try {
180
+ // Check for conflict on OpenCode path separately
181
+ if (openCodeExists && !overwriteAll && !skipAll) {
182
+ const resolution = await resolveConflict(openCodePath);
183
+
184
+ if (resolution === 'skip-all') {
185
+ skipAll = true;
186
+ result.skipped.push(openCodePath);
187
+ continue;
188
+ }
189
+ if (resolution === 'overwrite-all') {
190
+ overwriteAll = true;
191
+ }
192
+ if (resolution === 'skip') {
193
+ result.skipped.push(openCodePath);
194
+ continue;
195
+ }
196
+ } else if (openCodeExists && skipAll) {
197
+ result.skipped.push(openCodePath);
198
+ continue;
199
+ }
200
+
201
+ await copySetupFile(file, openCodePath);
202
+ result.copied.push(openCodePath);
203
+ } catch (err) {
204
+ result.errors.push({
205
+ path: openCodePath,
206
+ error: err instanceof Error ? err.message : String(err),
207
+ });
208
+ }
209
+ }
210
+ }
211
+
212
+ return result;
213
+ }
214
+
215
+ /**
216
+ * Get count of setup files for display purposes.
217
+ */
218
+ export function getSetupFileCount(): number {
219
+ return SETUP_FILES.length;
220
+ }
221
+
222
+ // Re-export for convenience
223
+ export { isCompiled };
224
+
225
+ /**
226
+ * Get the count of OpenCode command files for display purposes.
227
+ */
228
+ export function getOpenCodeCommandCount(): number {
229
+ return SETUP_FILES.filter(f => isCommandFile(f.relativePath)).length;
230
+ }