@claudetree/cli 0.5.0 → 0.5.2
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/dist/commands/log.d.ts +3 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +116 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
- package/dist/commands/batch.test.d.ts +0 -2
- package/dist/commands/batch.test.d.ts.map +0 -1
- package/dist/commands/batch.test.js +0 -230
- package/dist/commands/batch.test.js.map +0 -1
- package/dist/commands/bustercall/issueFilters.test.d.ts +0 -2
- package/dist/commands/bustercall/issueFilters.test.d.ts.map +0 -1
- package/dist/commands/bustercall/issueFilters.test.js +0 -151
- package/dist/commands/bustercall/issueFilters.test.js.map +0 -1
- package/dist/commands/bustercall/sessionManager.test.d.ts +0 -2
- package/dist/commands/bustercall/sessionManager.test.d.ts.map +0 -1
- package/dist/commands/bustercall/sessionManager.test.js +0 -79
- package/dist/commands/bustercall/sessionManager.test.js.map +0 -1
- package/dist/commands/bustercall/statusDisplay.test.d.ts +0 -2
- package/dist/commands/bustercall/statusDisplay.test.d.ts.map +0 -1
- package/dist/commands/bustercall/statusDisplay.test.js +0 -111
- package/dist/commands/bustercall/statusDisplay.test.js.map +0 -1
- package/dist/commands/bustercall.test.d.ts +0 -2
- package/dist/commands/bustercall.test.d.ts.map +0 -1
- package/dist/commands/bustercall.test.js +0 -258
- package/dist/commands/bustercall.test.js.map +0 -1
- package/dist/commands/chain/chainDisplay.test.d.ts +0 -2
- package/dist/commands/chain/chainDisplay.test.d.ts.map +0 -1
- package/dist/commands/chain/chainDisplay.test.js +0 -128
- package/dist/commands/chain/chainDisplay.test.js.map +0 -1
- package/dist/commands/chain/chainSession.test.d.ts +0 -2
- package/dist/commands/chain/chainSession.test.d.ts.map +0 -1
- package/dist/commands/chain/chainSession.test.js +0 -93
- package/dist/commands/chain/chainSession.test.js.map +0 -1
- package/dist/commands/chain.test.d.ts +0 -2
- package/dist/commands/chain.test.d.ts.map +0 -1
- package/dist/commands/chain.test.js +0 -216
- package/dist/commands/chain.test.js.map +0 -1
- package/dist/commands/clean.test.d.ts +0 -2
- package/dist/commands/clean.test.d.ts.map +0 -1
- package/dist/commands/clean.test.js +0 -193
- package/dist/commands/clean.test.js.map +0 -1
- package/dist/commands/demo.d.ts +0 -3
- package/dist/commands/demo.d.ts.map +0 -1
- package/dist/commands/demo.js +0 -175
- package/dist/commands/demo.js.map +0 -1
- package/dist/commands/demo.test.d.ts +0 -2
- package/dist/commands/demo.test.d.ts.map +0 -1
- package/dist/commands/demo.test.js +0 -34
- package/dist/commands/demo.test.js.map +0 -1
- package/dist/commands/doctor.test.d.ts +0 -2
- package/dist/commands/doctor.test.d.ts.map +0 -1
- package/dist/commands/doctor.test.js +0 -324
- package/dist/commands/doctor.test.js.map +0 -1
- package/dist/commands/init.test.d.ts +0 -2
- package/dist/commands/init.test.d.ts.map +0 -1
- package/dist/commands/init.test.js +0 -94
- package/dist/commands/init.test.js.map +0 -1
- package/dist/commands/list.test.d.ts +0 -2
- package/dist/commands/list.test.d.ts.map +0 -1
- package/dist/commands/list.test.js +0 -106
- package/dist/commands/list.test.js.map +0 -1
- package/dist/commands/resume.test.d.ts +0 -2
- package/dist/commands/resume.test.d.ts.map +0 -1
- package/dist/commands/resume.test.js +0 -362
- package/dist/commands/resume.test.js.map +0 -1
- package/dist/commands/start/buildPrompt.test.d.ts +0 -2
- package/dist/commands/start/buildPrompt.test.d.ts.map +0 -1
- package/dist/commands/start/buildPrompt.test.js +0 -182
- package/dist/commands/start/buildPrompt.test.js.map +0 -1
- package/dist/commands/start/createWorktree.test.d.ts +0 -2
- package/dist/commands/start/createWorktree.test.d.ts.map +0 -1
- package/dist/commands/start/createWorktree.test.js +0 -81
- package/dist/commands/start/createWorktree.test.js.map +0 -1
- package/dist/commands/start/parseIssueInput.test.d.ts +0 -2
- package/dist/commands/start/parseIssueInput.test.d.ts.map +0 -1
- package/dist/commands/start/parseIssueInput.test.js +0 -118
- package/dist/commands/start/parseIssueInput.test.js.map +0 -1
- package/dist/commands/start/progressTracker.test.d.ts +0 -2
- package/dist/commands/start/progressTracker.test.d.ts.map +0 -1
- package/dist/commands/start/progressTracker.test.js +0 -100
- package/dist/commands/start/progressTracker.test.js.map +0 -1
- package/dist/commands/start/validationGates.test.d.ts +0 -2
- package/dist/commands/start/validationGates.test.d.ts.map +0 -1
- package/dist/commands/start/validationGates.test.js +0 -66
- package/dist/commands/start/validationGates.test.js.map +0 -1
- package/dist/commands/start.test.d.ts +0 -2
- package/dist/commands/start.test.d.ts.map +0 -1
- package/dist/commands/start.test.js +0 -260
- package/dist/commands/start.test.js.map +0 -1
- package/dist/commands/stats.test.d.ts +0 -2
- package/dist/commands/stats.test.d.ts.map +0 -1
- package/dist/commands/stats.test.js +0 -127
- package/dist/commands/stats.test.js.map +0 -1
- package/dist/commands/status.test.d.ts +0 -2
- package/dist/commands/status.test.d.ts.map +0 -1
- package/dist/commands/status.test.js +0 -172
- package/dist/commands/status.test.js.map +0 -1
- package/dist/commands/stop.test.d.ts +0 -2
- package/dist/commands/stop.test.d.ts.map +0 -1
- package/dist/commands/stop.test.js +0 -152
- package/dist/commands/stop.test.js.map +0 -1
- package/dist/commands/tdd.d.ts +0 -3
- package/dist/commands/tdd.d.ts.map +0 -1
- package/dist/commands/tdd.js +0 -472
- package/dist/commands/tdd.js.map +0 -1
- package/dist/commands/web.test.d.ts +0 -2
- package/dist/commands/web.test.d.ts.map +0 -1
- package/dist/commands/web.test.js +0 -133
- package/dist/commands/web.test.js.map +0 -1
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { mkdtemp, rm, mkdir } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
// Create mock functions
|
|
6
|
-
const mockFindAll = vi.fn();
|
|
7
|
-
const mockSave = vi.fn();
|
|
8
|
-
// Mock FileSessionRepository
|
|
9
|
-
vi.mock('@claudetree/core', () => ({
|
|
10
|
-
FileSessionRepository: vi.fn().mockImplementation(() => ({
|
|
11
|
-
findAll: mockFindAll,
|
|
12
|
-
save: mockSave,
|
|
13
|
-
})),
|
|
14
|
-
}));
|
|
15
|
-
// Import after mock
|
|
16
|
-
import { stopCommand } from './stop.js';
|
|
17
|
-
describe('stopCommand', () => {
|
|
18
|
-
let testDir;
|
|
19
|
-
let originalCwd;
|
|
20
|
-
let originalExit;
|
|
21
|
-
let consoleLogSpy;
|
|
22
|
-
let consoleErrorSpy;
|
|
23
|
-
const createMockSession = (overrides = {}) => ({
|
|
24
|
-
id: 'test-session-id-123',
|
|
25
|
-
worktreeId: 'worktree-id-456',
|
|
26
|
-
claudeSessionId: null,
|
|
27
|
-
status: 'running',
|
|
28
|
-
issueNumber: 42,
|
|
29
|
-
prompt: 'Fix the bug',
|
|
30
|
-
createdAt: new Date('2024-01-15'),
|
|
31
|
-
updatedAt: new Date('2024-01-15'),
|
|
32
|
-
processId: null,
|
|
33
|
-
osProcessId: null,
|
|
34
|
-
lastHeartbeat: null,
|
|
35
|
-
errorCount: 0,
|
|
36
|
-
worktreePath: '/path/to/worktree',
|
|
37
|
-
usage: null,
|
|
38
|
-
progress: null,
|
|
39
|
-
...overrides,
|
|
40
|
-
});
|
|
41
|
-
beforeEach(async () => {
|
|
42
|
-
testDir = await mkdtemp(join(tmpdir(), 'claudetree-stop-test-'));
|
|
43
|
-
originalCwd = process.cwd();
|
|
44
|
-
process.chdir(testDir);
|
|
45
|
-
originalExit = process.exit;
|
|
46
|
-
process.exit = vi.fn();
|
|
47
|
-
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
48
|
-
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
49
|
-
mockFindAll.mockReset();
|
|
50
|
-
mockSave.mockReset();
|
|
51
|
-
});
|
|
52
|
-
afterEach(async () => {
|
|
53
|
-
process.chdir(originalCwd);
|
|
54
|
-
process.exit = originalExit;
|
|
55
|
-
consoleLogSpy.mockRestore();
|
|
56
|
-
consoleErrorSpy.mockRestore();
|
|
57
|
-
vi.clearAllMocks();
|
|
58
|
-
await rm(testDir, { recursive: true, force: true });
|
|
59
|
-
});
|
|
60
|
-
describe('when not initialized', () => {
|
|
61
|
-
it('should display error and exit with code 1', async () => {
|
|
62
|
-
const exitError = new Error('process.exit called');
|
|
63
|
-
process.exit.mockImplementation(() => {
|
|
64
|
-
throw exitError;
|
|
65
|
-
});
|
|
66
|
-
await expect(stopCommand.parseAsync(['node', 'test'])).rejects.toThrow('process.exit called');
|
|
67
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith('Error: claudetree not initialized. Run "claudetree init" first.');
|
|
68
|
-
expect(process.exit).toHaveBeenCalledWith(1);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
describe('when initialized', () => {
|
|
72
|
-
beforeEach(async () => {
|
|
73
|
-
await mkdir(join(testDir, '.claudetree'), { recursive: true });
|
|
74
|
-
});
|
|
75
|
-
describe('when no sessions exist', () => {
|
|
76
|
-
it('should display "No active sessions" message', async () => {
|
|
77
|
-
mockFindAll.mockResolvedValue([]);
|
|
78
|
-
await stopCommand.parseAsync(['node', 'test']);
|
|
79
|
-
expect(consoleLogSpy).toHaveBeenCalledWith('No active sessions.');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
describe('with session-id argument', () => {
|
|
83
|
-
it('should stop matching session by ID prefix', async () => {
|
|
84
|
-
const session = createMockSession({ id: 'test-session-abc123' });
|
|
85
|
-
mockFindAll.mockResolvedValue([session]);
|
|
86
|
-
mockSave.mockResolvedValue(undefined);
|
|
87
|
-
await stopCommand.parseAsync(['node', 'test', 'test-ses']);
|
|
88
|
-
expect(mockSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
89
|
-
id: 'test-session-abc123',
|
|
90
|
-
status: 'completed',
|
|
91
|
-
}));
|
|
92
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Stopping session: test-ses'));
|
|
93
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Stopped 1 session(s)'));
|
|
94
|
-
});
|
|
95
|
-
it('should error when no matching session found', async () => {
|
|
96
|
-
const session = createMockSession({ id: 'test-session-abc123' });
|
|
97
|
-
mockFindAll.mockResolvedValue([session]);
|
|
98
|
-
const exitError = new Error('process.exit called');
|
|
99
|
-
process.exit.mockImplementation(() => {
|
|
100
|
-
throw exitError;
|
|
101
|
-
});
|
|
102
|
-
await expect(stopCommand.parseAsync(['node', 'test', 'nonexistent'])).rejects.toThrow('process.exit called');
|
|
103
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith('No session found matching: nonexistent');
|
|
104
|
-
expect(process.exit).toHaveBeenCalledWith(1);
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
describe('with --all option', () => {
|
|
108
|
-
it('should stop all sessions', async () => {
|
|
109
|
-
const sessions = [
|
|
110
|
-
createMockSession({ id: 'session-1' }),
|
|
111
|
-
createMockSession({ id: 'session-2' }),
|
|
112
|
-
createMockSession({ id: 'session-3' }),
|
|
113
|
-
];
|
|
114
|
-
mockFindAll.mockResolvedValue(sessions);
|
|
115
|
-
mockSave.mockResolvedValue(undefined);
|
|
116
|
-
await stopCommand.parseAsync(['node', 'test', '--all']);
|
|
117
|
-
expect(mockSave).toHaveBeenCalledTimes(3);
|
|
118
|
-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Stopped 3 session(s)'));
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
describe('session status update', () => {
|
|
122
|
-
it('should update session status to completed', async () => {
|
|
123
|
-
const session = createMockSession({
|
|
124
|
-
id: 'test-session',
|
|
125
|
-
status: 'running',
|
|
126
|
-
});
|
|
127
|
-
mockFindAll.mockResolvedValue([session]);
|
|
128
|
-
mockSave.mockResolvedValue(undefined);
|
|
129
|
-
await stopCommand.parseAsync(['node', 'test', 'test']);
|
|
130
|
-
expect(mockSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
131
|
-
status: 'completed',
|
|
132
|
-
}));
|
|
133
|
-
});
|
|
134
|
-
it('should update the updatedAt timestamp', async () => {
|
|
135
|
-
const originalDate = new Date('2024-01-01');
|
|
136
|
-
const session = createMockSession({
|
|
137
|
-
id: 'test-session',
|
|
138
|
-
updatedAt: originalDate,
|
|
139
|
-
});
|
|
140
|
-
mockFindAll.mockResolvedValue([session]);
|
|
141
|
-
mockSave.mockResolvedValue(undefined);
|
|
142
|
-
await stopCommand.parseAsync(['node', 'test', 'test']);
|
|
143
|
-
expect(mockSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
144
|
-
updatedAt: expect.any(Date),
|
|
145
|
-
}));
|
|
146
|
-
const savedSession = mockSave.mock.calls[0][0];
|
|
147
|
-
expect(savedSession.updatedAt.getTime()).toBeGreaterThan(originalDate.getTime());
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
//# sourceMappingURL=stop.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"stop.test.js","sourceRoot":"","sources":["../../src/commands/stop.test.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/tdd.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tdd.d.ts","sourceRoot":"","sources":["../../src/commands/tdd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqHpC,eAAO,MAAM,UAAU,SAgcnB,CAAC"}
|
package/dist/commands/tdd.js
DELETED
|
@@ -1,472 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { access, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
5
|
-
import { GitWorktreeAdapter, ClaudeSessionAdapter, FileSessionRepository, FileEventRepository, GitHubAdapter, ValidationGateRunner, SlackNotifier, } from '@claudetree/core';
|
|
6
|
-
const CONFIG_DIR = '.claudetree';
|
|
7
|
-
async function loadConfig(cwd) {
|
|
8
|
-
try {
|
|
9
|
-
const configPath = join(cwd, CONFIG_DIR, 'config.json');
|
|
10
|
-
await access(configPath);
|
|
11
|
-
const content = await readFile(configPath, 'utf-8');
|
|
12
|
-
return JSON.parse(content);
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
function parseGates(gatesStr, testCommand) {
|
|
19
|
-
const gateNames = gatesStr.split(',').map(g => g.trim().toLowerCase());
|
|
20
|
-
const gates = [];
|
|
21
|
-
for (const name of gateNames) {
|
|
22
|
-
switch (name) {
|
|
23
|
-
case 'test':
|
|
24
|
-
gates.push({
|
|
25
|
-
name: 'test',
|
|
26
|
-
command: testCommand ?? 'pnpm test',
|
|
27
|
-
required: true,
|
|
28
|
-
});
|
|
29
|
-
break;
|
|
30
|
-
case 'type':
|
|
31
|
-
gates.push({
|
|
32
|
-
name: 'type',
|
|
33
|
-
command: 'pnpm tsc --noEmit',
|
|
34
|
-
required: true,
|
|
35
|
-
});
|
|
36
|
-
break;
|
|
37
|
-
case 'lint':
|
|
38
|
-
gates.push({
|
|
39
|
-
name: 'lint',
|
|
40
|
-
command: 'pnpm lint',
|
|
41
|
-
required: false,
|
|
42
|
-
});
|
|
43
|
-
break;
|
|
44
|
-
case 'build':
|
|
45
|
-
gates.push({
|
|
46
|
-
name: 'build',
|
|
47
|
-
command: 'pnpm build',
|
|
48
|
-
required: false,
|
|
49
|
-
});
|
|
50
|
-
break;
|
|
51
|
-
default:
|
|
52
|
-
console.warn(`Unknown gate: ${name}, skipping`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return gates;
|
|
56
|
-
}
|
|
57
|
-
function formatDuration(ms) {
|
|
58
|
-
const seconds = Math.floor(ms / 1000);
|
|
59
|
-
const minutes = Math.floor(seconds / 60);
|
|
60
|
-
const hours = Math.floor(minutes / 60);
|
|
61
|
-
if (hours > 0) {
|
|
62
|
-
return `${hours}h ${minutes % 60}m`;
|
|
63
|
-
}
|
|
64
|
-
if (minutes > 0) {
|
|
65
|
-
return `${minutes}m ${seconds % 60}s`;
|
|
66
|
-
}
|
|
67
|
-
return `${seconds}s`;
|
|
68
|
-
}
|
|
69
|
-
export const tddCommand = new Command('tdd')
|
|
70
|
-
.description('Run TDD mode: Write tests first, then implement with validation gates')
|
|
71
|
-
.argument('<issue>', 'Issue number, GitHub URL, or task name')
|
|
72
|
-
.option('-p, --prompt <prompt>', 'Additional prompt for Claude')
|
|
73
|
-
.option('--timeout <minutes>', 'Total session timeout in minutes', '120')
|
|
74
|
-
.option('--idle-timeout <minutes>', 'Idle timeout (no output) in minutes', '10')
|
|
75
|
-
.option('--max-iterations <n>', 'Maximum TDD iterations', '10')
|
|
76
|
-
.option('--max-retries <n>', 'Maximum retries per gate', '3')
|
|
77
|
-
.option('--gates <gates>', 'Validation gates (comma-separated: test,type,lint,build)', 'test,type')
|
|
78
|
-
.option('--test-command <cmd>', 'Custom test command', 'pnpm test')
|
|
79
|
-
.option('-b, --branch <branch>', 'Custom branch name')
|
|
80
|
-
.option('-t, --token <token>', 'GitHub token')
|
|
81
|
-
.option('--dry-run', 'Show what would be done without starting', false)
|
|
82
|
-
.action(async (issue, options) => {
|
|
83
|
-
const cwd = process.cwd();
|
|
84
|
-
const config = await loadConfig(cwd);
|
|
85
|
-
if (!config) {
|
|
86
|
-
console.error('Error: claudetree not initialized. Run "claudetree init" first.');
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
// Build TDD config
|
|
90
|
-
const tddConfig = {
|
|
91
|
-
timeout: (parseInt(String(options.timeout ?? '120'), 10)) * 60 * 1000,
|
|
92
|
-
idleTimeout: (parseInt(String(options.idleTimeout ?? '10'), 10)) * 60 * 1000,
|
|
93
|
-
maxIterations: parseInt(String(options.maxIterations ?? '10'), 10),
|
|
94
|
-
maxRetries: parseInt(String(options.maxRetries ?? '3'), 10),
|
|
95
|
-
gates: parseGates(options.gates ?? 'test,type', options.testCommand),
|
|
96
|
-
};
|
|
97
|
-
console.log('\n\x1b[36m╔══════════════════════════════════════════╗\x1b[0m');
|
|
98
|
-
console.log('\x1b[36m║ TDD Mode Session ║\x1b[0m');
|
|
99
|
-
console.log('\x1b[36m╚══════════════════════════════════════════╝\x1b[0m\n');
|
|
100
|
-
console.log('\x1b[33m⏱️ Time Limits:\x1b[0m');
|
|
101
|
-
console.log(` Session timeout: ${formatDuration(tddConfig.timeout)}`);
|
|
102
|
-
console.log(` Idle timeout: ${formatDuration(tddConfig.idleTimeout)}`);
|
|
103
|
-
console.log(` Max iterations: ${tddConfig.maxIterations}`);
|
|
104
|
-
console.log(` Max retries: ${tddConfig.maxRetries}`);
|
|
105
|
-
console.log('\n\x1b[33m✅ Validation Gates:\x1b[0m');
|
|
106
|
-
for (const gate of tddConfig.gates) {
|
|
107
|
-
const status = gate.required ? '\x1b[31m(required)\x1b[0m' : '\x1b[90m(optional)\x1b[0m';
|
|
108
|
-
console.log(` • ${gate.name}: ${gate.command} ${status}`);
|
|
109
|
-
}
|
|
110
|
-
if (options.dryRun) {
|
|
111
|
-
console.log('\n\x1b[33m[Dry Run]\x1b[0m Would start TDD session with above config.');
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
// Resolve issue
|
|
115
|
-
let issueNumber = null;
|
|
116
|
-
let issueData = null;
|
|
117
|
-
let branchName;
|
|
118
|
-
const ghToken = options.token ?? process.env.GITHUB_TOKEN ?? config.github?.token;
|
|
119
|
-
if (issue.includes('github.com')) {
|
|
120
|
-
if (!ghToken) {
|
|
121
|
-
console.error('\nError: GitHub token required for URL.');
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
const ghAdapter = new GitHubAdapter(ghToken);
|
|
125
|
-
const parsed = ghAdapter.parseIssueUrl(issue);
|
|
126
|
-
if (!parsed) {
|
|
127
|
-
console.error('Error: Invalid GitHub URL format.');
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
console.log(`\nFetching issue #${parsed.number}...`);
|
|
131
|
-
try {
|
|
132
|
-
issueData = await ghAdapter.getIssue(parsed.owner, parsed.repo, parsed.number);
|
|
133
|
-
issueNumber = issueData.number;
|
|
134
|
-
branchName = options.branch ?? ghAdapter.generateBranchName(issueNumber, issueData.title);
|
|
135
|
-
console.log(` Title: ${issueData.title}`);
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
console.error(`Error: Failed to fetch issue.`);
|
|
139
|
-
process.exit(1);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
const parsed = parseInt(issue, 10);
|
|
144
|
-
const isNumber = !isNaN(parsed);
|
|
145
|
-
if (isNumber && ghToken && config.github?.owner && config.github?.repo) {
|
|
146
|
-
const ghAdapter = new GitHubAdapter(ghToken);
|
|
147
|
-
try {
|
|
148
|
-
console.log(`\nFetching issue #${parsed}...`);
|
|
149
|
-
issueData = await ghAdapter.getIssue(config.github.owner, config.github.repo, parsed);
|
|
150
|
-
issueNumber = issueData.number;
|
|
151
|
-
branchName = options.branch ?? ghAdapter.generateBranchName(issueNumber, issueData.title);
|
|
152
|
-
console.log(` Title: ${issueData.title}`);
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
issueNumber = parsed;
|
|
156
|
-
branchName = options.branch ?? `issue-${issueNumber}`;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
else if (isNumber) {
|
|
160
|
-
issueNumber = parsed;
|
|
161
|
-
branchName = options.branch ?? `issue-${issueNumber}`;
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
branchName = options.branch ?? `task-${issue}`;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const worktreePath = join(cwd, config.worktreeDir, branchName);
|
|
168
|
-
// Create or use existing worktree
|
|
169
|
-
const gitAdapter = new GitWorktreeAdapter(cwd);
|
|
170
|
-
const existingWorktrees = await gitAdapter.list();
|
|
171
|
-
const existingWorktree = existingWorktrees.find((wt) => wt.branch === branchName || wt.path.endsWith(branchName));
|
|
172
|
-
let worktree;
|
|
173
|
-
if (existingWorktree) {
|
|
174
|
-
console.log(`\nUsing existing worktree: ${branchName}`);
|
|
175
|
-
worktree = {
|
|
176
|
-
id: randomUUID(),
|
|
177
|
-
path: existingWorktree.path,
|
|
178
|
-
branch: existingWorktree.branch,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
console.log(`\nCreating worktree: ${branchName}`);
|
|
183
|
-
try {
|
|
184
|
-
worktree = await gitAdapter.create({
|
|
185
|
-
path: worktreePath,
|
|
186
|
-
branch: branchName,
|
|
187
|
-
issueNumber: issueNumber ?? undefined,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
if (error instanceof Error) {
|
|
192
|
-
console.error(`Error: ${error.message}`);
|
|
193
|
-
}
|
|
194
|
-
process.exit(1);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
console.log(` Path: ${worktree.path}`);
|
|
198
|
-
// Check Claude availability
|
|
199
|
-
const claudeAdapter = new ClaudeSessionAdapter();
|
|
200
|
-
const available = await claudeAdapter.isClaudeAvailable();
|
|
201
|
-
if (!available) {
|
|
202
|
-
console.error('\nError: Claude CLI not found.');
|
|
203
|
-
process.exit(1);
|
|
204
|
-
}
|
|
205
|
-
// Create session
|
|
206
|
-
const sessionRepo = new FileSessionRepository(join(cwd, CONFIG_DIR));
|
|
207
|
-
const eventRepo = new FileEventRepository(join(cwd, CONFIG_DIR));
|
|
208
|
-
const session = {
|
|
209
|
-
id: randomUUID(),
|
|
210
|
-
worktreeId: worktree.id,
|
|
211
|
-
claudeSessionId: null,
|
|
212
|
-
status: 'pending',
|
|
213
|
-
issueNumber,
|
|
214
|
-
prompt: options.prompt ?? null,
|
|
215
|
-
createdAt: new Date(),
|
|
216
|
-
updatedAt: new Date(),
|
|
217
|
-
processId: null,
|
|
218
|
-
osProcessId: null,
|
|
219
|
-
lastHeartbeat: null,
|
|
220
|
-
errorCount: 0,
|
|
221
|
-
worktreePath: worktree.path,
|
|
222
|
-
usage: null,
|
|
223
|
-
progress: {
|
|
224
|
-
currentStep: 'analyzing',
|
|
225
|
-
completedSteps: [],
|
|
226
|
-
startedAt: new Date(),
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
await sessionRepo.save(session);
|
|
230
|
-
// Initialize TDD state
|
|
231
|
-
const tddState = {
|
|
232
|
-
phase: 'initializing',
|
|
233
|
-
currentIteration: 0,
|
|
234
|
-
gateResults: [],
|
|
235
|
-
failureCount: 0,
|
|
236
|
-
lastActivity: new Date(),
|
|
237
|
-
config: tddConfig,
|
|
238
|
-
};
|
|
239
|
-
// Save TDD state
|
|
240
|
-
const tddStatePath = join(cwd, CONFIG_DIR, 'tdd-state', `${session.id}.json`);
|
|
241
|
-
await mkdir(join(cwd, CONFIG_DIR, 'tdd-state'), { recursive: true });
|
|
242
|
-
await writeFile(tddStatePath, JSON.stringify(tddState, null, 2));
|
|
243
|
-
// Build TDD prompt
|
|
244
|
-
const tddSystemPrompt = `You are in TDD (Test-Driven Development) mode. Follow this STRICT workflow:
|
|
245
|
-
|
|
246
|
-
## TDD Cycle (Repeat until done)
|
|
247
|
-
|
|
248
|
-
### 1. RED Phase - Write Failing Test
|
|
249
|
-
- Write ONE failing test that describes the expected behavior
|
|
250
|
-
- Run the test to confirm it fails
|
|
251
|
-
- Commit: "test: add test for <feature>"
|
|
252
|
-
|
|
253
|
-
### 2. GREEN Phase - Minimal Implementation
|
|
254
|
-
- Write the MINIMUM code to make the test pass
|
|
255
|
-
- Run tests to confirm they pass
|
|
256
|
-
- Commit: "feat: implement <feature>"
|
|
257
|
-
|
|
258
|
-
### 3. REFACTOR Phase (Optional)
|
|
259
|
-
- Clean up code while keeping tests green
|
|
260
|
-
- Commit: "refactor: improve <description>"
|
|
261
|
-
|
|
262
|
-
## Rules
|
|
263
|
-
- NEVER write implementation before tests
|
|
264
|
-
- ONE test at a time
|
|
265
|
-
- Run tests after EVERY change
|
|
266
|
-
- Stop when all requirements are met
|
|
267
|
-
|
|
268
|
-
## Validation Gates (Must Pass)
|
|
269
|
-
${tddConfig.gates.map(g => `- ${g.name}: \`${g.command}\` ${g.required ? '(REQUIRED)' : '(optional)'}`).join('\n')}
|
|
270
|
-
|
|
271
|
-
## Time Limits
|
|
272
|
-
- Total: ${formatDuration(tddConfig.timeout)}
|
|
273
|
-
- Idle: ${formatDuration(tddConfig.idleTimeout)}
|
|
274
|
-
- Max iterations: ${tddConfig.maxIterations}
|
|
275
|
-
|
|
276
|
-
When done, create a PR to the develop branch.`;
|
|
277
|
-
let issuePrompt;
|
|
278
|
-
if (issueData) {
|
|
279
|
-
issuePrompt = `Issue #${issueNumber}: "${issueData.title}"
|
|
280
|
-
|
|
281
|
-
Description:
|
|
282
|
-
${issueData.body || 'No description provided.'}
|
|
283
|
-
|
|
284
|
-
${options.prompt ? `Additional context: ${options.prompt}` : ''}
|
|
285
|
-
|
|
286
|
-
Start with TDD - write a failing test first!`;
|
|
287
|
-
}
|
|
288
|
-
else if (issueNumber) {
|
|
289
|
-
issuePrompt = `Working on issue #${issueNumber}. Start with TDD - write a failing test first!`;
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
issuePrompt = `Working on ${branchName}. Start with TDD - write a failing test first!`;
|
|
293
|
-
}
|
|
294
|
-
console.log('\n\x1b[36m🧪 Starting TDD Session...\x1b[0m');
|
|
295
|
-
console.log(` Session: ${session.id.slice(0, 8)}`);
|
|
296
|
-
// Track timeouts
|
|
297
|
-
const sessionStartTime = Date.now();
|
|
298
|
-
let lastOutputTime = Date.now();
|
|
299
|
-
let sessionTimedOut = false;
|
|
300
|
-
let idleTimedOut = false;
|
|
301
|
-
// Timeout checker
|
|
302
|
-
const timeoutChecker = setInterval(() => {
|
|
303
|
-
const elapsed = Date.now() - sessionStartTime;
|
|
304
|
-
const idleTime = Date.now() - lastOutputTime;
|
|
305
|
-
if (elapsed >= tddConfig.timeout) {
|
|
306
|
-
sessionTimedOut = true;
|
|
307
|
-
console.log(`\n\x1b[31m[Timeout]\x1b[0m Session timeout (${formatDuration(tddConfig.timeout)}) exceeded.`);
|
|
308
|
-
clearInterval(timeoutChecker);
|
|
309
|
-
}
|
|
310
|
-
else if (idleTime >= tddConfig.idleTimeout) {
|
|
311
|
-
idleTimedOut = true;
|
|
312
|
-
console.log(`\n\x1b[31m[Timeout]\x1b[0m Idle timeout (${formatDuration(tddConfig.idleTimeout)}) exceeded.`);
|
|
313
|
-
clearInterval(timeoutChecker);
|
|
314
|
-
}
|
|
315
|
-
}, 5000);
|
|
316
|
-
// Setup event listener
|
|
317
|
-
claudeAdapter.on('output', async (event) => {
|
|
318
|
-
const { output } = event;
|
|
319
|
-
lastOutputTime = Date.now();
|
|
320
|
-
let eventType = 'output';
|
|
321
|
-
if (output.type === 'tool_use')
|
|
322
|
-
eventType = 'tool_call';
|
|
323
|
-
else if (output.type === 'error')
|
|
324
|
-
eventType = 'error';
|
|
325
|
-
try {
|
|
326
|
-
await eventRepo.append({
|
|
327
|
-
id: randomUUID(),
|
|
328
|
-
sessionId: session.id,
|
|
329
|
-
type: eventType,
|
|
330
|
-
content: output.content,
|
|
331
|
-
timestamp: output.timestamp,
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
// Ignore
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
// Start Claude session
|
|
339
|
-
const result = await claudeAdapter.start({
|
|
340
|
-
workingDir: worktree.path,
|
|
341
|
-
prompt: issuePrompt,
|
|
342
|
-
systemPrompt: tddSystemPrompt,
|
|
343
|
-
allowedTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'],
|
|
344
|
-
});
|
|
345
|
-
session.processId = result.processId;
|
|
346
|
-
session.osProcessId = result.osProcessId;
|
|
347
|
-
session.status = 'running';
|
|
348
|
-
session.lastHeartbeat = new Date();
|
|
349
|
-
await sessionRepo.save(session);
|
|
350
|
-
// Update TDD state
|
|
351
|
-
tddState.phase = 'writing_test';
|
|
352
|
-
tddState.currentIteration = 1;
|
|
353
|
-
await writeFile(tddStatePath, JSON.stringify(tddState, null, 2));
|
|
354
|
-
// Graceful shutdown
|
|
355
|
-
const handleShutdown = async () => {
|
|
356
|
-
console.log('\n[Info] Pausing TDD session...');
|
|
357
|
-
clearInterval(timeoutChecker);
|
|
358
|
-
session.status = 'paused';
|
|
359
|
-
tddState.phase = 'failed';
|
|
360
|
-
await sessionRepo.save(session);
|
|
361
|
-
await writeFile(tddStatePath, JSON.stringify(tddState, null, 2));
|
|
362
|
-
console.log(`Resume with: claudetree resume ${session.id.slice(0, 8)}`);
|
|
363
|
-
process.exit(0);
|
|
364
|
-
};
|
|
365
|
-
process.on('SIGINT', handleShutdown);
|
|
366
|
-
process.on('SIGTERM', handleShutdown);
|
|
367
|
-
console.log('\nClaude is working in TDD mode...\n');
|
|
368
|
-
// Process output
|
|
369
|
-
for await (const output of claudeAdapter.getOutput(result.processId)) {
|
|
370
|
-
session.lastHeartbeat = new Date();
|
|
371
|
-
lastOutputTime = Date.now();
|
|
372
|
-
// Check timeouts
|
|
373
|
-
if (sessionTimedOut || idleTimedOut) {
|
|
374
|
-
await claudeAdapter.stop(result.processId);
|
|
375
|
-
session.status = 'failed';
|
|
376
|
-
tddState.phase = 'failed';
|
|
377
|
-
break;
|
|
378
|
-
}
|
|
379
|
-
if (output.type === 'text') {
|
|
380
|
-
console.log(output.content);
|
|
381
|
-
}
|
|
382
|
-
else if (output.type === 'tool_use') {
|
|
383
|
-
console.log(`\x1b[36m[Tool]\x1b[0m ${output.content}`);
|
|
384
|
-
}
|
|
385
|
-
else if (output.type === 'error') {
|
|
386
|
-
console.error(`\x1b[31m[Error]\x1b[0m ${output.content}`);
|
|
387
|
-
}
|
|
388
|
-
else if (output.type === 'done') {
|
|
389
|
-
console.log(`\x1b[32m[Done]\x1b[0m Session ID: ${output.content}`);
|
|
390
|
-
if (output.content) {
|
|
391
|
-
session.claudeSessionId = output.content;
|
|
392
|
-
}
|
|
393
|
-
if (output.usage) {
|
|
394
|
-
session.usage = output.usage;
|
|
395
|
-
console.log(`\x1b[32m[Usage]\x1b[0m Tokens: ${output.usage.inputTokens} in / ${output.usage.outputTokens} out | Cost: $${output.usage.totalCostUsd.toFixed(4)}`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
clearInterval(timeoutChecker);
|
|
400
|
-
// Run validation gates
|
|
401
|
-
if (session.status !== 'failed') {
|
|
402
|
-
console.log('\n\x1b[36m╔══════════════════════════════════════════╗\x1b[0m');
|
|
403
|
-
console.log('\x1b[36m║ Running Validation Gates ║\x1b[0m');
|
|
404
|
-
console.log('\x1b[36m╚══════════════════════════════════════════╝\x1b[0m\n');
|
|
405
|
-
tddState.phase = 'validating';
|
|
406
|
-
await writeFile(tddStatePath, JSON.stringify(tddState, null, 2));
|
|
407
|
-
const gateRunner = new ValidationGateRunner();
|
|
408
|
-
const gateResults = await gateRunner.runWithAutoRetry(tddConfig.gates, {
|
|
409
|
-
cwd: worktree.path,
|
|
410
|
-
maxRetries: tddConfig.maxRetries,
|
|
411
|
-
onRetry: (attempt, failedGate) => {
|
|
412
|
-
console.log(`\x1b[33m[Retry]\x1b[0m Gate '${failedGate}' failed, attempt ${attempt + 1}/${tddConfig.maxRetries}`);
|
|
413
|
-
},
|
|
414
|
-
});
|
|
415
|
-
// Display results
|
|
416
|
-
console.log('\n\x1b[33m📊 Gate Results:\x1b[0m');
|
|
417
|
-
for (const result of gateResults.results) {
|
|
418
|
-
const icon = result.passed ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
|
|
419
|
-
const attempts = result.attempts > 1 ? ` (${result.attempts} attempts)` : '';
|
|
420
|
-
console.log(` ${icon} ${result.gateName}${attempts}`);
|
|
421
|
-
}
|
|
422
|
-
console.log(`\n Total time: ${formatDuration(gateResults.totalTime)}`);
|
|
423
|
-
tddState.gateResults = gateResults.results;
|
|
424
|
-
if (gateResults.allPassed) {
|
|
425
|
-
console.log('\n\x1b[32m✅ All validation gates passed!\x1b[0m');
|
|
426
|
-
session.status = 'completed';
|
|
427
|
-
tddState.phase = 'completed';
|
|
428
|
-
}
|
|
429
|
-
else {
|
|
430
|
-
console.log('\n\x1b[31m❌ Validation gates failed.\x1b[0m');
|
|
431
|
-
session.status = 'failed';
|
|
432
|
-
tddState.phase = 'failed';
|
|
433
|
-
// Show failed gate output
|
|
434
|
-
const failedGate = gateResults.results.find(r => !r.passed);
|
|
435
|
-
if (failedGate?.output) {
|
|
436
|
-
console.log(`\n\x1b[33mFailed gate output (${failedGate.gateName}):\x1b[0m`);
|
|
437
|
-
console.log(failedGate.output);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
// Final stats
|
|
442
|
-
const totalDuration = Date.now() - sessionStartTime;
|
|
443
|
-
console.log('\n\x1b[36m╔══════════════════════════════════════════╗\x1b[0m');
|
|
444
|
-
console.log('\x1b[36m║ Session Summary ║\x1b[0m');
|
|
445
|
-
console.log('\x1b[36m╚══════════════════════════════════════════╝\x1b[0m\n');
|
|
446
|
-
console.log(` Status: ${session.status === 'completed' ? '\x1b[32mcompleted\x1b[0m' : '\x1b[31mfailed\x1b[0m'}`);
|
|
447
|
-
console.log(` Duration: ${formatDuration(totalDuration)}`);
|
|
448
|
-
console.log(` Iterations: ${tddState.currentIteration}`);
|
|
449
|
-
if (session.usage) {
|
|
450
|
-
console.log(` Cost: $${session.usage.totalCostUsd.toFixed(4)}`);
|
|
451
|
-
}
|
|
452
|
-
// Save final state
|
|
453
|
-
session.updatedAt = new Date();
|
|
454
|
-
await sessionRepo.save(session);
|
|
455
|
-
await writeFile(tddStatePath, JSON.stringify(tddState, null, 2));
|
|
456
|
-
// Slack notification
|
|
457
|
-
if (config.slack?.webhookUrl) {
|
|
458
|
-
const slack = new SlackNotifier(config.slack.webhookUrl);
|
|
459
|
-
await slack.notifySession({
|
|
460
|
-
sessionId: session.id,
|
|
461
|
-
status: session.status === 'completed' ? 'completed' : 'failed',
|
|
462
|
-
issueNumber,
|
|
463
|
-
branch: branchName,
|
|
464
|
-
worktreePath: worktree.path,
|
|
465
|
-
duration: totalDuration,
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
if (session.status === 'failed') {
|
|
469
|
-
process.exit(1);
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
//# sourceMappingURL=tdd.js.map
|