@luquimbo/bi-superpowers 3.1.1 → 4.1.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/marketplace.json +5 -3
- package/.claude-plugin/plugin.json +28 -2
- package/.claude-plugin/skill-manifest.json +22 -6
- package/.plugin/plugin.json +1 -1
- package/AGENTS.md +52 -36
- package/CHANGELOG.md +295 -0
- package/README.md +75 -26
- package/bin/build-plugin.js +17 -10
- package/bin/cli.js +278 -322
- package/bin/commands/build-desktop.js +35 -16
- package/bin/commands/diff.js +31 -13
- package/bin/commands/install.js +93 -72
- package/bin/commands/lint.js +40 -26
- package/bin/commands/mcp-setup.js +3 -10
- package/bin/commands/update-check.js +389 -0
- package/bin/lib/agents.js +19 -0
- package/bin/lib/generators/claude-plugin.js +144 -6
- package/bin/lib/generators/shared.js +29 -33
- package/bin/lib/mcp-config.js +191 -16
- package/bin/lib/skills.js +115 -27
- package/bin/postinstall.js +4 -2
- package/bin/utils/mcp-detect.js +2 -2
- package/commands/bi-start.md +218 -0
- package/commands/pbi-connect.md +43 -65
- package/commands/project-kickoff.md +393 -673
- package/commands/report-design.md +403 -0
- package/desktop-extension/manifest.json +5 -12
- package/desktop-extension/server.js +34 -25
- package/package.json +6 -10
- package/skills/bi-start/SKILL.md +220 -0
- package/skills/bi-start/scripts/update-check.js +389 -0
- package/skills/pbi-connect/SKILL.md +45 -67
- package/skills/pbi-connect/scripts/update-check.js +389 -0
- package/skills/project-kickoff/SKILL.md +395 -675
- package/skills/project-kickoff/scripts/update-check.js +389 -0
- package/skills/report-design/SKILL.md +405 -0
- package/skills/report-design/references/cli-commands.md +184 -0
- package/skills/report-design/references/cli-setup.md +101 -0
- package/skills/report-design/references/close-write-open-pattern.md +80 -0
- package/skills/report-design/references/layouts/finance.md +65 -0
- package/skills/report-design/references/layouts/generic.md +46 -0
- package/skills/report-design/references/layouts/hr.md +48 -0
- package/skills/report-design/references/layouts/marketing.md +45 -0
- package/skills/report-design/references/layouts/operations.md +44 -0
- package/skills/report-design/references/layouts/sales.md +50 -0
- package/skills/report-design/references/native-visuals.md +341 -0
- package/skills/report-design/references/pbi-desktop-installation.md +87 -0
- package/skills/report-design/references/pbir-preview-activation.md +40 -0
- package/skills/report-design/references/slicer.md +89 -0
- package/skills/report-design/references/textbox.md +101 -0
- package/skills/report-design/references/themes/BISuperpowers.json +915 -0
- package/skills/report-design/references/troubleshooting.md +135 -0
- package/skills/report-design/references/visual-types.md +78 -0
- package/skills/report-design/scripts/apply-theme.js +243 -0
- package/skills/report-design/scripts/create-visual.js +878 -0
- package/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
- package/skills/report-design/scripts/update-check.js +389 -0
- package/skills/report-design/scripts/validate-pbir.js +322 -0
- package/src/content/base.md +12 -68
- package/src/content/mcp-requirements.json +0 -25
- package/src/content/routing.md +19 -74
- package/src/content/skills/bi-start.md +191 -0
- package/src/content/skills/pbi-connect.md +22 -65
- package/src/content/skills/project-kickoff.md +372 -673
- package/src/content/skills/report-design/SKILL.md +376 -0
- package/src/content/skills/report-design/references/cli-commands.md +184 -0
- package/src/content/skills/report-design/references/cli-setup.md +101 -0
- package/src/content/skills/report-design/references/close-write-open-pattern.md +80 -0
- package/src/content/skills/report-design/references/layouts/finance.md +65 -0
- package/src/content/skills/report-design/references/layouts/generic.md +46 -0
- package/src/content/skills/report-design/references/layouts/hr.md +48 -0
- package/src/content/skills/report-design/references/layouts/marketing.md +45 -0
- package/src/content/skills/report-design/references/layouts/operations.md +44 -0
- package/src/content/skills/report-design/references/layouts/sales.md +50 -0
- package/src/content/skills/report-design/references/native-visuals.md +341 -0
- package/src/content/skills/report-design/references/pbi-desktop-installation.md +87 -0
- package/src/content/skills/report-design/references/pbir-preview-activation.md +40 -0
- package/src/content/skills/report-design/references/slicer.md +89 -0
- package/src/content/skills/report-design/references/textbox.md +101 -0
- package/src/content/skills/report-design/references/themes/BISuperpowers.json +915 -0
- package/src/content/skills/report-design/references/troubleshooting.md +135 -0
- package/src/content/skills/report-design/references/visual-types.md +78 -0
- package/src/content/skills/report-design/scripts/apply-theme.js +243 -0
- package/src/content/skills/report-design/scripts/create-visual.js +878 -0
- package/src/content/skills/report-design/scripts/ensure-pbi-cli.sh +41 -0
- package/src/content/skills/report-design/scripts/validate-pbir.js +322 -0
- package/bin/commands/add.js +0 -533
- package/bin/commands/add.test.js +0 -77
- package/bin/commands/changelog.js +0 -443
- package/bin/commands/install.test.js +0 -289
- package/bin/commands/lint.test.js +0 -103
- package/bin/commands/pull.js +0 -287
- package/bin/commands/pull.test.js +0 -36
- package/bin/commands/push.js +0 -231
- package/bin/commands/push.test.js +0 -14
- package/bin/commands/search.js +0 -344
- package/bin/commands/search.test.js +0 -115
- package/bin/commands/setup.js +0 -545
- package/bin/commands/setup.test.js +0 -46
- package/bin/commands/sync-profile.js +0 -405
- package/bin/commands/sync-profile.test.js +0 -14
- package/bin/commands/sync-source.js +0 -418
- package/bin/commands/sync-source.test.js +0 -14
- package/bin/lib/generators/claude-plugin.test.js +0 -111
- package/bin/lib/mcp-config.test.js +0 -310
- package/bin/lib/microsoft-mcp.test.js +0 -115
- package/bin/utils/errors.js +0 -159
- package/bin/utils/git.js +0 -298
- package/bin/utils/logger.js +0 -142
- package/bin/utils/mcp-detect.test.js +0 -81
- package/bin/utils/pbix.js +0 -305
- package/bin/utils/pbix.test.js +0 -37
- package/bin/utils/profiles.js +0 -312
- package/bin/utils/projects.js +0 -169
- package/bin/utils/readline.js +0 -206
- package/bin/utils/readline.test.js +0 -47
- package/bin/utils/tui.test.js +0 -127
- package/docs/openrouter-free-models.md +0 -92
- package/library/examples/README.md +0 -151
- package/library/examples/finance-reporting/README.md +0 -351
- package/library/examples/finance-reporting/data-model.md +0 -267
- package/library/examples/finance-reporting/measures.dax +0 -557
- package/library/examples/hr-analytics/README.md +0 -371
- package/library/examples/hr-analytics/data-model.md +0 -315
- package/library/examples/hr-analytics/measures.dax +0 -460
- package/library/examples/marketing-analytics/README.md +0 -37
- package/library/examples/marketing-analytics/data-model.md +0 -62
- package/library/examples/marketing-analytics/measures.dax +0 -110
- package/library/examples/retail-analytics/README.md +0 -439
- package/library/examples/retail-analytics/data-model.md +0 -288
- package/library/examples/retail-analytics/measures.dax +0 -481
- package/library/examples/supply-chain/README.md +0 -37
- package/library/examples/supply-chain/data-model.md +0 -69
- package/library/examples/supply-chain/measures.dax +0 -77
- package/library/examples/udf-library/README.md +0 -228
- package/library/examples/udf-library/functions.dax +0 -571
- package/library/snippets/dax/README.md +0 -292
- package/library/snippets/dax/business-domains.md +0 -576
- package/library/snippets/dax/calculate-patterns.md +0 -276
- package/library/snippets/dax/calculation-groups.md +0 -489
- package/library/snippets/dax/error-handling.md +0 -495
- package/library/snippets/dax/iterators-and-aggregations.md +0 -474
- package/library/snippets/dax/kpis-and-metrics.md +0 -293
- package/library/snippets/dax/rankings-and-topn.md +0 -235
- package/library/snippets/dax/security-patterns.md +0 -413
- package/library/snippets/dax/text-and-formatting.md +0 -316
- package/library/snippets/dax/time-intelligence.md +0 -196
- package/library/snippets/dax/user-defined-functions.md +0 -477
- package/library/snippets/dax/virtual-tables.md +0 -546
- package/library/snippets/excel-formulas/README.md +0 -84
- package/library/snippets/excel-formulas/aggregations.md +0 -330
- package/library/snippets/excel-formulas/dates-and-times.md +0 -361
- package/library/snippets/excel-formulas/dynamic-arrays.md +0 -314
- package/library/snippets/excel-formulas/lookups.md +0 -169
- package/library/snippets/excel-formulas/text-functions.md +0 -363
- package/library/snippets/governance/naming-conventions.md +0 -97
- package/library/snippets/governance/review-checklists.md +0 -107
- package/library/snippets/power-query/README.md +0 -389
- package/library/snippets/power-query/api-integration.md +0 -707
- package/library/snippets/power-query/connections.md +0 -434
- package/library/snippets/power-query/data-cleaning.md +0 -298
- package/library/snippets/power-query/error-handling.md +0 -526
- package/library/snippets/power-query/parameters.md +0 -350
- package/library/snippets/power-query/performance.md +0 -506
- package/library/snippets/power-query/transformations.md +0 -330
- package/library/snippets/report-design/accessibility.md +0 -78
- package/library/snippets/report-design/chart-selection.md +0 -54
- package/library/snippets/report-design/layout-patterns.md +0 -87
- package/library/templates/data-models/README.md +0 -93
- package/library/templates/data-models/finance-model.md +0 -627
- package/library/templates/data-models/retail-star-schema.md +0 -473
- package/library/templates/excel/README.md +0 -83
- package/library/templates/excel/budget-tracker.md +0 -432
- package/library/templates/excel/data-entry-form.md +0 -533
- package/library/templates/power-bi/README.md +0 -72
- package/library/templates/power-bi/finance-report.md +0 -449
- package/library/templates/power-bi/kpi-scorecard.md +0 -461
- package/library/templates/power-bi/sales-dashboard.md +0 -281
- package/library/themes/excel/README.md +0 -436
- package/library/themes/power-bi/README.md +0 -271
- package/library/themes/power-bi/accessible.json +0 -307
- package/library/themes/power-bi/bi-superpowers-default.json +0 -858
- package/library/themes/power-bi/corporate-blue.json +0 -291
- package/library/themes/power-bi/dark-mode.json +0 -291
- package/library/themes/power-bi/minimal.json +0 -292
- package/library/themes/power-bi/print-friendly.json +0 -309
|
@@ -20,10 +20,12 @@ const {
|
|
|
20
20
|
} = require('../microsoft-mcp');
|
|
21
21
|
const { parseSkillMetadata, getSkillPurpose } = require('./shared');
|
|
22
22
|
|
|
23
|
-
// Currently the plugin ships
|
|
23
|
+
// Currently the plugin ships 4 skills (bi-start, project-kickoff,
|
|
24
|
+
// pbi-connect, report-design). bi-start is the session-opener that
|
|
25
|
+
// routes to the other three; the rest are specialists.
|
|
24
26
|
// The old reference-skills split is kept as an empty set so that any
|
|
25
27
|
// downstream code that checks `REFERENCE_SKILLS.has(x)` still works.
|
|
26
|
-
const COMMAND_SKILLS = new Set(['project-kickoff', 'pbi-connect']);
|
|
28
|
+
const COMMAND_SKILLS = new Set(['bi-start', 'project-kickoff', 'pbi-connect', 'report-design']);
|
|
27
29
|
|
|
28
30
|
const REFERENCE_SKILLS = new Set();
|
|
29
31
|
|
|
@@ -38,6 +40,36 @@ function ensureDirectory(directory) {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Recursively copy a directory tree from `source` to `target`.
|
|
45
|
+
* Follows the Node built-in cp() semantics: overwrites existing files,
|
|
46
|
+
* preserves directory structure. No-op if source does not exist.
|
|
47
|
+
*
|
|
48
|
+
* Used to propagate `references/` and `scripts/` subfolders from
|
|
49
|
+
* folder-based source skills into the generated plugin so the agent can
|
|
50
|
+
* load on-demand material and run bundled helper scripts.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} source - Source directory (must exist to do anything)
|
|
53
|
+
* @param {string} target - Target directory (created if missing)
|
|
54
|
+
*/
|
|
55
|
+
function copyDirectory(source, target) {
|
|
56
|
+
if (!fs.existsSync(source)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
ensureDirectory(target);
|
|
60
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
61
|
+
const sourcePath = path.join(source, entry.name);
|
|
62
|
+
const targetPath = path.join(target, entry.name);
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
copyDirectory(sourcePath, targetPath);
|
|
65
|
+
} else if (entry.isFile()) {
|
|
66
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
67
|
+
}
|
|
68
|
+
// Ignore symlinks / other entry types to match the conservative
|
|
69
|
+
// behaviour of readSkillDirectory().
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
41
73
|
/**
|
|
42
74
|
* Rewrite library references for generated plugin targets.
|
|
43
75
|
*
|
|
@@ -92,16 +124,57 @@ function toFrontmatterValue(value) {
|
|
|
92
124
|
return JSON.stringify(value);
|
|
93
125
|
}
|
|
94
126
|
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Update-check preamble
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Cross-agent "auto-update" mechanism (see ROADMAP item 4 / gstack pattern).
|
|
131
|
+
// The preamble is prepended to every generated SKILL.md and command Markdown
|
|
132
|
+
// so the agent reading the skill content runs the version check at the top
|
|
133
|
+
// of each conversation, surfaces an update notice if available, and then
|
|
134
|
+
// proceeds with the skill. The check caches for 24h on disk and degrades to
|
|
135
|
+
// no-output on network failure — it must never block the skill invocation.
|
|
136
|
+
//
|
|
137
|
+
// The update-check.js script is shipped alongside each skill's `scripts/`
|
|
138
|
+
// folder at generation time (see copyUpdateCheckScript) so `{skillBundleDir}`
|
|
139
|
+
// resolves correctly regardless of the install path (npm-based `super
|
|
140
|
+
// install` OR Claude Code plugin marketplace).
|
|
141
|
+
|
|
142
|
+
const UPDATE_CHECK_PREAMBLE = `<!-- bi-superpowers update-check preamble (auto-generated, do not edit) -->
|
|
143
|
+
## ⬆ Update check
|
|
144
|
+
|
|
145
|
+
Before handling the user's request below, run this command **once per conversation** and interpret the output:
|
|
146
|
+
|
|
147
|
+
\`\`\`bash
|
|
148
|
+
node "{skillBundleDir}/scripts/update-check.js" --silent-if-uptodate --silent-if-snoozed
|
|
149
|
+
\`\`\`
|
|
150
|
+
|
|
151
|
+
- Empty output or \`UPTODATE\` — proceed with the skill silently. No message.
|
|
152
|
+
- \`UPDATE_AVAILABLE <installed> <latest>\` — tell the user exactly once this conversation, before diving into the skill:
|
|
153
|
+
> "Hay **bi-superpowers v{latest}** disponible (estás en v{installed}). Actualizá con \`super upgrade\` (o \`/plugin update bi-superpowers\` en Claude Code) cuando te venga bien."
|
|
154
|
+
|
|
155
|
+
Then continue with the skill below.
|
|
156
|
+
- \`SNOOZED <iso>\` — proceed silently.
|
|
157
|
+
|
|
158
|
+
If the command fails (missing binary, permissions, offline), ignore the error and proceed with the skill. The update check must never block the user's request.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
<!-- /bi-superpowers update-check preamble -->
|
|
162
|
+
|
|
163
|
+
`;
|
|
164
|
+
|
|
95
165
|
/**
|
|
96
166
|
* Build command markdown with Claude Code frontmatter.
|
|
97
167
|
*
|
|
98
168
|
* @param {Object} skill - Skill definition object
|
|
99
169
|
* @param {string} libraryPrefix - Library prefix used in generated target
|
|
170
|
+
* @param {Object} [options] - Options
|
|
171
|
+
* @param {boolean} [options.skipPreamble] - Omit the update-check preamble (tests)
|
|
100
172
|
* @returns {string} Command markdown
|
|
101
173
|
*/
|
|
102
|
-
function buildCommandMarkdown(skill, libraryPrefix) {
|
|
174
|
+
function buildCommandMarkdown(skill, libraryPrefix, options = {}) {
|
|
103
175
|
const description = getSkillPurpose(skill.name);
|
|
104
176
|
const content = rewriteLibraryReferences(skill.content, libraryPrefix);
|
|
177
|
+
const preamble = options.skipPreamble ? '' : UPDATE_CHECK_PREAMBLE;
|
|
105
178
|
|
|
106
179
|
return `---
|
|
107
180
|
description: ${toFrontmatterValue(description)}
|
|
@@ -109,7 +182,7 @@ description: ${toFrontmatterValue(description)}
|
|
|
109
182
|
|
|
110
183
|
<!-- Generated by BI Agent Superpowers. Edit src/content/skills/${skill.name}.md instead. -->
|
|
111
184
|
|
|
112
|
-
${content}`;
|
|
185
|
+
${preamble}${content}`;
|
|
113
186
|
}
|
|
114
187
|
|
|
115
188
|
/**
|
|
@@ -118,10 +191,13 @@ ${content}`;
|
|
|
118
191
|
* @param {Object} skill - Skill definition object
|
|
119
192
|
* @param {string} version - Package version
|
|
120
193
|
* @param {string} libraryPrefix - Library prefix used in generated target
|
|
194
|
+
* @param {Object} [options] - Options
|
|
195
|
+
* @param {boolean} [options.skipPreamble] - Omit the update-check preamble (tests)
|
|
121
196
|
* @returns {string} Skill markdown
|
|
122
197
|
*/
|
|
123
|
-
function buildSkillMarkdown(skill, version, libraryPrefix) {
|
|
198
|
+
function buildSkillMarkdown(skill, version, libraryPrefix, options = {}) {
|
|
124
199
|
const content = rewriteLibraryReferences(skill.content, libraryPrefix);
|
|
200
|
+
const preamble = options.skipPreamble ? '' : UPDATE_CHECK_PREAMBLE;
|
|
125
201
|
|
|
126
202
|
return `---
|
|
127
203
|
name: ${toFrontmatterValue(skill.name)}
|
|
@@ -131,7 +207,29 @@ version: ${toFrontmatterValue(version)}
|
|
|
131
207
|
|
|
132
208
|
<!-- Generated by BI Agent Superpowers. Edit src/content/skills/${skill.name}.md instead. -->
|
|
133
209
|
|
|
134
|
-
${content}`;
|
|
210
|
+
${preamble}${content}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Copy bin/commands/update-check.js into each generated skill's scripts/
|
|
215
|
+
* directory so the preamble's `{skillBundleDir}/scripts/update-check.js`
|
|
216
|
+
* invocation works under both install paths (npm-based `super install` and
|
|
217
|
+
* Claude Code plugin marketplace). The file is bundled with the npm tarball
|
|
218
|
+
* via bin/, so this is purely a generation-time copy into skills/.
|
|
219
|
+
*
|
|
220
|
+
* @param {string} packageDir - npm package root (source of the canonical script)
|
|
221
|
+
* @param {string} targetDir - plugin target root (contains skills/<name>/)
|
|
222
|
+
* @param {Object[]} skills - Skills to wire the script into
|
|
223
|
+
*/
|
|
224
|
+
function copyUpdateCheckScript(packageDir, targetDir, skills) {
|
|
225
|
+
if (!packageDir) return;
|
|
226
|
+
const src = path.join(packageDir, 'bin', 'commands', 'update-check.js');
|
|
227
|
+
if (!fs.existsSync(src)) return;
|
|
228
|
+
for (const skill of skills) {
|
|
229
|
+
const scriptsDir = path.join(targetDir, 'skills', skill.name, 'scripts');
|
|
230
|
+
ensureDirectory(scriptsDir);
|
|
231
|
+
fs.copyFileSync(src, path.join(scriptsDir, 'update-check.js'));
|
|
232
|
+
}
|
|
135
233
|
}
|
|
136
234
|
|
|
137
235
|
/**
|
|
@@ -178,6 +276,25 @@ async function generate(targetDir, skills, options = {}) {
|
|
|
178
276
|
ensureDirectory(commandsDir);
|
|
179
277
|
ensureDirectory(skillsDir);
|
|
180
278
|
|
|
279
|
+
// Pull discoverable metadata from the installed package.json when available
|
|
280
|
+
// so the generated plugin.json has the same repo/license/keywords/homepage
|
|
281
|
+
// fields as the published npm package. Falls back to bare identity fields
|
|
282
|
+
// if options.packageDir is missing (unit tests, detached generation).
|
|
283
|
+
let pkgMetadata = {};
|
|
284
|
+
if (options.packageDir) {
|
|
285
|
+
try {
|
|
286
|
+
const pkg = require(path.join(options.packageDir, 'package.json'));
|
|
287
|
+
pkgMetadata = {
|
|
288
|
+
repository: pkg.repository,
|
|
289
|
+
homepage: pkg.homepage,
|
|
290
|
+
license: pkg.license,
|
|
291
|
+
keywords: Array.isArray(pkg.keywords) ? pkg.keywords : undefined,
|
|
292
|
+
};
|
|
293
|
+
} catch (_) {
|
|
294
|
+
// Package.json unreadable — generate a minimal manifest and move on.
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
181
298
|
const pluginManifest = {
|
|
182
299
|
name: 'bi-superpowers',
|
|
183
300
|
description:
|
|
@@ -186,6 +303,10 @@ async function generate(targetDir, skills, options = {}) {
|
|
|
186
303
|
author: {
|
|
187
304
|
name: 'Lucas Sanchez',
|
|
188
305
|
},
|
|
306
|
+
...(pkgMetadata.repository !== undefined ? { repository: pkgMetadata.repository } : {}),
|
|
307
|
+
...(pkgMetadata.homepage !== undefined ? { homepage: pkgMetadata.homepage } : {}),
|
|
308
|
+
...(pkgMetadata.license !== undefined ? { license: pkgMetadata.license } : {}),
|
|
309
|
+
...(pkgMetadata.keywords !== undefined ? { keywords: pkgMetadata.keywords } : {}),
|
|
189
310
|
};
|
|
190
311
|
|
|
191
312
|
fs.writeFileSync(
|
|
@@ -312,6 +433,9 @@ async function generate(targetDir, skills, options = {}) {
|
|
|
312
433
|
}
|
|
313
434
|
|
|
314
435
|
// ALL skills → skills/*/SKILL.md (universal, discoverable by all Claude tools)
|
|
436
|
+
// Folder-based skills additionally get their `references/` and `scripts/`
|
|
437
|
+
// subfolders copied verbatim so the agent can load on-demand material and
|
|
438
|
+
// invoke helper scripts bundled with the skill.
|
|
315
439
|
for (const skill of skills) {
|
|
316
440
|
const skillDir = path.join(skillsDir, skill.name);
|
|
317
441
|
ensureDirectory(skillDir);
|
|
@@ -319,8 +443,18 @@ async function generate(targetDir, skills, options = {}) {
|
|
|
319
443
|
path.join(skillDir, 'SKILL.md'),
|
|
320
444
|
buildSkillMarkdown(skill, version, libraryPrefix)
|
|
321
445
|
);
|
|
446
|
+
|
|
447
|
+
if (skill.bundleDir) {
|
|
448
|
+
copyDirectory(path.join(skill.bundleDir, 'references'), path.join(skillDir, 'references'));
|
|
449
|
+
copyDirectory(path.join(skill.bundleDir, 'scripts'), path.join(skillDir, 'scripts'));
|
|
450
|
+
}
|
|
322
451
|
}
|
|
323
452
|
|
|
453
|
+
// Ship the update-check helper alongside every skill so the SKILL.md
|
|
454
|
+
// preamble can invoke it via `{skillBundleDir}/scripts/update-check.js`
|
|
455
|
+
// regardless of install path.
|
|
456
|
+
copyUpdateCheckScript(options.packageDir, targetDir, skills);
|
|
457
|
+
|
|
324
458
|
console.log(' ✓ Created Claude Code plugin manifest');
|
|
325
459
|
console.log(' ✓ Created .mcp.json with official Microsoft MCP servers');
|
|
326
460
|
console.log(` ✓ Created ${commandSkills.length} plugin commands`);
|
|
@@ -331,6 +465,10 @@ module.exports = {
|
|
|
331
465
|
name: 'Claude Code Plugin',
|
|
332
466
|
description: 'Native Claude Code plugin (recommended)',
|
|
333
467
|
generate,
|
|
468
|
+
buildCommandMarkdown,
|
|
469
|
+
buildSkillMarkdown,
|
|
470
|
+
copyUpdateCheckScript,
|
|
471
|
+
UPDATE_CHECK_PREAMBLE,
|
|
334
472
|
COMMAND_SKILLS,
|
|
335
473
|
REFERENCE_SKILLS,
|
|
336
474
|
};
|
|
@@ -29,12 +29,19 @@ function parseSkillMetadata(content) {
|
|
|
29
29
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
30
30
|
if (titleMatch) metadata.title = titleMatch[1];
|
|
31
31
|
|
|
32
|
-
// Get triggers
|
|
32
|
+
// Get triggers — collect every double-quoted fragment in the Trigger
|
|
33
|
+
// section. This handles all three shapes our skills use:
|
|
34
|
+
// - Bare-quote bullets: - "connect Power BI", "PBI connection"
|
|
35
|
+
// - Prose bullets: - User mentions: "analizar proyecto", "new project"
|
|
36
|
+
// - Asterisk bullets: * "pattern"
|
|
37
|
+
// The regex intentionally doesn't anchor to bullet syntax — that's
|
|
38
|
+
// what broke extraction for project-kickoff (prose) and under-counted
|
|
39
|
+
// pbi-connect (first quote per bullet only) before this change.
|
|
33
40
|
const triggerSection = content.match(/##\s+Trigger[\s\S]*?(?=##|$)/i);
|
|
34
41
|
if (triggerSection) {
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
metadata.triggers =
|
|
42
|
+
const quotedFragments = triggerSection[0].match(/"([^"]+)"/g);
|
|
43
|
+
if (quotedFragments) {
|
|
44
|
+
metadata.triggers = quotedFragments.map((t) => t.slice(1, -1));
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
|
|
@@ -47,41 +54,29 @@ function parseSkillMetadata(content) {
|
|
|
47
54
|
return metadata;
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Map of skill name → one-line purpose description used in generated
|
|
59
|
+
* commands/*.md frontmatter and the CLI's `super powers` output.
|
|
60
|
+
*
|
|
61
|
+
* Keep this map in lock-step with `src/content/skills/`. Adding a skill
|
|
62
|
+
* without an entry here makes the generated description fall back to the
|
|
63
|
+
* generic `'Specialized BI assistance'` string and breaks the parity
|
|
64
|
+
* test in shared.test.js.
|
|
65
|
+
*/
|
|
66
|
+
const SKILL_PURPOSES = {
|
|
67
|
+
'bi-start': 'Session opener — environment snapshot, update check, and routing to other skills',
|
|
68
|
+
'project-kickoff': 'Project analysis and planning',
|
|
69
|
+
'pbi-connect': 'Power BI Desktop connection',
|
|
70
|
+
'report-design': '3-page PBIR report generation for Power BI Desktop (Windows)',
|
|
71
|
+
};
|
|
72
|
+
|
|
50
73
|
/**
|
|
51
74
|
* Get skill purpose description for table
|
|
52
75
|
* @param {string} skillName - Name of the skill
|
|
53
76
|
* @returns {string} Purpose description
|
|
54
77
|
*/
|
|
55
78
|
function getSkillPurpose(skillName) {
|
|
56
|
-
|
|
57
|
-
dax: 'DAX writing and optimization',
|
|
58
|
-
'power-query': 'Power Query / M language patterns',
|
|
59
|
-
'data-modeling': 'Star schema design',
|
|
60
|
-
'data-model-design': 'Interactive model builder',
|
|
61
|
-
'excel-formulas': 'Modern Excel 365 formulas',
|
|
62
|
-
'project-kickoff': 'Project analysis and planning',
|
|
63
|
-
'theme-tweaker': 'Power BI theme customization',
|
|
64
|
-
'rls-design': 'Row-level security design',
|
|
65
|
-
'query-performance': 'Performance optimization',
|
|
66
|
-
'data-quality': 'Data validation and profiling',
|
|
67
|
-
'fabric-scripts': 'Fabric automation scripts',
|
|
68
|
-
'fast-standard': 'FAST spreadsheet standard',
|
|
69
|
-
'testing-validation': 'Testing and validation patterns',
|
|
70
|
-
'pbi-connect': 'Power BI Desktop connection',
|
|
71
|
-
contributions: 'Contribution validation',
|
|
72
|
-
// New command skills
|
|
73
|
-
'dax-doctor': 'DAX debugging and optimization wizard',
|
|
74
|
-
'model-documenter': 'Semantic model documentation generator',
|
|
75
|
-
'migration-assistant': 'Migration and upgrade assistant',
|
|
76
|
-
'report-layout': 'Report page layout planner',
|
|
77
|
-
// New reference skills
|
|
78
|
-
governance: 'Naming conventions, standards, and governance',
|
|
79
|
-
'semantic-model': 'Semantic model best practices and patterns',
|
|
80
|
-
'report-design': 'Report design and visualization principles',
|
|
81
|
-
deployment: 'CI/CD and deployment patterns for BI',
|
|
82
|
-
'dax-udf': 'DAX user-defined functions (UDFs)',
|
|
83
|
-
};
|
|
84
|
-
return purposes[skillName] || 'Specialized BI assistance';
|
|
79
|
+
return SKILL_PURPOSES[skillName] || 'Specialized BI assistance';
|
|
85
80
|
}
|
|
86
81
|
|
|
87
82
|
/**
|
|
@@ -243,6 +238,7 @@ ${footer}`;
|
|
|
243
238
|
module.exports = {
|
|
244
239
|
parseSkillMetadata,
|
|
245
240
|
getSkillPurpose,
|
|
241
|
+
SKILL_PURPOSES,
|
|
246
242
|
generateSkillsSection,
|
|
247
243
|
getCodeStandards,
|
|
248
244
|
getFormatHeader,
|
package/bin/lib/mcp-config.js
CHANGED
|
@@ -29,7 +29,7 @@ const path = require('path');
|
|
|
29
29
|
const os = require('os');
|
|
30
30
|
|
|
31
31
|
const MICROSOFT_LEARN_URL = 'https://learn.microsoft.com/api/mcp';
|
|
32
|
-
const MODELING_SERVER_NAME = 'powerbi-modeling';
|
|
32
|
+
const MODELING_SERVER_NAME = 'powerbi-modeling-mcp';
|
|
33
33
|
const LEARN_SERVER_NAME = 'microsoft-learn';
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -42,6 +42,11 @@ function getLauncherAbsolutePath(packageDir) {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Safely read a JSON file. Returns null if missing or unparseable.
|
|
45
|
+
*
|
|
46
|
+
* NOTE: this swallows parse errors. Do NOT use it before a write — a
|
|
47
|
+
* silent null on a corrupt user config means a spread merge ends up
|
|
48
|
+
* replacing the whole config with our 2 servers (data loss). Use
|
|
49
|
+
* `readJsonStrict` for write paths instead.
|
|
45
50
|
*/
|
|
46
51
|
function readJsonSafe(filePath) {
|
|
47
52
|
if (!fs.existsSync(filePath)) return null;
|
|
@@ -52,6 +57,36 @@ function readJsonSafe(filePath) {
|
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Read a JSON file for write paths. Returns null only if the file is
|
|
62
|
+
* absent or empty. Throws an actionable error if the file exists with
|
|
63
|
+
* non-empty content but is not parseable — silently overwriting a
|
|
64
|
+
* user's corrupt config (especially `~/.claude.json`) destroys data.
|
|
65
|
+
*
|
|
66
|
+
* @throws {Error} if the file exists but is unreadable or unparseable
|
|
67
|
+
*/
|
|
68
|
+
function readJsonStrict(filePath) {
|
|
69
|
+
if (!fs.existsSync(filePath)) return null;
|
|
70
|
+
let raw;
|
|
71
|
+
try {
|
|
72
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
73
|
+
} catch (err) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Could not read ${filePath}: ${err.message}. ` + 'Check file permissions and retry.'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (raw.trim() === '') return null;
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(raw);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Refusing to overwrite ${filePath}: file exists but is not valid JSON ` +
|
|
84
|
+
`(${err.message}). Inspect or move it manually before retrying. ` +
|
|
85
|
+
`If a previous-install backup exists at ${filePath}.bak, restore from there.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
55
90
|
/**
|
|
56
91
|
* Reject symlink attacks before writing to a file. We use lstatSync so
|
|
57
92
|
* we detect the link itself (not what it points to). If a user home has
|
|
@@ -69,16 +104,132 @@ function assertNotSymlink(filePath) {
|
|
|
69
104
|
}
|
|
70
105
|
|
|
71
106
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
107
|
+
* Walk the ancestors of `filePath` up to (but not including) the user's
|
|
108
|
+
* home directory and reject if any ancestor is a symbolic link. Needed
|
|
109
|
+
* because `assertNotSymlink` only checks the leaf — a symlink at a
|
|
110
|
+
* parent (e.g. `~/.copilot` → `/tmp/attacker/`) would still redirect
|
|
111
|
+
* every `.tmp` / `.bak` / final rename through the link, bypassing the
|
|
112
|
+
* leaf-only hardening.
|
|
113
|
+
*
|
|
114
|
+
* Trust boundaries:
|
|
115
|
+
* - We stop walking at `os.homedir()` itself. If the user's home is a
|
|
116
|
+
* symlink, that's their machine setup, not something we can police.
|
|
117
|
+
* - We also stop if, after `realpathSync`, the ancestor resolves
|
|
118
|
+
* outside the home subtree — defensive, covers the case where an
|
|
119
|
+
* earlier ancestor already redirected us elsewhere.
|
|
120
|
+
* - We stop at filesystem root (`path.dirname(x) === x`).
|
|
121
|
+
*
|
|
122
|
+
* Known Windows gap: this uses `lstatSync().isSymbolicLink()`. NTFS
|
|
123
|
+
* junctions are NOT classified as symlinks by Node, so a junction at
|
|
124
|
+
* `~/.copilot` would pass this check. Documented as a residual risk
|
|
125
|
+
* in `coordination/TO_CODEX_REVIEW.md`; addressing it needs
|
|
126
|
+
* `fs.readlinkSync` with reparse-point inspection and is out of scope
|
|
127
|
+
* for this commit.
|
|
128
|
+
*
|
|
129
|
+
* @throws {Error} if any ancestor of filePath (up to home) is a symlink
|
|
74
130
|
*/
|
|
75
|
-
function
|
|
131
|
+
function assertNoSymlinkedAncestor(filePath) {
|
|
132
|
+
const home = os.homedir();
|
|
133
|
+
const homeResolved = fs.existsSync(home) ? fs.realpathSync(home) : home;
|
|
134
|
+
|
|
135
|
+
let current = path.dirname(filePath);
|
|
136
|
+
const visited = new Set();
|
|
137
|
+
|
|
138
|
+
while (current && !visited.has(current)) {
|
|
139
|
+
visited.add(current);
|
|
140
|
+
|
|
141
|
+
// Stop at the user's home — trust boundary.
|
|
142
|
+
if (current === home || current === homeResolved) return;
|
|
143
|
+
|
|
144
|
+
if (fs.existsSync(current)) {
|
|
145
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Refusing to write MCP config: parent directory ${current} is a symbolic link. ` +
|
|
148
|
+
'Remove the symlink (or redirect super install at a different path) and retry.'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Defensive: if realpath has already pulled us outside the home
|
|
153
|
+
// subtree, stop walking — we're in territory we can't reason about.
|
|
154
|
+
const currentResolved = fs.realpathSync(current);
|
|
155
|
+
if (!currentResolved.startsWith(homeResolved)) return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const parent = path.dirname(current);
|
|
159
|
+
if (parent === current) return; // filesystem root
|
|
160
|
+
current = parent;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Full pre-write safety check: leaf must not be a symlink, and no
|
|
166
|
+
* ancestor up to `os.homedir()` may be a symlink either. Use this
|
|
167
|
+
* instead of `assertNotSymlink` on every write path inside the user
|
|
168
|
+
* home directory.
|
|
169
|
+
*/
|
|
170
|
+
function assertPathSafeForWrite(filePath) {
|
|
76
171
|
assertNotSymlink(filePath);
|
|
172
|
+
assertNoSymlinkedAncestor(filePath);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Write any text content to a file using a safe pattern:
|
|
177
|
+
*
|
|
178
|
+
* 1. Refuse if the target is a symbolic link (assertNotSymlink).
|
|
179
|
+
* 2. If the target exists, copy it to `<target>.bak` first. This
|
|
180
|
+
* gives the user a one-slot recovery file overwritten on each
|
|
181
|
+
* install — enough to recover from a bad merge without
|
|
182
|
+
* accumulating stale backups across many install runs.
|
|
183
|
+
* 3. Write to `<target>.tmp` then atomically rename to the final
|
|
184
|
+
* path. `fs.renameSync` is atomic on POSIX and functionally
|
|
185
|
+
* atomic for same-volume renames on NTFS, which is the only
|
|
186
|
+
* shape we hit when writing into the user home (~/...).
|
|
187
|
+
* 4. On any error mid-process, best-effort cleanup of the .tmp
|
|
188
|
+
* file so we never leave half-written turds behind.
|
|
189
|
+
*
|
|
190
|
+
* The atomic + backup pattern matters most for `~/.claude.json` —
|
|
191
|
+
* Claude Code's primary user config holds project pointers, OAuth
|
|
192
|
+
* state, conversation history. A crash mid-write or a bad merge
|
|
193
|
+
* would brick the user's Claude install otherwise.
|
|
194
|
+
*/
|
|
195
|
+
function writeFileAtomic(filePath, content) {
|
|
196
|
+
assertPathSafeForWrite(filePath);
|
|
77
197
|
const dir = path.dirname(filePath);
|
|
78
198
|
if (!fs.existsSync(dir)) {
|
|
79
199
|
fs.mkdirSync(dir, { recursive: true });
|
|
80
200
|
}
|
|
81
|
-
|
|
201
|
+
|
|
202
|
+
if (fs.existsSync(filePath)) {
|
|
203
|
+
try {
|
|
204
|
+
fs.copyFileSync(filePath, `${filePath}.bak`);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Could not back up ${filePath} before writing: ${err.message}. ` +
|
|
208
|
+
'Aborting to avoid losing the existing file.'
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const tmpPath = `${filePath}.tmp`;
|
|
214
|
+
try {
|
|
215
|
+
fs.writeFileSync(tmpPath, content);
|
|
216
|
+
fs.renameSync(tmpPath, filePath);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
try {
|
|
219
|
+
fs.unlinkSync(tmpPath);
|
|
220
|
+
} catch (_) {
|
|
221
|
+
// Best-effort cleanup; ignore if there is nothing to remove.
|
|
222
|
+
}
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Write a JSON file using the atomic + backup pattern.
|
|
229
|
+
* Refuses to overwrite symbolic links for safety.
|
|
230
|
+
*/
|
|
231
|
+
function writeJson(filePath, data) {
|
|
232
|
+
writeFileAtomic(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
82
233
|
}
|
|
83
234
|
|
|
84
235
|
/**
|
|
@@ -124,11 +275,19 @@ function escapeRegex(str) {
|
|
|
124
275
|
//
|
|
125
276
|
// We describe each agent in a single table and build the writer functions
|
|
126
277
|
// from it so adding a new JSON agent is a one-line change.
|
|
278
|
+
//
|
|
279
|
+
// IMPORTANT: every entry below has a source URL pointing at the official
|
|
280
|
+
// docs where the path/format comes from. If you change any of these,
|
|
281
|
+
// verify the new value against the linked source first. Silent drift is
|
|
282
|
+
// the hardest class of bug in this file because the writes succeed even
|
|
283
|
+
// when the agent never reads the file.
|
|
127
284
|
|
|
128
285
|
const JSON_AGENT_CONFIGS = {
|
|
129
286
|
'claude-code': {
|
|
130
|
-
//
|
|
131
|
-
//
|
|
287
|
+
// Source: https://code.claude.com/docs/en/mcp
|
|
288
|
+
// "User-scoped servers are stored in ~/.claude.json"
|
|
289
|
+
// Note: ~/.claude/settings.json does NOT work for MCPs (known docs bug).
|
|
290
|
+
// stdio entries omit `type` here; HTTP entries include it.
|
|
132
291
|
configPath: () => path.join(os.homedir(), '.claude.json'),
|
|
133
292
|
wrapperKey: 'mcpServers',
|
|
134
293
|
stdioIncludesType: false,
|
|
@@ -136,7 +295,9 @@ const JSON_AGENT_CONFIGS = {
|
|
|
136
295
|
httpField: 'url',
|
|
137
296
|
},
|
|
138
297
|
'github-copilot': {
|
|
139
|
-
//
|
|
298
|
+
// Source: https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers
|
|
299
|
+
// "MCP servers are saved to ~/.copilot/mcp-config.json"
|
|
300
|
+
// Copilot uses `servers` (NOT mcpServers) and every server needs
|
|
140
301
|
// an explicit `type` field.
|
|
141
302
|
configPath: () => path.join(os.homedir(), '.copilot', 'mcp-config.json'),
|
|
142
303
|
wrapperKey: 'servers',
|
|
@@ -145,6 +306,8 @@ const JSON_AGENT_CONFIGS = {
|
|
|
145
306
|
httpField: 'url',
|
|
146
307
|
},
|
|
147
308
|
'gemini-cli': {
|
|
309
|
+
// Source: https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md
|
|
310
|
+
// "MCP config goes in ~/.gemini/settings.json under mcpServers"
|
|
148
311
|
// Gemini uses `httpUrl` (NOT `url`) for HTTP transports and omits
|
|
149
312
|
// `type` — it's inferred from which key is present.
|
|
150
313
|
configPath: () => path.join(os.homedir(), '.gemini', 'settings.json'),
|
|
@@ -154,10 +317,12 @@ const JSON_AGENT_CONFIGS = {
|
|
|
154
317
|
httpField: 'httpUrl',
|
|
155
318
|
},
|
|
156
319
|
kilo: {
|
|
320
|
+
// Source: https://kilo.ai/docs/automate/mcp/using-in-kilo-code
|
|
157
321
|
// Kilo Code uses ~/.kilo/ as the user config root (consistent with
|
|
158
322
|
// ~/.kilo/skills/). The global MCP settings live at the natural
|
|
159
323
|
// neighbor path — mcp_settings.json — which is what the Kilo VS Code
|
|
160
|
-
// extension and CLI both read.
|
|
324
|
+
// extension and CLI both read. (NOT ~/.kilocode/ — that path is
|
|
325
|
+
// project-level only.)
|
|
161
326
|
configPath: () => path.join(os.homedir(), '.kilo', 'mcp_settings.json'),
|
|
162
327
|
wrapperKey: 'mcpServers',
|
|
163
328
|
stdioIncludesType: false,
|
|
@@ -212,7 +377,10 @@ function makeJsonWriter(agentId) {
|
|
|
212
377
|
const agentConfig = JSON_AGENT_CONFIGS[agentId];
|
|
213
378
|
return function jsonWriter(packageDir) {
|
|
214
379
|
const configPath = agentConfig.configPath();
|
|
215
|
-
|
|
380
|
+
// Use strict reader on the write path: a silent parse failure here
|
|
381
|
+
// would discard the user's other config keys (history, OAuth, etc.)
|
|
382
|
+
// when the empty {} fallback is spread into the new write.
|
|
383
|
+
const existing = readJsonStrict(configPath) || {};
|
|
216
384
|
const wrapperKey = agentConfig.wrapperKey;
|
|
217
385
|
|
|
218
386
|
const newServers = buildJsonServers(agentConfig, packageDir);
|
|
@@ -226,12 +394,17 @@ function makeJsonWriter(agentId) {
|
|
|
226
394
|
// ============================================
|
|
227
395
|
// CODEX (OpenAI) — TOML format
|
|
228
396
|
// ============================================
|
|
397
|
+
// Source: https://developers.openai.com/codex/mcp
|
|
398
|
+
// Also: https://github.com/openai/codex/blob/main/docs/config.md
|
|
399
|
+
// "Codex CLI reads MCP server config from ~/.codex/config.toml, where
|
|
400
|
+
// each server gets its own section with format [mcp_servers.my-server]"
|
|
401
|
+
//
|
|
229
402
|
// Writes ~/.codex/config.toml, appending [mcp_servers.*] sections.
|
|
230
403
|
// Preserves existing content by removing only our own sections before
|
|
231
404
|
// appending the fresh ones.
|
|
232
405
|
function writeCodexConfig(packageDir) {
|
|
233
406
|
const configPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
234
|
-
|
|
407
|
+
assertPathSafeForWrite(configPath);
|
|
235
408
|
const launcher = getLauncherAbsolutePath(packageDir);
|
|
236
409
|
|
|
237
410
|
let existing = '';
|
|
@@ -259,11 +432,9 @@ function writeCodexConfig(packageDir) {
|
|
|
259
432
|
|
|
260
433
|
const content = existing.trimEnd() + newSections;
|
|
261
434
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
fs.writeFileSync(configPath, content);
|
|
435
|
+
// Use the same atomic + backup pattern as the JSON writers — a crash
|
|
436
|
+
// mid-write to ~/.codex/config.toml would brick the user's Codex install.
|
|
437
|
+
writeFileAtomic(configPath, content);
|
|
267
438
|
return configPath;
|
|
268
439
|
}
|
|
269
440
|
|
|
@@ -303,9 +474,13 @@ module.exports = {
|
|
|
303
474
|
MICROSOFT_LEARN_URL,
|
|
304
475
|
// Exported for testing
|
|
305
476
|
readJsonSafe,
|
|
477
|
+
readJsonStrict,
|
|
306
478
|
writeJson,
|
|
479
|
+
writeFileAtomic,
|
|
307
480
|
tomlEscape,
|
|
308
481
|
escapeRegex,
|
|
309
482
|
assertNotSymlink,
|
|
483
|
+
assertNoSymlinkedAncestor,
|
|
484
|
+
assertPathSafeForWrite,
|
|
310
485
|
getLauncherAbsolutePath,
|
|
311
486
|
};
|