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.
@@ -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
+ }