@vexdo/cli 0.1.0 → 0.1.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.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +1597 -0
  4. package/package.json +9 -1
  5. package/.eslintrc.json +0 -23
  6. package/.github/workflows/ci.yml +0 -84
  7. package/.idea/copilot.data.migration.ask2agent.xml +0 -6
  8. package/.idea/go.imports.xml +0 -11
  9. package/.idea/misc.xml +0 -6
  10. package/.idea/modules.xml +0 -8
  11. package/.idea/vcs.xml +0 -7
  12. package/.idea/vexdo-cli.iml +0 -9
  13. package/.prettierrc +0 -5
  14. package/CLAUDE.md +0 -93
  15. package/CONTRIBUTING.md +0 -62
  16. package/src/commands/abort.ts +0 -66
  17. package/src/commands/fix.ts +0 -106
  18. package/src/commands/init.ts +0 -142
  19. package/src/commands/logs.ts +0 -74
  20. package/src/commands/review.ts +0 -107
  21. package/src/commands/start.ts +0 -197
  22. package/src/commands/status.ts +0 -52
  23. package/src/commands/submit.ts +0 -38
  24. package/src/index.ts +0 -42
  25. package/src/lib/claude.ts +0 -259
  26. package/src/lib/codex.ts +0 -96
  27. package/src/lib/config.ts +0 -157
  28. package/src/lib/gh.ts +0 -78
  29. package/src/lib/git.ts +0 -119
  30. package/src/lib/logger.ts +0 -147
  31. package/src/lib/requirements.ts +0 -18
  32. package/src/lib/review-loop.ts +0 -154
  33. package/src/lib/state.ts +0 -121
  34. package/src/lib/submit-task.ts +0 -43
  35. package/src/lib/tasks.ts +0 -94
  36. package/src/prompts/arbiter.ts +0 -21
  37. package/src/prompts/reviewer.ts +0 -20
  38. package/src/types/index.ts +0 -96
  39. package/test/config.test.ts +0 -124
  40. package/test/state.test.ts +0 -147
  41. package/test/unit/claude.test.ts +0 -117
  42. package/test/unit/codex.test.ts +0 -67
  43. package/test/unit/gh.test.ts +0 -49
  44. package/test/unit/git.test.ts +0 -120
  45. package/test/unit/review-loop.test.ts +0 -198
  46. package/tests/integration/review.test.ts +0 -137
  47. package/tests/integration/start.test.ts +0 -220
  48. package/tests/unit/init.test.ts +0 -91
  49. package/tsconfig.json +0 -15
  50. package/tsup.config.ts +0 -8
  51. package/vitest.config.ts +0 -7
@@ -1,74 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- import type { Command } from 'commander';
5
-
6
- import { findProjectRoot } from '../lib/config.js';
7
- import * as logger from '../lib/logger.js';
8
- import { getLogsDir, getStateDir, loadState } from '../lib/state.js';
9
-
10
- interface LogsOptions { full?: boolean }
11
-
12
- function fatalAndExit(message: string): never {
13
- logger.fatal(message);
14
- process.exit(1);
15
- }
16
-
17
- export function runLogs(taskIdArg?: string, options?: LogsOptions): void {
18
- const projectRoot = findProjectRoot();
19
- if (!projectRoot) {
20
- fatalAndExit('Not inside a vexdo project.');
21
- }
22
-
23
- const state = loadState(projectRoot);
24
- const taskId = taskIdArg ?? state?.taskId;
25
-
26
- if (!taskId) {
27
- const base = path.join(getStateDir(projectRoot), 'logs');
28
- if (!fs.existsSync(base)) {
29
- logger.info('No logs available.');
30
- return;
31
- }
32
-
33
- const tasks = fs.readdirSync(base, { withFileTypes: true }).filter((entry) => entry.isDirectory());
34
- for (const dir of tasks) {
35
- logger.info(dir.name);
36
- }
37
- return;
38
- }
39
-
40
- const logsDir = getLogsDir(projectRoot, taskId);
41
- if (!fs.existsSync(logsDir)) {
42
- fatalAndExit(`No logs found for task '${taskId}'.`);
43
- }
44
-
45
- const files = fs.readdirSync(logsDir).filter((name) => name.endsWith('-arbiter.json'));
46
- for (const arbiterFile of files) {
47
- const base = arbiterFile.replace(/-arbiter\.json$/, '');
48
- const arbiterPath = path.join(logsDir, `${base}-arbiter.json`);
49
- const reviewPath = path.join(logsDir, `${base}-review.json`);
50
- const diffPath = path.join(logsDir, `${base}-diff.txt`);
51
-
52
- const arbiter = JSON.parse(fs.readFileSync(arbiterPath, 'utf8')) as { decision: string; summary: string };
53
- const review = JSON.parse(fs.readFileSync(reviewPath, 'utf8')) as { comments?: unknown[] };
54
-
55
- logger.info(`${base}: decision=${arbiter.decision}, comments=${String(review.comments?.length ?? 0)}, summary=${arbiter.summary}`);
56
-
57
- if (options?.full) {
58
- console.log(fs.readFileSync(diffPath, 'utf8'));
59
- console.log(JSON.stringify(review, null, 2));
60
- console.log(JSON.stringify(arbiter, null, 2));
61
- }
62
- }
63
- }
64
-
65
- export function registerLogsCommand(program: Command): void {
66
- program
67
- .command('logs')
68
- .description('Show iteration logs')
69
- .argument('[task-id]')
70
- .option('--full', 'Print full diff and comments')
71
- .action((taskId?: string, options?: LogsOptions) => {
72
- runLogs(taskId, options);
73
- });
74
- }
@@ -1,107 +0,0 @@
1
- import fs from 'node:fs';
2
-
3
- import type { Command } from 'commander';
4
-
5
- import { ClaudeClient } from '../lib/claude.js';
6
- import { findProjectRoot, loadConfig } from '../lib/config.js';
7
- import * as logger from '../lib/logger.js';
8
- import { requireAnthropicApiKey } from '../lib/requirements.js';
9
- import { runReviewLoop } from '../lib/review-loop.js';
10
- import { loadState, saveState } from '../lib/state.js';
11
- import { ensureTaskDirectory, loadAndValidateTask, moveTaskFileAtomically } from '../lib/tasks.js';
12
-
13
- interface ReviewOptions { dryRun?: boolean; verbose?: boolean }
14
-
15
- function fatalAndExit(message: string): never {
16
- logger.fatal(message);
17
- process.exit(1);
18
- }
19
-
20
- export async function runReview(options: ReviewOptions): Promise<void> {
21
- try {
22
- const projectRoot = findProjectRoot();
23
- if (!projectRoot) {
24
- fatalAndExit('Not inside a vexdo project.');
25
- }
26
-
27
- const config = loadConfig(projectRoot);
28
- const state = loadState(projectRoot);
29
- if (!state) {
30
- fatalAndExit('No active task.');
31
- }
32
-
33
- if (!options.dryRun) {
34
- requireAnthropicApiKey();
35
- }
36
-
37
- const currentStep = state.steps.find((step) => step.status === 'in_progress' || step.status === 'pending');
38
- if (!currentStep) {
39
- fatalAndExit('No in-progress step found in active task.');
40
- }
41
-
42
- if (!fs.existsSync(state.taskPath)) {
43
- fatalAndExit(`Task file not found: ${state.taskPath}`);
44
- }
45
-
46
- const task = loadAndValidateTask(state.taskPath, config);
47
- const step = task.steps.find((item) => item.service === currentStep.service);
48
- if (!step) {
49
- fatalAndExit(`Could not locate task step for service '${currentStep.service}'.`);
50
- }
51
-
52
- const result = await runReviewLoop({
53
- taskId: task.id,
54
- task,
55
- step,
56
- stepState: currentStep,
57
- projectRoot,
58
- config,
59
- claude: new ClaudeClient(process.env.ANTHROPIC_API_KEY ?? ''),
60
- dryRun: options.dryRun,
61
- verbose: options.verbose,
62
- });
63
-
64
- if (result.decision === 'escalate') {
65
- logger.escalation({
66
- taskId: task.id,
67
- service: step.service,
68
- iteration: result.finalIteration,
69
- spec: step.spec,
70
- diff: '',
71
- reviewComments: result.lastReviewComments,
72
- arbiterReasoning: result.lastArbiterResult.reasoning,
73
- summary: result.lastArbiterResult.summary,
74
- });
75
-
76
- currentStep.status = 'escalated';
77
- state.status = 'escalated';
78
-
79
- if (!options.dryRun) {
80
- saveState(projectRoot, state);
81
- const blockedDir = ensureTaskDirectory(projectRoot, 'blocked');
82
- state.taskPath = moveTaskFileAtomically(state.taskPath, blockedDir);
83
- saveState(projectRoot, state);
84
- }
85
-
86
- process.exit(1);
87
- }
88
-
89
- currentStep.status = 'done';
90
- if (!options.dryRun) {
91
- saveState(projectRoot, state);
92
- }
93
- } catch (error: unknown) {
94
- const message = error instanceof Error ? error.message : String(error);
95
- fatalAndExit(message);
96
- }
97
- }
98
-
99
- export function registerReviewCommand(program: Command): void {
100
- program
101
- .command('review')
102
- .description('Run review loop for the current step')
103
- .action(async (options: ReviewOptions, command: Command) => {
104
- const merged = command.optsWithGlobals();
105
- await runReview({ ...options, ...merged });
106
- });
107
- }
@@ -1,197 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- import type { Command } from 'commander';
5
-
6
- import { ClaudeClient } from '../lib/claude.js';
7
- import * as codex from '../lib/codex.js';
8
- import { findProjectRoot, loadConfig } from '../lib/config.js';
9
- import * as git from '../lib/git.js';
10
- import * as logger from '../lib/logger.js';
11
- import { requireAnthropicApiKey } from '../lib/requirements.js';
12
- import { runReviewLoop } from '../lib/review-loop.js';
13
- import { createState, hasActiveTask, loadState, saveState } from '../lib/state.js';
14
- import { submitActiveTask } from '../lib/submit-task.js';
15
- import { buildInitialStepState, ensureTaskDirectory, loadAndValidateTask, moveTaskFileAtomically } from '../lib/tasks.js';
16
-
17
- export interface StartCommandOptions {
18
- dryRun?: boolean;
19
- verbose?: boolean;
20
- resume?: boolean;
21
- }
22
-
23
- function fatalAndExit(message: string, hint?: string): never {
24
- logger.fatal(message, hint);
25
- process.exit(1);
26
- }
27
-
28
- export async function runStart(taskFile: string, options: StartCommandOptions): Promise<void> {
29
- try {
30
- const projectRoot = findProjectRoot();
31
- if (!projectRoot) {
32
- fatalAndExit('Not inside a vexdo project. Could not find .vexdo.yml.');
33
- }
34
-
35
- const config = loadConfig(projectRoot);
36
- const taskPath = path.resolve(taskFile);
37
- const task = loadAndValidateTask(taskPath, config);
38
-
39
- if (hasActiveTask(projectRoot) && !options.resume) {
40
- fatalAndExit('An active task already exists.', "Use --resume to continue or 'vexdo abort' to cancel.");
41
- }
42
-
43
- if (!options.dryRun) {
44
- requireAnthropicApiKey();
45
- await codex.checkCodexAvailable();
46
- }
47
-
48
- let state = loadState(projectRoot);
49
-
50
- if (!options.resume) {
51
- let taskPathInProgress = taskPath;
52
- if (!options.dryRun) {
53
- const inProgressDir = ensureTaskDirectory(projectRoot, 'in_progress');
54
- taskPathInProgress = moveTaskFileAtomically(taskPath, inProgressDir);
55
- }
56
-
57
- state = createState(task.id, task.title, taskPathInProgress, buildInitialStepState(task));
58
- if (!options.dryRun) {
59
- saveState(projectRoot, state);
60
- }
61
- }
62
-
63
- if (!state) {
64
- fatalAndExit('No resumable task state found.');
65
- }
66
-
67
- const claude = new ClaudeClient(process.env.ANTHROPIC_API_KEY ?? '');
68
- const total = task.steps.length;
69
-
70
- for (let i = 0; i < task.steps.length; i += 1) {
71
- const step = task.steps[i];
72
- const stepState = state.steps[i];
73
- if (!step || !stepState) {
74
- continue;
75
- }
76
-
77
- if (stepState.status === 'done') {
78
- continue;
79
- }
80
-
81
- if (step.depends_on && step.depends_on.length > 0) {
82
- for (const depService of step.depends_on) {
83
- const depState = state.steps.find((item) => item.service === depService);
84
- if (depState?.status !== 'done') {
85
- fatalAndExit(`Step dependency '${depService}' for service '${step.service}' is not done.`);
86
- }
87
- }
88
- }
89
-
90
- logger.step(i + 1, total, `${step.service}: ${task.title}`);
91
-
92
- const serviceCfg = config.services.find((service) => service.name === step.service);
93
- if (!serviceCfg) {
94
- fatalAndExit(`Unknown service in step: ${step.service}`);
95
- }
96
-
97
- const serviceRoot = path.resolve(projectRoot, serviceCfg.path);
98
- const branch = git.getBranchName(task.id, step.service);
99
-
100
- if (!options.dryRun) {
101
- if (options.resume) {
102
- await git.checkoutBranch(stepState.branch ?? branch, serviceRoot);
103
- } else {
104
- await git.createBranch(branch, serviceRoot);
105
- }
106
- }
107
-
108
- stepState.status = 'in_progress';
109
- stepState.branch = branch;
110
- if (!options.dryRun) {
111
- saveState(projectRoot, state);
112
- }
113
-
114
- if (!options.resume && !options.dryRun) {
115
- await codex.exec({
116
- spec: step.spec,
117
- model: config.codex.model,
118
- cwd: serviceRoot,
119
- verbose: options.verbose,
120
- });
121
- } else if (options.dryRun) {
122
- logger.info(`[dry-run] Would run codex for service ${step.service}`);
123
- }
124
-
125
- const result = await runReviewLoop({
126
- taskId: task.id,
127
- task,
128
- step,
129
- stepState,
130
- projectRoot,
131
- config,
132
- claude,
133
- dryRun: options.dryRun,
134
- verbose: options.verbose,
135
- });
136
-
137
- if (result.decision === 'escalate') {
138
- logger.escalation({
139
- taskId: task.id,
140
- service: step.service,
141
- iteration: result.finalIteration,
142
- spec: step.spec,
143
- diff: '',
144
- reviewComments: result.lastReviewComments,
145
- arbiterReasoning: result.lastArbiterResult.reasoning,
146
- summary: result.lastArbiterResult.summary,
147
- });
148
-
149
- stepState.status = 'escalated';
150
- state.status = 'escalated';
151
-
152
- if (!options.dryRun) {
153
- saveState(projectRoot, state);
154
- const blockedDir = ensureTaskDirectory(projectRoot, 'blocked');
155
- state.taskPath = moveTaskFileAtomically(state.taskPath, blockedDir);
156
- saveState(projectRoot, state);
157
- }
158
-
159
- process.exit(1);
160
- }
161
-
162
- stepState.status = 'done';
163
- if (!options.dryRun) {
164
- saveState(projectRoot, state);
165
- }
166
- }
167
-
168
- state.status = 'review';
169
- if (!options.dryRun) {
170
- const reviewDir = ensureTaskDirectory(projectRoot, 'review');
171
- state.taskPath = moveTaskFileAtomically(state.taskPath, reviewDir);
172
- saveState(projectRoot, state);
173
- }
174
-
175
- if (config.review.auto_submit && !options.dryRun) {
176
- await submitActiveTask(projectRoot, config, state);
177
- return;
178
- }
179
-
180
- logger.success("Task ready for PR. Run 'vexdo submit' to create PR.");
181
- } catch (error: unknown) {
182
- const message = error instanceof Error ? error.message : String(error);
183
- fatalAndExit(message);
184
- }
185
- }
186
-
187
- export function registerStartCommand(program: Command): void {
188
- program
189
- .command('start')
190
- .description('Start a task from a YAML file')
191
- .argument('<task-file>')
192
- .option('--resume', 'Resume an existing active task')
193
- .action(async (taskFile: string, options: StartCommandOptions, command: Command) => {
194
- const merged = command.optsWithGlobals();
195
- await runStart(taskFile, { ...options, ...merged });
196
- });
197
- }
@@ -1,52 +0,0 @@
1
- import type { Command } from 'commander';
2
-
3
- import { findProjectRoot } from '../lib/config.js';
4
- import * as logger from '../lib/logger.js';
5
- import { loadState } from '../lib/state.js';
6
-
7
- function fatalAndExit(message: string): never {
8
- logger.fatal(message);
9
- process.exit(1);
10
- }
11
-
12
- function formatElapsed(startedAt: string): string {
13
- const elapsedMs = Date.now() - new Date(startedAt).getTime();
14
- const minutes = Math.floor(elapsedMs / 1000 / 60);
15
- const hours = Math.floor(minutes / 60);
16
- if (hours > 0) {
17
- return `${String(hours)}h ${String(minutes % 60)}m`;
18
- }
19
- return `${String(minutes)}m`;
20
- }
21
-
22
- export function runStatus(): void {
23
- const projectRoot = findProjectRoot();
24
- if (!projectRoot) {
25
- fatalAndExit('Not inside a vexdo project.');
26
- }
27
-
28
- const state = loadState(projectRoot);
29
- if (!state) {
30
- fatalAndExit('No active task.');
31
- }
32
-
33
- logger.info(`Task: ${state.taskId} — ${state.taskTitle}`);
34
- logger.info(`Status: ${state.status}`);
35
- console.log('service | status | iteration | branch');
36
- for (const step of state.steps) {
37
- console.log(`${step.service} | ${step.status} | ${String(step.iteration)} | ${step.branch ?? '-'}`);
38
- }
39
-
40
- const inProgress = state.steps.find((step) => step.status === 'in_progress');
41
- if (inProgress?.lastArbiterResult?.summary) {
42
- logger.info(`Last arbiter summary: ${inProgress.lastArbiterResult.summary}`);
43
- }
44
-
45
- logger.info(`Elapsed: ${formatElapsed(state.startedAt)}`);
46
- }
47
-
48
- export function registerStatusCommand(program: Command): void {
49
- program.command('status').description('Print active task status').action(() => {
50
- runStatus();
51
- });
52
- }
@@ -1,38 +0,0 @@
1
- import type { Command } from 'commander';
2
-
3
- import { findProjectRoot, loadConfig } from '../lib/config.js';
4
- import * as logger from '../lib/logger.js';
5
- import { requireGhAvailable } from '../lib/requirements.js';
6
- import { loadState } from '../lib/state.js';
7
- import { submitActiveTask } from '../lib/submit-task.js';
8
-
9
- function fatalAndExit(message: string): never {
10
- logger.fatal(message);
11
- process.exit(1);
12
- }
13
-
14
- export async function runSubmit(): Promise<void> {
15
- try {
16
- const projectRoot = findProjectRoot();
17
- if (!projectRoot) {
18
- fatalAndExit('Not inside a vexdo project.');
19
- }
20
-
21
- const config = loadConfig(projectRoot);
22
- const state = loadState(projectRoot);
23
- if (!state) {
24
- fatalAndExit('No active task.');
25
- }
26
-
27
- await requireGhAvailable();
28
- await submitActiveTask(projectRoot, config, state);
29
- } catch (error: unknown) {
30
- fatalAndExit(error instanceof Error ? error.message : String(error));
31
- }
32
- }
33
-
34
- export function registerSubmitCommand(program: Command): void {
35
- program.command('submit').description('Create PRs for active task').action(async () => {
36
- await runSubmit();
37
- });
38
- }
package/src/index.ts DELETED
@@ -1,42 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- import { Command } from 'commander';
5
-
6
- import { registerAbortCommand } from './commands/abort.js';
7
- import { registerFixCommand } from './commands/fix.js';
8
- import { registerInitCommand } from './commands/init.js';
9
- import { registerLogsCommand } from './commands/logs.js';
10
- import { registerReviewCommand } from './commands/review.js';
11
- import { registerStartCommand } from './commands/start.js';
12
- import { registerStatusCommand } from './commands/status.js';
13
- import { registerSubmitCommand } from './commands/submit.js';
14
- import * as logger from './lib/logger.js';
15
-
16
- const packageJsonPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', 'package.json');
17
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as { version: string };
18
-
19
- const program = new Command();
20
-
21
- program
22
- .name('vexdo')
23
- .description('Vexdo CLI')
24
- .version(packageJson.version)
25
- .option('--verbose', 'Enable verbose logs')
26
- .option('--dry-run', 'Print plan without making changes');
27
-
28
- program.hook('preAction', (_thisCommand, actionCommand) => {
29
- const globalOpts = actionCommand.optsWithGlobals();
30
- logger.setVerbose(Boolean(globalOpts.verbose));
31
- });
32
-
33
- registerInitCommand(program);
34
- registerStartCommand(program);
35
- registerReviewCommand(program);
36
- registerFixCommand(program);
37
- registerSubmitCommand(program);
38
- registerStatusCommand(program);
39
- registerAbortCommand(program);
40
- registerLogsCommand(program);
41
-
42
- program.parse(process.argv);