wiggum-cli 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/agents/codebase-analyzer.d.ts.map +1 -1
- package/dist/ai/agents/codebase-analyzer.js +9 -2
- package/dist/ai/agents/codebase-analyzer.js.map +1 -1
- package/dist/ai/agents/mcp-detector.d.ts +4 -2
- package/dist/ai/agents/mcp-detector.d.ts.map +1 -1
- package/dist/ai/agents/mcp-detector.js +9 -6
- package/dist/ai/agents/mcp-detector.js.map +1 -1
- package/dist/ai/agents/types.d.ts +12 -2
- package/dist/ai/agents/types.d.ts.map +1 -1
- package/dist/ai/enhancer.d.ts +27 -1
- package/dist/ai/enhancer.d.ts.map +1 -1
- package/dist/ai/enhancer.js +52 -7
- package/dist/ai/enhancer.js.map +1 -1
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +113 -112
- package/dist/commands/init.js.map +1 -1
- package/dist/utils/colors.d.ts +47 -0
- package/dist/utils/colors.d.ts.map +1 -1
- package/dist/utils/colors.js +108 -0
- package/dist/utils/colors.js.map +1 -1
- package/dist/utils/header.d.ts.map +1 -1
- package/dist/utils/header.js +19 -2
- package/dist/utils/header.js.map +1 -1
- package/dist/utils/spinner.d.ts +74 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +208 -0
- package/dist/utils/spinner.js.map +1 -0
- package/package.json +1 -1
- package/src/ai/agents/codebase-analyzer.ts +12 -3
- package/src/ai/agents/mcp-detector.test.ts +30 -7
- package/src/ai/agents/mcp-detector.ts +10 -7
- package/src/ai/agents/types.ts +13 -2
- package/src/ai/enhancer.ts +88 -9
- package/src/ai/index.ts +2 -0
- package/src/commands/init.ts +129 -124
- package/src/utils/colors.ts +139 -0
- package/src/utils/header.ts +19 -2
- package/src/utils/spinner.ts +262 -0
package/src/utils/colors.ts
CHANGED
|
@@ -101,3 +101,142 @@ export function sectionHeader(title: string): string {
|
|
|
101
101
|
const line = drawLine(50);
|
|
102
102
|
return `\n${line}\n${simpson.yellow(title)}\n${line}`;
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Compact section header (single line style)
|
|
107
|
+
*/
|
|
108
|
+
export function compactHeader(title: string): string {
|
|
109
|
+
const titlePart = `─── ${title} ───`;
|
|
110
|
+
return simpson.yellow(titlePart);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Progress phase indicators
|
|
115
|
+
*/
|
|
116
|
+
export const phase = {
|
|
117
|
+
pending: '○',
|
|
118
|
+
active: '◐',
|
|
119
|
+
complete: '✓',
|
|
120
|
+
error: '✗',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Display a stack info box
|
|
125
|
+
*/
|
|
126
|
+
export function stackBox(stack: {
|
|
127
|
+
framework?: string;
|
|
128
|
+
language?: string;
|
|
129
|
+
testing?: string;
|
|
130
|
+
packageManager?: string;
|
|
131
|
+
}): string {
|
|
132
|
+
const lines: string[] = [];
|
|
133
|
+
const maxLabelWidth = 10;
|
|
134
|
+
|
|
135
|
+
const addLine = (label: string, value: string | undefined) => {
|
|
136
|
+
if (value) {
|
|
137
|
+
const paddedLabel = label.padEnd(maxLabelWidth);
|
|
138
|
+
lines.push(`│ ${simpson.brown(paddedLabel)} ${value}`);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
lines.push(simpson.brown('┌─ Detected Stack ────────────────────────────┐'));
|
|
143
|
+
addLine('Framework:', stack.framework);
|
|
144
|
+
addLine('Language:', stack.language);
|
|
145
|
+
addLine('Testing:', stack.testing);
|
|
146
|
+
addLine('Package:', stack.packageManager);
|
|
147
|
+
lines.push(simpson.brown('└─────────────────────────────────────────────┘'));
|
|
148
|
+
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Progress phases display
|
|
154
|
+
*/
|
|
155
|
+
export interface ProgressPhase {
|
|
156
|
+
name: string;
|
|
157
|
+
status: 'pending' | 'active' | 'complete' | 'error';
|
|
158
|
+
detail?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function progressPhases(phases: ProgressPhase[]): string {
|
|
162
|
+
return phases.map((p, i) => {
|
|
163
|
+
const num = `${i + 1}.`;
|
|
164
|
+
const icon = phase[p.status];
|
|
165
|
+
const statusColor = p.status === 'complete' ? simpson.yellow :
|
|
166
|
+
p.status === 'active' ? simpson.white :
|
|
167
|
+
p.status === 'error' ? simpson.pink :
|
|
168
|
+
simpson.brown;
|
|
169
|
+
const detail = p.detail ? simpson.brown(` (${p.detail})`) : '';
|
|
170
|
+
return ` ${num} ${statusColor(icon)} ${p.name}${detail}`;
|
|
171
|
+
}).join('\n');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* File tree display for generated files
|
|
176
|
+
*/
|
|
177
|
+
export function fileTree(basePath: string, files: string[]): string {
|
|
178
|
+
// Group files by directory
|
|
179
|
+
const tree: Record<string, string[]> = {};
|
|
180
|
+
|
|
181
|
+
for (const file of files) {
|
|
182
|
+
const parts = file.split('/');
|
|
183
|
+
if (parts.length === 1) {
|
|
184
|
+
tree[''] = tree[''] || [];
|
|
185
|
+
tree[''].push(parts[0]);
|
|
186
|
+
} else {
|
|
187
|
+
const dir = parts.slice(0, -1).join('/');
|
|
188
|
+
const filename = parts[parts.length - 1];
|
|
189
|
+
tree[dir] = tree[dir] || [];
|
|
190
|
+
tree[dir].push(filename);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const lines: string[] = [` ${simpson.yellow(basePath + '/')}`];
|
|
195
|
+
const dirs = Object.keys(tree).sort();
|
|
196
|
+
|
|
197
|
+
for (let i = 0; i < dirs.length; i++) {
|
|
198
|
+
const dir = dirs[i];
|
|
199
|
+
const isLastDir = i === dirs.length - 1;
|
|
200
|
+
const dirPrefix = isLastDir ? ' └── ' : ' ├── ';
|
|
201
|
+
const filePrefix = isLastDir ? ' ' : ' │ ';
|
|
202
|
+
|
|
203
|
+
if (dir) {
|
|
204
|
+
lines.push(`${dirPrefix}${simpson.brown(dir + '/')}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const filesInDir = tree[dir].sort();
|
|
208
|
+
for (let j = 0; j < filesInDir.length; j++) {
|
|
209
|
+
const file = filesInDir[j];
|
|
210
|
+
const isLastFile = j === filesInDir.length - 1;
|
|
211
|
+
const connector = dir ? (isLastFile ? '└── ' : '├── ') : (isLastFile && isLastDir ? '└── ' : '├── ');
|
|
212
|
+
const prefix = dir ? filePrefix : ' ';
|
|
213
|
+
lines.push(`${prefix}${connector}${file}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return lines.join('\n');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Next steps box
|
|
222
|
+
*/
|
|
223
|
+
export function nextStepsBox(steps: Array<{ command: string; description: string }>): string {
|
|
224
|
+
const lines: string[] = [];
|
|
225
|
+
lines.push('');
|
|
226
|
+
lines.push(compactHeader('Next Steps'));
|
|
227
|
+
lines.push('');
|
|
228
|
+
|
|
229
|
+
for (const step of steps) {
|
|
230
|
+
lines.push(` ${simpson.yellow('$')} ${step.command}`);
|
|
231
|
+
lines.push(` ${simpson.brown(step.description)}`);
|
|
232
|
+
lines.push('');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Fixed-length password mask (32 characters)
|
|
240
|
+
*/
|
|
241
|
+
export const PASSWORD_MASK_LENGTH = 32;
|
|
242
|
+
export const PASSWORD_MASK = '▪'.repeat(PASSWORD_MASK_LENGTH);
|
package/src/utils/header.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import cfonts from 'cfonts';
|
|
2
2
|
import { simpson, drawBox, SIMPSON_COLORS } from './colors.js';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
|
|
7
|
+
// Get version from package.json
|
|
8
|
+
function getVersion(): string {
|
|
9
|
+
try {
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const packagePath = join(__dirname, '..', '..', 'package.json');
|
|
13
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
14
|
+
return pkg.version || '0.5.0';
|
|
15
|
+
} catch {
|
|
16
|
+
return '0.5.0';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
3
19
|
|
|
4
20
|
/**
|
|
5
21
|
* Display the WIGGUM CLI ASCII header with welcome box
|
|
6
22
|
*/
|
|
7
23
|
export function displayHeader(): void {
|
|
8
|
-
|
|
9
|
-
|
|
24
|
+
const version = getVersion();
|
|
25
|
+
// Welcome box like Claude Code with version
|
|
26
|
+
const welcomeText = '🍩 Welcome to ' + simpson.yellow('Wiggum CLI') + ': AI-powered ' + simpson.yellow('Ralph') + ' development loop CLI ' + simpson.pink(`v${version}`) + ' 🍩';
|
|
10
27
|
console.log('');
|
|
11
28
|
console.log(drawBox(welcomeText, 2));
|
|
12
29
|
console.log('');
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Spinner with Shimmer Effect, Timer, and Token Tracking
|
|
3
|
+
* Inspired by Claude Code's loading experience
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { simpson } from './colors.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shimmer frames for the spinner animation
|
|
10
|
+
* Creates a wave-like effect similar to Claude Code
|
|
11
|
+
*/
|
|
12
|
+
const SHIMMER_FRAMES = [
|
|
13
|
+
'░▒▓█▓▒░',
|
|
14
|
+
'▒▓█▓▒░░',
|
|
15
|
+
'▓█▓▒░░▒',
|
|
16
|
+
'█▓▒░░▒▓',
|
|
17
|
+
'▓▒░░▒▓█',
|
|
18
|
+
'▒░░▒▓█▓',
|
|
19
|
+
'░░▒▓█▓▒',
|
|
20
|
+
'░▒▓█▓▒░',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Braille spinner frames (alternative style)
|
|
25
|
+
*/
|
|
26
|
+
const BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Dots spinner frames
|
|
30
|
+
*/
|
|
31
|
+
const DOTS_FRAMES = ['⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format milliseconds to human-readable time
|
|
35
|
+
*/
|
|
36
|
+
function formatTime(ms: number): string {
|
|
37
|
+
const seconds = Math.floor(ms / 1000);
|
|
38
|
+
const minutes = Math.floor(seconds / 60);
|
|
39
|
+
const remainingSeconds = seconds % 60;
|
|
40
|
+
|
|
41
|
+
if (minutes > 0) {
|
|
42
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
43
|
+
}
|
|
44
|
+
return `${seconds}s`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format token count with commas
|
|
49
|
+
*/
|
|
50
|
+
function formatTokens(tokens: number): string {
|
|
51
|
+
return tokens.toLocaleString();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ShimmerSpinnerOptions {
|
|
55
|
+
/** Enable shimmer effect (default: true) */
|
|
56
|
+
shimmer?: boolean;
|
|
57
|
+
/** Show elapsed time (default: true) */
|
|
58
|
+
showTimer?: boolean;
|
|
59
|
+
/** Show token usage (default: false) */
|
|
60
|
+
showTokens?: boolean;
|
|
61
|
+
/** Spinner style (default: 'shimmer') */
|
|
62
|
+
style?: 'shimmer' | 'braille' | 'dots';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TokenUsage {
|
|
66
|
+
inputTokens: number;
|
|
67
|
+
outputTokens: number;
|
|
68
|
+
totalTokens: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Custom spinner with shimmer effect, timer, and token tracking
|
|
73
|
+
*/
|
|
74
|
+
export class ShimmerSpinner {
|
|
75
|
+
private message: string = '';
|
|
76
|
+
private isRunning: boolean = false;
|
|
77
|
+
private intervalId: NodeJS.Timeout | null = null;
|
|
78
|
+
private frameIndex: number = 0;
|
|
79
|
+
private startTime: number = 0;
|
|
80
|
+
private tokens: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
81
|
+
private options: Required<ShimmerSpinnerOptions>;
|
|
82
|
+
private frames: string[];
|
|
83
|
+
|
|
84
|
+
constructor(options: ShimmerSpinnerOptions = {}) {
|
|
85
|
+
this.options = {
|
|
86
|
+
shimmer: options.shimmer ?? true,
|
|
87
|
+
showTimer: options.showTimer ?? true,
|
|
88
|
+
showTokens: options.showTokens ?? false,
|
|
89
|
+
style: options.style ?? 'shimmer',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Select frames based on style
|
|
93
|
+
switch (this.options.style) {
|
|
94
|
+
case 'braille':
|
|
95
|
+
this.frames = BRAILLE_FRAMES;
|
|
96
|
+
break;
|
|
97
|
+
case 'dots':
|
|
98
|
+
this.frames = DOTS_FRAMES;
|
|
99
|
+
break;
|
|
100
|
+
case 'shimmer':
|
|
101
|
+
default:
|
|
102
|
+
this.frames = SHIMMER_FRAMES;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Start the spinner with a message
|
|
109
|
+
*/
|
|
110
|
+
start(message: string): void {
|
|
111
|
+
this.message = message;
|
|
112
|
+
this.isRunning = true;
|
|
113
|
+
this.startTime = Date.now();
|
|
114
|
+
this.frameIndex = 0;
|
|
115
|
+
|
|
116
|
+
// Clear line and hide cursor
|
|
117
|
+
process.stdout.write('\x1B[?25l');
|
|
118
|
+
|
|
119
|
+
this.render();
|
|
120
|
+
this.intervalId = setInterval(() => {
|
|
121
|
+
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
122
|
+
this.render();
|
|
123
|
+
}, 80);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Update the spinner message
|
|
128
|
+
*/
|
|
129
|
+
update(message: string): void {
|
|
130
|
+
this.message = message;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Update token usage (adds to existing)
|
|
135
|
+
*/
|
|
136
|
+
updateTokens(usage: TokenUsage): void {
|
|
137
|
+
this.tokens = {
|
|
138
|
+
inputTokens: this.tokens.inputTokens + usage.inputTokens,
|
|
139
|
+
outputTokens: this.tokens.outputTokens + usage.outputTokens,
|
|
140
|
+
totalTokens: this.tokens.totalTokens + usage.totalTokens,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Set token usage directly
|
|
146
|
+
*/
|
|
147
|
+
setTokens(usage: TokenUsage): void {
|
|
148
|
+
this.tokens = usage;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Render the spinner
|
|
153
|
+
*/
|
|
154
|
+
private render(): void {
|
|
155
|
+
if (!this.isRunning) return;
|
|
156
|
+
|
|
157
|
+
const elapsed = Date.now() - this.startTime;
|
|
158
|
+
const frame = this.frames[this.frameIndex];
|
|
159
|
+
|
|
160
|
+
let line = '';
|
|
161
|
+
|
|
162
|
+
// Shimmer/spinner frame
|
|
163
|
+
if (this.options.shimmer && this.options.style === 'shimmer') {
|
|
164
|
+
line += simpson.yellow(frame) + ' ';
|
|
165
|
+
} else {
|
|
166
|
+
line += simpson.yellow(frame) + ' ';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Message
|
|
170
|
+
line += this.message;
|
|
171
|
+
|
|
172
|
+
// Timer
|
|
173
|
+
if (this.options.showTimer) {
|
|
174
|
+
line += simpson.brown(` [${formatTime(elapsed)}]`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Tokens
|
|
178
|
+
if (this.options.showTokens && this.tokens.totalTokens > 0) {
|
|
179
|
+
line += simpson.pink(` (${formatTokens(this.tokens.totalTokens)} tokens)`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Clear line and write
|
|
183
|
+
process.stdout.write('\r\x1B[K' + line);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Stop the spinner with a final message
|
|
188
|
+
*/
|
|
189
|
+
stop(finalMessage?: string): void {
|
|
190
|
+
this.isRunning = false;
|
|
191
|
+
|
|
192
|
+
if (this.intervalId) {
|
|
193
|
+
clearInterval(this.intervalId);
|
|
194
|
+
this.intervalId = null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const elapsed = Date.now() - this.startTime;
|
|
198
|
+
|
|
199
|
+
// Build final line
|
|
200
|
+
let line = '';
|
|
201
|
+
line += simpson.yellow('✓') + ' ';
|
|
202
|
+
line += finalMessage || this.message;
|
|
203
|
+
|
|
204
|
+
if (this.options.showTimer) {
|
|
205
|
+
line += simpson.brown(` [${formatTime(elapsed)}]`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (this.options.showTokens && this.tokens.totalTokens > 0) {
|
|
209
|
+
line += simpson.pink(` (${formatTokens(this.tokens.totalTokens)} tokens)`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Clear line, show cursor, print final
|
|
213
|
+
process.stdout.write('\r\x1B[K' + line + '\n');
|
|
214
|
+
process.stdout.write('\x1B[?25h');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Stop with error
|
|
219
|
+
*/
|
|
220
|
+
fail(message?: string): void {
|
|
221
|
+
this.isRunning = false;
|
|
222
|
+
|
|
223
|
+
if (this.intervalId) {
|
|
224
|
+
clearInterval(this.intervalId);
|
|
225
|
+
this.intervalId = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const elapsed = Date.now() - this.startTime;
|
|
229
|
+
|
|
230
|
+
let line = '';
|
|
231
|
+
line += simpson.pink('✗') + ' ';
|
|
232
|
+
line += message || this.message;
|
|
233
|
+
|
|
234
|
+
if (this.options.showTimer) {
|
|
235
|
+
line += simpson.brown(` [${formatTime(elapsed)}]`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
process.stdout.write('\r\x1B[K' + line + '\n');
|
|
239
|
+
process.stdout.write('\x1B[?25h');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get elapsed time in milliseconds
|
|
244
|
+
*/
|
|
245
|
+
getElapsed(): number {
|
|
246
|
+
return Date.now() - this.startTime;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get current token usage
|
|
251
|
+
*/
|
|
252
|
+
getTokens(): TokenUsage {
|
|
253
|
+
return { ...this.tokens };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Create a shimmer spinner instance
|
|
259
|
+
*/
|
|
260
|
+
export function createShimmerSpinner(options?: ShimmerSpinnerOptions): ShimmerSpinner {
|
|
261
|
+
return new ShimmerSpinner(options);
|
|
262
|
+
}
|