@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.
- package/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +25 -0
- package/AGENTS.md +244 -0
- package/CHANGELOG.md +265 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/bin/build-plugin.js +30 -0
- package/bin/cli.js +1064 -0
- package/bin/commands/add.js +533 -0
- package/bin/commands/add.test.js +77 -0
- package/bin/commands/build-desktop.js +166 -0
- package/bin/commands/changelog.js +443 -0
- package/bin/commands/diff.js +325 -0
- package/bin/commands/lint.js +419 -0
- package/bin/commands/lint.test.js +103 -0
- package/bin/commands/mcp-setup.js +246 -0
- package/bin/commands/pull.js +287 -0
- package/bin/commands/pull.test.js +36 -0
- package/bin/commands/push.js +231 -0
- package/bin/commands/push.test.js +14 -0
- package/bin/commands/search.js +344 -0
- package/bin/commands/search.test.js +115 -0
- package/bin/commands/setup.js +545 -0
- package/bin/commands/setup.test.js +46 -0
- package/bin/commands/sync-profile.js +405 -0
- package/bin/commands/sync-profile.test.js +14 -0
- package/bin/commands/sync-source.js +418 -0
- package/bin/commands/sync-source.test.js +14 -0
- package/bin/commands/watch.js +206 -0
- package/bin/lib/generators/claude-plugin.js +266 -0
- package/bin/lib/generators/claude-plugin.test.js +110 -0
- package/bin/lib/generators/index.js +116 -0
- package/bin/lib/generators/shared.js +282 -0
- package/bin/lib/licensing/index.js +35 -0
- package/bin/lib/licensing/storage.js +364 -0
- package/bin/lib/licensing/storage.test.js +55 -0
- package/bin/lib/licensing/validator.js +213 -0
- package/bin/lib/licensing/validator.test.js +137 -0
- package/bin/lib/microsoft-mcp.js +176 -0
- package/bin/lib/microsoft-mcp.test.js +106 -0
- package/bin/lib/skills.js +84 -0
- package/bin/mcp/powerbi-modeling-launcher.js +38 -0
- package/bin/postinstall.js +44 -0
- package/bin/utils/errors.js +159 -0
- package/bin/utils/git.js +298 -0
- package/bin/utils/logger.js +142 -0
- package/bin/utils/mcp-detect.js +274 -0
- package/bin/utils/mcp-detect.test.js +105 -0
- package/bin/utils/pbix.js +305 -0
- package/bin/utils/pbix.test.js +37 -0
- package/bin/utils/profiles.js +312 -0
- package/bin/utils/projects.js +168 -0
- package/bin/utils/readline.js +206 -0
- package/bin/utils/readline.test.js +47 -0
- package/bin/utils/tui.js +314 -0
- package/bin/utils/tui.test.js +127 -0
- package/commands/contributions.md +265 -0
- package/commands/data-model-design.md +468 -0
- package/commands/dax-doctor.md +248 -0
- package/commands/fabric-scripts.md +452 -0
- package/commands/migration-assistant.md +290 -0
- package/commands/model-documenter.md +242 -0
- package/commands/pbi-connect.md +239 -0
- package/commands/project-kickoff.md +905 -0
- package/commands/report-layout.md +296 -0
- package/commands/rls-design.md +533 -0
- package/commands/theme-tweaker.md +624 -0
- package/config.example.json +23 -0
- package/config.json +23 -0
- package/desktop-extension/manifest.json +37 -0
- package/desktop-extension/package.json +10 -0
- package/desktop-extension/server.js +95 -0
- package/docs/openrouter-free-models.md +92 -0
- package/library/examples/README.md +151 -0
- package/library/examples/finance-reporting/README.md +351 -0
- package/library/examples/finance-reporting/data-model.md +267 -0
- package/library/examples/finance-reporting/measures.dax +557 -0
- package/library/examples/hr-analytics/README.md +371 -0
- package/library/examples/hr-analytics/data-model.md +315 -0
- package/library/examples/hr-analytics/measures.dax +460 -0
- package/library/examples/marketing-analytics/README.md +37 -0
- package/library/examples/marketing-analytics/data-model.md +62 -0
- package/library/examples/marketing-analytics/measures.dax +110 -0
- package/library/examples/retail-analytics/README.md +439 -0
- package/library/examples/retail-analytics/data-model.md +288 -0
- package/library/examples/retail-analytics/measures.dax +481 -0
- package/library/examples/supply-chain/README.md +37 -0
- package/library/examples/supply-chain/data-model.md +69 -0
- package/library/examples/supply-chain/measures.dax +77 -0
- package/library/examples/udf-library/README.md +228 -0
- package/library/examples/udf-library/functions.dax +571 -0
- package/library/snippets/dax/README.md +292 -0
- package/library/snippets/dax/business-domains.md +576 -0
- package/library/snippets/dax/calculate-patterns.md +276 -0
- package/library/snippets/dax/calculation-groups.md +489 -0
- package/library/snippets/dax/error-handling.md +495 -0
- package/library/snippets/dax/iterators-and-aggregations.md +474 -0
- package/library/snippets/dax/kpis-and-metrics.md +293 -0
- package/library/snippets/dax/rankings-and-topn.md +235 -0
- package/library/snippets/dax/security-patterns.md +413 -0
- package/library/snippets/dax/text-and-formatting.md +316 -0
- package/library/snippets/dax/time-intelligence.md +196 -0
- package/library/snippets/dax/user-defined-functions.md +477 -0
- package/library/snippets/dax/virtual-tables.md +546 -0
- package/library/snippets/excel-formulas/README.md +84 -0
- package/library/snippets/excel-formulas/aggregations.md +330 -0
- package/library/snippets/excel-formulas/dates-and-times.md +361 -0
- package/library/snippets/excel-formulas/dynamic-arrays.md +314 -0
- package/library/snippets/excel-formulas/lookups.md +169 -0
- package/library/snippets/excel-formulas/text-functions.md +363 -0
- package/library/snippets/governance/naming-conventions.md +97 -0
- package/library/snippets/governance/review-checklists.md +107 -0
- package/library/snippets/power-query/README.md +389 -0
- package/library/snippets/power-query/api-integration.md +707 -0
- package/library/snippets/power-query/connections.md +434 -0
- package/library/snippets/power-query/data-cleaning.md +298 -0
- package/library/snippets/power-query/error-handling.md +526 -0
- package/library/snippets/power-query/parameters.md +350 -0
- package/library/snippets/power-query/performance.md +506 -0
- package/library/snippets/power-query/transformations.md +330 -0
- package/library/snippets/report-design/accessibility.md +78 -0
- package/library/snippets/report-design/chart-selection.md +54 -0
- package/library/snippets/report-design/layout-patterns.md +87 -0
- package/library/templates/data-models/README.md +93 -0
- package/library/templates/data-models/finance-model.md +627 -0
- package/library/templates/data-models/retail-star-schema.md +473 -0
- package/library/templates/excel/README.md +83 -0
- package/library/templates/excel/budget-tracker.md +432 -0
- package/library/templates/excel/data-entry-form.md +533 -0
- package/library/templates/power-bi/README.md +72 -0
- package/library/templates/power-bi/finance-report.md +449 -0
- package/library/templates/power-bi/kpi-scorecard.md +461 -0
- package/library/templates/power-bi/sales-dashboard.md +281 -0
- package/library/themes/excel/README.md +436 -0
- package/library/themes/power-bi/README.md +271 -0
- package/library/themes/power-bi/accessible.json +307 -0
- package/library/themes/power-bi/bi-superpowers-default.json +858 -0
- package/library/themes/power-bi/corporate-blue.json +291 -0
- package/library/themes/power-bi/dark-mode.json +291 -0
- package/library/themes/power-bi/minimal.json +292 -0
- package/library/themes/power-bi/print-friendly.json +309 -0
- package/package.json +93 -0
- package/skills/contributions/SKILL.md +267 -0
- package/skills/data-model-design/SKILL.md +470 -0
- package/skills/data-modeling/SKILL.md +254 -0
- package/skills/data-quality/SKILL.md +664 -0
- package/skills/dax/SKILL.md +708 -0
- package/skills/dax-doctor/SKILL.md +250 -0
- package/skills/dax-udf/SKILL.md +489 -0
- package/skills/deployment/SKILL.md +320 -0
- package/skills/excel-formulas/SKILL.md +463 -0
- package/skills/fabric-scripts/SKILL.md +454 -0
- package/skills/fast-standard/SKILL.md +509 -0
- package/skills/governance/SKILL.md +205 -0
- package/skills/migration-assistant/SKILL.md +292 -0
- package/skills/model-documenter/SKILL.md +244 -0
- package/skills/pbi-connect/SKILL.md +241 -0
- package/skills/power-query/SKILL.md +406 -0
- package/skills/project-kickoff/SKILL.md +907 -0
- package/skills/query-performance/SKILL.md +480 -0
- package/skills/report-design/SKILL.md +207 -0
- package/skills/report-layout/SKILL.md +298 -0
- package/skills/rls-design/SKILL.md +535 -0
- package/skills/semantic-model/SKILL.md +237 -0
- package/skills/testing-validation/SKILL.md +643 -0
- package/skills/theme-tweaker/SKILL.md +626 -0
- package/src/content/base.md +237 -0
- package/src/content/mcp-requirements.json +69 -0
- package/src/content/routing.md +203 -0
- package/src/content/skills/contributions.md +259 -0
- package/src/content/skills/data-model-design.md +462 -0
- package/src/content/skills/data-modeling.md +246 -0
- package/src/content/skills/data-quality.md +656 -0
- package/src/content/skills/dax-doctor.md +242 -0
- package/src/content/skills/dax-udf.md +481 -0
- package/src/content/skills/dax.md +700 -0
- package/src/content/skills/deployment.md +312 -0
- package/src/content/skills/excel-formulas.md +455 -0
- package/src/content/skills/fabric-scripts.md +446 -0
- package/src/content/skills/fast-standard.md +501 -0
- package/src/content/skills/governance.md +197 -0
- package/src/content/skills/migration-assistant.md +284 -0
- package/src/content/skills/model-documenter.md +236 -0
- package/src/content/skills/pbi-connect.md +233 -0
- package/src/content/skills/power-query.md +398 -0
- package/src/content/skills/project-kickoff.md +899 -0
- package/src/content/skills/query-performance.md +472 -0
- package/src/content/skills/report-design.md +199 -0
- package/src/content/skills/report-layout.md +290 -0
- package/src/content/skills/rls-design.md +527 -0
- package/src/content/skills/semantic-model.md +229 -0
- package/src/content/skills/testing-validation.md +635 -0
- package/src/content/skills/theme-tweaker.md +618 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Source Command - Bidirectional Synchronization
|
|
3
|
+
* =====================================================
|
|
4
|
+
*
|
|
5
|
+
* Detects which is newer (repo or original file) and syncs accordingly.
|
|
6
|
+
* Combines pull and push logic with automatic conflict detection.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* super sync Sync current project
|
|
10
|
+
* super sync <project-name> Sync specific project
|
|
11
|
+
* super sync --all Sync all projects
|
|
12
|
+
*
|
|
13
|
+
* @module commands/sync-source
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const profiles = require('../utils/profiles');
|
|
20
|
+
const pbix = require('../utils/pbix');
|
|
21
|
+
const git = require('../utils/git');
|
|
22
|
+
const rl = require('../utils/readline');
|
|
23
|
+
const { getAllProjects, getProject, detectCurrentProject } = require('../utils/projects');
|
|
24
|
+
|
|
25
|
+
// Using shared readline utilities
|
|
26
|
+
const { createReadline } = rl;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse command line arguments
|
|
30
|
+
* @param {string[]} args - CLI arguments
|
|
31
|
+
* @returns {Object} Parsed options
|
|
32
|
+
*/
|
|
33
|
+
function parseArgs(args) {
|
|
34
|
+
const options = {
|
|
35
|
+
projectName: null,
|
|
36
|
+
all: false,
|
|
37
|
+
force: false,
|
|
38
|
+
help: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < args.length; i++) {
|
|
42
|
+
const arg = args[i];
|
|
43
|
+
|
|
44
|
+
if (arg === '--all' || arg === '-a') {
|
|
45
|
+
options.all = true;
|
|
46
|
+
} else if (arg === '--force' || arg === '-f') {
|
|
47
|
+
options.force = true;
|
|
48
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
49
|
+
options.help = true;
|
|
50
|
+
} else if (!arg.startsWith('-') && !options.projectName) {
|
|
51
|
+
options.projectName = arg;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return options;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Show help message
|
|
60
|
+
*/
|
|
61
|
+
function showHelp() {
|
|
62
|
+
console.log(`
|
|
63
|
+
super sync - Sincronización bidireccional
|
|
64
|
+
|
|
65
|
+
Detecta automáticamente qué es más nuevo (repo o archivo original)
|
|
66
|
+
y sincroniza en la dirección correcta.
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
super sync Sync proyecto actual
|
|
70
|
+
super sync <proyecto> Sync proyecto específico
|
|
71
|
+
super sync --all Sync todos los proyectos
|
|
72
|
+
|
|
73
|
+
Options:
|
|
74
|
+
--all, -a Sincronizar todos los proyectos
|
|
75
|
+
--force, -f Forzar sin confirmación
|
|
76
|
+
--help, -h Mostrar esta ayuda
|
|
77
|
+
|
|
78
|
+
Ejemplos:
|
|
79
|
+
super sync sales-q4
|
|
80
|
+
super sync --all
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Note: getAllProjects, getProject, and detectCurrentProject are imported from ../utils/projects
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Determine sync direction based on timestamps and hashes
|
|
88
|
+
* @param {Object} project - Project config
|
|
89
|
+
* @returns {Object} Sync info { direction, reason, sourceModified, repoModified }
|
|
90
|
+
*/
|
|
91
|
+
function determineSyncDirection(project) {
|
|
92
|
+
const result = {
|
|
93
|
+
direction: 'none', // 'pull', 'push', 'conflict', 'none'
|
|
94
|
+
reason: '',
|
|
95
|
+
sourceModified: null,
|
|
96
|
+
repoModified: null,
|
|
97
|
+
hasChanges: false,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const sourcePath = project.source?.path;
|
|
101
|
+
if (!sourcePath || !fs.existsSync(sourcePath)) {
|
|
102
|
+
result.reason = 'Archivo original no encontrado';
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Get source file modification time
|
|
107
|
+
const sourceStats = fs.statSync(sourcePath);
|
|
108
|
+
result.sourceModified = sourceStats.mtime;
|
|
109
|
+
|
|
110
|
+
// Get repo definition modification time
|
|
111
|
+
const definitionPath = path.join(project.projectPath, 'definition');
|
|
112
|
+
if (fs.existsSync(definitionPath)) {
|
|
113
|
+
const repoFiles = pbix.getAllFiles(definitionPath);
|
|
114
|
+
if (repoFiles.length > 0) {
|
|
115
|
+
// Find most recent repo file
|
|
116
|
+
let latestRepoTime = new Date(0);
|
|
117
|
+
for (const file of repoFiles) {
|
|
118
|
+
const stats = fs.statSync(file);
|
|
119
|
+
if (stats.mtime > latestRepoTime) {
|
|
120
|
+
latestRepoTime = stats.mtime;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
result.repoModified = latestRepoTime;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Get last sync time
|
|
128
|
+
const lastSync = project.source?.lastSync ? new Date(project.source.lastSync) : null;
|
|
129
|
+
|
|
130
|
+
// Determine direction
|
|
131
|
+
if (!result.repoModified) {
|
|
132
|
+
// No repo content yet, need to pull
|
|
133
|
+
result.direction = 'pull';
|
|
134
|
+
result.reason = 'Repo vacío - necesita pull inicial';
|
|
135
|
+
result.hasChanges = true;
|
|
136
|
+
} else if (!lastSync) {
|
|
137
|
+
// Never synced, check which is newer
|
|
138
|
+
if (result.sourceModified > result.repoModified) {
|
|
139
|
+
result.direction = 'pull';
|
|
140
|
+
result.reason = 'Archivo original más nuevo (nunca sincronizado)';
|
|
141
|
+
result.hasChanges = true;
|
|
142
|
+
} else if (result.repoModified > result.sourceModified) {
|
|
143
|
+
result.direction = 'push';
|
|
144
|
+
result.reason = 'Repo más nuevo (nunca sincronizado)';
|
|
145
|
+
result.hasChanges = true;
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// Check what changed since last sync
|
|
149
|
+
const sourceChangedSinceSync = result.sourceModified > lastSync;
|
|
150
|
+
const repoChangedSinceSync = result.repoModified > lastSync;
|
|
151
|
+
|
|
152
|
+
if (sourceChangedSinceSync && repoChangedSinceSync) {
|
|
153
|
+
result.direction = 'conflict';
|
|
154
|
+
result.reason = 'Ambos modificados desde último sync';
|
|
155
|
+
result.hasChanges = true;
|
|
156
|
+
} else if (sourceChangedSinceSync) {
|
|
157
|
+
result.direction = 'pull';
|
|
158
|
+
result.reason = 'Archivo original modificado';
|
|
159
|
+
result.hasChanges = true;
|
|
160
|
+
} else if (repoChangedSinceSync) {
|
|
161
|
+
result.direction = 'push';
|
|
162
|
+
result.reason = 'Repo modificado';
|
|
163
|
+
result.hasChanges = true;
|
|
164
|
+
} else {
|
|
165
|
+
result.direction = 'none';
|
|
166
|
+
result.reason = 'Sin cambios desde último sync';
|
|
167
|
+
result.hasChanges = false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Execute pull for a project
|
|
176
|
+
* @param {Object} project - Project config
|
|
177
|
+
* @returns {Object} Result
|
|
178
|
+
*/
|
|
179
|
+
function executePull(project) {
|
|
180
|
+
const result = { success: false, message: '', filesChanged: 0 };
|
|
181
|
+
|
|
182
|
+
if (project.type === 'power-bi-project') {
|
|
183
|
+
const extractResult = pbix.extractPbipToRepo(project.source.path, project.projectPath);
|
|
184
|
+
|
|
185
|
+
if (extractResult.success) {
|
|
186
|
+
// Update project.json
|
|
187
|
+
project.source.lastSync = new Date().toISOString();
|
|
188
|
+
project.source.hash = pbix.getFileHash(project.source.path);
|
|
189
|
+
|
|
190
|
+
const configPath = path.join(project.projectPath, 'project.json');
|
|
191
|
+
fs.writeFileSync(configPath, JSON.stringify(project, null, 2));
|
|
192
|
+
|
|
193
|
+
result.success = true;
|
|
194
|
+
result.filesChanged = extractResult.files.length;
|
|
195
|
+
result.message = `Pulled ${extractResult.files.length} files`;
|
|
196
|
+
} else {
|
|
197
|
+
result.message = extractResult.error;
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
result.message = `Pull no soportado para tipo: ${project.type}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Execute push for a project
|
|
208
|
+
* @param {Object} project - Project config
|
|
209
|
+
* @returns {Object} Result
|
|
210
|
+
*/
|
|
211
|
+
function executePush(project) {
|
|
212
|
+
const result = { success: false, message: '', filesChanged: 0 };
|
|
213
|
+
|
|
214
|
+
if (project.type === 'power-bi-project') {
|
|
215
|
+
const definitionPath = pbix.getPbipDefinitionPath(project.source.path);
|
|
216
|
+
const repoDefinition = path.join(project.projectPath, 'definition');
|
|
217
|
+
|
|
218
|
+
if (!definitionPath) {
|
|
219
|
+
result.message = 'No se encontró carpeta definition en PBIP';
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!fs.existsSync(repoDefinition)) {
|
|
224
|
+
result.message = 'No hay definition en el repo';
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const files = pbix.copyDirectoryRecursive(repoDefinition, definitionPath);
|
|
230
|
+
|
|
231
|
+
// Update project.json
|
|
232
|
+
project.source.lastSync = new Date().toISOString();
|
|
233
|
+
const configPath = path.join(project.projectPath, 'project.json');
|
|
234
|
+
fs.writeFileSync(configPath, JSON.stringify(project, null, 2));
|
|
235
|
+
|
|
236
|
+
result.success = true;
|
|
237
|
+
result.filesChanged = files.length;
|
|
238
|
+
result.message = `Pushed ${files.length} files`;
|
|
239
|
+
} catch (e) {
|
|
240
|
+
result.message = e.message;
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
result.message = `Push no soportado para tipo: ${project.type}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Sync a single project
|
|
251
|
+
* @param {Object} project - Project config
|
|
252
|
+
* @param {Object} options - Sync options
|
|
253
|
+
* @param {readline.Interface} rl - Readline interface for prompts
|
|
254
|
+
* @returns {Promise<Object>} Sync result
|
|
255
|
+
*/
|
|
256
|
+
async function syncProject(project, options, rl) {
|
|
257
|
+
const syncInfo = determineSyncDirection(project);
|
|
258
|
+
|
|
259
|
+
console.log(`\n ${project.name}`);
|
|
260
|
+
console.log(` Tipo: ${project.type}`);
|
|
261
|
+
console.log(` Original: ${project.source?.path || 'No configurado'}`);
|
|
262
|
+
console.log(` Estado: ${syncInfo.reason}`);
|
|
263
|
+
|
|
264
|
+
if (!syncInfo.hasChanges) {
|
|
265
|
+
console.log(' → Sin cambios necesarios');
|
|
266
|
+
return { synced: false, direction: 'none' };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (syncInfo.direction === 'conflict') {
|
|
270
|
+
console.log(' ⚠ CONFLICTO: Ambos han sido modificados');
|
|
271
|
+
|
|
272
|
+
if (!options.force) {
|
|
273
|
+
const answer = await new Promise((resolve) => {
|
|
274
|
+
rl.question(' ¿Qué hacer? (pull/push/skip): ', resolve);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (answer === 'pull') {
|
|
278
|
+
syncInfo.direction = 'pull';
|
|
279
|
+
} else if (answer === 'push') {
|
|
280
|
+
syncInfo.direction = 'push';
|
|
281
|
+
} else {
|
|
282
|
+
console.log(' → Omitido');
|
|
283
|
+
return { synced: false, direction: 'skip' };
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
// Force mode defaults to pull (safer)
|
|
287
|
+
syncInfo.direction = 'pull';
|
|
288
|
+
console.log(' → Forzando pull (más seguro)');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let result;
|
|
293
|
+
if (syncInfo.direction === 'pull') {
|
|
294
|
+
console.log(' → Ejecutando pull...');
|
|
295
|
+
result = executePull(project);
|
|
296
|
+
} else if (syncInfo.direction === 'push') {
|
|
297
|
+
console.log(' → Ejecutando push...');
|
|
298
|
+
result = executePush(project);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (result.success) {
|
|
302
|
+
console.log(` ✓ ${result.message}`);
|
|
303
|
+
return { synced: true, direction: syncInfo.direction, filesChanged: result.filesChanged };
|
|
304
|
+
} else {
|
|
305
|
+
console.log(` ✗ ${result.message}`);
|
|
306
|
+
return { synced: false, direction: syncInfo.direction, error: result.message };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Main sync command handler
|
|
312
|
+
*/
|
|
313
|
+
async function syncSourceCommand(args, _config) {
|
|
314
|
+
const options = parseArgs(args);
|
|
315
|
+
|
|
316
|
+
if (options.help) {
|
|
317
|
+
showHelp();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Check if repo exists
|
|
322
|
+
const repoPath = profiles.getRepoPath();
|
|
323
|
+
if (!repoPath || !fs.existsSync(repoPath)) {
|
|
324
|
+
console.log(`
|
|
325
|
+
No se encontró el repositorio de BI.
|
|
326
|
+
|
|
327
|
+
Ejecuta primero:
|
|
328
|
+
super setup
|
|
329
|
+
`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log(`
|
|
334
|
+
════════════════════════════════════════════════════════════════
|
|
335
|
+
Sync - Sincronización Bidireccional
|
|
336
|
+
════════════════════════════════════════════════════════════════
|
|
337
|
+
|
|
338
|
+
Repositorio: ${repoPath}
|
|
339
|
+
`);
|
|
340
|
+
|
|
341
|
+
// Get projects to sync
|
|
342
|
+
let projects = [];
|
|
343
|
+
|
|
344
|
+
if (options.all) {
|
|
345
|
+
projects = getAllProjects(repoPath);
|
|
346
|
+
if (projects.length === 0) {
|
|
347
|
+
console.log('No hay proyectos en el repo.\n');
|
|
348
|
+
console.log('Añade uno con: super add "ruta/archivo.pbix"');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
console.log(`Sincronizando ${projects.length} proyecto(s)...`);
|
|
352
|
+
} else if (options.projectName) {
|
|
353
|
+
const project = getProject(repoPath, options.projectName);
|
|
354
|
+
if (!project) {
|
|
355
|
+
console.log(`✗ Proyecto no encontrado: ${options.projectName}`);
|
|
356
|
+
console.log('\nProyectos disponibles:');
|
|
357
|
+
getAllProjects(repoPath).forEach((p) => {
|
|
358
|
+
console.log(` - ${p.name}`);
|
|
359
|
+
});
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
projects = [project];
|
|
363
|
+
} else {
|
|
364
|
+
const current = detectCurrentProject(repoPath);
|
|
365
|
+
if (current) {
|
|
366
|
+
projects = [current];
|
|
367
|
+
} else {
|
|
368
|
+
// Show all projects status
|
|
369
|
+
projects = getAllProjects(repoPath);
|
|
370
|
+
if (projects.length === 0) {
|
|
371
|
+
console.log('No hay proyectos en el repo.\n');
|
|
372
|
+
console.log('Añade uno con: super add "ruta/archivo.pbix"');
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
console.log('Estado de todos los proyectos:');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Create readline interface
|
|
380
|
+
const rlInterface = createReadline();
|
|
381
|
+
|
|
382
|
+
// Sync each project
|
|
383
|
+
let syncedCount = 0;
|
|
384
|
+
let totalFilesChanged = 0;
|
|
385
|
+
|
|
386
|
+
for (const project of projects) {
|
|
387
|
+
const result = await syncProject(project, options, rlInterface);
|
|
388
|
+
if (result.synced) {
|
|
389
|
+
syncedCount++;
|
|
390
|
+
totalFilesChanged += result.filesChanged || 0;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
rlInterface.close();
|
|
395
|
+
|
|
396
|
+
// Commit changes if any
|
|
397
|
+
if (syncedCount > 0 && git.isGitRepo(repoPath)) {
|
|
398
|
+
console.log('\nCommiteando cambios...');
|
|
399
|
+
git.stageFiles(repoPath, '.');
|
|
400
|
+
const projectNames = projects
|
|
401
|
+
.filter((p) => p)
|
|
402
|
+
.map((p) => p.name)
|
|
403
|
+
.join(', ');
|
|
404
|
+
git.commit(repoPath, `Sync: ${projectNames}`);
|
|
405
|
+
console.log('✓ Cambios commiteados');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
console.log(`
|
|
409
|
+
════════════════════════════════════════════════════════════════
|
|
410
|
+
Resumen
|
|
411
|
+
════════════════════════════════════════════════════════════════
|
|
412
|
+
|
|
413
|
+
Proyectos sincronizados: ${syncedCount}/${projects.length}
|
|
414
|
+
Archivos cambiados: ${totalFilesChanged}
|
|
415
|
+
`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
module.exports = syncSourceCommand;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Sync Source Command
|
|
3
|
+
* @module commands/sync-source.test
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { test, describe } = require('node:test');
|
|
7
|
+
const assert = require('node:assert');
|
|
8
|
+
|
|
9
|
+
describe('Sync Source Command', () => {
|
|
10
|
+
test('module exports a function', () => {
|
|
11
|
+
const syncSourceCommand = require('./sync-source');
|
|
12
|
+
assert.strictEqual(typeof syncSourceCommand, 'function');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch Command (sentinel)
|
|
3
|
+
* ========================
|
|
4
|
+
* Watches for changes in skills and auto-regenerates the Claude Code plugin
|
|
5
|
+
* plus any configured legacy adapters.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* super sentinel Watch all configured outputs
|
|
9
|
+
* super sentinel --tool claude-plugin Watch a specific output only
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const tui = require('../utils/tui');
|
|
15
|
+
|
|
16
|
+
// Try to require chokidar, but handle if not available
|
|
17
|
+
let chokidar;
|
|
18
|
+
try {
|
|
19
|
+
chokidar = require('chokidar');
|
|
20
|
+
} catch (e) {
|
|
21
|
+
chokidar = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse command line arguments
|
|
26
|
+
* @param {string[]} args - CLI arguments
|
|
27
|
+
* @returns {Object} Parsed options
|
|
28
|
+
*/
|
|
29
|
+
function parseArgs(args) {
|
|
30
|
+
const options = {
|
|
31
|
+
tool: null, // null means all configured outputs
|
|
32
|
+
targetDir: process.cwd(),
|
|
33
|
+
debounce: 500, // ms
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
const arg = args[i];
|
|
38
|
+
|
|
39
|
+
if (arg === '--tool' || arg === '-t') {
|
|
40
|
+
options.tool = args[++i];
|
|
41
|
+
} else if (arg === '--debounce') {
|
|
42
|
+
options.debounce = parseInt(args[++i], 10) || 500;
|
|
43
|
+
} else if (!arg.startsWith('-')) {
|
|
44
|
+
options.targetDir = arg;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return options;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Main watch command handler
|
|
53
|
+
* @param {string[]} args - Command arguments
|
|
54
|
+
* @param {Object} config - CLI configuration with paths
|
|
55
|
+
* @param {Object} cliModule - Reference to main CLI module for regeneration
|
|
56
|
+
*/
|
|
57
|
+
function watchCommand(args, config, cliModule) {
|
|
58
|
+
if (!chokidar) {
|
|
59
|
+
tui.error('Watch mode requires the chokidar package.');
|
|
60
|
+
tui.info('Install dependencies with: npm install');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const options = parseArgs(args);
|
|
65
|
+
const skillsDir = config.skillsDir;
|
|
66
|
+
const targetDir = options.targetDir;
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(skillsDir)) {
|
|
69
|
+
tui.error('Skills directory not found. Run "bi-superpowers unlock" first.');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
tui.header('BI Agent Superpowers', 'Watch Mode');
|
|
74
|
+
|
|
75
|
+
tui.info(`Watching: ${skillsDir}`);
|
|
76
|
+
tui.info(`Target: ${targetDir}`);
|
|
77
|
+
if (options.tool) {
|
|
78
|
+
tui.info(`Tool: ${options.tool}`);
|
|
79
|
+
} else {
|
|
80
|
+
tui.info('Tool: All configured tools');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log('');
|
|
84
|
+
tui.muted('Press Ctrl+C to stop watching');
|
|
85
|
+
console.log('');
|
|
86
|
+
|
|
87
|
+
// Debounce timer
|
|
88
|
+
let debounceTimer = null;
|
|
89
|
+
const pendingChanges = new Set();
|
|
90
|
+
|
|
91
|
+
// Create watcher
|
|
92
|
+
const watcher = chokidar.watch(path.join(skillsDir, '*.md'), {
|
|
93
|
+
persistent: true,
|
|
94
|
+
ignoreInitial: true,
|
|
95
|
+
awaitWriteFinish: {
|
|
96
|
+
stabilityThreshold: 200,
|
|
97
|
+
pollInterval: 100,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle file change
|
|
103
|
+
* @param {string} changedPath - Path to changed file
|
|
104
|
+
* @param {string} eventType - Type of event (add, change, unlink)
|
|
105
|
+
*/
|
|
106
|
+
function handleChange(changedPath, _eventType) {
|
|
107
|
+
const filename = path.basename(changedPath);
|
|
108
|
+
pendingChanges.add(filename);
|
|
109
|
+
|
|
110
|
+
// Clear existing timer
|
|
111
|
+
if (debounceTimer) {
|
|
112
|
+
clearTimeout(debounceTimer);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Set new timer
|
|
116
|
+
debounceTimer = setTimeout(() => {
|
|
117
|
+
const changes = Array.from(pendingChanges);
|
|
118
|
+
pendingChanges.clear();
|
|
119
|
+
|
|
120
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
121
|
+
console.log(`\n${tui.colors.muted(`[${timestamp}]`)} ${tui.icons.watch} Changes detected:`);
|
|
122
|
+
changes.forEach((file) => {
|
|
123
|
+
console.log(` ${tui.icons.bullet} ${tui.colors.info(file)}`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Trigger regeneration
|
|
127
|
+
regenerateConfigs(targetDir, options.tool, cliModule);
|
|
128
|
+
}, options.debounce);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Watch events
|
|
132
|
+
watcher
|
|
133
|
+
.on('add', (path) => handleChange(path, 'add'))
|
|
134
|
+
.on('change', (path) => handleChange(path, 'change'))
|
|
135
|
+
.on('unlink', (path) => handleChange(path, 'unlink'))
|
|
136
|
+
.on('error', (error) => {
|
|
137
|
+
tui.error(`Watcher error: ${error.message}`);
|
|
138
|
+
})
|
|
139
|
+
.on('ready', () => {
|
|
140
|
+
tui.success('Watching for changes...');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Handle graceful shutdown
|
|
144
|
+
process.on('SIGINT', () => {
|
|
145
|
+
console.log('\n');
|
|
146
|
+
tui.info('Stopping watch mode...');
|
|
147
|
+
watcher.close();
|
|
148
|
+
process.exit(0);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Regenerate configs after changes detected
|
|
154
|
+
* @param {string} targetDir - Target directory
|
|
155
|
+
* @param {string|null} specificTool - Specific tool or null for all
|
|
156
|
+
* @param {Object} cliModule - CLI module reference
|
|
157
|
+
*/
|
|
158
|
+
function regenerateConfigs(targetDir, specificTool, cliModule) {
|
|
159
|
+
const ora = require('ora');
|
|
160
|
+
const spinner = ora('Regenerating configs...').start();
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
// Load the saved tool config
|
|
164
|
+
const configPath = path.join(targetDir, '.bi-superpowers.json');
|
|
165
|
+
let tools = ['claude-plugin'];
|
|
166
|
+
|
|
167
|
+
if (fs.existsSync(configPath)) {
|
|
168
|
+
try {
|
|
169
|
+
const savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
170
|
+
if (savedConfig.tools) {
|
|
171
|
+
tools = savedConfig.tools;
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// Ignore parse errors, use defaults
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!tools.includes('claude-plugin')) {
|
|
179
|
+
tools = ['claude-plugin', ...tools];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Filter to specific tool if requested
|
|
183
|
+
if (specificTool) {
|
|
184
|
+
tools = tools.filter((t) => t === specificTool);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (tools.length === 0) {
|
|
188
|
+
spinner.warn('No tools configured');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Call the sync logic from CLI module
|
|
193
|
+
if (cliModule && typeof cliModule.syncProjectInternal === 'function') {
|
|
194
|
+
cliModule.syncProjectInternal(targetDir, tools);
|
|
195
|
+
spinner.succeed(`Regenerated configs for: ${tools.join(', ')}`);
|
|
196
|
+
} else {
|
|
197
|
+
// Fallback: just report what would be done
|
|
198
|
+
spinner.succeed(`Would regenerate configs for: ${tools.join(', ')}`);
|
|
199
|
+
tui.muted('Note: Run "bi-superpowers recharge" manually to apply changes');
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
spinner.fail(`Regeneration failed: ${error.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = watchCommand;
|