@luquimbo/bi-superpowers 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.
Files changed (193) hide show
  1. package/.claude-plugin/plugin.json +8 -0
  2. package/.mcp.json +25 -0
  3. package/AGENTS.md +244 -0
  4. package/CHANGELOG.md +265 -0
  5. package/LICENSE +21 -0
  6. package/README.md +211 -0
  7. package/bin/build-plugin.js +30 -0
  8. package/bin/cli.js +1064 -0
  9. package/bin/commands/add.js +533 -0
  10. package/bin/commands/add.test.js +77 -0
  11. package/bin/commands/build-desktop.js +166 -0
  12. package/bin/commands/changelog.js +443 -0
  13. package/bin/commands/diff.js +325 -0
  14. package/bin/commands/lint.js +419 -0
  15. package/bin/commands/lint.test.js +103 -0
  16. package/bin/commands/mcp-setup.js +246 -0
  17. package/bin/commands/pull.js +287 -0
  18. package/bin/commands/pull.test.js +36 -0
  19. package/bin/commands/push.js +231 -0
  20. package/bin/commands/push.test.js +14 -0
  21. package/bin/commands/search.js +344 -0
  22. package/bin/commands/search.test.js +115 -0
  23. package/bin/commands/setup.js +545 -0
  24. package/bin/commands/setup.test.js +46 -0
  25. package/bin/commands/sync-profile.js +405 -0
  26. package/bin/commands/sync-profile.test.js +14 -0
  27. package/bin/commands/sync-source.js +418 -0
  28. package/bin/commands/sync-source.test.js +14 -0
  29. package/bin/commands/watch.js +206 -0
  30. package/bin/lib/generators/claude-plugin.js +266 -0
  31. package/bin/lib/generators/claude-plugin.test.js +110 -0
  32. package/bin/lib/generators/index.js +116 -0
  33. package/bin/lib/generators/shared.js +282 -0
  34. package/bin/lib/licensing/index.js +35 -0
  35. package/bin/lib/licensing/storage.js +364 -0
  36. package/bin/lib/licensing/storage.test.js +55 -0
  37. package/bin/lib/licensing/validator.js +213 -0
  38. package/bin/lib/licensing/validator.test.js +137 -0
  39. package/bin/lib/microsoft-mcp.js +176 -0
  40. package/bin/lib/microsoft-mcp.test.js +106 -0
  41. package/bin/lib/skills.js +84 -0
  42. package/bin/mcp/powerbi-modeling-launcher.js +38 -0
  43. package/bin/postinstall.js +44 -0
  44. package/bin/utils/errors.js +159 -0
  45. package/bin/utils/git.js +298 -0
  46. package/bin/utils/logger.js +142 -0
  47. package/bin/utils/mcp-detect.js +274 -0
  48. package/bin/utils/mcp-detect.test.js +105 -0
  49. package/bin/utils/pbix.js +305 -0
  50. package/bin/utils/pbix.test.js +37 -0
  51. package/bin/utils/profiles.js +312 -0
  52. package/bin/utils/projects.js +168 -0
  53. package/bin/utils/readline.js +206 -0
  54. package/bin/utils/readline.test.js +47 -0
  55. package/bin/utils/tui.js +314 -0
  56. package/bin/utils/tui.test.js +127 -0
  57. package/commands/contributions.md +265 -0
  58. package/commands/data-model-design.md +468 -0
  59. package/commands/dax-doctor.md +248 -0
  60. package/commands/fabric-scripts.md +452 -0
  61. package/commands/migration-assistant.md +290 -0
  62. package/commands/model-documenter.md +242 -0
  63. package/commands/pbi-connect.md +239 -0
  64. package/commands/project-kickoff.md +905 -0
  65. package/commands/report-layout.md +296 -0
  66. package/commands/rls-design.md +533 -0
  67. package/commands/theme-tweaker.md +624 -0
  68. package/config.example.json +23 -0
  69. package/config.json +23 -0
  70. package/desktop-extension/manifest.json +37 -0
  71. package/desktop-extension/package.json +10 -0
  72. package/desktop-extension/server.js +95 -0
  73. package/docs/openrouter-free-models.md +92 -0
  74. package/library/examples/README.md +151 -0
  75. package/library/examples/finance-reporting/README.md +351 -0
  76. package/library/examples/finance-reporting/data-model.md +267 -0
  77. package/library/examples/finance-reporting/measures.dax +557 -0
  78. package/library/examples/hr-analytics/README.md +371 -0
  79. package/library/examples/hr-analytics/data-model.md +315 -0
  80. package/library/examples/hr-analytics/measures.dax +460 -0
  81. package/library/examples/marketing-analytics/README.md +37 -0
  82. package/library/examples/marketing-analytics/data-model.md +62 -0
  83. package/library/examples/marketing-analytics/measures.dax +110 -0
  84. package/library/examples/retail-analytics/README.md +439 -0
  85. package/library/examples/retail-analytics/data-model.md +288 -0
  86. package/library/examples/retail-analytics/measures.dax +481 -0
  87. package/library/examples/supply-chain/README.md +37 -0
  88. package/library/examples/supply-chain/data-model.md +69 -0
  89. package/library/examples/supply-chain/measures.dax +77 -0
  90. package/library/examples/udf-library/README.md +228 -0
  91. package/library/examples/udf-library/functions.dax +571 -0
  92. package/library/snippets/dax/README.md +292 -0
  93. package/library/snippets/dax/business-domains.md +576 -0
  94. package/library/snippets/dax/calculate-patterns.md +276 -0
  95. package/library/snippets/dax/calculation-groups.md +489 -0
  96. package/library/snippets/dax/error-handling.md +495 -0
  97. package/library/snippets/dax/iterators-and-aggregations.md +474 -0
  98. package/library/snippets/dax/kpis-and-metrics.md +293 -0
  99. package/library/snippets/dax/rankings-and-topn.md +235 -0
  100. package/library/snippets/dax/security-patterns.md +413 -0
  101. package/library/snippets/dax/text-and-formatting.md +316 -0
  102. package/library/snippets/dax/time-intelligence.md +196 -0
  103. package/library/snippets/dax/user-defined-functions.md +477 -0
  104. package/library/snippets/dax/virtual-tables.md +546 -0
  105. package/library/snippets/excel-formulas/README.md +84 -0
  106. package/library/snippets/excel-formulas/aggregations.md +330 -0
  107. package/library/snippets/excel-formulas/dates-and-times.md +361 -0
  108. package/library/snippets/excel-formulas/dynamic-arrays.md +314 -0
  109. package/library/snippets/excel-formulas/lookups.md +169 -0
  110. package/library/snippets/excel-formulas/text-functions.md +363 -0
  111. package/library/snippets/governance/naming-conventions.md +97 -0
  112. package/library/snippets/governance/review-checklists.md +107 -0
  113. package/library/snippets/power-query/README.md +389 -0
  114. package/library/snippets/power-query/api-integration.md +707 -0
  115. package/library/snippets/power-query/connections.md +434 -0
  116. package/library/snippets/power-query/data-cleaning.md +298 -0
  117. package/library/snippets/power-query/error-handling.md +526 -0
  118. package/library/snippets/power-query/parameters.md +350 -0
  119. package/library/snippets/power-query/performance.md +506 -0
  120. package/library/snippets/power-query/transformations.md +330 -0
  121. package/library/snippets/report-design/accessibility.md +78 -0
  122. package/library/snippets/report-design/chart-selection.md +54 -0
  123. package/library/snippets/report-design/layout-patterns.md +87 -0
  124. package/library/templates/data-models/README.md +93 -0
  125. package/library/templates/data-models/finance-model.md +627 -0
  126. package/library/templates/data-models/retail-star-schema.md +473 -0
  127. package/library/templates/excel/README.md +83 -0
  128. package/library/templates/excel/budget-tracker.md +432 -0
  129. package/library/templates/excel/data-entry-form.md +533 -0
  130. package/library/templates/power-bi/README.md +72 -0
  131. package/library/templates/power-bi/finance-report.md +449 -0
  132. package/library/templates/power-bi/kpi-scorecard.md +461 -0
  133. package/library/templates/power-bi/sales-dashboard.md +281 -0
  134. package/library/themes/excel/README.md +436 -0
  135. package/library/themes/power-bi/README.md +271 -0
  136. package/library/themes/power-bi/accessible.json +307 -0
  137. package/library/themes/power-bi/bi-superpowers-default.json +858 -0
  138. package/library/themes/power-bi/corporate-blue.json +291 -0
  139. package/library/themes/power-bi/dark-mode.json +291 -0
  140. package/library/themes/power-bi/minimal.json +292 -0
  141. package/library/themes/power-bi/print-friendly.json +309 -0
  142. package/package.json +93 -0
  143. package/skills/contributions/SKILL.md +267 -0
  144. package/skills/data-model-design/SKILL.md +470 -0
  145. package/skills/data-modeling/SKILL.md +254 -0
  146. package/skills/data-quality/SKILL.md +664 -0
  147. package/skills/dax/SKILL.md +708 -0
  148. package/skills/dax-doctor/SKILL.md +250 -0
  149. package/skills/dax-udf/SKILL.md +489 -0
  150. package/skills/deployment/SKILL.md +320 -0
  151. package/skills/excel-formulas/SKILL.md +463 -0
  152. package/skills/fabric-scripts/SKILL.md +454 -0
  153. package/skills/fast-standard/SKILL.md +509 -0
  154. package/skills/governance/SKILL.md +205 -0
  155. package/skills/migration-assistant/SKILL.md +292 -0
  156. package/skills/model-documenter/SKILL.md +244 -0
  157. package/skills/pbi-connect/SKILL.md +241 -0
  158. package/skills/power-query/SKILL.md +406 -0
  159. package/skills/project-kickoff/SKILL.md +907 -0
  160. package/skills/query-performance/SKILL.md +480 -0
  161. package/skills/report-design/SKILL.md +207 -0
  162. package/skills/report-layout/SKILL.md +298 -0
  163. package/skills/rls-design/SKILL.md +535 -0
  164. package/skills/semantic-model/SKILL.md +237 -0
  165. package/skills/testing-validation/SKILL.md +643 -0
  166. package/skills/theme-tweaker/SKILL.md +626 -0
  167. package/src/content/base.md +237 -0
  168. package/src/content/mcp-requirements.json +69 -0
  169. package/src/content/routing.md +203 -0
  170. package/src/content/skills/contributions.md +259 -0
  171. package/src/content/skills/data-model-design.md +462 -0
  172. package/src/content/skills/data-modeling.md +246 -0
  173. package/src/content/skills/data-quality.md +656 -0
  174. package/src/content/skills/dax-doctor.md +242 -0
  175. package/src/content/skills/dax-udf.md +481 -0
  176. package/src/content/skills/dax.md +700 -0
  177. package/src/content/skills/deployment.md +312 -0
  178. package/src/content/skills/excel-formulas.md +455 -0
  179. package/src/content/skills/fabric-scripts.md +446 -0
  180. package/src/content/skills/fast-standard.md +501 -0
  181. package/src/content/skills/governance.md +197 -0
  182. package/src/content/skills/migration-assistant.md +284 -0
  183. package/src/content/skills/model-documenter.md +236 -0
  184. package/src/content/skills/pbi-connect.md +233 -0
  185. package/src/content/skills/power-query.md +398 -0
  186. package/src/content/skills/project-kickoff.md +899 -0
  187. package/src/content/skills/query-performance.md +472 -0
  188. package/src/content/skills/report-design.md +199 -0
  189. package/src/content/skills/report-layout.md +290 -0
  190. package/src/content/skills/rls-design.md +527 -0
  191. package/src/content/skills/semantic-model.md +229 -0
  192. package/src/content/skills/testing-validation.md +635 -0
  193. package/src/content/skills/theme-tweaker.md +618 -0
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Power BI File Utilities for BI Agent Superpowers
3
+ * =================================================
4
+ *
5
+ * Handles extraction and manipulation of Power BI files (.pbix, .pbip).
6
+ *
7
+ * Note: .pbix files are ZIP archives containing:
8
+ * - DataModel (binary - Analysis Services database)
9
+ * - Report/Layout (JSON)
10
+ * - Connections (JSON)
11
+ * - Metadata (XML)
12
+ *
13
+ * For TMDL extraction, we rely on Power BI Desktop's "Save as PBIP" feature
14
+ * or the pbi-tools utility. Direct extraction from .pbix is complex.
15
+ *
16
+ * @module utils/pbix
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ /**
23
+ * Supported file types and their handlers
24
+ */
25
+ const FILE_TYPES = {
26
+ '.pbix': 'power-bi',
27
+ '.pbip': 'power-bi-project',
28
+ '.xlsx': 'excel',
29
+ '.xlsm': 'excel-macro',
30
+ };
31
+
32
+ /**
33
+ * Detect the type of BI file
34
+ * @param {string} filePath - Path to the file
35
+ * @returns {Object} File info object
36
+ */
37
+ function detectFileType(filePath) {
38
+ const ext = path.extname(filePath).toLowerCase();
39
+ const type = FILE_TYPES[ext] || 'unknown';
40
+
41
+ return {
42
+ path: filePath,
43
+ extension: ext,
44
+ type,
45
+ name: path.basename(filePath, ext),
46
+ exists: fs.existsSync(filePath),
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Check if a file is a Power BI project (.pbip)
52
+ * @param {string} filePath - Path to check
53
+ * @returns {boolean} True if it's a PBIP project
54
+ */
55
+ function isPbipProject(filePath) {
56
+ const ext = path.extname(filePath).toLowerCase();
57
+ return ext === '.pbip';
58
+ }
59
+
60
+ /**
61
+ * Get the TMDL definition folder for a PBIP project
62
+ * @param {string} pbipPath - Path to .pbip file
63
+ * @returns {string|null} Path to definition folder or null
64
+ */
65
+ function getPbipDefinitionPath(pbipPath) {
66
+ const dir = path.dirname(pbipPath);
67
+ const name = path.basename(pbipPath, '.pbip');
68
+
69
+ // PBIP structure: Project.pbip + Project.SemanticModel/definition/
70
+ const semanticModelDir = path.join(dir, `${name}.SemanticModel`);
71
+ const definitionDir = path.join(semanticModelDir, 'definition');
72
+
73
+ if (fs.existsSync(definitionDir)) {
74
+ return definitionDir;
75
+ }
76
+
77
+ // Alternative structure: definition folder next to .pbip
78
+ const altDefinitionDir = path.join(dir, 'definition');
79
+ if (fs.existsSync(altDefinitionDir)) {
80
+ return altDefinitionDir;
81
+ }
82
+
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * Copy PBIP definition to the repo project folder
88
+ * @param {string} pbipPath - Path to .pbip file
89
+ * @param {string} projectDir - Target project directory in repo
90
+ * @returns {Object} Result with success status and copied files
91
+ */
92
+ function extractPbipToRepo(pbipPath, projectDir) {
93
+ const result = {
94
+ success: false,
95
+ files: [],
96
+ error: null,
97
+ };
98
+
99
+ const definitionPath = getPbipDefinitionPath(pbipPath);
100
+
101
+ if (!definitionPath) {
102
+ result.error = 'Could not find TMDL definition folder for PBIP project';
103
+ return result;
104
+ }
105
+
106
+ const targetDir = path.join(projectDir, 'definition');
107
+
108
+ try {
109
+ // Create target directory
110
+ if (!fs.existsSync(targetDir)) {
111
+ fs.mkdirSync(targetDir, { recursive: true });
112
+ }
113
+
114
+ // Copy all files recursively
115
+ result.files = copyDirectoryRecursive(definitionPath, targetDir);
116
+ result.success = true;
117
+ } catch (e) {
118
+ result.error = e.message;
119
+ }
120
+
121
+ return result;
122
+ }
123
+
124
+ /**
125
+ * Copy directory recursively
126
+ * @param {string} src - Source directory
127
+ * @param {string} dest - Destination directory
128
+ * @returns {string[]} List of copied files (relative paths)
129
+ */
130
+ function copyDirectoryRecursive(src, dest, baseSrc = src) {
131
+ const copied = [];
132
+
133
+ if (!fs.existsSync(dest)) {
134
+ fs.mkdirSync(dest, { recursive: true });
135
+ }
136
+
137
+ const entries = fs.readdirSync(src, { withFileTypes: true });
138
+
139
+ for (const entry of entries) {
140
+ const srcPath = path.join(src, entry.name);
141
+ const destPath = path.join(dest, entry.name);
142
+
143
+ if (entry.isDirectory()) {
144
+ copied.push(...copyDirectoryRecursive(srcPath, destPath, baseSrc));
145
+ } else {
146
+ fs.copyFileSync(srcPath, destPath);
147
+ copied.push(path.relative(baseSrc, srcPath));
148
+ }
149
+ }
150
+
151
+ return copied;
152
+ }
153
+
154
+ /**
155
+ * Get file hash for change detection
156
+ * @param {string} filePath - Path to file
157
+ * @returns {string} Hash string
158
+ */
159
+ function getFileHash(filePath) {
160
+ if (!fs.existsSync(filePath)) {
161
+ return '';
162
+ }
163
+
164
+ const crypto = require('crypto');
165
+ const content = fs.readFileSync(filePath);
166
+ return crypto.createHash('md5').update(content).digest('hex');
167
+ }
168
+
169
+ /**
170
+ * Get last modified time of a file
171
+ * @param {string} filePath - Path to file
172
+ * @returns {Date|null} Last modified date or null
173
+ */
174
+ function getLastModified(filePath) {
175
+ if (!fs.existsSync(filePath)) {
176
+ return null;
177
+ }
178
+
179
+ const stats = fs.statSync(filePath);
180
+ return stats.mtime;
181
+ }
182
+
183
+ /**
184
+ * Compare two directories and find differences
185
+ * @param {string} dir1 - First directory
186
+ * @param {string} dir2 - Second directory
187
+ * @returns {Object} Differences object
188
+ */
189
+ function compareDirectories(dir1, dir2) {
190
+ const diff = {
191
+ added: [],
192
+ modified: [],
193
+ deleted: [],
194
+ };
195
+
196
+ const files1 = getAllFiles(dir1);
197
+ const files2 = getAllFiles(dir2);
198
+
199
+ const set1 = new Set(files1.map((f) => path.relative(dir1, f)));
200
+ const set2 = new Set(files2.map((f) => path.relative(dir2, f)));
201
+
202
+ // Find added (in dir2 but not dir1)
203
+ for (const file of set2) {
204
+ if (!set1.has(file)) {
205
+ diff.added.push(file);
206
+ }
207
+ }
208
+
209
+ // Find deleted (in dir1 but not dir2)
210
+ for (const file of set1) {
211
+ if (!set2.has(file)) {
212
+ diff.deleted.push(file);
213
+ }
214
+ }
215
+
216
+ // Find modified (in both but different)
217
+ for (const file of set1) {
218
+ if (set2.has(file)) {
219
+ const path1 = path.join(dir1, file);
220
+ const path2 = path.join(dir2, file);
221
+
222
+ if (getFileHash(path1) !== getFileHash(path2)) {
223
+ diff.modified.push(file);
224
+ }
225
+ }
226
+ }
227
+
228
+ return diff;
229
+ }
230
+
231
+ /**
232
+ * Get all files in a directory recursively
233
+ * @param {string} dir - Directory to scan
234
+ * @returns {string[]} Array of file paths
235
+ */
236
+ function getAllFiles(dir) {
237
+ const files = [];
238
+
239
+ if (!fs.existsSync(dir)) {
240
+ return files;
241
+ }
242
+
243
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
244
+
245
+ for (const entry of entries) {
246
+ const fullPath = path.join(dir, entry.name);
247
+
248
+ if (entry.isDirectory()) {
249
+ files.push(...getAllFiles(fullPath));
250
+ } else {
251
+ files.push(fullPath);
252
+ }
253
+ }
254
+
255
+ return files;
256
+ }
257
+
258
+ /**
259
+ * Create project.json for a new project
260
+ * @param {Object} options - Project options
261
+ * @returns {Object} Project configuration object
262
+ */
263
+ function createProjectConfig(options) {
264
+ return {
265
+ name: options.name,
266
+ displayName: options.displayName || options.name,
267
+ type: options.type || 'power-bi',
268
+ profile: options.profile || 'default',
269
+ source: {
270
+ path: options.sourcePath,
271
+ lastSync: new Date().toISOString(),
272
+ hash: options.sourcePath ? getFileHash(options.sourcePath) : '',
273
+ },
274
+ created: new Date().toISOString(),
275
+ description: options.description || '',
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Generate a slug from a display name
281
+ * @param {string} name - Display name
282
+ * @returns {string} URL-friendly slug
283
+ */
284
+ function generateSlug(name) {
285
+ return name
286
+ .toLowerCase()
287
+ .replace(/[^a-z0-9]+/g, '-')
288
+ .replace(/^-+|-+$/g, '')
289
+ .substring(0, 50);
290
+ }
291
+
292
+ module.exports = {
293
+ FILE_TYPES,
294
+ detectFileType,
295
+ isPbipProject,
296
+ getPbipDefinitionPath,
297
+ extractPbipToRepo,
298
+ copyDirectoryRecursive,
299
+ getFileHash,
300
+ getLastModified,
301
+ compareDirectories,
302
+ getAllFiles,
303
+ createProjectConfig,
304
+ generateSlug,
305
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Tests for Power BI file utilities
3
+ * @module utils/pbix.test
4
+ */
5
+
6
+ const test = require('node:test');
7
+ const assert = require('node:assert/strict');
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+
12
+ const pbix = require('./pbix');
13
+
14
+ function makeTempDir() {
15
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'bi-superpowers-pbix-'));
16
+ }
17
+
18
+ test('copyDirectoryRecursive returns relative paths for nested files', () => {
19
+ const dir = makeTempDir();
20
+
21
+ const src = path.join(dir, 'src');
22
+ const dest = path.join(dir, 'dest');
23
+
24
+ fs.mkdirSync(path.join(src, 'subdir'), { recursive: true });
25
+ fs.writeFileSync(path.join(src, 'root.txt'), 'root');
26
+ fs.writeFileSync(path.join(src, 'subdir', 'nested.txt'), 'nested');
27
+
28
+ const copied = pbix.copyDirectoryRecursive(src, dest);
29
+
30
+ // Returned paths should be relative to the source root
31
+ assert.ok(copied.includes('root.txt'));
32
+ assert.ok(copied.includes(path.join('subdir', 'nested.txt')));
33
+
34
+ // Files should exist in destination with the same structure
35
+ assert.equal(fs.readFileSync(path.join(dest, 'root.txt'), 'utf8'), 'root');
36
+ assert.equal(fs.readFileSync(path.join(dest, 'subdir', 'nested.txt'), 'utf8'), 'nested');
37
+ });
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Profile Management Utilities for BI Agent Superpowers
3
+ * =====================================================
4
+ *
5
+ * Handles profile configuration, inheritance, and management.
6
+ * Profiles allow users to have different sets of snippets for
7
+ * different industries (finance, retail, healthcare, etc.)
8
+ *
9
+ * @module utils/profiles
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+
16
+ /** Base directory for BI Superpowers global config */
17
+ const BASE_CONFIG_DIR = path.join(os.homedir(), '.bi-superpowers');
18
+
19
+ /** Path to global config file */
20
+ const CONFIG_FILE = path.join(BASE_CONFIG_DIR, 'config.json');
21
+
22
+ /** Path to profiles directory */
23
+ const PROFILES_DIR = path.join(BASE_CONFIG_DIR, 'profiles');
24
+
25
+ /**
26
+ * Default configuration structure
27
+ */
28
+ const DEFAULT_CONFIG = {
29
+ version: '3.0',
30
+ defaultProfile: 'default',
31
+ profiles: {},
32
+ repoPath: null,
33
+ };
34
+
35
+ /**
36
+ * Ensure the base config directory exists
37
+ */
38
+ function ensureConfigDir() {
39
+ if (!fs.existsSync(BASE_CONFIG_DIR)) {
40
+ fs.mkdirSync(BASE_CONFIG_DIR, { recursive: true });
41
+ }
42
+ if (!fs.existsSync(PROFILES_DIR)) {
43
+ fs.mkdirSync(PROFILES_DIR, { recursive: true });
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Load the global configuration
49
+ * @returns {Object} Configuration object
50
+ */
51
+ function loadConfig() {
52
+ ensureConfigDir();
53
+
54
+ if (fs.existsSync(CONFIG_FILE)) {
55
+ try {
56
+ const data = fs.readFileSync(CONFIG_FILE, 'utf8');
57
+ return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
58
+ } catch (e) {
59
+ return { ...DEFAULT_CONFIG };
60
+ }
61
+ }
62
+
63
+ return { ...DEFAULT_CONFIG };
64
+ }
65
+
66
+ /**
67
+ * Save the global configuration
68
+ * @param {Object} config - Configuration to save
69
+ */
70
+ function saveConfig(config) {
71
+ ensureConfigDir();
72
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
73
+ }
74
+
75
+ /**
76
+ * Get the path to the user's bi-repo
77
+ * @returns {string|null} Path to bi-repo or null if not configured
78
+ */
79
+ function getRepoPath() {
80
+ const config = loadConfig();
81
+ return config.repoPath;
82
+ }
83
+
84
+ /**
85
+ * Set the path to the user's bi-repo
86
+ * @param {string} repoPath - Path to the bi-repo
87
+ */
88
+ function setRepoPath(repoPath) {
89
+ const config = loadConfig();
90
+ config.repoPath = repoPath;
91
+ saveConfig(config);
92
+ }
93
+
94
+ /**
95
+ * Get list of available profiles
96
+ * @returns {string[]} Array of profile names
97
+ */
98
+ function listProfiles() {
99
+ ensureConfigDir();
100
+
101
+ if (!fs.existsSync(PROFILES_DIR)) {
102
+ return ['default'];
103
+ }
104
+
105
+ const profiles = fs
106
+ .readdirSync(PROFILES_DIR)
107
+ .filter((f) => fs.statSync(path.join(PROFILES_DIR, f)).isDirectory());
108
+
109
+ // Always include default
110
+ if (!profiles.includes('default')) {
111
+ profiles.unshift('default');
112
+ }
113
+
114
+ return profiles;
115
+ }
116
+
117
+ /**
118
+ * Check if a profile exists
119
+ * @param {string} profileName - Name of the profile
120
+ * @returns {boolean} True if profile exists
121
+ */
122
+ function profileExists(profileName) {
123
+ const profilePath = path.join(PROFILES_DIR, profileName);
124
+ return fs.existsSync(profilePath);
125
+ }
126
+
127
+ /**
128
+ * Create a new profile
129
+ * @param {string} profileName - Name of the new profile
130
+ * @param {Object} options - Options for the profile
131
+ * @param {string} options.inheritsFrom - Parent profile name
132
+ * @returns {boolean} True if successful
133
+ */
134
+ function createProfile(profileName, options = {}) {
135
+ ensureConfigDir();
136
+
137
+ const profilePath = path.join(PROFILES_DIR, profileName);
138
+
139
+ if (fs.existsSync(profilePath)) {
140
+ return false;
141
+ }
142
+
143
+ // Create profile directory structure
144
+ fs.mkdirSync(profilePath, { recursive: true });
145
+ fs.mkdirSync(path.join(profilePath, 'snippets'), { recursive: true });
146
+
147
+ // Update config with inheritance
148
+ const config = loadConfig();
149
+ if (options.inheritsFrom) {
150
+ config.profiles[profileName] = { inheritsFrom: options.inheritsFrom };
151
+ } else {
152
+ config.profiles[profileName] = {};
153
+ }
154
+ saveConfig(config);
155
+
156
+ return true;
157
+ }
158
+
159
+ /**
160
+ * Get the inheritance chain for a profile
161
+ * @param {string} profileName - Name of the profile
162
+ * @returns {string[]} Array of profile names from most specific to base
163
+ */
164
+ function getInheritanceChain(profileName) {
165
+ const config = loadConfig();
166
+ const chain = [profileName];
167
+
168
+ let current = profileName;
169
+ const visited = new Set([profileName]);
170
+
171
+ while (config.profiles[current]?.inheritsFrom) {
172
+ const parent = config.profiles[current].inheritsFrom;
173
+
174
+ // Prevent circular dependencies
175
+ if (visited.has(parent)) {
176
+ break;
177
+ }
178
+
179
+ chain.push(parent);
180
+ visited.add(parent);
181
+ current = parent;
182
+ }
183
+
184
+ return chain;
185
+ }
186
+
187
+ /**
188
+ * Get all snippets for a profile, including inherited ones
189
+ * @param {string} profileName - Name of the profile
190
+ * @returns {Object[]} Array of snippet objects with path and profile
191
+ */
192
+ function getProfileSnippets(profileName) {
193
+ const chain = getInheritanceChain(profileName);
194
+ const snippets = [];
195
+ const seen = new Set();
196
+
197
+ // Process from most specific to least specific
198
+ for (const profile of chain) {
199
+ const profilePath = path.join(PROFILES_DIR, profile, 'snippets');
200
+
201
+ if (!fs.existsSync(profilePath)) {
202
+ continue;
203
+ }
204
+
205
+ // Recursively find all .md files
206
+ const files = findMdFiles(profilePath);
207
+
208
+ for (const file of files) {
209
+ const relativePath = path.relative(profilePath, file);
210
+
211
+ // Skip if we already have this snippet from a more specific profile
212
+ if (!seen.has(relativePath)) {
213
+ seen.add(relativePath);
214
+ snippets.push({
215
+ path: file,
216
+ relativePath,
217
+ profile,
218
+ });
219
+ }
220
+ }
221
+ }
222
+
223
+ return snippets;
224
+ }
225
+
226
+ /**
227
+ * Recursively find all .md files in a directory
228
+ * @param {string} dir - Directory to search
229
+ * @returns {string[]} Array of file paths
230
+ */
231
+ function findMdFiles(dir) {
232
+ const files = [];
233
+
234
+ if (!fs.existsSync(dir)) {
235
+ return files;
236
+ }
237
+
238
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
239
+
240
+ for (const entry of entries) {
241
+ const fullPath = path.join(dir, entry.name);
242
+
243
+ if (entry.isDirectory()) {
244
+ files.push(...findMdFiles(fullPath));
245
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
246
+ files.push(fullPath);
247
+ }
248
+ }
249
+
250
+ return files;
251
+ }
252
+
253
+ /**
254
+ * Copy a snippet to a profile
255
+ * @param {string} sourcePath - Path to the source snippet
256
+ * @param {string} profileName - Target profile name
257
+ * @param {string} relativePath - Relative path within snippets folder
258
+ * @returns {boolean} True if successful
259
+ */
260
+ function copySnippetToProfile(sourcePath, profileName, relativePath) {
261
+ const targetPath = path.join(PROFILES_DIR, profileName, 'snippets', relativePath);
262
+ const targetDir = path.dirname(targetPath);
263
+
264
+ if (!fs.existsSync(targetDir)) {
265
+ fs.mkdirSync(targetDir, { recursive: true });
266
+ }
267
+
268
+ try {
269
+ fs.copyFileSync(sourcePath, targetPath);
270
+ return true;
271
+ } catch (e) {
272
+ return false;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Get the default profile name
278
+ * @returns {string} Default profile name
279
+ */
280
+ function getDefaultProfile() {
281
+ const config = loadConfig();
282
+ return config.defaultProfile || 'default';
283
+ }
284
+
285
+ /**
286
+ * Set the default profile
287
+ * @param {string} profileName - Profile to set as default
288
+ */
289
+ function setDefaultProfile(profileName) {
290
+ const config = loadConfig();
291
+ config.defaultProfile = profileName;
292
+ saveConfig(config);
293
+ }
294
+
295
+ module.exports = {
296
+ BASE_CONFIG_DIR,
297
+ PROFILES_DIR,
298
+ ensureConfigDir,
299
+ loadConfig,
300
+ saveConfig,
301
+ getRepoPath,
302
+ setRepoPath,
303
+ listProfiles,
304
+ profileExists,
305
+ createProfile,
306
+ getInheritanceChain,
307
+ getProfileSnippets,
308
+ findMdFiles,
309
+ copySnippetToProfile,
310
+ getDefaultProfile,
311
+ setDefaultProfile,
312
+ };