@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,70 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props for Header component
|
|
6
|
+
*/
|
|
7
|
+
export interface HeaderProps {
|
|
8
|
+
children: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Header component (blue with newline before)
|
|
13
|
+
* Displays a section header in blue text with spacing
|
|
14
|
+
*/
|
|
15
|
+
export function Header({ children }: HeaderProps) {
|
|
16
|
+
return (
|
|
17
|
+
<Box flexDirection="column">
|
|
18
|
+
<Text>{''}</Text>
|
|
19
|
+
<Text color="blue">{children}</Text>
|
|
20
|
+
</Box>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Props for Section component
|
|
26
|
+
*/
|
|
27
|
+
export interface SectionProps {
|
|
28
|
+
children: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Section component (cyan with newline before)
|
|
33
|
+
* Displays a sub-section header in cyan text with spacing
|
|
34
|
+
*/
|
|
35
|
+
export function Section({ children }: SectionProps) {
|
|
36
|
+
return (
|
|
37
|
+
<Box flexDirection="column">
|
|
38
|
+
<Text>{''}</Text>
|
|
39
|
+
<Text color="cyan">{children}</Text>
|
|
40
|
+
</Box>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Props for ListItem component
|
|
46
|
+
*/
|
|
47
|
+
export interface ListItemProps {
|
|
48
|
+
children: string;
|
|
49
|
+
symbol?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* ListItem component (gray symbol with indentation)
|
|
54
|
+
* Displays a list item with a symbol prefix and indentation
|
|
55
|
+
*/
|
|
56
|
+
export function ListItem({ children, symbol = '•' }: ListItemProps) {
|
|
57
|
+
return (
|
|
58
|
+
<Box>
|
|
59
|
+
<Text> </Text>
|
|
60
|
+
<Text color="gray">{symbol}</Text>
|
|
61
|
+
<Text> {children}</Text>
|
|
62
|
+
</Box>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Helper functions to create elements without JSX (for use in .ts files)
|
|
67
|
+
export const createHeader = (children: string) => createElement(Header, { children });
|
|
68
|
+
export const createSection = (children: string) => createElement(Section, { children });
|
|
69
|
+
export const createListItem = (children: string, symbol?: string) =>
|
|
70
|
+
createElement(ListItem, { children, symbol });
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props for message components
|
|
6
|
+
*/
|
|
7
|
+
export interface MessageProps {
|
|
8
|
+
message: string;
|
|
9
|
+
children?: never;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Success message component (green)
|
|
14
|
+
* Displays a success message in green text
|
|
15
|
+
*/
|
|
16
|
+
export function Success({ message }: MessageProps) {
|
|
17
|
+
return <Text color="green">{message}</Text>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Error message component (red)
|
|
22
|
+
* Displays an error message in red text
|
|
23
|
+
*/
|
|
24
|
+
export function Error({ message }: MessageProps) {
|
|
25
|
+
return <Text color="red">{message}</Text>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Info message component (cyan)
|
|
30
|
+
* Displays an informational message in cyan text
|
|
31
|
+
*/
|
|
32
|
+
export function Info({ message }: MessageProps) {
|
|
33
|
+
return <Text color="cyan">{message}</Text>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Warning message component (yellow)
|
|
38
|
+
* Displays a warning message in yellow text
|
|
39
|
+
*/
|
|
40
|
+
export function Warn({ message }: MessageProps) {
|
|
41
|
+
return <Text color="yellow">{message}</Text>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Helper functions to create elements without JSX (for use in .ts files)
|
|
45
|
+
export const createSuccess = (message: string) => createElement(Success, { message });
|
|
46
|
+
export const createError = (message: string) => createElement(Error, { message });
|
|
47
|
+
export const createInfo = (message: string) => createElement(Info, { message });
|
|
48
|
+
export const createWarn = (message: string) => createElement(Warn, { message });
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props for Spinner component
|
|
6
|
+
*/
|
|
7
|
+
export interface SpinnerProps {
|
|
8
|
+
label?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Spinner component with rotating animation
|
|
13
|
+
* Displays an animated spinner with optional label
|
|
14
|
+
*/
|
|
15
|
+
export function Spinner({ label }: SpinnerProps) {
|
|
16
|
+
const [frame, setFrame] = useState(0);
|
|
17
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const timer = setInterval(() => {
|
|
21
|
+
setFrame(prevFrame => (prevFrame + 1) % frames.length);
|
|
22
|
+
}, 80);
|
|
23
|
+
|
|
24
|
+
return () => clearInterval(timer);
|
|
25
|
+
}, [frames.length]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Box>
|
|
29
|
+
<Text color="cyan">{frames[frame]}</Text>
|
|
30
|
+
{label && <Text> {label}</Text>}
|
|
31
|
+
</Box>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Props for ProgressBar component
|
|
37
|
+
*/
|
|
38
|
+
export interface ProgressBarProps {
|
|
39
|
+
/** Current progress value (0-100) */
|
|
40
|
+
value: number;
|
|
41
|
+
/** Width of the progress bar in characters (default: 40) */
|
|
42
|
+
width?: number;
|
|
43
|
+
/** Optional label to display */
|
|
44
|
+
label?: string;
|
|
45
|
+
/** Show percentage (default: true) */
|
|
46
|
+
showPercentage?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* ProgressBar component with visual bar display
|
|
51
|
+
* Displays a progress bar with percentage and optional label
|
|
52
|
+
*/
|
|
53
|
+
export function ProgressBar({ value, width = 40, label, showPercentage = true }: ProgressBarProps) {
|
|
54
|
+
const percentage = Math.min(100, Math.max(0, value));
|
|
55
|
+
const filledWidth = Math.round((percentage / 100) * width);
|
|
56
|
+
const emptyWidth = width - filledWidth;
|
|
57
|
+
|
|
58
|
+
const filledBar = '█'.repeat(filledWidth);
|
|
59
|
+
const emptyBar = '░'.repeat(emptyWidth);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Box flexDirection="column">
|
|
63
|
+
{label && <Text>{label}</Text>}
|
|
64
|
+
<Box>
|
|
65
|
+
<Text color="green">{filledBar}</Text>
|
|
66
|
+
<Text color="gray">{emptyBar}</Text>
|
|
67
|
+
{showPercentage && <Text> {percentage.toFixed(0)}%</Text>}
|
|
68
|
+
</Box>
|
|
69
|
+
</Box>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Box, Text, useInput } from 'ink';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Option for Select component
|
|
6
|
+
*/
|
|
7
|
+
export interface SelectOption {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Props for Select component
|
|
14
|
+
*/
|
|
15
|
+
export interface SelectProps {
|
|
16
|
+
/** Array of options to choose from */
|
|
17
|
+
options: SelectOption[];
|
|
18
|
+
/** Callback when an option is selected */
|
|
19
|
+
onSelect?: (value: string) => void;
|
|
20
|
+
/** Enable multi-select mode (default: false) */
|
|
21
|
+
multiSelect?: boolean;
|
|
22
|
+
/** Callback for multi-select completion */
|
|
23
|
+
onSubmit?: (values: string[]) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Select component with keyboard navigation
|
|
28
|
+
* Provides option selection with up/down arrow keys and Enter to confirm
|
|
29
|
+
*/
|
|
30
|
+
export function Select({ options, onSelect, multiSelect = false, onSubmit }: SelectProps) {
|
|
31
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
32
|
+
const [selectedValues, setSelectedValues] = useState<Set<string>>(new Set());
|
|
33
|
+
|
|
34
|
+
useInput((input, key) => {
|
|
35
|
+
if (key.upArrow) {
|
|
36
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : options.length - 1));
|
|
37
|
+
} else if (key.downArrow) {
|
|
38
|
+
setSelectedIndex(prev => (prev < options.length - 1 ? prev + 1 : 0));
|
|
39
|
+
} else if (key.return) {
|
|
40
|
+
const selectedOption = options[selectedIndex];
|
|
41
|
+
if (multiSelect) {
|
|
42
|
+
// Toggle selection in multi-select mode
|
|
43
|
+
setSelectedValues(prev => {
|
|
44
|
+
const next = new Set(prev);
|
|
45
|
+
if (next.has(selectedOption.value)) {
|
|
46
|
+
next.delete(selectedOption.value);
|
|
47
|
+
} else {
|
|
48
|
+
next.add(selectedOption.value);
|
|
49
|
+
}
|
|
50
|
+
return next;
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
// Single select mode
|
|
54
|
+
onSelect?.(selectedOption.value);
|
|
55
|
+
}
|
|
56
|
+
} else if (input === ' ' && multiSelect) {
|
|
57
|
+
// Space bar submits in multi-select mode
|
|
58
|
+
onSubmit?.(Array.from(selectedValues));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Box flexDirection="column">
|
|
64
|
+
{options.map((option, index) => {
|
|
65
|
+
const isHighlighted = index === selectedIndex;
|
|
66
|
+
const isSelected = selectedValues.has(option.value);
|
|
67
|
+
const indicator = multiSelect
|
|
68
|
+
? isSelected
|
|
69
|
+
? '[✓]'
|
|
70
|
+
: '[ ]'
|
|
71
|
+
: isHighlighted
|
|
72
|
+
? '›'
|
|
73
|
+
: ' ';
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Box key={option.value}>
|
|
77
|
+
<Text color={isHighlighted ? 'cyan' : undefined}>
|
|
78
|
+
{indicator} {option.label}
|
|
79
|
+
</Text>
|
|
80
|
+
</Box>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
{multiSelect && (
|
|
84
|
+
<Box marginTop={1}>
|
|
85
|
+
<Text color="gray">Press Enter to toggle, Space to submit</Text>
|
|
86
|
+
</Box>
|
|
87
|
+
)}
|
|
88
|
+
</Box>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Status type for display
|
|
5
|
+
*/
|
|
6
|
+
export type StatusType = 'pending' | 'running' | 'success' | 'error';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Single status line item
|
|
10
|
+
*/
|
|
11
|
+
export interface StatusLine {
|
|
12
|
+
id: string;
|
|
13
|
+
message: string;
|
|
14
|
+
status: StatusType;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Props for StatusDisplay component
|
|
19
|
+
*/
|
|
20
|
+
export interface StatusDisplayProps {
|
|
21
|
+
/** Array of status lines to display */
|
|
22
|
+
statuses: StatusLine[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the appropriate icon for a status type
|
|
27
|
+
*/
|
|
28
|
+
function getStatusIcon(status: StatusType): string {
|
|
29
|
+
switch (status) {
|
|
30
|
+
case 'pending':
|
|
31
|
+
return '○';
|
|
32
|
+
case 'running':
|
|
33
|
+
return '⟳';
|
|
34
|
+
case 'success':
|
|
35
|
+
return '✓';
|
|
36
|
+
case 'error':
|
|
37
|
+
return '✗';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the appropriate color for a status type
|
|
43
|
+
*/
|
|
44
|
+
function getStatusColor(status: StatusType): string {
|
|
45
|
+
switch (status) {
|
|
46
|
+
case 'pending':
|
|
47
|
+
return 'gray';
|
|
48
|
+
case 'running':
|
|
49
|
+
return 'cyan';
|
|
50
|
+
case 'success':
|
|
51
|
+
return 'green';
|
|
52
|
+
case 'error':
|
|
53
|
+
return 'red';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* StatusDisplay component for tracking multiple operations
|
|
59
|
+
* Displays multiple status lines that can be independently updated
|
|
60
|
+
*/
|
|
61
|
+
export function StatusDisplay({ statuses }: StatusDisplayProps) {
|
|
62
|
+
return (
|
|
63
|
+
<Box flexDirection="column">
|
|
64
|
+
{statuses.map(statusLine => (
|
|
65
|
+
<Box key={statusLine.id}>
|
|
66
|
+
<Text color={getStatusColor(statusLine.status)}>
|
|
67
|
+
{getStatusIcon(statusLine.status)}
|
|
68
|
+
</Text>
|
|
69
|
+
<Text> {statusLine.message}</Text>
|
|
70
|
+
</Box>
|
|
71
|
+
))}
|
|
72
|
+
</Box>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Column definition for Table component
|
|
5
|
+
*/
|
|
6
|
+
export interface TableColumn<T> {
|
|
7
|
+
/** Column header text */
|
|
8
|
+
header: string;
|
|
9
|
+
/** Key or accessor function to get the cell value */
|
|
10
|
+
accessor: keyof T | ((row: T) => string | number);
|
|
11
|
+
/** Optional width (auto-calculated if not provided) */
|
|
12
|
+
width?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Props for Table component
|
|
17
|
+
*/
|
|
18
|
+
export interface TableProps<T> {
|
|
19
|
+
/** Array of data rows */
|
|
20
|
+
data: T[];
|
|
21
|
+
/** Column definitions */
|
|
22
|
+
columns: TableColumn<T>[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Table component with flexible column layout
|
|
27
|
+
* Uses Flexbox for column layout with automatic width calculation
|
|
28
|
+
*/
|
|
29
|
+
export function Table<T>({ data, columns }: TableProps<T>) {
|
|
30
|
+
// Helper function to get cell value
|
|
31
|
+
const getCellValue = (row: T, column: TableColumn<T>): string => {
|
|
32
|
+
if (typeof column.accessor === 'function') {
|
|
33
|
+
return String(column.accessor(row));
|
|
34
|
+
}
|
|
35
|
+
return String(row[column.accessor]);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Calculate column widths based on content if not specified
|
|
39
|
+
const columnWidths = columns.map(column => {
|
|
40
|
+
if (column.width) {
|
|
41
|
+
return column.width;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Calculate based on header and data content
|
|
45
|
+
const headerLength = column.header.length;
|
|
46
|
+
const maxDataLength = Math.max(...data.map(row => getCellValue(row, column).length), 0);
|
|
47
|
+
|
|
48
|
+
return Math.max(headerLength, maxDataLength);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Box flexDirection="column">
|
|
53
|
+
{/* Header row */}
|
|
54
|
+
<Box>
|
|
55
|
+
{columns.map((column, index) => (
|
|
56
|
+
<Box key={column.header} width={columnWidths[index] + 2}>
|
|
57
|
+
<Text bold color="cyan">
|
|
58
|
+
{column.header.padEnd(columnWidths[index])}
|
|
59
|
+
</Text>
|
|
60
|
+
</Box>
|
|
61
|
+
))}
|
|
62
|
+
</Box>
|
|
63
|
+
|
|
64
|
+
{/* Data rows */}
|
|
65
|
+
{data.map((row, rowIndex) => (
|
|
66
|
+
<Box key={rowIndex}>
|
|
67
|
+
{columns.map((column, colIndex) => {
|
|
68
|
+
const cellValue = getCellValue(row, column);
|
|
69
|
+
return (
|
|
70
|
+
<Box key={colIndex} width={columnWidths[colIndex] + 2}>
|
|
71
|
+
<Text>{cellValue.padEnd(columnWidths[colIndex])}</Text>
|
|
72
|
+
</Box>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</Box>
|
|
76
|
+
))}
|
|
77
|
+
</Box>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED FILE - DO NOT EDIT
|
|
3
|
+
* Generated by scripts/generate-setup-manifest.ts
|
|
4
|
+
*
|
|
5
|
+
* This file embeds all setup files and resources into the compiled binary using
|
|
6
|
+
* Bun's file import attribute. Regenerate after modifying setup/ or resources/ contents.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import file_claude_commands_start_work_md from '../setup/.claude/commands/start-work.md' with { type: 'file' };
|
|
10
|
+
import file_claude_commands_complete_work_md from '../setup/.claude/commands/complete-work.md' with { type: 'file' };
|
|
11
|
+
import file_claude_commands_e2e_md from '../setup/.claude/commands/e2e.md' with { type: 'file' };
|
|
12
|
+
import file_claude_commands_commit_work_md from '../setup/.claude/commands/commit-work.md' with { type: 'file' };
|
|
13
|
+
import file_claude_skills_pm_SKILL_md from '../setup/.claude/skills/pm/SKILL.md' with { type: 'file' };
|
|
14
|
+
import file_claude_skills_pm_templates_bug_fix_md from '../setup/.claude/skills/pm/templates/bug-fix.md' with { type: 'file' };
|
|
15
|
+
import file_claude_skills_pm_templates_ui_component_md from '../setup/.claude/skills/pm/templates/ui-component.md' with { type: 'file' };
|
|
16
|
+
import file_claude_skills_pm_templates_api_endpoint_md from '../setup/.claude/skills/pm/templates/api-endpoint.md' with { type: 'file' };
|
|
17
|
+
import file_claude_skills_pm_templates_feature_md from '../setup/.claude/skills/pm/templates/feature.md' with { type: 'file' };
|
|
18
|
+
|
|
19
|
+
import resource_copy_prompt_reporter_ts from '../resources/copy-prompt-reporter.ts' with { type: 'file' };
|
|
20
|
+
|
|
21
|
+
import extension_avantmedia_openspec_0_1_0_vsix from '../vscode-extension/avantmedia-openspec-0.1.0.vsix' with { type: 'file' };
|
|
22
|
+
|
|
23
|
+
export interface SetupFile {
|
|
24
|
+
/** Path relative to setup/ folder (e.g., ".claude/settings.local.json") */
|
|
25
|
+
relativePath: string;
|
|
26
|
+
/** Path to embedded file (bun internal path or dev file path) */
|
|
27
|
+
embeddedPath: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ResourceFile {
|
|
31
|
+
/** File name (e.g., "copy-prompt-reporter.ts") */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Path to embedded file (bun internal path or dev file path) */
|
|
34
|
+
embeddedPath: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const SETUP_FILES: SetupFile[] = [
|
|
38
|
+
{ relativePath: '.claude/commands/start-work.md', embeddedPath: file_claude_commands_start_work_md },
|
|
39
|
+
{ relativePath: '.claude/commands/complete-work.md', embeddedPath: file_claude_commands_complete_work_md },
|
|
40
|
+
{ relativePath: '.claude/commands/e2e.md', embeddedPath: file_claude_commands_e2e_md },
|
|
41
|
+
{ relativePath: '.claude/commands/commit-work.md', embeddedPath: file_claude_commands_commit_work_md },
|
|
42
|
+
{ relativePath: '.claude/skills/pm/SKILL.md', embeddedPath: file_claude_skills_pm_SKILL_md },
|
|
43
|
+
{ relativePath: '.claude/skills/pm/templates/bug-fix.md', embeddedPath: file_claude_skills_pm_templates_bug_fix_md },
|
|
44
|
+
{ relativePath: '.claude/skills/pm/templates/ui-component.md', embeddedPath: file_claude_skills_pm_templates_ui_component_md },
|
|
45
|
+
{ relativePath: '.claude/skills/pm/templates/api-endpoint.md', embeddedPath: file_claude_skills_pm_templates_api_endpoint_md },
|
|
46
|
+
{ relativePath: '.claude/skills/pm/templates/feature.md', embeddedPath: file_claude_skills_pm_templates_feature_md },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export const RESOURCE_FILES: ResourceFile[] = [
|
|
50
|
+
{ name: 'copy-prompt-reporter.ts', embeddedPath: resource_copy_prompt_reporter_ts },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export interface ExtensionFile {
|
|
54
|
+
/** File name (e.g., "openspec-0.1.0.vsix") */
|
|
55
|
+
name: string;
|
|
56
|
+
/** Path to embedded file (bun internal path or dev file path) */
|
|
57
|
+
embeddedPath: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const EXTENSION_FILE: ExtensionFile | null = { name: 'avantmedia-openspec-0.1.0.vsix', embeddedPath: extension_avantmedia_openspec_0_1_0_vsix };
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if running from compiled binary (embedded files contain $bunfs in path)
|
|
64
|
+
*/
|
|
65
|
+
export function isCompiled(): boolean {
|
|
66
|
+
return SETUP_FILES.length > 0 && SETUP_FILES[0].embeddedPath.includes('$bunfs');
|
|
67
|
+
}
|
package/git-worktree.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export interface Worktree {
|
|
4
|
+
path: string;
|
|
5
|
+
branch: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Checks if the current directory is inside a git repository.
|
|
10
|
+
*
|
|
11
|
+
* @returns true if inside a git repository, false otherwise
|
|
12
|
+
*/
|
|
13
|
+
export function isGitRepository(): boolean {
|
|
14
|
+
try {
|
|
15
|
+
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Gets the current HEAD commit hash.
|
|
24
|
+
*
|
|
25
|
+
* @returns The current HEAD commit hash
|
|
26
|
+
* @throws Error if unable to get HEAD commit
|
|
27
|
+
*/
|
|
28
|
+
export function getCurrentHeadCommit(): string {
|
|
29
|
+
try {
|
|
30
|
+
const commit = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
|
|
31
|
+
return commit;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof Error) {
|
|
34
|
+
throw new Error(`Failed to get current HEAD commit: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
throw new Error('Failed to get current HEAD commit');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Lists all git worktrees in the repository.
|
|
42
|
+
*
|
|
43
|
+
* Parses the output of `git worktree list --porcelain` to extract
|
|
44
|
+
* worktree paths and their associated branches.
|
|
45
|
+
*
|
|
46
|
+
* @returns Array of worktree objects with path and branch information
|
|
47
|
+
* @throws Error if unable to list worktrees
|
|
48
|
+
*/
|
|
49
|
+
export function listWorktrees(): Worktree[] {
|
|
50
|
+
try {
|
|
51
|
+
const output = execSync('git worktree list --porcelain', { encoding: 'utf-8' });
|
|
52
|
+
return parseWorktreeList(output);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
throw new Error(`Failed to list worktrees: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
throw new Error('Failed to list worktrees');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parses the porcelain output from `git worktree list --porcelain`.
|
|
63
|
+
*
|
|
64
|
+
* The porcelain format outputs each worktree as a block separated by blank lines:
|
|
65
|
+
* worktree <path>
|
|
66
|
+
* HEAD <sha>
|
|
67
|
+
* branch <branch-ref>
|
|
68
|
+
*
|
|
69
|
+
* @param output The raw output from git worktree list --porcelain
|
|
70
|
+
* @returns Array of parsed worktree objects
|
|
71
|
+
*/
|
|
72
|
+
function parseWorktreeList(output: string): Worktree[] {
|
|
73
|
+
const worktrees: Worktree[] = [];
|
|
74
|
+
const lines = output.trim().split('\n');
|
|
75
|
+
|
|
76
|
+
let currentPath: string | null = null;
|
|
77
|
+
let currentBranch: string | null = null;
|
|
78
|
+
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (line.startsWith('worktree ')) {
|
|
81
|
+
currentPath = line.substring('worktree '.length);
|
|
82
|
+
} else if (line.startsWith('branch ')) {
|
|
83
|
+
const branchRef = line.substring('branch '.length);
|
|
84
|
+
currentBranch = getWorktreeBranch(branchRef);
|
|
85
|
+
} else if (line === '') {
|
|
86
|
+
// End of worktree block
|
|
87
|
+
if (currentPath && currentBranch) {
|
|
88
|
+
worktrees.push({ path: currentPath, branch: currentBranch });
|
|
89
|
+
}
|
|
90
|
+
currentPath = null;
|
|
91
|
+
currentBranch = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Handle last worktree if file doesn't end with blank line
|
|
96
|
+
if (currentPath && currentBranch) {
|
|
97
|
+
worktrees.push({ path: currentPath, branch: currentBranch });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return worktrees;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extracts the branch name from a git branch reference.
|
|
105
|
+
*
|
|
106
|
+
* Converts refs/heads/branch-name to branch-name
|
|
107
|
+
*
|
|
108
|
+
* @param branchRef The full branch reference (e.g., "refs/heads/main")
|
|
109
|
+
* @returns The branch name without the refs/heads/ prefix
|
|
110
|
+
*/
|
|
111
|
+
export function getWorktreeBranch(branchRef: string): string {
|
|
112
|
+
if (branchRef.startsWith('refs/heads/')) {
|
|
113
|
+
return branchRef.substring('refs/heads/'.length);
|
|
114
|
+
}
|
|
115
|
+
return branchRef;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Checks if a worktree has uncommitted changes.
|
|
120
|
+
*
|
|
121
|
+
* Uses `git status --porcelain` which outputs a blank string when clean,
|
|
122
|
+
* or non-empty output when there are changes.
|
|
123
|
+
*
|
|
124
|
+
* @param worktreePath The path to the worktree to check
|
|
125
|
+
* @returns true if the worktree has uncommitted changes, false if clean
|
|
126
|
+
* @throws Error if unable to check git status
|
|
127
|
+
*/
|
|
128
|
+
export function hasUncommittedChanges(worktreePath: string): boolean {
|
|
129
|
+
try {
|
|
130
|
+
const output = execSync('git status --porcelain', {
|
|
131
|
+
cwd: worktreePath,
|
|
132
|
+
encoding: 'utf-8',
|
|
133
|
+
});
|
|
134
|
+
return output.trim().length > 0;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof Error) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Failed to check git status in worktree '${worktreePath}': ${error.message}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
throw new Error(`Failed to check git status in worktree '${worktreePath}'`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resets a worktree to a specific revision using `git reset --hard`.
|
|
147
|
+
*
|
|
148
|
+
* @param worktreePath The path to the worktree to reset
|
|
149
|
+
* @param revision The git revision (commit hash, branch, tag) to reset to
|
|
150
|
+
* @throws Error if the reset operation fails
|
|
151
|
+
*/
|
|
152
|
+
export function resetWorktree(worktreePath: string, revision: string): void {
|
|
153
|
+
try {
|
|
154
|
+
execSync(`git reset --hard ${revision}`, {
|
|
155
|
+
cwd: worktreePath,
|
|
156
|
+
stdio: 'ignore',
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error instanceof Error) {
|
|
160
|
+
throw new Error(`Failed to reset worktree '${worktreePath}': ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`Failed to reset worktree '${worktreePath}'`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Force-pushes a worktree to its remote repository.
|
|
168
|
+
*
|
|
169
|
+
* @param worktreePath The path to the worktree to push
|
|
170
|
+
* @throws Error if the push operation fails
|
|
171
|
+
*/
|
|
172
|
+
export function pushWorktree(worktreePath: string): void {
|
|
173
|
+
try {
|
|
174
|
+
execSync('git push --force', {
|
|
175
|
+
cwd: worktreePath,
|
|
176
|
+
stdio: 'ignore',
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error instanceof Error) {
|
|
180
|
+
throw new Error(`Failed to push worktree '${worktreePath}': ${error.message}`);
|
|
181
|
+
}
|
|
182
|
+
throw new Error(`Failed to push worktree '${worktreePath}'`);
|
|
183
|
+
}
|
|
184
|
+
}
|