@craftpipe/contextpack 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/index.js ADDED
@@ -0,0 +1,428 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+
8
+ const { loadConfig, mergeWithFlags } = require('./lib/config.js');
9
+ const { scanProject, buildFileTree, filterFiles } = require('./lib/scanner.js');
10
+ const { analyzeFile, analyzeProject, extractSymbols, buildDependencyGraph } = require('./lib/analyzer.js');
11
+ const { createBundle, formatAsJSON, formatAsMarkdown } = require('./lib/bundler.js');
12
+ const { validateBundle, checkCircularDependencies, checkSymbolConflicts } = require('./lib/validator.js');
13
+ const { estimateTokens, calculateBundleSize, estimateSavings } = require('./lib/tokenizer.js');
14
+ const { getLicenseStatus, assertProLicense, showUpgradePrompt } = require('./lib/license.js');
15
+
16
+ const TOOL_NAME = 'contextpack';
17
+ const VERSION = (() => {
18
+ try {
19
+ return require('./package.json').version;
20
+ } catch {
21
+ return '0.0.0';
22
+ }
23
+ })();
24
+
25
+ function printHelp() {
26
+ console.log(`
27
+ ${TOOL_NAME} v${VERSION}
28
+ Analyze your codebase and generate compact, token-optimized context bundles for AI coding sessions.
29
+
30
+ USAGE:
31
+ ${TOOL_NAME} [command] [options]
32
+
33
+ COMMANDS:
34
+ scan Scan project structure and build file tree
35
+ analyze Analyze files, extract symbols and dependencies
36
+ bundle Generate a context bundle from analysis
37
+ validate Validate an existing context bundle
38
+ run Run the full pipeline: scan → summarize → bundle → validate
39
+ info Show token estimates and bundle size info for a bundle file
40
+
41
+ OPTIONS:
42
+ -d, --dir <path> Root directory to scan (default: current working directory)
43
+ -o, --output <file> Output file path (default: contextpack.json)
44
+ -f, --format <fmt> Output format: json | markdown (default: json)
45
+ -i, --include <globs> Comma-separated glob patterns to include
46
+ -e, --exclude <globs> Comma-separated glob patterns to exclude
47
+ -t, --types <exts> Comma-separated file extensions to include (e.g. js,ts,py)
48
+ --config <file> Path to config file (default: .contextpackrc or contextpack.config.js)
49
+ --no-validate Skip validation step in full pipeline
50
+ --no-symbols Skip symbol extraction
51
+ --no-deps Skip dependency graph generation
52
+ --pro Assert Pro license features
53
+ -v, --version Print version number
54
+ -h, --help Show this help message
55
+
56
+ EXAMPLES:
57
+ # Run full pipeline on current directory
58
+ ${TOOL_NAME} run
59
+
60
+ # Scan a specific directory and output as markdown
61
+ ${TOOL_NAME} run --dir ./src --format markdown --output context.md
62
+
63
+ # Only scan and analyze, skip bundling
64
+ ${TOOL_NAME} analyze --dir ./src --output analysis.json
65
+
66
+ # Validate an existing bundle
67
+ ${TOOL_NAME} validate --dir . contextpack.json
68
+
69
+ # Get token estimates for a bundle
70
+ ${TOOL_NAME} info contextpack.json
71
+
72
+ # Include only JS and TS files, exclude tests
73
+ ${TOOL_NAME} run --types js,ts --exclude "**/*.test.*,**/__tests__/**"
74
+
75
+ # Use a custom config file
76
+ ${TOOL_NAME} run --config ./my-contextpack.config.js
77
+ `);
78
+ }
79
+
80
+ function printVersion() {
81
+ console.log(`${TOOL_NAME} v${VERSION}`);
82
+ }
83
+
84
+ function parseArgs(argv) {
85
+ const args = argv.slice(2);
86
+ const flags = {
87
+ command: null,
88
+ dir: process.cwd(),
89
+ output: 'contextpack.json',
90
+ format: 'json',
91
+ include: null,
92
+ exclude: null,
93
+ types: null,
94
+ config: null,
95
+ validate: true,
96
+ symbols: true,
97
+ deps: true,
98
+ pro: false,
99
+ help: false,
100
+ version: false,
101
+ positional: [],
102
+ };
103
+
104
+ const commands = new Set(['scan', 'analyze', 'bundle', 'validate', 'run', 'info']);
105
+
106
+ let i = 0;
107
+ while (i < args.length) {
108
+ const arg = args[i];
109
+
110
+ if (arg === '-h' || arg === '--help') {
111
+ flags.help = true;
112
+ } else if (arg === '-v' || arg === '--version') {
113
+ flags.version = true;
114
+ } else if (arg === '--no-validate') {
115
+ flags.validate = false;
116
+ } else if (arg === '--no-symbols') {
117
+ flags.symbols = false;
118
+ } else if (arg === '--no-deps') {
119
+ flags.deps = false;
120
+ } else if (arg === '--pro') {
121
+ flags.pro = true;
122
+ } else if ((arg === '-d' || arg === '--dir') && args[i + 1]) {
123
+ flags.dir = path.resolve(args[++i]);
124
+ } else if ((arg === '-o' || arg === '--output') && args[i + 1]) {
125
+ flags.output = args[++i];
126
+ } else if ((arg === '-f' || arg === '--format') && args[i + 1]) {
127
+ flags.format = args[++i];
128
+ } else if ((arg === '-i' || arg === '--include') && args[i + 1]) {
129
+ flags.include = args[++i].split(',').map(s => s.trim());
130
+ } else if ((arg === '-e' || arg === '--exclude') && args[i + 1]) {
131
+ flags.exclude = args[++i].split(',').map(s => s.trim());
132
+ } else if ((arg === '-t' || arg === '--types') && args[i + 1]) {
133
+ flags.types = args[++i].split(',').map(s => s.trim().replace(/^\./, ''));
134
+ } else if (arg === '--config' && args[i + 1]) {
135
+ flags.config = path.resolve(args[++i]);
136
+ } else if (!arg.startsWith('-')) {
137
+ if (commands.has(arg) && flags.command === null) {
138
+ flags.command = arg;
139
+ } else {
140
+ flags.positional.push(arg);
141
+ }
142
+ } else {
143
+ console.warn(`Warning: Unknown flag "${arg}" — ignoring.`);
144
+ }
145
+
146
+ i++;
147
+ }
148
+
149
+ return flags;
150
+ }
151
+
152
+ function resolveOutputPath(flags) {
153
+ return path.resolve(flags.dir, flags.output);
154
+ }
155
+
156
+ function writeOutput(content, outputPath) {
157
+ const dir = path.dirname(outputPath);
158
+ if (!fs.existsSync(dir)) {
159
+ fs.mkdirSync(dir, { recursive: true });
160
+ }
161
+ fs.writeFileSync(outputPath, content, 'utf8');
162
+ }
163
+
164
+ async function cmdScan(flags, config) {
165
+ console.log(`[scan] Scanning project at: ${flags.dir}`);
166
+
167
+ const rawFiles = await scanProject(flags.dir, config);
168
+ const fileTree = buildFileTree(rawFiles, flags.dir);
169
+
170
+ const filterOptions = {
171
+ include: flags.include || config.include,
172
+ exclude: flags.exclude || config.exclude,
173
+ types: flags.types || config.types,
174
+ };
175
+
176
+ const filteredFiles = filterFiles(rawFiles, filterOptions);
177
+
178
+ console.log(`[scan] Found ${rawFiles.length} files, ${filteredFiles.length} after filtering.`);
179
+
180
+ return { rawFiles, fileTree, filteredFiles };
181
+ }
182
+
183
+ async function cmdAnalyze(flags, config, filteredFiles) {
184
+ console.log(`[analyze] Analyzing ${filteredFiles.length} files...`);
185
+
186
+ const projectAnalysis = await analyzeProject(filteredFiles, config);
187
+
188
+ let symbols = null;
189
+ if (flags.symbols) {
190
+ console.log('[analyze] Extracting symbols...');
191
+ symbols = await extractSymbols(filteredFiles, config);
192
+ }
193
+
194
+ let depGraph = null;
195
+ if (flags.deps) {
196
+ console.log('[analyze] Building dependency graph...');
197
+ depGraph = await buildDependencyGraph(filteredFiles, config);
198
+ }
199
+
200
+ return { projectAnalysis, symbols, depGraph };
201
+ }
202
+
203
+ async function cmdBundle(flags, config, scanResult, analyzeResult) {
204
+ console.log(`[bundle] Creating bundle (format: ${flags.format})...`);
205
+
206
+ const bundleInput = {
207
+ fileTree: scanResult.fileTree,
208
+ filteredFiles: scanResult.filteredFiles,
209
+ projectAnalysis: analyzeResult.projectAnalysis,
210
+ symbols: analyzeResult.symbols,
211
+ depGraph: analyzeResult.depGraph,
212
+ meta: {
213
+ generatedAt: new Date().toISOString(),
214
+ rootDir: flags.dir,
215
+ version: VERSION,
216
+ format: flags.format,
217
+ },
218
+ };
219
+
220
+ const bundle = await createBundle(bundleInput, config);
221
+
222
+ let formatted;
223
+ if (flags.format === 'markdown') {
224
+ formatted = formatAsMarkdown(bundle, config);
225
+ } else {
226
+ formatted = formatAsJSON(bundle, config);
227
+ }
228
+
229
+ const outputPath = resolveOutputPath(flags);
230
+ writeOutput(formatted, outputPath);
231
+ console.log(`[bundle] Bundle written to: ${outputPath}`);
232
+
233
+ return { bundle, formatted, outputPath };
234
+ }
235
+
236
+ async function cmdValidate(flags, config, bundle) {
237
+ console.log('[validate] Validating bundle...');
238
+
239
+ const validationResult = await validateBundle(bundle, config);
240
+
241
+ let circularResult = null;
242
+ if (bundle.depGraph) {
243
+ circularResult = await checkCircularDependencies(bundle.depGraph, config);
244
+ if (circularResult.hasCircular) {
245
+ console.warn(`[validate] Warning: ${circularResult.cycles.length} circular dependency cycle(s) detected.`);
246
+ } else {
247
+ console.log('[validate] No circular dependencies found.');
248
+ }
249
+ }
250
+
251
+ let conflictResult = null;
252
+ if (bundle.symbols) {
253
+ conflictResult = await checkSymbolConflicts(bundle.symbols, config);
254
+ if (conflictResult.hasConflicts) {
255
+ console.warn(`[validate] Warning: ${conflictResult.conflicts.length} symbol conflict(s) detected.`);
256
+ } else {
257
+ console.log('[validate] No symbol conflicts found.');
258
+ }
259
+ }
260
+
261
+ if (validationResult.valid) {
262
+ console.log('[validate] Bundle is valid.');
263
+ } else {
264
+ console.warn(`[validate] Bundle validation issues: ${validationResult.errors.length} error(s).`);
265
+ validationResult.errors.forEach(err => console.warn(` - ${err}`));
266
+ }
267
+
268
+ return { validationResult, circularResult, conflictResult };
269
+ }
270
+
271
+ async function cmdInfo(flags, config) {
272
+ const targetFile = flags.positional[0] || resolveOutputPath(flags);
273
+ const resolvedTarget = path.resolve(targetFile);
274
+
275
+ if (!fs.existsSync(resolvedTarget)) {
276
+ throw new Error(`Bundle file not found: ${resolvedTarget}`);
277
+ }
278
+
279
+ console.log(`[info] Reading bundle: ${resolvedTarget}`);
280
+ const content = fs.readFileSync(resolvedTarget, 'utf8');
281
+
282
+ const tokenCount = estimateTokens(content);
283
+ const bundleSize = calculateBundleSize(content);
284
+ const savings = estimateSavings(content, config);
285
+
286
+ console.log(`\nBundle Info:`);
287
+ console.log(` File: ${resolvedTarget}`);
288
+ console.log(` Size: ${(bundleSize / 1024).toFixed(2)} KB`);
289
+ console.log(` Estimated tokens: ${tokenCount.toLocaleString()}`);
290
+ console.log(` Estimated savings: ${savings.percentage}% vs raw source (${savings.rawTokens.toLocaleString()} raw tokens)`);
291
+ console.log('');
292
+ }
293
+
294
+ async function cmdRun(flags, config) {
295
+ console.log(`[run] Starting full pipeline for: ${flags.dir}`);
296
+
297
+ const scanResult = await cmdScan(flags, config);
298
+ const analyzeResult = await cmdAnalyze(flags, config, scanResult.filteredFiles);
299
+ const bundleResult = await cmdBundle(flags, config, scanResult, analyzeResult);
300
+
301
+ if (flags.validate) {
302
+ await cmdValidate(flags, config, bundleResult.bundle);
303
+ } else {
304
+ console.log('[run] Skipping validation (--no-validate).');
305
+ }
306
+
307
+ const tokenCount = estimateTokens(bundleResult.formatted);
308
+ const savings = estimateSavings(bundleResult.formatted, config);
309
+
310
+ console.log(`\n✓ ContextPack complete!`);
311
+ console.log(` Output: ${bundleResult.outputPath}`);
312
+ console.log(` Files bundled: ${scanResult.filteredFiles.length}`);
313
+ console.log(` Estimated tokens: ${tokenCount.toLocaleString()}`);
314
+ console.log(` Token savings: ~${savings.percentage}% vs raw source`);
315
+ console.log('');
316
+ }
317
+
318
+ async function main() {
319
+ const flags = parseArgs(process.argv);
320
+
321
+ if (flags.help || (!flags.command && flags.positional.length === 0)) {
322
+ printHelp();
323
+ process.exit(0);
324
+ }
325
+
326
+ if (flags.version) {
327
+ printVersion();
328
+ process.exit(0);
329
+ }
330
+
331
+ let config;
332
+ try {
333
+ const rawConfig = loadConfig(flags.config || flags.dir);
334
+ config = mergeWithFlags(rawConfig, flags);
335
+ } catch (err) {
336
+ console.warn(`[config] Could not load config file: ${err.message}. Using defaults.`);
337
+ config = mergeWithFlags({}, flags);
338
+ }
339
+
340
+ if (flags.pro) {
341
+ try {
342
+ assertProLicense(config);
343
+ } catch (err) {
344
+ console.error(`[license] Pro feature required: ${err.message}`);
345
+ showUpgradePrompt();
346
+ process.exit(1);
347
+ }
348
+ } else {
349
+ const licenseStatus = getLicenseStatus(config);
350
+ if (licenseStatus && licenseStatus.type === 'pro') {
351
+ console.log(`[license] Pro license active.`);
352
+ }
353
+ }
354
+
355
+ const command = flags.command || 'run';
356
+
357
+ try {
358
+ switch (command) {
359
+ case 'scan': {
360
+ const result = await cmdScan(flags, config);
361
+ const outputPath = resolveOutputPath(flags);
362
+ const out = JSON.stringify({ fileTree: result.fileTree, filteredFiles: result.filteredFiles }, null, 2);
363
+ writeOutput(out, outputPath);
364
+ console.log(`[scan] Scan result written to: ${outputPath}`);
365
+ break;
366
+ }
367
+
368
+ case 'analyze': {
369
+ const scanResult = await cmdScan(flags, config);
370
+ const analyzeResult = await cmdAnalyze(flags, config, scanResult.filteredFiles);
371
+ const outputPath = resolveOutputPath(flags);
372
+ const out = JSON.stringify(analyzeResult, null, 2);
373
+ writeOutput(out, outputPath);
374
+ console.log(`[analyze] Analysis written to: ${outputPath}`);
375
+ break;
376
+ }
377
+
378
+ case 'bundle': {
379
+ const scanResult = await cmdScan(flags, config);
380
+ const analyzeResult = await cmdAnalyze(flags, config, scanResult.filteredFiles);
381
+ await cmdBundle(flags, config, scanResult, analyzeResult);
382
+ break;
383
+ }
384
+
385
+ case 'validate': {
386
+ const targetFile = flags.positional[0] || resolveOutputPath(flags);
387
+ const resolvedTarget = path.resolve(targetFile);
388
+
389
+ if (!fs.existsSync(resolvedTarget)) {
390
+ throw new Error(`Bundle file not found: ${resolvedTarget}`);
391
+ }
392
+
393
+ const content = fs.readFileSync(resolvedTarget, 'utf8');
394
+ let bundle;
395
+ try {
396
+ bundle = JSON.parse(content);
397
+ } catch {
398
+ throw new Error(`Could not parse bundle as JSON: ${resolvedTarget}`);
399
+ }
400
+
401
+ await cmdValidate(flags, config, bundle);
402
+ break;
403
+ }
404
+
405
+ case 'info': {
406
+ await cmdInfo(flags, config);
407
+ break;
408
+ }
409
+
410
+ case 'run': {
411
+ await cmdRun(flags, config);
412
+ break;
413
+ }
414
+
415
+ default:
416
+ console.error(`Unknown command: "${command}". Run "${TOOL_NAME} --help" for usage.`);
417
+ process.exit(1);
418
+ }
419
+ } catch (err) {
420
+ console.error(`\n[error] ${err.message}`);
421
+ if (process.env.DEBUG) {
422
+ console.error(err.stack);
423
+ }
424
+ process.exit(1);
425
+ }
426
+ }
427
+
428
+ main();