@luquimbo/bi-superpowers 1.2.2 → 2.0.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.
Files changed (48) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.claude-plugin/skill-manifest.json +1 -2
  4. package/.plugin/plugin.json +1 -1
  5. package/AGENTS.md +2 -3
  6. package/CHANGELOG.md +25 -0
  7. package/README.md +14 -5
  8. package/bin/cli.js +26 -246
  9. package/bin/commands/build-desktop.js +3 -1
  10. package/bin/commands/diff.js +3 -1
  11. package/bin/commands/install.js +4 -6
  12. package/bin/commands/lint.js +3 -1
  13. package/bin/commands/search.js +1 -1
  14. package/bin/commands/watch.js +3 -1
  15. package/bin/lib/generators/claude-plugin.js +2 -1
  16. package/bin/lib/generators/shared.js +0 -31
  17. package/bin/lib/skills.js +8 -34
  18. package/bin/postinstall.js +20 -18
  19. package/package.json +1 -1
  20. package/skills/contributions/SKILL.md +1 -1
  21. package/skills/data-model-design/SKILL.md +1 -1
  22. package/skills/data-modeling/SKILL.md +1 -1
  23. package/skills/data-quality/SKILL.md +1 -1
  24. package/skills/dax/SKILL.md +1 -1
  25. package/skills/dax-doctor/SKILL.md +1 -1
  26. package/skills/dax-udf/SKILL.md +1 -1
  27. package/skills/deployment/SKILL.md +1 -1
  28. package/skills/excel-formulas/SKILL.md +1 -1
  29. package/skills/fabric-scripts/SKILL.md +1 -1
  30. package/skills/fast-standard/SKILL.md +1 -1
  31. package/skills/governance/SKILL.md +1 -1
  32. package/skills/migration-assistant/SKILL.md +1 -1
  33. package/skills/model-documenter/SKILL.md +1 -1
  34. package/skills/pbi-connect/SKILL.md +1 -1
  35. package/skills/power-query/SKILL.md +1 -1
  36. package/skills/project-kickoff/SKILL.md +1 -1
  37. package/skills/query-performance/SKILL.md +1 -1
  38. package/skills/report-design/SKILL.md +1 -1
  39. package/skills/report-layout/SKILL.md +1 -1
  40. package/skills/rls-design/SKILL.md +1 -1
  41. package/skills/semantic-model/SKILL.md +1 -1
  42. package/skills/testing-validation/SKILL.md +1 -1
  43. package/skills/theme-tweaker/SKILL.md +1 -1
  44. package/bin/lib/licensing/index.js +0 -35
  45. package/bin/lib/licensing/storage.js +0 -404
  46. package/bin/lib/licensing/storage.test.js +0 -55
  47. package/bin/lib/licensing/validator.js +0 -213
  48. package/bin/lib/licensing/validator.test.js +0 -137
@@ -1,404 +0,0 @@
1
- /**
2
- * License Storage Module
3
- * ======================
4
- *
5
- * Handles license persistence and premium content management.
6
- * Licenses are stored in ~/.bi-superpowers-license as JSON.
7
- *
8
- * @module lib/licensing/storage
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
- const os = require('os');
14
- const https = require('https');
15
- const AdmZip = require('adm-zip');
16
-
17
- /** Path to the license file stored in user's home directory */
18
- const LICENSE_FILE = path.join(os.homedir(), '.bi-superpowers-license');
19
-
20
- /** Directory for cached premium content (skills, snippets, themes) */
21
- const CONTENT_CACHE_DIR = path.join(os.homedir(), '.bi-superpowers');
22
-
23
- /** Directory containing skill definition files (.md) */
24
- const SKILLS_DIR = path.join(CONTENT_CACHE_DIR, '.agents', 'prompts', 'skills');
25
-
26
- /**
27
- * Load saved license from disk
28
- *
29
- * Reads the license file from the user's home directory. The license contains:
30
- * - license: The license key string
31
- * - email: User's email address
32
- * - name: User's name (optional)
33
- * - activatedAt: ISO timestamp of activation
34
- * - contentVersion: Version when content was last downloaded
35
- *
36
- * @returns {Object|null} License data object or null if not found/invalid
37
- */
38
- function loadLicense() {
39
- try {
40
- if (fs.existsSync(LICENSE_FILE)) {
41
- const data = JSON.parse(fs.readFileSync(LICENSE_FILE, 'utf8'));
42
- return data;
43
- }
44
- } catch (e) {
45
- if (process.env.DEBUG === 'true') {
46
- console.error(`[DEBUG] Failed to load license: ${e.message}`);
47
- }
48
- }
49
- return null;
50
- }
51
-
52
- /**
53
- * Save license to disk
54
- *
55
- * Persists license data to ~/.bi-superpowers-license for future sessions.
56
- *
57
- * @param {Object} data - License data to save
58
- * @param {string} data.license - The license key
59
- * @param {string} data.email - User's email
60
- * @param {string} [data.name] - User's name
61
- * @param {string} data.activatedAt - Activation timestamp
62
- * @param {string} data.contentVersion - Content version
63
- */
64
- function saveLicense(data) {
65
- try {
66
- fs.writeFileSync(LICENSE_FILE, JSON.stringify(data, null, 2));
67
- } catch (err) {
68
- throw new Error(
69
- `No pude guardar la licencia en ${LICENSE_FILE}: ${err.message}. ` +
70
- 'Revisá los permisos de tu directorio home.'
71
- );
72
- }
73
- }
74
-
75
- /**
76
- * Clear saved license from disk
77
- *
78
- * Removes the license file, effectively logging out the user.
79
- * Used when a license becomes invalid or user wants to switch accounts.
80
- */
81
- function clearLicense() {
82
- try {
83
- if (fs.existsSync(LICENSE_FILE)) {
84
- fs.unlinkSync(LICENSE_FILE);
85
- }
86
- } catch (e) {
87
- if (process.env.DEBUG === 'true') {
88
- console.error(`[DEBUG] Failed to clear license: ${e.message}`);
89
- }
90
- }
91
- }
92
-
93
- /**
94
- * Check if premium content is installed
95
- *
96
- * Verifies that the skills directory exists and contains at least one .md file.
97
- * Used to determine if content needs to be downloaded during license validation.
98
- *
99
- * @returns {boolean} True if skills directory exists with content
100
- */
101
- function isContentInstalled() {
102
- return (
103
- fs.existsSync(SKILLS_DIR) &&
104
- fs.readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md')).length > 0
105
- );
106
- }
107
-
108
- /**
109
- * Download file from URL to local filesystem
110
- *
111
- * Streams the response directly to a file to handle large downloads efficiently.
112
- * Automatically follows HTTP 301/302 redirects.
113
- *
114
- * @param {string} url - URL of the file to download
115
- * @param {string} destPath - Local path where file will be saved
116
- * @returns {Promise<void>} Resolves when download is complete
117
- * @throws {Error} On network errors or non-200 status codes
118
- */
119
- function downloadFile(url, destPath) {
120
- return downloadFileInternal(url, destPath, 0);
121
- }
122
-
123
- /**
124
- * Resolve a redirect Location header to an absolute URL.
125
- *
126
- * @param {string|URL} currentUrl - The current request URL
127
- * @param {string} location - The Location header value
128
- * @returns {string} Absolute URL string
129
- */
130
- function resolveRedirectUrl(currentUrl, location) {
131
- const baseUrl = currentUrl instanceof URL ? currentUrl : new URL(currentUrl);
132
- return new URL(location, baseUrl).toString();
133
- }
134
-
135
- function safeUnlink(filePath) {
136
- try {
137
- fs.unlinkSync(filePath);
138
- } catch (e) {
139
- // Ignore missing file or cleanup failures
140
- }
141
- }
142
-
143
- function downloadFileInternal(url, destPath, redirectCount) {
144
- return new Promise((resolve, reject) => {
145
- const urlObj = new URL(url);
146
- if (urlObj.protocol !== 'https:') {
147
- reject(new Error(`Insecure download URL (HTTPS required): ${url}`));
148
- return;
149
- }
150
-
151
- const MAX_REDIRECTS = 5;
152
- const TIMEOUT_MS = 30000;
153
- const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
154
-
155
- const request = https.get(urlObj, (response) => {
156
- const status = response.statusCode || 0;
157
-
158
- // Handle redirects (including relative Location headers)
159
- if (REDIRECT_STATUSES.has(status)) {
160
- response.resume();
161
-
162
- if (redirectCount >= MAX_REDIRECTS) {
163
- reject(new Error(`Download failed: too many redirects (${MAX_REDIRECTS})`));
164
- return;
165
- }
166
-
167
- const location = response.headers.location;
168
- if (!location) {
169
- reject(new Error(`Download failed: redirect (${status}) without Location header`));
170
- return;
171
- }
172
-
173
- const nextUrl = resolveRedirectUrl(urlObj, location);
174
- downloadFileInternal(nextUrl, destPath, redirectCount + 1)
175
- .then(resolve)
176
- .catch(reject);
177
- return;
178
- }
179
-
180
- if (status !== 200) {
181
- response.resume();
182
- reject(new Error(`Download failed with status ${status}`));
183
- return;
184
- }
185
-
186
- // Only create the output file once we know the response is successful.
187
- const file = fs.createWriteStream(destPath);
188
-
189
- file.on('error', (err) => {
190
- response.destroy();
191
- file.close(() => {
192
- safeUnlink(destPath);
193
- reject(err);
194
- });
195
- });
196
-
197
- response.on('error', (err) => {
198
- file.close(() => {
199
- safeUnlink(destPath);
200
- reject(err);
201
- });
202
- });
203
-
204
- response.pipe(file);
205
-
206
- file.on('finish', () => {
207
- file.close(() => resolve());
208
- });
209
- });
210
-
211
- request.setTimeout(TIMEOUT_MS, () => {
212
- request.destroy(new Error(`Download request timed out after ${TIMEOUT_MS}ms`));
213
- });
214
-
215
- request.on('error', (err) => {
216
- safeUnlink(destPath);
217
- reject(err);
218
- });
219
- });
220
- }
221
-
222
- /**
223
- * Extract ZIP file to destination directory
224
- * Uses native unzip command (macOS/Linux) or PowerShell (Windows)
225
- *
226
- * Security: Uses spawnSync with array arguments to prevent command injection
227
- *
228
- * @param {string} zipPath - Path to the ZIP file to extract
229
- * @param {string} destDir - Destination directory for extraction
230
- * @returns {Promise<void>} Resolves when extraction is complete
231
- * @throws {Error} If extraction fails
232
- */
233
- function isUnsafeZipEntry(entryName, destRoot) {
234
- if (!entryName) return true;
235
-
236
- const normalized = entryName.replace(/\\/g, '/');
237
-
238
- // Disallow absolute paths and Windows drive letters
239
- if (normalized.startsWith('/') || normalized.startsWith('\\')) return true;
240
- if (/^[A-Za-z]:/.test(normalized)) return true;
241
- if (normalized.startsWith('//')) return true;
242
-
243
- const targetPath = path.resolve(destRoot, entryName);
244
- const relative = path.relative(destRoot, targetPath);
245
-
246
- return relative.startsWith('..') || path.isAbsolute(relative);
247
- }
248
-
249
- /**
250
- * Verifica que el archivo tenga los magic bytes de un ZIP válido.
251
- * Los ZIP empiezan con la signature PK\x03\x04 (o PK\x05\x06 si está vacío).
252
- * @param {string} zipPath - Ruta al archivo a verificar
253
- * @throws {Error} Si el archivo no tiene signature de ZIP
254
- */
255
- function verifyZipMagicBytes(zipPath) {
256
- const header = Buffer.alloc(4);
257
- const fd = fs.openSync(zipPath, 'r');
258
- try {
259
- fs.readSync(fd, header, 0, 4, 0);
260
- } finally {
261
- fs.closeSync(fd);
262
- }
263
-
264
- // ZIP local file header: PK\x03\x04
265
- // ZIP empty archive: PK\x05\x06
266
- const isLocalFile =
267
- header[0] === 0x50 && header[1] === 0x4b && header[2] === 0x03 && header[3] === 0x04;
268
- const isEmpty =
269
- header[0] === 0x50 && header[1] === 0x4b && header[2] === 0x05 && header[3] === 0x06;
270
-
271
- if (!isLocalFile && !isEmpty) {
272
- throw new Error(
273
- `El archivo descargado no es un ZIP válido (magic bytes inesperados: ${header.toString('hex')}). ` +
274
- 'Puede que el servidor haya retornado HTML u otro formato.'
275
- );
276
- }
277
- }
278
-
279
- function extractZip(zipPath, destDir) {
280
- return new Promise((resolve, reject) => {
281
- try {
282
- // Ensure destination directory exists
283
- if (!fs.existsSync(destDir)) {
284
- fs.mkdirSync(destDir, { recursive: true });
285
- }
286
-
287
- // Verify magic bytes before handing to AdmZip
288
- verifyZipMagicBytes(zipPath);
289
-
290
- const destRoot = path.resolve(destDir);
291
- const zip = new AdmZip(zipPath);
292
- const entries = zip.getEntries();
293
-
294
- for (const entry of entries) {
295
- if (isUnsafeZipEntry(entry.entryName, destRoot)) {
296
- throw new Error(`Unsafe ZIP entry detected: ${entry.entryName}`);
297
- }
298
- }
299
-
300
- zip.extractAllTo(destRoot, true);
301
-
302
- resolve();
303
- } catch (error) {
304
- reject(new Error(`Failed to extract ZIP safely: ${error.message}`));
305
- }
306
- });
307
- }
308
-
309
- /**
310
- * Download and install premium content from license server
311
- *
312
- * This function:
313
- * 1. Creates the content cache directory if needed
314
- * 2. Downloads a ZIP file containing skills, snippets, themes, etc.
315
- * 3. Extracts the ZIP to ~/.bi-superpowers/
316
- * 4. Cleans up the temporary ZIP file
317
- * 5. Verifies the skills directory was created correctly
318
- *
319
- * @param {string} downloadUrl - URL provided by license validation API
320
- * @returns {Promise<boolean>} True if download and extraction succeeded
321
- */
322
- async function downloadPremiumContent(downloadUrl) {
323
- console.log('Downloading premium content...');
324
-
325
- // Ensure cache directory exists
326
- if (!fs.existsSync(CONTENT_CACHE_DIR)) {
327
- fs.mkdirSync(CONTENT_CACHE_DIR, { recursive: true });
328
- }
329
-
330
- const zipPath = path.join(CONTENT_CACHE_DIR, 'content.zip');
331
-
332
- try {
333
- // Download the ZIP file
334
- await downloadFile(downloadUrl, zipPath);
335
- console.log('✓ Download complete');
336
-
337
- // Extract the ZIP file
338
- console.log('Extracting content...');
339
- await extractZip(zipPath, CONTENT_CACHE_DIR);
340
- console.log('✓ Extraction complete');
341
-
342
- // Clean up ZIP file
343
- fs.unlinkSync(zipPath);
344
-
345
- // Verify skills directory exists
346
- if (!fs.existsSync(SKILLS_DIR)) {
347
- throw new Error('Skills directory not found after extraction');
348
- }
349
-
350
- const skillCount = fs.readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md')).length;
351
- console.log(`✓ ${skillCount} skills installed`);
352
-
353
- return true;
354
- } catch (error) {
355
- console.error('Content download failed:', error.message);
356
- // Clean up failed download
357
- if (fs.existsSync(zipPath)) {
358
- fs.unlinkSync(zipPath);
359
- }
360
- return false;
361
- }
362
- }
363
-
364
- /**
365
- * Get the content cache directory path
366
- * @returns {string} Path to content cache
367
- */
368
- function getContentCacheDir() {
369
- return CONTENT_CACHE_DIR;
370
- }
371
-
372
- /**
373
- * Get the skills directory path
374
- * @returns {string} Path to skills directory
375
- */
376
- function getSkillsDir() {
377
- return SKILLS_DIR;
378
- }
379
-
380
- /**
381
- * Get the license file path
382
- * @returns {string} Path to license file
383
- */
384
- function getLicenseFile() {
385
- return LICENSE_FILE;
386
- }
387
-
388
- module.exports = {
389
- loadLicense,
390
- saveLicense,
391
- clearLicense,
392
- isContentInstalled,
393
- downloadFile,
394
- extractZip,
395
- downloadPremiumContent,
396
- getContentCacheDir,
397
- getSkillsDir,
398
- getLicenseFile,
399
- _isUnsafeZipEntry: isUnsafeZipEntry,
400
- _resolveRedirectUrl: resolveRedirectUrl,
401
- LICENSE_FILE,
402
- CONTENT_CACHE_DIR,
403
- SKILLS_DIR,
404
- };
@@ -1,55 +0,0 @@
1
- const test = require('node:test');
2
- const assert = require('node:assert/strict');
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
- const AdmZip = require('adm-zip');
7
- const storage = require('./storage');
8
-
9
- function makeTempDir() {
10
- return fs.mkdtempSync(path.join(os.tmpdir(), 'bi-superpowers-'));
11
- }
12
-
13
- test('extractZip extracts safe entries', async () => {
14
- const dir = makeTempDir();
15
- const zipPath = path.join(dir, 'safe.zip');
16
- const zip = new AdmZip();
17
- zip.addFile('foo.txt', Buffer.from('ok'));
18
- zip.writeZip(zipPath);
19
-
20
- const dest = path.join(dir, 'out');
21
- await storage.extractZip(zipPath, dest);
22
-
23
- const content = fs.readFileSync(path.join(dest, 'foo.txt'), 'utf8');
24
- assert.equal(content, 'ok');
25
- });
26
-
27
- test('rejects path traversal entries', () => {
28
- const destRoot = path.resolve(makeTempDir());
29
- assert.equal(storage._isUnsafeZipEntry('../evil.txt', destRoot), true);
30
- assert.equal(storage._isUnsafeZipEntry('..\\\\evil.txt', destRoot), true);
31
- });
32
-
33
- test('rejects absolute path entries', () => {
34
- const destRoot = path.resolve(makeTempDir());
35
- assert.equal(storage._isUnsafeZipEntry('/abs.txt', destRoot), true);
36
- assert.equal(storage._isUnsafeZipEntry('C:\\\\abs.txt', destRoot), true);
37
- });
38
-
39
- test('downloadFile rejects non-https URLs', async () => {
40
- const dir = makeTempDir();
41
- const dest = path.join(dir, 'file.zip');
42
-
43
- await assert.rejects(
44
- () => storage.downloadFile('http://example.com/file.zip', dest),
45
- /HTTPS required/
46
- );
47
- });
48
-
49
- test('_resolveRedirectUrl resolves relative redirects', () => {
50
- const resolved = storage._resolveRedirectUrl(
51
- 'https://example.com/path/file.zip',
52
- '/downloads/content.zip'
53
- );
54
- assert.equal(resolved, 'https://example.com/downloads/content.zip');
55
- });
@@ -1,213 +0,0 @@
1
- /**
2
- * License Validator Module
3
- * ========================
4
- *
5
- * Handles license validation against the Acadevor API server.
6
- *
7
- * @module lib/licensing/validator
8
- */
9
-
10
- const https = require('https');
11
- const http = require('http');
12
-
13
- const storage = require('./storage');
14
-
15
- /** Base URL for license validation and content download API */
16
- const API_BASE_URL = process.env.BIAS_API_URL || 'https://acadevor.com';
17
-
18
- /**
19
- * Make HTTP(S) request with JSON body
20
- *
21
- * Generic HTTP client that:
22
- * - Automatically selects http/https based on URL protocol
23
- * - Handles JSON serialization/deserialization
24
- * - Returns both status code and parsed data
25
- *
26
- * @param {string} url - Full URL to request
27
- * @param {Object} options - Node.js http/https request options
28
- * @param {string} [options.method='GET'] - HTTP method
29
- * @param {Object} [options.headers] - Request headers
30
- * @param {Object} [data] - Request body (will be JSON.stringify'd)
31
- * @returns {Promise<{status: number, data: Object|string}>} Response object
32
- * @throws {Error} On network errors
33
- */
34
- function makeRequest(url, options, data) {
35
- return new Promise((resolve, reject) => {
36
- const urlObj = new URL(url);
37
- const protocol = urlObj.protocol === 'https:' ? https : http;
38
-
39
- const req = protocol.request(url, options, (res) => {
40
- let body = '';
41
- res.on('data', (chunk) => (body += chunk));
42
- res.on('end', () => {
43
- try {
44
- resolve({
45
- status: res.statusCode,
46
- data: body ? JSON.parse(body) : null,
47
- });
48
- } catch (e) {
49
- resolve({
50
- status: res.statusCode,
51
- data: body,
52
- });
53
- }
54
- });
55
- });
56
-
57
- req.on('error', reject);
58
-
59
- if (data) {
60
- req.write(JSON.stringify(data));
61
- }
62
-
63
- req.end();
64
- });
65
- }
66
-
67
- /**
68
- * Validate license with the Acadevor API server
69
- *
70
- * Makes a POST request to /api/licenses/validate to check if a license key
71
- * is valid and active. Returns license metadata and download URL if valid.
72
- *
73
- * @param {string} licenseKey - The license key to validate
74
- * @returns {Promise<Object>} Validation result
75
- * @returns {boolean} .valid - Whether the license is valid
76
- * @returns {string} [.email] - User's email if valid
77
- * @returns {string} [.name] - User's name if valid
78
- * @returns {string} [.downloadUrl] - URL to download premium content
79
- * @returns {string} [.error] - Error message if invalid
80
- */
81
- async function validateLicense(licenseKey) {
82
- // Skip validation in dev mode (requires secret env var)
83
- if (process.env.BIAS_DEV_MODE === 'true') {
84
- return { valid: true, email: 'developer@local' };
85
- }
86
-
87
- try {
88
- const response = await makeRequest(
89
- `${API_BASE_URL}/api/licenses/validate`,
90
- {
91
- method: 'POST',
92
- headers: { 'Content-Type': 'application/json' },
93
- },
94
- { license: licenseKey }
95
- );
96
-
97
- return response.data;
98
- } catch (error) {
99
- console.error('Validation error:', error.message);
100
- return { valid: false, error: 'Connection failed' };
101
- }
102
- }
103
-
104
- /**
105
- * Check if license is valid before running premium commands
106
- *
107
- * This function acts as a gatekeeper for commands that require premium content.
108
- * It performs the following checks:
109
- * 1. Verifies a local license file exists
110
- * 2. Re-validates the license against the API server
111
- * 3. Downloads content if not already installed
112
- *
113
- * Called by kickoff, recharge, and other premium commands.
114
- *
115
- * @param {string} version - Current package version for content versioning
116
- * @returns {Promise<Object>} Validation result from API
117
- * @throws {Error} Exits process if license is invalid or content download fails
118
- */
119
- async function requireLicense(version) {
120
- const license = storage.loadLicense();
121
-
122
- if (!license) {
123
- console.log(`
124
- ═══════════════════════════════════════════════════════════════
125
- LICENSE REQUIRED
126
- ═══════════════════════════════════════════════════════════════
127
-
128
- BI Agent Superpowers requires a valid license to use.
129
-
130
- To activate your license, run:
131
-
132
- super unlock
133
-
134
- Get your license at: https://acadevor.com/bi-superpowers
135
-
136
- ═══════════════════════════════════════════════════════════════
137
- `);
138
- process.exit(1);
139
- }
140
-
141
- // Re-validate license with server
142
- console.log('Verifying license...');
143
- const result = await validateLicense(license.license);
144
-
145
- if (!result.valid) {
146
- console.log(`\n✗ License no longer valid: ${result.error}`);
147
- storage.clearLicense();
148
- console.log('\nPlease run "bi-superpowers unlock" with a valid license key.');
149
- process.exit(1);
150
- }
151
-
152
- console.log(`✓ License valid (${license.email})`);
153
-
154
- const contentInstalled = storage.isContentInstalled();
155
-
156
- // Check if content is installed, download if needed
157
- if (!contentInstalled) {
158
- console.log('\nPremium content not found. Downloading...');
159
- if (result.downloadUrl) {
160
- const downloaded = await storage.downloadPremiumContent(result.downloadUrl);
161
- if (!downloaded) {
162
- console.log('\n✗ Content download failed. Please check your internet connection.');
163
- console.log('You can retry with "bi-superpowers unlock"');
164
- process.exit(1);
165
- }
166
- // Persist the content version once we successfully downloaded the content.
167
- storage.saveLicense({ ...license, contentVersion: version });
168
- } else {
169
- console.log('\n✗ No download URL provided. Please re-activate your license.');
170
- console.log('Run "bi-superpowers unlock" to download content.');
171
- process.exit(1);
172
- }
173
- } else if (license.contentVersion !== version) {
174
- // Best-effort refresh: do not block if the user already has content installed.
175
- const previousVersion = license.contentVersion || 'unknown';
176
- console.log(
177
- `\nPremium content may be outdated (${previousVersion} → ${version}). Refreshing...`
178
- );
179
-
180
- if (result.downloadUrl) {
181
- const refreshed = await storage.downloadPremiumContent(result.downloadUrl);
182
- if (refreshed) {
183
- storage.saveLicense({ ...license, contentVersion: version });
184
- console.log('✓ Premium content refreshed');
185
- } else {
186
- console.log('⚠ Premium content refresh failed. Continuing with existing content.');
187
- }
188
- } else {
189
- console.log(
190
- '⚠ No download URL provided to refresh premium content. Continuing with existing content.'
191
- );
192
- }
193
- }
194
-
195
- console.log('');
196
- return result;
197
- }
198
-
199
- /**
200
- * Get the API base URL
201
- * @returns {string} API base URL
202
- */
203
- function getApiBaseUrl() {
204
- return API_BASE_URL;
205
- }
206
-
207
- module.exports = {
208
- makeRequest,
209
- validateLicense,
210
- requireLicense,
211
- getApiBaseUrl,
212
- API_BASE_URL,
213
- };