@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,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
|
+
}
|
package/commands/todo.ts
ADDED
|
@@ -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
|
+
}
|