wiggum-cli 0.5.4 → 0.7.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.
- package/README.md +88 -22
- package/dist/ai/conversation/conversation-manager.d.ts +84 -0
- package/dist/ai/conversation/conversation-manager.d.ts.map +1 -0
- package/dist/ai/conversation/conversation-manager.js +159 -0
- package/dist/ai/conversation/conversation-manager.js.map +1 -0
- package/dist/ai/conversation/index.d.ts +8 -0
- package/dist/ai/conversation/index.d.ts.map +1 -0
- package/dist/ai/conversation/index.js +8 -0
- package/dist/ai/conversation/index.js.map +1 -0
- package/dist/ai/conversation/spec-generator.d.ts +62 -0
- package/dist/ai/conversation/spec-generator.d.ts.map +1 -0
- package/dist/ai/conversation/spec-generator.js +267 -0
- package/dist/ai/conversation/spec-generator.js.map +1 -0
- package/dist/ai/conversation/url-fetcher.d.ts +26 -0
- package/dist/ai/conversation/url-fetcher.d.ts.map +1 -0
- package/dist/ai/conversation/url-fetcher.js +145 -0
- package/dist/ai/conversation/url-fetcher.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +44 -34
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts +19 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +61 -21
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/new.d.ts +11 -1
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +102 -43
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/run.js +3 -3
- package/dist/commands/run.js.map +1 -1
- package/dist/generator/config.d.ts +3 -3
- package/dist/generator/config.d.ts.map +1 -1
- package/dist/generator/config.js +5 -3
- package/dist/generator/config.js.map +1 -1
- package/dist/generator/index.js +1 -1
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/writer.js +1 -1
- package/dist/generator/writer.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -1
- package/dist/repl/command-parser.d.ts +84 -0
- package/dist/repl/command-parser.d.ts.map +1 -0
- package/dist/repl/command-parser.js +112 -0
- package/dist/repl/command-parser.js.map +1 -0
- package/dist/repl/index.d.ts +8 -0
- package/dist/repl/index.d.ts.map +1 -0
- package/dist/repl/index.js +8 -0
- package/dist/repl/index.js.map +1 -0
- package/dist/repl/repl-loop.d.ts +30 -0
- package/dist/repl/repl-loop.d.ts.map +1 -0
- package/dist/repl/repl-loop.js +262 -0
- package/dist/repl/repl-loop.js.map +1 -0
- package/dist/repl/session-state.d.ts +37 -0
- package/dist/repl/session-state.d.ts.map +1 -0
- package/dist/repl/session-state.js +26 -0
- package/dist/repl/session-state.js.map +1 -0
- package/dist/templates/root/README.md.tmpl +1 -1
- package/dist/templates/scripts/feature-loop.sh.tmpl +17 -17
- package/dist/templates/scripts/loop.sh.tmpl +7 -7
- package/dist/templates/scripts/ralph-monitor.sh.tmpl +5 -5
- package/dist/utils/config.d.ts +7 -7
- package/dist/utils/config.js +4 -4
- package/dist/utils/config.js.map +1 -1
- package/package.json +1 -1
- package/src/ai/conversation/conversation-manager.ts +230 -0
- package/src/ai/conversation/index.ts +23 -0
- package/src/ai/conversation/spec-generator.ts +327 -0
- package/src/ai/conversation/url-fetcher.ts +180 -0
- package/src/cli.ts +47 -34
- package/src/commands/init.ts +86 -22
- package/src/commands/new.ts +121 -44
- package/src/commands/run.ts +3 -3
- package/src/generator/config.ts +5 -3
- package/src/generator/index.ts +1 -1
- package/src/generator/writer.ts +1 -1
- package/src/index.ts +46 -0
- package/src/repl/command-parser.ts +154 -0
- package/src/repl/index.ts +23 -0
- package/src/repl/repl-loop.ts +339 -0
- package/src/repl/session-state.ts +63 -0
- package/src/templates/config/ralph.config.cjs.tmpl +38 -0
- package/src/templates/root/README.md.tmpl +1 -1
- package/src/templates/scripts/feature-loop.sh.tmpl +17 -17
- package/src/templates/scripts/loop.sh.tmpl +7 -7
- package/src/templates/scripts/ralph-monitor.sh.tmpl +5 -5
- package/src/utils/config.ts +9 -9
- /package/{src/templates/config/ralph.config.js.tmpl → dist/templates/config/ralph.config.cjs.tmpl} +0 -0
package/src/commands/new.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* New Command
|
|
3
|
-
* Create a new feature specification from template
|
|
3
|
+
* Create a new feature specification from template or AI interview
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
@@ -9,6 +9,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { logger } from '../utils/logger.js';
|
|
11
11
|
import { loadConfigWithDefaults, hasConfig } from '../utils/config.js';
|
|
12
|
+
import { getAvailableProvider, type AIProvider, AVAILABLE_MODELS } from '../ai/providers.js';
|
|
13
|
+
import { SpecGenerator } from '../ai/conversation/index.js';
|
|
14
|
+
import { Scanner, type ScanResult } from '../scanner/index.js';
|
|
12
15
|
import pc from 'picocolors';
|
|
13
16
|
import * as prompts from '@clack/prompts';
|
|
14
17
|
|
|
@@ -21,6 +24,14 @@ export interface NewOptions {
|
|
|
21
24
|
yes?: boolean;
|
|
22
25
|
/** Force overwrite if file exists */
|
|
23
26
|
force?: boolean;
|
|
27
|
+
/** Use AI interview to generate spec */
|
|
28
|
+
ai?: boolean;
|
|
29
|
+
/** AI provider (anthropic, openai, openrouter) */
|
|
30
|
+
provider?: AIProvider;
|
|
31
|
+
/** Model to use for AI generation */
|
|
32
|
+
model?: string;
|
|
33
|
+
/** Pre-loaded scan result (from REPL session) */
|
|
34
|
+
scanResult?: ScanResult;
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
/**
|
|
@@ -97,6 +108,15 @@ Describe what this feature does and why it's needed.
|
|
|
97
108
|
- [ ] Question 2 - Clarification required
|
|
98
109
|
`;
|
|
99
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Get default model for a provider
|
|
113
|
+
*/
|
|
114
|
+
function getDefaultModelForProvider(provider: AIProvider): string {
|
|
115
|
+
const models = AVAILABLE_MODELS[provider];
|
|
116
|
+
const recommended = models.find(m => m.hint?.includes('recommended'));
|
|
117
|
+
return recommended?.value || models[0].value;
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
/**
|
|
101
121
|
* Find the _example.md template
|
|
102
122
|
*/
|
|
@@ -178,7 +198,7 @@ export async function newCommand(feature: string, options: NewOptions = {}): Pro
|
|
|
178
198
|
// Sanitize feature name (allow alphanumeric, hyphens, underscores)
|
|
179
199
|
if (!/^[a-zA-Z0-9_-]+$/.test(feature)) {
|
|
180
200
|
logger.error('Feature name must contain only letters, numbers, hyphens, and underscores');
|
|
181
|
-
logger.info('Example:
|
|
201
|
+
logger.info('Example: wiggum new my-feature or wiggum new user_auth');
|
|
182
202
|
process.exit(1);
|
|
183
203
|
}
|
|
184
204
|
|
|
@@ -194,7 +214,7 @@ export async function newCommand(feature: string, options: NewOptions = {}): Pro
|
|
|
194
214
|
|
|
195
215
|
// Check for config
|
|
196
216
|
if (!hasConfig(projectRoot)) {
|
|
197
|
-
logger.warn('No ralph.config.
|
|
217
|
+
logger.warn('No ralph.config.cjs found. Run "wiggum init" first to configure your project.');
|
|
198
218
|
logger.info('Using default paths...');
|
|
199
219
|
console.log('');
|
|
200
220
|
}
|
|
@@ -234,55 +254,112 @@ export async function newCommand(feature: string, options: NewOptions = {}): Pro
|
|
|
234
254
|
logger.warn('Overwriting existing spec file');
|
|
235
255
|
}
|
|
236
256
|
|
|
237
|
-
//
|
|
238
|
-
|
|
257
|
+
// Determine if we should use AI generation
|
|
258
|
+
const provider = options.provider || getAvailableProvider();
|
|
259
|
+
const useAi = options.ai && provider !== null;
|
|
239
260
|
|
|
240
|
-
|
|
241
|
-
const exampleTemplate = await findExampleTemplate(projectRoot);
|
|
242
|
-
if (exampleTemplate) {
|
|
243
|
-
logger.info(`Using template: ${exampleTemplate}`);
|
|
244
|
-
templateContent = readFileSync(exampleTemplate, 'utf-8');
|
|
245
|
-
} else {
|
|
246
|
-
// Try package template
|
|
247
|
-
const packageTemplateDir = getPackageTemplateDir();
|
|
248
|
-
const packageTemplate = join(packageTemplateDir, '_example.md.tmpl');
|
|
249
|
-
if (existsSync(packageTemplate)) {
|
|
250
|
-
logger.info(`Using package template`);
|
|
251
|
-
templateContent = readFileSync(packageTemplate, 'utf-8');
|
|
252
|
-
} else {
|
|
253
|
-
// Use default template
|
|
254
|
-
logger.info('Using default template');
|
|
255
|
-
templateContent = DEFAULT_SPEC_TEMPLATE;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
261
|
+
let specContent: string;
|
|
258
262
|
|
|
259
|
-
|
|
260
|
-
|
|
263
|
+
if (useAi && provider) {
|
|
264
|
+
// Use AI-powered spec generation
|
|
265
|
+
const model = options.model || getDefaultModelForProvider(provider);
|
|
266
|
+
logger.info(`Using AI spec generation (${provider}/${model})`);
|
|
261
267
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
console.log('');
|
|
268
|
-
|
|
269
|
-
// Show first few lines of the processed template
|
|
270
|
-
const previewLines = specContent.split('\n').slice(0, 15);
|
|
271
|
-
console.log(pc.dim(previewLines.join('\n')));
|
|
272
|
-
if (specContent.split('\n').length > 15) {
|
|
273
|
-
console.log(pc.dim('...'));
|
|
268
|
+
// Get or perform scan
|
|
269
|
+
let scanResult = options.scanResult;
|
|
270
|
+
if (!scanResult) {
|
|
271
|
+
const scanner = new Scanner();
|
|
272
|
+
scanResult = await scanner.scan(projectRoot);
|
|
274
273
|
}
|
|
275
|
-
console.log('');
|
|
276
274
|
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
275
|
+
const specGenerator = new SpecGenerator({
|
|
276
|
+
featureName: feature,
|
|
277
|
+
projectRoot,
|
|
278
|
+
provider,
|
|
279
|
+
model,
|
|
280
|
+
scanResult,
|
|
280
281
|
});
|
|
281
282
|
|
|
282
|
-
|
|
283
|
-
|
|
283
|
+
const generatedSpec = await specGenerator.run();
|
|
284
|
+
|
|
285
|
+
if (!generatedSpec) {
|
|
286
|
+
logger.info('Spec generation cancelled');
|
|
284
287
|
return;
|
|
285
288
|
}
|
|
289
|
+
|
|
290
|
+
specContent = generatedSpec;
|
|
291
|
+
|
|
292
|
+
// Confirm saving (unless --yes)
|
|
293
|
+
if (!options.yes) {
|
|
294
|
+
console.log('');
|
|
295
|
+
const shouldSave = await prompts.confirm({
|
|
296
|
+
message: `Save spec to ${specPath}?`,
|
|
297
|
+
initialValue: true,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (prompts.isCancel(shouldSave) || !shouldSave) {
|
|
301
|
+
logger.info('Spec not saved');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
// Use template-based generation
|
|
307
|
+
if (options.ai && !provider) {
|
|
308
|
+
logger.warn('No API key found. Falling back to template mode.');
|
|
309
|
+
logger.info('Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or OPENROUTER_API_KEY for AI generation.');
|
|
310
|
+
console.log('');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Find or use default template
|
|
314
|
+
let templateContent: string;
|
|
315
|
+
|
|
316
|
+
// Try to find _example.md template
|
|
317
|
+
const exampleTemplate = await findExampleTemplate(projectRoot);
|
|
318
|
+
if (exampleTemplate) {
|
|
319
|
+
logger.info(`Using template: ${exampleTemplate}`);
|
|
320
|
+
templateContent = readFileSync(exampleTemplate, 'utf-8');
|
|
321
|
+
} else {
|
|
322
|
+
// Try package template
|
|
323
|
+
const packageTemplateDir = getPackageTemplateDir();
|
|
324
|
+
const packageTemplate = join(packageTemplateDir, '_example.md.tmpl');
|
|
325
|
+
if (existsSync(packageTemplate)) {
|
|
326
|
+
logger.info(`Using package template`);
|
|
327
|
+
templateContent = readFileSync(packageTemplate, 'utf-8');
|
|
328
|
+
} else {
|
|
329
|
+
// Use default template
|
|
330
|
+
logger.info('Using default template');
|
|
331
|
+
templateContent = DEFAULT_SPEC_TEMPLATE;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Process template
|
|
336
|
+
specContent = processTemplate(templateContent, feature);
|
|
337
|
+
|
|
338
|
+
// Confirm with user (unless --yes)
|
|
339
|
+
if (!options.yes) {
|
|
340
|
+
console.log('');
|
|
341
|
+
console.log(pc.cyan('--- Spec Preview ---'));
|
|
342
|
+
console.log(`File: ${specPath}`);
|
|
343
|
+
console.log('');
|
|
344
|
+
|
|
345
|
+
// Show first few lines of the processed template
|
|
346
|
+
const previewLines = specContent.split('\n').slice(0, 15);
|
|
347
|
+
console.log(pc.dim(previewLines.join('\n')));
|
|
348
|
+
if (specContent.split('\n').length > 15) {
|
|
349
|
+
console.log(pc.dim('...'));
|
|
350
|
+
}
|
|
351
|
+
console.log('');
|
|
352
|
+
|
|
353
|
+
const shouldCreate = await prompts.confirm({
|
|
354
|
+
message: 'Create this spec file?',
|
|
355
|
+
initialValue: true,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if (prompts.isCancel(shouldCreate) || !shouldCreate) {
|
|
359
|
+
logger.info('Cancelled');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
286
363
|
}
|
|
287
364
|
|
|
288
365
|
// Write the spec file
|
|
@@ -308,5 +385,5 @@ export async function newCommand(feature: string, options: NewOptions = {}): Pro
|
|
|
308
385
|
console.log('');
|
|
309
386
|
console.log('Next steps:');
|
|
310
387
|
console.log(` 1. Edit the spec: ${pc.cyan(`$EDITOR ${specPath}`)}`);
|
|
311
|
-
console.log(` 2. When ready, run: ${pc.cyan(`
|
|
388
|
+
console.log(` 2. When ready, run: ${pc.cyan(`wiggum run ${feature}`)}`);
|
|
312
389
|
}
|
package/src/commands/run.ts
CHANGED
|
@@ -94,7 +94,7 @@ export async function runCommand(feature: string, options: RunOptions = {}): Pro
|
|
|
94
94
|
|
|
95
95
|
// Check for config
|
|
96
96
|
if (!hasConfig(projectRoot)) {
|
|
97
|
-
logger.warn('No ralph.config.
|
|
97
|
+
logger.warn('No ralph.config.cjs found. Run "wiggum init" first to configure your project.');
|
|
98
98
|
logger.info('Attempting to run with default settings...');
|
|
99
99
|
console.log('');
|
|
100
100
|
}
|
|
@@ -106,7 +106,7 @@ export async function runCommand(feature: string, options: RunOptions = {}): Pro
|
|
|
106
106
|
const specFile = await validateSpecFile(projectRoot, feature);
|
|
107
107
|
if (!specFile) {
|
|
108
108
|
logger.error(`Spec file not found: ${feature}.md`);
|
|
109
|
-
logger.info(`Create the spec first:
|
|
109
|
+
logger.info(`Create the spec first: wiggum new ${feature}`);
|
|
110
110
|
logger.info(`Expected location: ${join(projectRoot, config.paths.specs, `${feature}.md`)}`);
|
|
111
111
|
process.exit(1);
|
|
112
112
|
}
|
|
@@ -118,7 +118,7 @@ export async function runCommand(feature: string, options: RunOptions = {}): Pro
|
|
|
118
118
|
if (!scriptPath) {
|
|
119
119
|
logger.error('feature-loop.sh script not found');
|
|
120
120
|
logger.info('The script should be in .ralph/scripts/ or the ralph/ directory');
|
|
121
|
-
logger.info('Run "
|
|
121
|
+
logger.info('Run "wiggum init" to generate the necessary scripts');
|
|
122
122
|
process.exit(1);
|
|
123
123
|
}
|
|
124
124
|
|
package/src/generator/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Config Generator
|
|
3
|
-
* Generates ralph.config.
|
|
3
|
+
* Generates ralph.config.cjs file from scan results
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ScanResult } from '../scanner/types.js';
|
|
@@ -95,9 +95,11 @@ export function generateConfig(scanResult: ScanResult, customVars: Record<string
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
* Generate ralph.config.
|
|
98
|
+
* Generate ralph.config.cjs file content as CommonJS module
|
|
99
99
|
*/
|
|
100
100
|
export function generateConfigFile(config: RalphConfig): string {
|
|
101
|
+
// Use CommonJS module.exports for compatibility with both CJS and ESM projects
|
|
102
|
+
// ESM projects can import CJS modules, but CJS projects can't use 'export default'
|
|
101
103
|
const content = `module.exports = ${JSON.stringify(config, null, 2)};
|
|
102
104
|
`;
|
|
103
105
|
|
|
@@ -108,7 +110,7 @@ export function generateConfigFile(config: RalphConfig): string {
|
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
/**
|
|
111
|
-
* Generate ralph.config.
|
|
113
|
+
* Generate ralph.config.cjs from scan result
|
|
112
114
|
*/
|
|
113
115
|
export function generateConfigFileFromScan(scanResult: ScanResult, customVars: Record<string, string> = {}): string {
|
|
114
116
|
const config = generateConfig(scanResult, customVars);
|
package/src/generator/index.ts
CHANGED
|
@@ -128,7 +128,7 @@ export class Generator {
|
|
|
128
128
|
if (this.options.generateConfig) {
|
|
129
129
|
config = generateConfig(scanResult, this.options.customVariables || {});
|
|
130
130
|
const configContent = generateConfigFile(config);
|
|
131
|
-
processedTemplates.set('config/ralph.config.
|
|
131
|
+
processedTemplates.set('config/ralph.config.cjs', configContent);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
// Map template outputs to final paths
|
package/src/generator/writer.ts
CHANGED
|
@@ -242,7 +242,7 @@ export function mapTemplateOutputPaths(templateOutputs: Map<string, string>): Ma
|
|
|
242
242
|
// Scripts go to .ralph/scripts/
|
|
243
243
|
finalPath = `.ralph/${outputPath}`;
|
|
244
244
|
} else if (outputPath.startsWith('config/')) {
|
|
245
|
-
// ralph.config.
|
|
245
|
+
// ralph.config.cjs goes to project root
|
|
246
246
|
finalPath = outputPath.replace('config/', '');
|
|
247
247
|
} else if (outputPath.startsWith('root/')) {
|
|
248
248
|
// Root files go to .ralph/
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,55 @@
|
|
|
1
1
|
import { createCli } from './cli.js';
|
|
2
|
+
import { startRepl, createSessionState } from './repl/index.js';
|
|
3
|
+
import { hasConfig, loadConfigWithDefaults } from './utils/config.js';
|
|
4
|
+
import { getAvailableProvider } from './ai/providers.js';
|
|
5
|
+
import { displayHeader } from './utils/header.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Start REPL-first mode
|
|
9
|
+
* Called when wiggum is invoked with no arguments
|
|
10
|
+
*/
|
|
11
|
+
async function startReplFirst(): Promise<void> {
|
|
12
|
+
const projectRoot = process.cwd();
|
|
13
|
+
const provider = getAvailableProvider();
|
|
14
|
+
|
|
15
|
+
// Display header
|
|
16
|
+
displayHeader();
|
|
17
|
+
|
|
18
|
+
// Check if already initialized
|
|
19
|
+
const isInitialized = hasConfig(projectRoot);
|
|
20
|
+
let config = null;
|
|
21
|
+
|
|
22
|
+
if (isInitialized) {
|
|
23
|
+
config = await loadConfigWithDefaults(projectRoot);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Create initial state (may not have config yet)
|
|
27
|
+
const initialState = createSessionState(
|
|
28
|
+
projectRoot,
|
|
29
|
+
provider, // May be null if no API key
|
|
30
|
+
'sonnet', // Default model, will be updated after /init
|
|
31
|
+
undefined, // No scan result yet
|
|
32
|
+
config,
|
|
33
|
+
isInitialized
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
await startRepl(initialState);
|
|
37
|
+
}
|
|
2
38
|
|
|
3
39
|
/**
|
|
4
40
|
* Main entry point for the Ralph CLI
|
|
41
|
+
* REPL-first: no args = start REPL, otherwise use CLI
|
|
5
42
|
*/
|
|
6
43
|
export async function main(): Promise<void> {
|
|
44
|
+
const args = process.argv.slice(2);
|
|
45
|
+
|
|
46
|
+
// REPL-first: no args = start REPL
|
|
47
|
+
if (args.length === 0) {
|
|
48
|
+
await startReplFirst();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Legacy CLI mode for backward compatibility
|
|
7
53
|
const program = createCli();
|
|
8
54
|
await program.parseAsync(process.argv);
|
|
9
55
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Parser
|
|
3
|
+
* Parses slash commands and natural language input for the REPL
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parsed slash command
|
|
8
|
+
*/
|
|
9
|
+
export interface SlashCommand {
|
|
10
|
+
/** Command name (without slash) */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Command arguments */
|
|
13
|
+
args: string[];
|
|
14
|
+
/** Raw input string */
|
|
15
|
+
raw: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Input types
|
|
20
|
+
*/
|
|
21
|
+
export type InputType = 'slash-command' | 'natural-language' | 'empty';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parsed input result
|
|
25
|
+
*/
|
|
26
|
+
export interface ParsedInput {
|
|
27
|
+
type: InputType;
|
|
28
|
+
command?: SlashCommand;
|
|
29
|
+
text?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Available REPL commands
|
|
34
|
+
*/
|
|
35
|
+
export const REPL_COMMANDS = {
|
|
36
|
+
init: {
|
|
37
|
+
description: 'Initialize Wiggum in this project',
|
|
38
|
+
usage: '/init',
|
|
39
|
+
aliases: ['i'],
|
|
40
|
+
},
|
|
41
|
+
new: {
|
|
42
|
+
description: 'Create a new feature specification',
|
|
43
|
+
usage: '/new <feature-name>',
|
|
44
|
+
aliases: ['n'],
|
|
45
|
+
},
|
|
46
|
+
run: {
|
|
47
|
+
description: 'Run the feature development loop',
|
|
48
|
+
usage: '/run <feature-name>',
|
|
49
|
+
aliases: ['r'],
|
|
50
|
+
},
|
|
51
|
+
monitor: {
|
|
52
|
+
description: 'Monitor a running feature loop',
|
|
53
|
+
usage: '/monitor <feature-name>',
|
|
54
|
+
aliases: ['m'],
|
|
55
|
+
},
|
|
56
|
+
help: {
|
|
57
|
+
description: 'Show available commands',
|
|
58
|
+
usage: '/help',
|
|
59
|
+
aliases: ['h', '?'],
|
|
60
|
+
},
|
|
61
|
+
exit: {
|
|
62
|
+
description: 'Exit the REPL',
|
|
63
|
+
usage: '/exit',
|
|
64
|
+
aliases: ['quit', 'q'],
|
|
65
|
+
},
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
export type ReplCommandName = keyof typeof REPL_COMMANDS;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if input is a slash command
|
|
72
|
+
*/
|
|
73
|
+
export function isSlashCommand(input: string): boolean {
|
|
74
|
+
return input.trim().startsWith('/');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse a slash command from input
|
|
79
|
+
*/
|
|
80
|
+
export function parseSlashCommand(input: string): SlashCommand {
|
|
81
|
+
const trimmed = input.trim();
|
|
82
|
+
const parts = trimmed.slice(1).split(/\s+/);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
name: parts[0]?.toLowerCase() || '',
|
|
86
|
+
args: parts.slice(1),
|
|
87
|
+
raw: input,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve command aliases to canonical command names
|
|
93
|
+
*/
|
|
94
|
+
export function resolveCommandAlias(name: string): ReplCommandName | null {
|
|
95
|
+
// Check direct match
|
|
96
|
+
if (name in REPL_COMMANDS) {
|
|
97
|
+
return name as ReplCommandName;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check aliases
|
|
101
|
+
for (const [cmdName, cmdDef] of Object.entries(REPL_COMMANDS)) {
|
|
102
|
+
if ((cmdDef.aliases as readonly string[]).includes(name)) {
|
|
103
|
+
return cmdName as ReplCommandName;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse user input into structured format
|
|
112
|
+
*/
|
|
113
|
+
export function parseInput(input: string): ParsedInput {
|
|
114
|
+
const trimmed = input.trim();
|
|
115
|
+
|
|
116
|
+
if (!trimmed) {
|
|
117
|
+
return { type: 'empty' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (isSlashCommand(trimmed)) {
|
|
121
|
+
return {
|
|
122
|
+
type: 'slash-command',
|
|
123
|
+
command: parseSlashCommand(trimmed),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
type: 'natural-language',
|
|
129
|
+
text: trimmed,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Format help text for all commands
|
|
135
|
+
*/
|
|
136
|
+
export function formatHelpText(): string {
|
|
137
|
+
const lines: string[] = [
|
|
138
|
+
'Available commands:',
|
|
139
|
+
'',
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
for (const [name, def] of Object.entries(REPL_COMMANDS)) {
|
|
143
|
+
const aliases = def.aliases.length > 0
|
|
144
|
+
? ` (aliases: ${def.aliases.map(a => '/' + a).join(', ')})`
|
|
145
|
+
: '';
|
|
146
|
+
lines.push(` ${def.usage}${aliases}`);
|
|
147
|
+
lines.push(` ${def.description}`);
|
|
148
|
+
lines.push('');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
lines.push('Or just type naturally to chat with the AI.');
|
|
152
|
+
|
|
153
|
+
return lines.join('\n');
|
|
154
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL Module
|
|
3
|
+
* Interactive command-line interface for Wiggum
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { startRepl, processInput, executeCommand } from './repl-loop.js';
|
|
7
|
+
export {
|
|
8
|
+
type SessionState,
|
|
9
|
+
createSessionState,
|
|
10
|
+
updateSessionState,
|
|
11
|
+
} from './session-state.js';
|
|
12
|
+
export {
|
|
13
|
+
type SlashCommand,
|
|
14
|
+
type ParsedInput,
|
|
15
|
+
type InputType,
|
|
16
|
+
type ReplCommandName,
|
|
17
|
+
parseInput,
|
|
18
|
+
parseSlashCommand,
|
|
19
|
+
isSlashCommand,
|
|
20
|
+
resolveCommandAlias,
|
|
21
|
+
formatHelpText,
|
|
22
|
+
REPL_COMMANDS,
|
|
23
|
+
} from './command-parser.js';
|