@fractary/faber-cli 1.1.0 → 1.3.0

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.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Plan command - FABER CLI planning command
3
+ *
4
+ * Batch workflow planning for GitHub issues
5
+ */
6
+ import { Command } from 'commander';
7
+ /**
8
+ * Create the plan command
9
+ */
10
+ export declare function createPlanCommand(): Command;
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/plan/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+CpC;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAkB3C"}
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Plan command - FABER CLI planning command
3
+ *
4
+ * Batch workflow planning for GitHub issues
5
+ */
6
+ import { Command } from 'commander';
7
+ import chalk from 'chalk';
8
+ import { AnthropicClient } from '../../lib/anthropic-client.js';
9
+ import { RepoClient } from '../../lib/repo-client.js';
10
+ import { ConfigManager } from '../../lib/config.js';
11
+ import { prompt } from '../../utils/prompt.js';
12
+ import { validateWorkIds, validateLabels, validateWorkflowName, validatePlanId, } from '../../utils/validation.js';
13
+ import fs from 'fs/promises';
14
+ import path from 'path';
15
+ /**
16
+ * Create the plan command
17
+ */
18
+ export function createPlanCommand() {
19
+ return new Command('plan')
20
+ .description('Plan workflows for GitHub issues')
21
+ .option('--work-id <ids>', 'Comma-separated list of work item IDs (e.g., "258,259,260")')
22
+ .option('--work-label <labels>', 'Comma-separated label filters (e.g., "workflow:etl,status:approved")')
23
+ .option('--workflow <name>', 'Override workflow (default: read from issue "workflow:*" label)')
24
+ .option('--no-worktree', 'Skip worktree creation')
25
+ .option('--no-branch', 'Skip branch creation')
26
+ .option('--skip-confirm', 'Skip confirmation prompt (use with caution)')
27
+ .option('--output <format>', 'Output format: text|json|yaml', 'text')
28
+ .option('--json', 'Output as JSON (shorthand for --output json)')
29
+ .action(async (options) => {
30
+ try {
31
+ await executePlanCommand(options);
32
+ }
33
+ catch (error) {
34
+ handlePlanError(error, options);
35
+ }
36
+ });
37
+ }
38
+ /**
39
+ * Main execution logic for plan command
40
+ */
41
+ async function executePlanCommand(options) {
42
+ const outputFormat = options.json ? 'json' : options.output || 'text';
43
+ // Validate arguments
44
+ if (!options.workId && !options.workLabel) {
45
+ throw new Error('Either --work-id or --work-label must be provided');
46
+ }
47
+ if (options.workId && options.workLabel) {
48
+ throw new Error('Cannot use both --work-id and --work-label at the same time');
49
+ }
50
+ // Initialize clients
51
+ const config = await ConfigManager.load();
52
+ const repoClient = new RepoClient(config);
53
+ const anthropicClient = new AnthropicClient(config);
54
+ if (outputFormat === 'text') {
55
+ console.log(chalk.blue('FABER CLI - Workflow Planning'));
56
+ console.log(chalk.gray('═'.repeat(50)));
57
+ }
58
+ // Step 1: Fetch issues from GitHub
59
+ if (outputFormat === 'text') {
60
+ console.log(chalk.cyan('\n→ Fetching issues from GitHub...'));
61
+ }
62
+ let issues;
63
+ try {
64
+ if (options.workId) {
65
+ // Validate work IDs before fetching
66
+ const ids = validateWorkIds(options.workId);
67
+ issues = await repoClient.fetchIssues(ids);
68
+ if (outputFormat === 'text') {
69
+ console.log(chalk.green(` ✓ Fetched ${issues.length} issue(s) by ID`));
70
+ }
71
+ }
72
+ else if (options.workLabel) {
73
+ // Validate labels before searching
74
+ const labels = validateLabels(options.workLabel);
75
+ issues = await repoClient.searchIssues(labels);
76
+ if (outputFormat === 'text') {
77
+ console.log(chalk.green(` ✓ Found ${issues.length} issue(s) matching labels`));
78
+ }
79
+ }
80
+ else {
81
+ throw new Error('No issues to process');
82
+ }
83
+ }
84
+ catch (error) {
85
+ if (error instanceof Error && error.message.includes('not yet implemented')) {
86
+ if (outputFormat === 'text') {
87
+ console.log(chalk.yellow('\n⚠️ fractary-repo commands not yet available'));
88
+ console.log(chalk.gray(' This command requires fractary-repo plugin implementation.'));
89
+ console.log(chalk.gray(' See SPEC-00030-FRACTARY-REPO-ENHANCEMENTS.md'));
90
+ }
91
+ else {
92
+ console.log(JSON.stringify({
93
+ status: 'error',
94
+ error: {
95
+ code: 'DEPENDENCY_NOT_AVAILABLE',
96
+ message: 'fractary-repo commands not yet implemented',
97
+ details: 'See SPEC-00030-FRACTARY-REPO-ENHANCEMENTS.md',
98
+ },
99
+ }, null, 2));
100
+ }
101
+ return;
102
+ }
103
+ throw error;
104
+ }
105
+ if (issues.length === 0) {
106
+ if (outputFormat === 'text') {
107
+ console.log(chalk.yellow('\n⚠️ No issues found'));
108
+ }
109
+ else {
110
+ console.log(JSON.stringify({ status: 'success', issues: [], message: 'No issues found' }, null, 2));
111
+ }
112
+ return;
113
+ }
114
+ // Step 2: Extract workflows from labels or prompt user
115
+ if (outputFormat === 'text') {
116
+ console.log(chalk.cyan('\n→ Identifying workflows...'));
117
+ }
118
+ const availableWorkflows = await loadAvailableWorkflows(config);
119
+ const issuesWithWorkflows = await assignWorkflows(issues, availableWorkflows, options, outputFormat);
120
+ // Step 3: Show confirmation prompt
121
+ if (!options.skipConfirm) {
122
+ const confirmed = await showConfirmationPrompt(issuesWithWorkflows, config, outputFormat);
123
+ if (!confirmed) {
124
+ if (outputFormat === 'text') {
125
+ console.log(chalk.yellow('\n✖ Planning cancelled'));
126
+ }
127
+ else {
128
+ console.log(JSON.stringify({ status: 'cancelled', message: 'User cancelled planning' }, null, 2));
129
+ }
130
+ return;
131
+ }
132
+ }
133
+ // Step 4: Plan each issue
134
+ if (outputFormat === 'text') {
135
+ console.log(chalk.cyan('\n→ Planning workflows...'));
136
+ }
137
+ const results = [];
138
+ for (const issue of issuesWithWorkflows) {
139
+ if (outputFormat === 'text') {
140
+ console.log(chalk.gray(`\n[${results.length + 1}/${issuesWithWorkflows.length}] Issue #${issue.number}: ${issue.title}`));
141
+ }
142
+ try {
143
+ const result = await planSingleIssue(issue, config, repoClient, anthropicClient, options, outputFormat);
144
+ results.push(result);
145
+ }
146
+ catch (error) {
147
+ const errorMessage = error instanceof Error ? error.message : String(error);
148
+ if (outputFormat === 'text') {
149
+ console.log(chalk.red(` ✗ Error: ${errorMessage}`));
150
+ }
151
+ results.push({
152
+ issue,
153
+ planId: '',
154
+ branch: '',
155
+ worktree: '',
156
+ error: errorMessage,
157
+ });
158
+ }
159
+ }
160
+ // Step 5: Output summary
161
+ if (outputFormat === 'json') {
162
+ console.log(JSON.stringify({
163
+ status: 'success',
164
+ total: results.length,
165
+ successful: results.filter(r => !r.error).length,
166
+ failed: results.filter(r => r.error).length,
167
+ results,
168
+ }, null, 2));
169
+ }
170
+ else {
171
+ outputTextSummary(results);
172
+ }
173
+ }
174
+ /**
175
+ * Load available workflow configurations
176
+ */
177
+ async function loadAvailableWorkflows(config) {
178
+ const workflowDir = config.workflow?.config_path || './plugins/faber/config/workflows';
179
+ try {
180
+ const files = await fs.readdir(workflowDir);
181
+ return files
182
+ .filter(f => f.endsWith('.json'))
183
+ .map(f => path.basename(f, '.json'));
184
+ }
185
+ catch (error) {
186
+ // Default workflows if directory doesn't exist
187
+ return ['core', 'etl', 'bugfix', 'feature'];
188
+ }
189
+ }
190
+ /**
191
+ * Assign workflows to issues (extract from labels or prompt user)
192
+ */
193
+ async function assignWorkflows(issues, availableWorkflows, options, outputFormat) {
194
+ const issuesWithWorkflows = [];
195
+ for (const issue of issues) {
196
+ let workflow = options.workflow; // Command-line override
197
+ // Validate workflow override if provided
198
+ if (workflow) {
199
+ validateWorkflowName(workflow);
200
+ }
201
+ if (!workflow) {
202
+ // Extract from issue labels
203
+ const workflowLabel = issue.labels.find(label => label.startsWith('workflow:'));
204
+ if (workflowLabel) {
205
+ workflow = workflowLabel.replace('workflow:', '');
206
+ // Validate extracted workflow name
207
+ validateWorkflowName(workflow);
208
+ }
209
+ }
210
+ if (!workflow) {
211
+ // Prompt user
212
+ if (outputFormat === 'text') {
213
+ console.log(chalk.yellow(`\n⚠️ Issue #${issue.number} is missing a workflow label:`));
214
+ console.log(chalk.gray(` ${issue.title}`));
215
+ console.log(chalk.gray(` Available workflows: ${availableWorkflows.join(', ')}`));
216
+ workflow = await prompt(` Select workflow for this issue [${availableWorkflows[0]}]: `);
217
+ if (!workflow) {
218
+ workflow = availableWorkflows[0];
219
+ }
220
+ if (!availableWorkflows.includes(workflow)) {
221
+ throw new Error(`Invalid workflow: ${workflow}. Available: ${availableWorkflows.join(', ')}`);
222
+ }
223
+ }
224
+ else {
225
+ throw new Error(`Issue #${issue.number} is missing workflow label and interactive prompts are disabled in JSON mode`);
226
+ }
227
+ }
228
+ issuesWithWorkflows.push({ ...issue, workflow });
229
+ }
230
+ if (outputFormat === 'text') {
231
+ console.log(chalk.green(` ✓ All issues have workflows assigned`));
232
+ }
233
+ return issuesWithWorkflows;
234
+ }
235
+ /**
236
+ * Show confirmation prompt before planning
237
+ */
238
+ async function showConfirmationPrompt(issues, config, outputFormat) {
239
+ if (outputFormat !== 'text') {
240
+ return true; // Skip in JSON mode
241
+ }
242
+ console.log(chalk.cyan('\n📋 Will plan workflows for the following issues:\n'));
243
+ for (const issue of issues) {
244
+ const { organization, project } = getRepoInfoFromConfig(config);
245
+ const branch = `feature/${issue.number}`;
246
+ const worktree = `~/.claude-worktrees/${organization}-${project}-${issue.number}`;
247
+ console.log(chalk.bold(`#${issue.number}: ${issue.title}`));
248
+ console.log(chalk.gray(` Workflow: ${issue.workflow}`));
249
+ console.log(chalk.gray(` Branch: ${branch}`));
250
+ console.log(chalk.gray(` Worktree: ${worktree}`));
251
+ console.log();
252
+ }
253
+ const response = await prompt('Proceed? [Y/n]: ');
254
+ return !response || response.toLowerCase() === 'y' || response.toLowerCase() === 'yes';
255
+ }
256
+ /**
257
+ * Plan a single issue
258
+ */
259
+ async function planSingleIssue(issue, config, repoClient, anthropicClient, options, outputFormat) {
260
+ const { organization, project } = getRepoInfoFromConfig(config);
261
+ const branch = `feature/${issue.number}`;
262
+ const worktree = `~/.claude-worktrees/${organization}-${project}-${issue.number}`;
263
+ // Generate plan via Anthropic API
264
+ if (outputFormat === 'text') {
265
+ console.log(chalk.gray(' → Generating plan...'));
266
+ }
267
+ const plan = await anthropicClient.generatePlan({
268
+ workflow: issue.workflow,
269
+ issueTitle: issue.title,
270
+ issueDescription: issue.description,
271
+ issueNumber: issue.number,
272
+ });
273
+ const planId = plan.plan_id;
274
+ // Create branch
275
+ if (!options.noBranch) {
276
+ if (outputFormat === 'text') {
277
+ console.log(chalk.gray(` → Creating branch: ${branch}...`));
278
+ }
279
+ await repoClient.createBranch(branch);
280
+ }
281
+ // Create worktree
282
+ let worktreePath = worktree;
283
+ if (!options.noWorktree) {
284
+ if (outputFormat === 'text') {
285
+ console.log(chalk.gray(` → Creating worktree: ${worktree}...`));
286
+ }
287
+ const worktreeResult = await repoClient.createWorktree({
288
+ workId: issue.number.toString(),
289
+ path: worktree,
290
+ });
291
+ worktreePath = worktreeResult.absolute_path;
292
+ }
293
+ // Write plan to worktree
294
+ if (!options.noWorktree) {
295
+ // Validate plan ID format (prevent path traversal via malicious plan IDs)
296
+ validatePlanId(planId);
297
+ const planDir = path.join(worktreePath, '.fractary', 'plans');
298
+ await fs.mkdir(planDir, { recursive: true });
299
+ // Construct and validate path
300
+ const planPath = path.join(planDir, `${planId}.json`);
301
+ // Note: path.join automatically normalizes and prevents basic traversal,
302
+ // but we validate the plan ID format as an additional layer of defense
303
+ await fs.writeFile(planPath, JSON.stringify(plan, null, 2));
304
+ if (outputFormat === 'text') {
305
+ console.log(chalk.gray(` → Plan written to ${planPath}`));
306
+ }
307
+ }
308
+ // Update GitHub issue with plan_id
309
+ if (outputFormat === 'text') {
310
+ console.log(chalk.gray(` → Updating GitHub issue...`));
311
+ }
312
+ await repoClient.updateIssue({
313
+ id: issue.number.toString(),
314
+ comment: `🤖 Workflow plan created: ${planId}`,
315
+ addLabel: 'faber:planned',
316
+ });
317
+ if (outputFormat === 'text') {
318
+ console.log(chalk.green(` ✓ Plan: ${planId}`));
319
+ }
320
+ return {
321
+ issue,
322
+ planId,
323
+ branch,
324
+ worktree: worktreePath,
325
+ };
326
+ }
327
+ /**
328
+ * Get repository info from config
329
+ */
330
+ function getRepoInfoFromConfig(config) {
331
+ return {
332
+ organization: config.github?.organization || 'unknown',
333
+ project: config.github?.project || 'unknown',
334
+ };
335
+ }
336
+ /**
337
+ * Output text summary
338
+ */
339
+ function outputTextSummary(results) {
340
+ console.log(chalk.cyan('\n' + '═'.repeat(50)));
341
+ const successful = results.filter(r => !r.error);
342
+ const failed = results.filter(r => r.error);
343
+ if (successful.length > 0) {
344
+ console.log(chalk.green(`\n✓ Planned ${successful.length} workflow(s) successfully:\n`));
345
+ successful.forEach((result, index) => {
346
+ console.log(chalk.bold(`[${index + 1}/${successful.length}] Issue #${result.issue.number}: ${result.issue.title}`));
347
+ console.log(chalk.gray(` Workflow: ${result.issue.workflow}`));
348
+ console.log(chalk.gray(` Plan: ${result.planId}`));
349
+ console.log(chalk.gray(` Branch: ${result.branch}`));
350
+ console.log(chalk.gray(` Worktree: ${result.worktree}`));
351
+ console.log();
352
+ console.log(chalk.cyan(' To execute:'));
353
+ console.log(chalk.gray(` cd ${result.worktree} && claude`));
354
+ console.log(chalk.gray(` /fractary-faber:workflow-run ${result.issue.number}`));
355
+ console.log();
356
+ });
357
+ }
358
+ if (failed.length > 0) {
359
+ console.log(chalk.red(`\n✗ Failed to plan ${failed.length} workflow(s):\n`));
360
+ failed.forEach((result, index) => {
361
+ console.log(chalk.bold(`[${index + 1}/${failed.length}] Issue #${result.issue.number}: ${result.issue.title}`));
362
+ console.log(chalk.red(` Error: ${result.error}`));
363
+ console.log();
364
+ });
365
+ }
366
+ }
367
+ /**
368
+ * Error handling
369
+ */
370
+ function handlePlanError(error, options) {
371
+ const message = error instanceof Error ? error.message : String(error);
372
+ const outputFormat = options.json ? 'json' : options.output || 'text';
373
+ if (outputFormat === 'json') {
374
+ console.error(JSON.stringify({
375
+ status: 'error',
376
+ error: { code: 'PLAN_ERROR', message },
377
+ }));
378
+ }
379
+ else {
380
+ console.error(chalk.red('Error:'), message);
381
+ }
382
+ process.exit(1);
383
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC;;GAEG;AACH,wBAAgB,cAAc,IAAI,OAAO,CA0IxC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC;;GAEG;AACH,wBAAgB,cAAc,IAAI,OAAO,CA2IxC"}
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { createRepoCommand } from './commands/repo/index.js';
13
13
  import { createSpecCommand } from './commands/spec/index.js';
14
14
  import { createLogsCommand } from './commands/logs/index.js';
15
15
  import { createInitCommand } from './commands/init.js';
16
+ import { createPlanCommand } from './commands/plan/index.js';
16
17
  const version = '1.0.0';
17
18
  /**
18
19
  * Create and configure the main CLI program
@@ -27,6 +28,7 @@ export function createFaberCLI() {
27
28
  program.option('--debug', 'Enable debug output');
28
29
  // Workflow commands (top-level) - NEW NAMES
29
30
  program.addCommand(createInitCommand()); // workflow-init
31
+ program.addCommand(createPlanCommand()); // plan - NEW
30
32
  program.addCommand(createRunCommand()); // workflow-run
31
33
  program.addCommand(createStatusCommand()); // workflow-status
32
34
  program.addCommand(createResumeCommand()); // workflow-resume
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Anthropic API Client
3
+ *
4
+ * Generates workflow plans via Claude API
5
+ */
6
+ import type { FaberConfig } from '../types/config.js';
7
+ interface GeneratePlanInput {
8
+ workflow: string;
9
+ issueTitle: string;
10
+ issueDescription: string;
11
+ issueNumber: number;
12
+ }
13
+ interface WorkflowPlan {
14
+ plan_id: string;
15
+ created_by: string;
16
+ cli_version: string;
17
+ created_at: string;
18
+ issue: {
19
+ source: string;
20
+ id: string;
21
+ url: string;
22
+ };
23
+ branch: string;
24
+ worktree: string;
25
+ workflow: string;
26
+ phases: any[];
27
+ [key: string]: any;
28
+ }
29
+ /**
30
+ * Anthropic API Client
31
+ */
32
+ export declare class AnthropicClient {
33
+ private client;
34
+ private config;
35
+ private git;
36
+ private ajv;
37
+ private planSchema;
38
+ constructor(config: FaberConfig);
39
+ /**
40
+ * Load plan JSON schema for validation
41
+ */
42
+ private loadPlanSchema;
43
+ /**
44
+ * Validate plan JSON against schema
45
+ */
46
+ private validatePlan;
47
+ /**
48
+ * Generate workflow plan via Claude API
49
+ */
50
+ generatePlan(input: GeneratePlanInput): Promise<WorkflowPlan>;
51
+ /**
52
+ * Load workflow configuration
53
+ */
54
+ private loadWorkflowConfig;
55
+ /**
56
+ * Construct planning prompt for Claude
57
+ */
58
+ private constructPlanningPrompt;
59
+ /**
60
+ * Extract JSON from Claude response
61
+ */
62
+ private extractJsonFromResponse;
63
+ /**
64
+ * Extract repository organization and project name using SDK Git class
65
+ */
66
+ private extractRepoInfo;
67
+ }
68
+ export {};
69
+ //# sourceMappingURL=anthropic-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic-client.d.ts","sourceRoot":"","sources":["../../src/lib/anthropic-client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAKtD,UAAU,iBAAiB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,YAAY;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE;QACL,MAAM,EAAE,MAAM,CAAC;QACf,EAAE,EAAE,MAAM,CAAC;QACX,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,GAAG,EAAE,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,UAAU,CAAM;gBAEZ,MAAM,EAAE,WAAW;IAe/B;;OAEG;YACW,cAAc;IAiB5B;;OAEG;IACH,OAAO,CAAC,YAAY;IAepB;;OAEG;IACG,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC;IA6DnE;;OAEG;YACW,kBAAkB;IAchC;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAqD/B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAgB/B;;OAEG;YACW,eAAe;CAqB9B"}