cc-starter 1.0.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/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/cc-starter.js +55 -0
- package/lib/constants.js +32 -0
- package/lib/detect.js +109 -0
- package/lib/plugins.js +74 -0
- package/lib/scaffold.js +261 -0
- package/lib/wizard.js +99 -0
- package/package.json +30 -0
- package/template/CLAUDE.md.hbs +44 -0
- package/template/claude/commands/kickstart.md +16 -0
- package/template/claude/memory/MEMORY.md +11 -0
- package/template/claude/project/README.md +7 -0
- package/template/claude/reference/README.md +7 -0
- package/template/claude/rules/01-general.md +36 -0
- package/template/claude/rules/02-code-standards.md +23 -0
- package/template/claude/rules/03-dev-ops.md +20 -0
- package/template/claude/settings.json +4 -0
- package/template/scripts/stats/cocomo.js +178 -0
- package/template/scripts/stats/project-report.js +640 -0
- package/template/scripts/stats/vibe-code.js +533 -0
- package/template/scripts/stats/vibe-stats.js +249 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/stats/vibe-code.js — Multi-language token-saving extraction tool
|
|
3
|
+
// Usage: node scripts/stats/vibe-code.js <command> <target>
|
|
4
|
+
//
|
|
5
|
+
// Commands:
|
|
6
|
+
// types <file> Extract TS interfaces, types, enums
|
|
7
|
+
// tree [dir] Clean directory structure (default: src or .)
|
|
8
|
+
// imports <file> Show import statements grouped
|
|
9
|
+
// functions <file> Extract function/method signatures
|
|
10
|
+
// help Show all commands with examples
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Stats logging — writes to .vibe-stats.json in cwd, silent on errors
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
function logToStats(operation, fullSize, extractedSize, filename) {
|
|
19
|
+
try {
|
|
20
|
+
const STATS_FILE = path.join(process.cwd(), '.vibe-stats.json');
|
|
21
|
+
|
|
22
|
+
let stats = { sessions: [], totalSaved: 0, operationsCount: 0, created: new Date().toISOString() };
|
|
23
|
+
if (fs.existsSync(STATS_FILE)) {
|
|
24
|
+
stats = JSON.parse(fs.readFileSync(STATS_FILE, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const traditionalTokens = Math.ceil(fullSize / 4);
|
|
28
|
+
const vibeModeTokens = Math.ceil(extractedSize / 4);
|
|
29
|
+
const saved = traditionalTokens - vibeModeTokens;
|
|
30
|
+
const percentage = traditionalTokens > 0 ? Math.round((saved / traditionalTokens) * 100) : 0;
|
|
31
|
+
|
|
32
|
+
stats.sessions.push({
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
operation,
|
|
35
|
+
file: filename,
|
|
36
|
+
traditionalTokens,
|
|
37
|
+
vibeModeTokens,
|
|
38
|
+
savedTokens: saved,
|
|
39
|
+
percentage,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
stats.totalSaved += saved;
|
|
43
|
+
stats.operationsCount += 1;
|
|
44
|
+
|
|
45
|
+
fs.writeFileSync(STATS_FILE, JSON.stringify(stats, null, 2));
|
|
46
|
+
} catch (_) {
|
|
47
|
+
// Silently ignore — stats logging must never break the tool
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function printSavings(label, fullSize, extractedSize) {
|
|
52
|
+
const pct = fullSize > 0 ? Math.round(((fullSize - extractedSize) / fullSize) * 100) : 0;
|
|
53
|
+
const fullTokens = Math.ceil(fullSize / 4);
|
|
54
|
+
const extractedTokens = Math.ceil(extractedSize / 4);
|
|
55
|
+
console.log(
|
|
56
|
+
`\n--- Token savings: ~${fullTokens} -> ~${extractedTokens} tokens (${pct}% saved) ---`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
function resolveFile(target) {
|
|
64
|
+
if (!target) {
|
|
65
|
+
console.error('Error: Please provide a file path.');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const abs = path.resolve(target);
|
|
69
|
+
if (!fs.existsSync(abs)) {
|
|
70
|
+
console.error(`Error: File not found — ${abs}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
return abs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readFile(filePath) {
|
|
77
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Command: types <file>
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
function cmdTypes(target) {
|
|
84
|
+
const filePath = resolveFile(target);
|
|
85
|
+
const content = readFile(filePath);
|
|
86
|
+
const fullSize = Buffer.byteLength(content, 'utf8');
|
|
87
|
+
const lines = content.split('\n');
|
|
88
|
+
|
|
89
|
+
// Patterns that start a type block
|
|
90
|
+
const blockStartPatterns = [
|
|
91
|
+
/^export\s+(interface|type|enum)\s+/,
|
|
92
|
+
/^(interface|type|enum)\s+/,
|
|
93
|
+
/^export\s+declare\s+(interface|type|enum)\s+/,
|
|
94
|
+
/^declare\s+(interface|type|enum)\s+/,
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
// Single-line type alias: type X = string | number;
|
|
98
|
+
const singleLineType = /^(export\s+)?type\s+\w+.*=.*[^{]\s*;?\s*$/;
|
|
99
|
+
|
|
100
|
+
const extracted = [];
|
|
101
|
+
let insideBlock = false;
|
|
102
|
+
let braceDepth = 0;
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < lines.length; i++) {
|
|
105
|
+
const trimmed = lines[i].trimStart();
|
|
106
|
+
|
|
107
|
+
if (!insideBlock) {
|
|
108
|
+
// Check for single-line type alias
|
|
109
|
+
if (singleLineType.test(trimmed) && !trimmed.includes('{')) {
|
|
110
|
+
extracted.push(lines[i]);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check for block start
|
|
115
|
+
const isBlockStart = blockStartPatterns.some((p) => p.test(trimmed));
|
|
116
|
+
if (isBlockStart) {
|
|
117
|
+
insideBlock = true;
|
|
118
|
+
braceDepth = 0;
|
|
119
|
+
// Count braces on this line
|
|
120
|
+
for (const ch of lines[i]) {
|
|
121
|
+
if (ch === '{') braceDepth++;
|
|
122
|
+
if (ch === '}') braceDepth--;
|
|
123
|
+
}
|
|
124
|
+
extracted.push(lines[i]);
|
|
125
|
+
if (braceDepth <= 0 && lines[i].includes('}')) {
|
|
126
|
+
insideBlock = false;
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
// Inside a block — count braces
|
|
132
|
+
for (const ch of lines[i]) {
|
|
133
|
+
if (ch === '{') braceDepth++;
|
|
134
|
+
if (ch === '}') braceDepth--;
|
|
135
|
+
}
|
|
136
|
+
extracted.push(lines[i]);
|
|
137
|
+
if (braceDepth <= 0) {
|
|
138
|
+
insideBlock = false;
|
|
139
|
+
extracted.push(''); // blank separator
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const output = extracted.join('\n').trim();
|
|
145
|
+
if (!output) {
|
|
146
|
+
console.log(`No types/interfaces/enums found in ${path.basename(filePath)}`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const extractedSize = Buffer.byteLength(output, 'utf8');
|
|
151
|
+
console.log(`// Types extracted from ${path.basename(filePath)}\n`);
|
|
152
|
+
console.log(output);
|
|
153
|
+
printSavings('types', fullSize, extractedSize);
|
|
154
|
+
logToStats('types', fullSize, extractedSize, path.basename(filePath));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Command: tree [dir]
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
function cmdTree(target) {
|
|
161
|
+
const IGNORED_DIRS = new Set([
|
|
162
|
+
'node_modules', '.git', '.next', 'dist', 'build', '.cache',
|
|
163
|
+
'__pycache__', '.venv', 'venv', 'env', 'target', 'vendor',
|
|
164
|
+
'.mypy_cache', '.pytest_cache', '.tox', 'coverage', '.nyc_output',
|
|
165
|
+
'.turbo', '.parcel-cache', '.DS_Store',
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
const RELEVANT_EXTS = new Set([
|
|
169
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
170
|
+
'.py', '.go', '.rs', '.java', '.rb', '.php', '.cs',
|
|
171
|
+
'.vue', '.svelte', '.astro',
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
const TEST_PATTERNS = ['.test.', '.spec.', '_test.', '_spec.'];
|
|
175
|
+
|
|
176
|
+
// Determine root directory
|
|
177
|
+
let rootDir;
|
|
178
|
+
if (target) {
|
|
179
|
+
rootDir = path.resolve(target);
|
|
180
|
+
} else if (fs.existsSync(path.resolve('src'))) {
|
|
181
|
+
rootDir = path.resolve('src');
|
|
182
|
+
} else {
|
|
183
|
+
rootDir = process.cwd();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!fs.existsSync(rootDir)) {
|
|
187
|
+
console.error(`Error: Directory not found — ${rootDir}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isTestFile(name) {
|
|
192
|
+
return TEST_PATTERNS.some((p) => name.includes(p));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function walk(dir, prefix, isLast) {
|
|
196
|
+
const entries = [];
|
|
197
|
+
try {
|
|
198
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
199
|
+
// Sort: directories first, then files, both alphabetical
|
|
200
|
+
const dirs = items.filter((e) => e.isDirectory() && !IGNORED_DIRS.has(e.name) && !e.name.startsWith('.'));
|
|
201
|
+
const files = items.filter((e) => e.isFile() && RELEVANT_EXTS.has(path.extname(e.name)) && !isTestFile(e.name));
|
|
202
|
+
|
|
203
|
+
dirs.sort((a, b) => a.name.localeCompare(b.name));
|
|
204
|
+
files.sort((a, b) => a.name.localeCompare(b.name));
|
|
205
|
+
|
|
206
|
+
const all = [...dirs, ...files];
|
|
207
|
+
all.forEach((entry, idx) => {
|
|
208
|
+
const last = idx === all.length - 1;
|
|
209
|
+
const connector = last ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ';
|
|
210
|
+
const childPrefix = prefix + (last ? ' ' : '\u2502 ');
|
|
211
|
+
|
|
212
|
+
if (entry.isDirectory()) {
|
|
213
|
+
entries.push(`${prefix}${connector}${entry.name}/`);
|
|
214
|
+
entries.push(...walk(path.join(dir, entry.name), childPrefix, last));
|
|
215
|
+
} else {
|
|
216
|
+
entries.push(`${prefix}${connector}${entry.name}`);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
} catch (_) {
|
|
220
|
+
// Permission error — skip
|
|
221
|
+
}
|
|
222
|
+
return entries;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const relRoot = path.relative(process.cwd(), rootDir) || '.';
|
|
226
|
+
const lines = [`${relRoot}/`, ...walk(rootDir, '', true)];
|
|
227
|
+
console.log(lines.join('\n'));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Command: imports <file>
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
function cmdImports(target) {
|
|
234
|
+
const filePath = resolveFile(target);
|
|
235
|
+
const content = readFile(filePath);
|
|
236
|
+
const lines = content.split('\n');
|
|
237
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
238
|
+
|
|
239
|
+
const external = [];
|
|
240
|
+
const local = [];
|
|
241
|
+
|
|
242
|
+
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
|
|
243
|
+
// JS/TS imports
|
|
244
|
+
const importRe = /^import\s+.+\s+from\s+['"]([^'"]+)['"]/;
|
|
245
|
+
const importSideEffect = /^import\s+['"]([^'"]+)['"]/;
|
|
246
|
+
const requireRe = /(?:const|let|var)\s+.+=\s*require\(\s*['"]([^'"]+)['"]\s*\)/;
|
|
247
|
+
|
|
248
|
+
for (const line of lines) {
|
|
249
|
+
const trimmed = line.trim();
|
|
250
|
+
let mod = null;
|
|
251
|
+
let match;
|
|
252
|
+
|
|
253
|
+
if ((match = trimmed.match(importRe))) {
|
|
254
|
+
mod = match[1];
|
|
255
|
+
} else if ((match = trimmed.match(importSideEffect))) {
|
|
256
|
+
mod = match[1];
|
|
257
|
+
} else if ((match = trimmed.match(requireRe))) {
|
|
258
|
+
mod = match[1];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (mod) {
|
|
262
|
+
const bucket = (mod.startsWith('.') || mod.startsWith('/') || mod.startsWith('@/')) ? local : external;
|
|
263
|
+
bucket.push(trimmed);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} else if (ext === '.py') {
|
|
267
|
+
// Python imports
|
|
268
|
+
const pyImport = /^(?:from\s+(\S+)\s+import|import\s+(\S+))/;
|
|
269
|
+
for (const line of lines) {
|
|
270
|
+
const trimmed = line.trim();
|
|
271
|
+
const match = trimmed.match(pyImport);
|
|
272
|
+
if (match) {
|
|
273
|
+
const mod = match[1] || match[2];
|
|
274
|
+
const bucket = mod.startsWith('.') ? local : external;
|
|
275
|
+
bucket.push(trimmed);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} else if (ext === '.go') {
|
|
279
|
+
// Go imports — handle both single and block import
|
|
280
|
+
let inBlock = false;
|
|
281
|
+
for (const line of lines) {
|
|
282
|
+
const trimmed = line.trim();
|
|
283
|
+
if (trimmed.startsWith('import (')) {
|
|
284
|
+
inBlock = true;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (inBlock && trimmed === ')') {
|
|
288
|
+
inBlock = false;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (inBlock) {
|
|
292
|
+
const cleaned = trimmed.replace(/^[\w.]+\s+/, '').replace(/['"]/g, '');
|
|
293
|
+
if (cleaned) {
|
|
294
|
+
const bucket = cleaned.includes('.') || cleaned.includes('/') ? external : external;
|
|
295
|
+
// Go stdlib has no dots in most packages but uses no domain prefix
|
|
296
|
+
// Packages with dots (github.com/...) are external, rest is stdlib
|
|
297
|
+
const isThirdParty = cleaned.includes('.');
|
|
298
|
+
(isThirdParty ? external : local).push(trimmed);
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const singleMatch = trimmed.match(/^import\s+["']([^"']+)["']/);
|
|
303
|
+
if (singleMatch) {
|
|
304
|
+
const mod = singleMatch[1];
|
|
305
|
+
const isThirdParty = mod.includes('.');
|
|
306
|
+
(isThirdParty ? external : local).push(trimmed);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log(`// Imports from ${path.basename(filePath)}\n`);
|
|
312
|
+
|
|
313
|
+
if (external.length) {
|
|
314
|
+
console.log(`--- External (${external.length}) ---`);
|
|
315
|
+
external.forEach((l) => console.log(` ${l}`));
|
|
316
|
+
}
|
|
317
|
+
if (local.length) {
|
|
318
|
+
if (external.length) console.log('');
|
|
319
|
+
console.log(`--- Local (${local.length}) ---`);
|
|
320
|
+
local.forEach((l) => console.log(` ${l}`));
|
|
321
|
+
}
|
|
322
|
+
if (!external.length && !local.length) {
|
|
323
|
+
console.log('No imports found.');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Command: functions <file>
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
function cmdFunctions(target) {
|
|
331
|
+
const filePath = resolveFile(target);
|
|
332
|
+
const content = readFile(filePath);
|
|
333
|
+
const fullSize = Buffer.byteLength(content, 'utf8');
|
|
334
|
+
const lines = content.split('\n');
|
|
335
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
336
|
+
|
|
337
|
+
const signatures = [];
|
|
338
|
+
|
|
339
|
+
// Helper: collect a possibly multi-line signature starting at lineIdx.
|
|
340
|
+
// Reads lines until balanced parens and hits `{` or `=>` or `:` (Python).
|
|
341
|
+
function collectSignature(startIdx, terminator) {
|
|
342
|
+
let sig = '';
|
|
343
|
+
let parenDepth = 0;
|
|
344
|
+
let foundOpenParen = false;
|
|
345
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
346
|
+
const line = lines[i];
|
|
347
|
+
for (const ch of line) {
|
|
348
|
+
if (ch === '(') { parenDepth++; foundOpenParen = true; }
|
|
349
|
+
if (ch === ')') parenDepth--;
|
|
350
|
+
}
|
|
351
|
+
sig += (i === startIdx ? '' : ' ') + line.trim();
|
|
352
|
+
// Once parens are balanced after opening, check for terminator
|
|
353
|
+
if (foundOpenParen && parenDepth <= 0) {
|
|
354
|
+
// Trim everything from the block opener onward
|
|
355
|
+
sig = sig.replace(/\s*\{[\s\S]*$/, '').replace(/\s*=>[\s\S]*$/, ' => ...').trimEnd();
|
|
356
|
+
return sig;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Fallback: return what we have
|
|
360
|
+
return sig.replace(/\s*\{[\s\S]*$/, '').trimEnd();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
|
|
364
|
+
const startPatterns = [
|
|
365
|
+
/^export\s+(async\s+)?function\s+/,
|
|
366
|
+
/^export\s+default\s+(async\s+)?function/,
|
|
367
|
+
/^export\s+const\s+\w+\s*=\s*(async\s*)?\(/,
|
|
368
|
+
/^export\s+const\s+\w+\s*=\s*(async\s*)?function/,
|
|
369
|
+
/^(async\s+)?function\s+\w+/,
|
|
370
|
+
/^export\s+(default\s+)?const\s+\w+\s*=\s*(React\.)?(memo|forwardRef)\(/,
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
const methodPattern = /^\s*(public|private|protected|static|async|get|set)\s+[\w]+\s*\(/;
|
|
374
|
+
|
|
375
|
+
for (let i = 0; i < lines.length; i++) {
|
|
376
|
+
const trimmed = lines[i].trimStart();
|
|
377
|
+
let matched = false;
|
|
378
|
+
|
|
379
|
+
for (const pattern of startPatterns) {
|
|
380
|
+
if (pattern.test(trimmed)) {
|
|
381
|
+
matched = true;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (!matched && methodPattern.test(trimmed)) {
|
|
386
|
+
matched = true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (matched) {
|
|
390
|
+
const sig = collectSignature(i, '{');
|
|
391
|
+
if (sig.trim()) signatures.push(sig);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
} else if (ext === '.py') {
|
|
395
|
+
for (let i = 0; i < lines.length; i++) {
|
|
396
|
+
const trimmed = lines[i].trimStart();
|
|
397
|
+
if (/^(async\s+)?def\s+\w+/.test(trimmed) || /^class\s+\w+/.test(trimmed)) {
|
|
398
|
+
let sig = '';
|
|
399
|
+
// Collect until `:` at end of line with balanced parens
|
|
400
|
+
let parenDepth = 0;
|
|
401
|
+
for (let j = i; j < lines.length; j++) {
|
|
402
|
+
const line = lines[j];
|
|
403
|
+
for (const ch of line) {
|
|
404
|
+
if (ch === '(') parenDepth++;
|
|
405
|
+
if (ch === ')') parenDepth--;
|
|
406
|
+
}
|
|
407
|
+
sig += (j === i ? '' : ' ') + line.trim();
|
|
408
|
+
if (parenDepth <= 0 && line.trim().endsWith(':')) {
|
|
409
|
+
sig = sig.replace(/:\s*$/, '');
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
signatures.push(sig);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} else if (ext === '.go') {
|
|
417
|
+
for (let i = 0; i < lines.length; i++) {
|
|
418
|
+
const trimmed = lines[i].trimStart();
|
|
419
|
+
if (/^func\s+/.test(trimmed)) {
|
|
420
|
+
const sig = collectSignature(i, '{');
|
|
421
|
+
if (sig.trim()) signatures.push(sig);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} else if (ext === '.rs') {
|
|
425
|
+
const rsPatterns = [
|
|
426
|
+
/^(pub\s+)?(async\s+)?fn\s+\w+/,
|
|
427
|
+
/^(pub\s+)?impl\s+/,
|
|
428
|
+
/^(pub\s+)?struct\s+\w+/,
|
|
429
|
+
/^(pub\s+)?trait\s+\w+/,
|
|
430
|
+
];
|
|
431
|
+
for (let i = 0; i < lines.length; i++) {
|
|
432
|
+
const trimmed = lines[i].trimStart();
|
|
433
|
+
for (const pattern of rsPatterns) {
|
|
434
|
+
if (pattern.test(trimmed)) {
|
|
435
|
+
const sig = collectSignature(i, '{');
|
|
436
|
+
if (sig.trim()) signatures.push(sig);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} else if (ext === '.java' || ext === '.cs') {
|
|
442
|
+
const javaPatterns = [
|
|
443
|
+
/^\s*(public|private|protected|static|final|abstract|synchronized|override|virtual|async)\s+/,
|
|
444
|
+
/^class\s+\w+/,
|
|
445
|
+
];
|
|
446
|
+
for (let i = 0; i < lines.length; i++) {
|
|
447
|
+
const trimmed = lines[i].trimStart();
|
|
448
|
+
if (javaPatterns.some((p) => p.test(trimmed)) && (trimmed.includes('(') || trimmed.startsWith('class '))) {
|
|
449
|
+
const sig = collectSignature(i, '{');
|
|
450
|
+
if (sig.trim()) signatures.push(sig);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const output = signatures.join('\n');
|
|
456
|
+
if (!output) {
|
|
457
|
+
console.log(`No function signatures found in ${path.basename(filePath)}`);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const extractedSize = Buffer.byteLength(output, 'utf8');
|
|
462
|
+
console.log(`// Function signatures from ${path.basename(filePath)}\n`);
|
|
463
|
+
console.log(output);
|
|
464
|
+
printSavings('functions', fullSize, extractedSize);
|
|
465
|
+
logToStats('functions', fullSize, extractedSize, path.basename(filePath));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// Command: help
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
function cmdHelp() {
|
|
472
|
+
console.log(`
|
|
473
|
+
cc-starter vibe-code — Multi-language token-saving extraction tool
|
|
474
|
+
|
|
475
|
+
Usage: node scripts/stats/vibe-code.js <command> [target]
|
|
476
|
+
|
|
477
|
+
Commands:
|
|
478
|
+
types <file> Extract TypeScript interfaces, types, enums, type aliases
|
|
479
|
+
tree [dir] Clean directory tree (default: src/ or .)
|
|
480
|
+
imports <file> Show import statements grouped (external vs local)
|
|
481
|
+
functions <file> Extract function/method signatures only
|
|
482
|
+
help Show this help message
|
|
483
|
+
|
|
484
|
+
Supported languages:
|
|
485
|
+
JS/TS (.js, .jsx, .ts, .tsx, .mjs, .cjs)
|
|
486
|
+
Python (.py)
|
|
487
|
+
Go (.go)
|
|
488
|
+
Rust (.rs)
|
|
489
|
+
Java (.java)
|
|
490
|
+
C# (.cs)
|
|
491
|
+
|
|
492
|
+
Examples:
|
|
493
|
+
node scripts/stats/vibe-code.js types src/types/user.ts
|
|
494
|
+
node scripts/stats/vibe-code.js tree src
|
|
495
|
+
node scripts/stats/vibe-code.js imports lib/utils.py
|
|
496
|
+
node scripts/stats/vibe-code.js functions main.go
|
|
497
|
+
node scripts/stats/vibe-code.js tree
|
|
498
|
+
|
|
499
|
+
Token savings are logged to .vibe-stats.json in the current directory.
|
|
500
|
+
`.trim());
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
// Main
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
const cmd = process.argv[2];
|
|
507
|
+
const target = process.argv[3];
|
|
508
|
+
|
|
509
|
+
switch (cmd) {
|
|
510
|
+
case 'types':
|
|
511
|
+
cmdTypes(target);
|
|
512
|
+
break;
|
|
513
|
+
case 'tree':
|
|
514
|
+
cmdTree(target);
|
|
515
|
+
break;
|
|
516
|
+
case 'imports':
|
|
517
|
+
cmdImports(target);
|
|
518
|
+
break;
|
|
519
|
+
case 'functions':
|
|
520
|
+
cmdFunctions(target);
|
|
521
|
+
break;
|
|
522
|
+
case 'help':
|
|
523
|
+
case '--help':
|
|
524
|
+
case '-h':
|
|
525
|
+
cmdHelp();
|
|
526
|
+
break;
|
|
527
|
+
default:
|
|
528
|
+
if (cmd) {
|
|
529
|
+
console.error(`Unknown command: ${cmd}\n`);
|
|
530
|
+
}
|
|
531
|
+
cmdHelp();
|
|
532
|
+
process.exit(cmd ? 1 : 0);
|
|
533
|
+
}
|