@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/lib/config.js ADDED
@@ -0,0 +1,269 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Default configuration values for ContextPack
8
+ */
9
+ const DEFAULTS = {
10
+ include: ['**/*'],
11
+ exclude: [
12
+ 'node_modules/**',
13
+ '.git/**',
14
+ 'dist/**',
15
+ 'build/**',
16
+ 'coverage/**',
17
+ '.nyc_output/**',
18
+ '**/*.min.js',
19
+ '**/*.map',
20
+ ],
21
+ output: 'contextpack-output.json',
22
+ format: 'json',
23
+ maxFileSummaryLength: 500,
24
+ includeDependencyMap: true,
25
+ includeSymbolIndex: true,
26
+ verbose: false,
27
+ rootDir: process.cwd(),
28
+ };
29
+
30
+ /**
31
+ * Attempt to read and parse a JSON file from disk.
32
+ * Returns null if the file does not exist or cannot be parsed.
33
+ *
34
+ * @param {string} filePath - Absolute path to the JSON file
35
+ * @returns {object|null} Parsed JSON object or null
36
+ */
37
+ function readJSONFile(filePath) {
38
+ try {
39
+ const raw = fs.readFileSync(filePath, 'utf8');
40
+ return JSON.parse(raw);
41
+ } catch (_err) {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Locate and load the ContextPack configuration from one of the supported
48
+ * sources, in priority order:
49
+ * 1. .contextpackrc.json in the given root directory
50
+ * 2. "contextpack" key inside package.json in the given root directory
51
+ *
52
+ * If neither source is found, an empty object is returned so that defaults
53
+ * can be applied downstream.
54
+ *
55
+ * @param {string} [rootDir] - Directory to search for config files.
56
+ * Defaults to process.cwd() when not provided or falsy.
57
+ * @returns {object} Raw configuration object loaded from disk (may be empty)
58
+ */
59
+ function loadConfig(rootDir) {
60
+ const searchDir = (rootDir && typeof rootDir === 'string') ? rootDir : process.cwd();
61
+
62
+ // 1. Try .contextpackrc.json
63
+ const rcPath = path.join(searchDir, '.contextpackrc.json');
64
+ const rcConfig = readJSONFile(rcPath);
65
+ if (rcConfig !== null && typeof rcConfig === 'object') {
66
+ return normalizeConfig(rcConfig, searchDir);
67
+ }
68
+
69
+ // 2. Try "contextpack" key in package.json
70
+ const pkgPath = path.join(searchDir, 'package.json');
71
+ const pkg = readJSONFile(pkgPath);
72
+ if (pkg !== null && typeof pkg === 'object' && pkg.contextpack && typeof pkg.contextpack === 'object') {
73
+ return normalizeConfig(pkg.contextpack, searchDir);
74
+ }
75
+
76
+ // 3. No config found — return defaults
77
+ return normalizeConfig({}, searchDir);
78
+ }
79
+
80
+ /**
81
+ * Deep-merge two plain objects. Values from `override` take precedence over
82
+ * values from `base`. Arrays are replaced entirely (not concatenated) so that
83
+ * CLI flags or file config can fully replace default arrays.
84
+ *
85
+ * @param {object} base - Base object
86
+ * @param {object} override - Object whose values override base
87
+ * @returns {object} New merged object
88
+ */
89
+ function deepMerge(base, override) {
90
+ const result = Object.assign({}, base);
91
+
92
+ for (const key of Object.keys(override)) {
93
+ const overrideVal = override[key];
94
+ const baseVal = result[key];
95
+
96
+ if (
97
+ overrideVal !== null &&
98
+ typeof overrideVal === 'object' &&
99
+ !Array.isArray(overrideVal) &&
100
+ baseVal !== null &&
101
+ typeof baseVal === 'object' &&
102
+ !Array.isArray(baseVal)
103
+ ) {
104
+ result[key] = deepMerge(baseVal, overrideVal);
105
+ } else {
106
+ result[key] = overrideVal;
107
+ }
108
+ }
109
+
110
+ return result;
111
+ }
112
+
113
+ /**
114
+ * Apply defaults to a raw config object and coerce values to their expected
115
+ * types. This ensures the returned config is always fully populated and safe
116
+ * to use without further null-checks by callers.
117
+ *
118
+ * @param {object} raw - Raw config object (may be partial or empty)
119
+ * @param {string} [rootDir] - Root directory to embed in the config
120
+ * @returns {object} Normalized config object with all defaults applied
121
+ */
122
+ function normalizeConfig(raw, rootDir) {
123
+ const src = (raw !== null && typeof raw === 'object') ? raw : {};
124
+ const merged = deepMerge(DEFAULTS, src);
125
+
126
+ // Ensure rootDir is always set
127
+ if (rootDir && typeof rootDir === 'string') {
128
+ merged.rootDir = rootDir;
129
+ }
130
+ if (!merged.rootDir || typeof merged.rootDir !== 'string') {
131
+ merged.rootDir = process.cwd();
132
+ }
133
+
134
+ // Coerce arrays
135
+ if (!Array.isArray(merged.include)) {
136
+ merged.include = DEFAULTS.include.slice();
137
+ }
138
+ if (!Array.isArray(merged.exclude)) {
139
+ merged.exclude = DEFAULTS.exclude.slice();
140
+ }
141
+
142
+ // Coerce strings
143
+ if (typeof merged.output !== 'string' || !merged.output) {
144
+ merged.output = DEFAULTS.output;
145
+ }
146
+ if (typeof merged.format !== 'string' || !merged.format) {
147
+ merged.format = DEFAULTS.format;
148
+ }
149
+
150
+ // Normalise format to lowercase
151
+ merged.format = merged.format.toLowerCase();
152
+ if (!['json', 'markdown', 'md'].includes(merged.format)) {
153
+ merged.format = DEFAULTS.format;
154
+ }
155
+ // Normalise 'md' alias
156
+ if (merged.format === 'md') {
157
+ merged.format = 'markdown';
158
+ }
159
+
160
+ // Coerce numbers
161
+ const maxLen = parseInt(merged.maxFileSummaryLength, 10);
162
+ merged.maxFileSummaryLength = isNaN(maxLen) || maxLen < 0 ? DEFAULTS.maxFileSummaryLength : maxLen;
163
+
164
+ // Coerce booleans
165
+ merged.includeDependencyMap = Boolean(merged.includeDependencyMap);
166
+ merged.includeSymbolIndex = Boolean(merged.includeSymbolIndex);
167
+ merged.verbose = Boolean(merged.verbose);
168
+
169
+ return merged;
170
+ }
171
+
172
+ /**
173
+ * Merge a file-based (or default) config object with CLI flags. CLI flags
174
+ * take the highest precedence. Only flags that are explicitly provided
175
+ * (i.e. not undefined) are applied so that absent CLI flags do not
176
+ * accidentally overwrite file-based config values.
177
+ *
178
+ * Recognised flag keys and their mappings:
179
+ * flags.include → config.include (array or comma-separated string)
180
+ * flags.exclude → config.exclude (array or comma-separated string)
181
+ * flags.output → config.output
182
+ * flags.format → config.format
183
+ * flags.maxFileSummaryLength → config.maxFileSummaryLength
184
+ * flags.includeDependencyMap → config.includeDependencyMap
185
+ * flags.includeSymbolIndex → config.includeSymbolIndex
186
+ * flags.verbose → config.verbose
187
+ * flags.rootDir → config.rootDir
188
+ *
189
+ * @param {object} fileConfig - Config object produced by loadConfig()
190
+ * @param {object} [flags] - CLI flag object (e.g. from minimist or yargs).
191
+ * May be null or undefined — treated as an empty object in that case.
192
+ * @returns {object} New normalized config object with CLI flags applied
193
+ */
194
+ function mergeWithFlags(fileConfig, flags) {
195
+ const base = (fileConfig !== null && typeof fileConfig === 'object') ? fileConfig : {};
196
+ const cliFlags = (flags !== null && typeof flags === 'object') ? flags : {};
197
+
198
+ const overrides = {};
199
+
200
+ // --include
201
+ if (cliFlags.include !== undefined) {
202
+ overrides.include = normalizeArrayFlag(cliFlags.include);
203
+ }
204
+
205
+ // --exclude
206
+ if (cliFlags.exclude !== undefined) {
207
+ overrides.exclude = normalizeArrayFlag(cliFlags.exclude);
208
+ }
209
+
210
+ // --output / -o
211
+ if (cliFlags.output !== undefined && cliFlags.output !== null) {
212
+ overrides.output = String(cliFlags.output);
213
+ }
214
+
215
+ // --format / -f
216
+ if (cliFlags.format !== undefined && cliFlags.format !== null) {
217
+ overrides.format = String(cliFlags.format);
218
+ }
219
+
220
+ // --maxFileSummaryLength
221
+ if (cliFlags.maxFileSummaryLength !== undefined) {
222
+ const parsed = parseInt(cliFlags.maxFileSummaryLength, 10);
223
+ if (!isNaN(parsed) && parsed >= 0) {
224
+ overrides.maxFileSummaryLength = parsed;
225
+ }
226
+ }
227
+
228
+ // --includeDependencyMap / --no-includeDependencyMap
229
+ if (cliFlags.includeDependencyMap !== undefined) {
230
+ overrides.includeDependencyMap = Boolean(cliFlags.includeDependencyMap);
231
+ }
232
+
233
+ // --includeSymbolIndex / --no-includeSymbolIndex
234
+ if (cliFlags.includeSymbolIndex !== undefined) {
235
+ overrides.includeSymbolIndex = Boolean(cliFlags.includeSymbolIndex);
236
+ }
237
+
238
+ // --verbose / -v
239
+ if (cliFlags.verbose !== undefined) {
240
+ overrides.verbose = Boolean(cliFlags.verbose);
241
+ }
242
+
243
+ // --rootDir
244
+ if (cliFlags.rootDir !== undefined && cliFlags.rootDir !== null && typeof cliFlags.rootDir === 'string' && cliFlags.rootDir.trim() !== '') {
245
+ overrides.rootDir = cliFlags.rootDir.trim();
246
+ }
247
+
248
+ const merged = deepMerge(base, overrides);
249
+ return normalizeConfig(merged, merged.rootDir || base.rootDir);
250
+ }
251
+
252
+ /**
253
+ * Normalise a flag value that should represent an array of strings.
254
+ * Accepts an existing array, a comma-separated string, or a single string.
255
+ *
256
+ * @param {string|string[]} value - Raw flag value
257
+ * @returns {string[]} Array of trimmed, non-empty strings
258
+ */
259
+ function normalizeArrayFlag(value) {
260
+ if (Array.isArray(value)) {
261
+ return value.map(String).map(function (s) { return s.trim(); }).filter(Boolean);
262
+ }
263
+ if (typeof value === 'string') {
264
+ return value.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
265
+ }
266
+ return [];
267
+ }
268
+
269
+ module.exports = { loadConfig, mergeWithFlags };
package/lib/license.js ADDED
@@ -0,0 +1,180 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * lib/license.js
5
+ * Checks PRO_LICENSE environment variable; returns current license tier (free or pro);
6
+ * asserts pro license for gated features and shows a formatted upgrade prompt when license is absent.
7
+ *
8
+ * Part of the ContextPack product.
9
+ */
10
+
11
+ /**
12
+ * Valid license tiers
13
+ */
14
+ const TIER_FREE = 'free';
15
+ const TIER_PRO = 'pro';
16
+
17
+ /**
18
+ * Upgrade prompt lines shown when a premium feature is requested without a valid license
19
+ */
20
+ const UPGRADE_PROMPT_LINES = [
21
+ '',
22
+ '╔══════════════════════════════════════════════════════════════╗',
23
+ '║ ContextPack — Upgrade to PRO ║',
24
+ '╠══════════════════════════════════════════════════════════════╣',
25
+ '║ This feature requires a ContextPack PRO license. ║',
26
+ '║ ║',
27
+ '║ PRO features include: ║',
28
+ '║ • Watch mode — auto-regenerate bundles on file change ║',
29
+ '║ • HTML report — rich visual dependency & token report ║',
30
+ '║ • Enhanced config — per-directory rules & filters ║',
31
+ '║ ║',
32
+ '║ To activate PRO, set the environment variable: ║',
33
+ '║ PRO_LICENSE=<your-license-key> ║',
34
+ '║ ║',
35
+ '║ Get your license at: https://contextpack.dev/pro ║',
36
+ '╚══════════════════════════════════════════════════════════════╝',
37
+ '',
38
+ ];
39
+
40
+ /**
41
+ * Determine whether the supplied license key string is considered valid.
42
+ * A key is valid when it is a non-empty string after trimming whitespace.
43
+ *
44
+ * @param {*} key - Raw value of the PRO_LICENSE environment variable
45
+ * @returns {boolean} True when the key is a non-empty string
46
+ */
47
+ function isValidKey(key) {
48
+ return typeof key === 'string' && key.trim().length > 0;
49
+ }
50
+
51
+ /**
52
+ * Returns the current license status based on the PRO_LICENSE environment variable.
53
+ *
54
+ * @returns {{ tier: string, isProLicensed: boolean, licenseKey: string|null }}
55
+ * An object describing the current license state:
56
+ * - tier: 'pro' when a valid PRO_LICENSE is present, otherwise 'free'
57
+ * - isProLicensed: boolean shorthand for tier === 'pro'
58
+ * - licenseKey: the trimmed license key string, or null when absent/invalid
59
+ */
60
+ function getLicenseStatus() {
61
+ const raw = process.env.PRO_LICENSE;
62
+
63
+ if (isValidKey(raw)) {
64
+ return {
65
+ tier: TIER_PRO,
66
+ isProLicensed: true,
67
+ licenseKey: raw.trim(),
68
+ };
69
+ }
70
+
71
+ return {
72
+ tier: TIER_FREE,
73
+ isProLicensed: false,
74
+ licenseKey: null,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Builds and returns the formatted upgrade prompt string.
80
+ * The prompt is also printed to stderr so it is visible in terminal output
81
+ * without polluting stdout (which may carry JSON or Markdown bundle output).
82
+ *
83
+ * @param {object} [options] - Optional display options
84
+ * @param {string} [options.featureName] - Name of the premium feature that was requested;
85
+ * when provided it is included in the prompt header for context.
86
+ * @param {boolean} [options.printToStderr=true] - When true (default) the prompt is
87
+ * written to process.stderr in addition to being returned as a string.
88
+ * @returns {string} The formatted upgrade prompt text
89
+ */
90
+ function showUpgradePrompt(options) {
91
+ const opts = options || {};
92
+ const featureName = (typeof opts.featureName === 'string' && opts.featureName.trim().length > 0)
93
+ ? opts.featureName.trim()
94
+ : null;
95
+ const printToStderr = opts.printToStderr !== false;
96
+
97
+ const lines = UPGRADE_PROMPT_LINES.slice();
98
+
99
+ if (featureName) {
100
+ // Insert a feature-specific line after the first separator
101
+ const insertIndex = 4;
102
+ lines.splice(insertIndex, 0, '║ Requested feature: ' + padRight(featureName, 41) + '║');
103
+ }
104
+
105
+ const prompt = lines.join('\n');
106
+
107
+ if (printToStderr) {
108
+ try {
109
+ process.stderr.write(prompt + '\n');
110
+ } catch (_err) {
111
+ // Silently ignore write errors (e.g. in test environments with closed stderr)
112
+ }
113
+ }
114
+
115
+ return prompt;
116
+ }
117
+
118
+ /**
119
+ * Asserts that a valid PRO_LICENSE is present in the environment.
120
+ * When the license is absent or invalid, displays the upgrade prompt and
121
+ * throws an Error so that callers can handle the gate gracefully.
122
+ *
123
+ * @param {object} [options] - Optional options forwarded to showUpgradePrompt
124
+ * @param {string} [options.featureName] - Name of the premium feature being gated
125
+ * @param {boolean} [options.printToStderr=true] - Whether to print the prompt to stderr
126
+ * @throws {Error} When no valid PRO_LICENSE is found
127
+ * @returns {void} Returns normally when a valid license is present
128
+ */
129
+ function assertProLicense(options) {
130
+ const opts = options || {};
131
+ const status = getLicenseStatus();
132
+
133
+ if (status.isProLicensed) {
134
+ return;
135
+ }
136
+
137
+ showUpgradePrompt(opts);
138
+
139
+ const featureName = (typeof opts.featureName === 'string' && opts.featureName.trim().length > 0)
140
+ ? opts.featureName.trim()
141
+ : 'this feature';
142
+
143
+ throw new Error(
144
+ 'ContextPack PRO license required to use ' + featureName + '. ' +
145
+ 'Set the PRO_LICENSE environment variable to your license key. ' +
146
+ 'Visit https://contextpack.dev/pro to obtain a license.'
147
+ );
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Internal helpers
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /**
155
+ * Right-pads a string with spaces to the given total length.
156
+ * Truncates with an ellipsis if the string exceeds the target length.
157
+ *
158
+ * @param {string} str - The string to pad
159
+ * @param {number} length - Desired total length
160
+ * @returns {string} Padded (or truncated) string
161
+ */
162
+ function padRight(str, length) {
163
+ if (typeof str !== 'string') {
164
+ str = String(str);
165
+ }
166
+ if (str.length > length) {
167
+ return str.slice(0, length - 1) + '…';
168
+ }
169
+ return str + ' '.repeat(length - str.length);
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Exports
174
+ // ---------------------------------------------------------------------------
175
+
176
+ module.exports = {
177
+ getLicenseStatus,
178
+ assertProLicense,
179
+ showUpgradePrompt,
180
+ };