@brunosps00/dev-workflow 1.0.1 → 1.0.4

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 (43) hide show
  1. package/README.md +33 -16
  2. package/bin/dev-workflow.js +24 -7
  3. package/lib/aws-categories.js +80 -0
  4. package/lib/azure-categories.js +168 -0
  5. package/lib/constants.js +14 -6
  6. package/lib/init.js +28 -0
  7. package/lib/install-aws-skills.js +345 -0
  8. package/lib/install-azure-skills.js +231 -0
  9. package/lib/mcp.js +32 -21
  10. package/lib/prompts.js +38 -1
  11. package/package.json +1 -1
  12. package/scaffold/en/agent-instructions.md +23 -0
  13. package/scaffold/en/commands/dw-analyze-project.md +64 -0
  14. package/scaffold/en/commands/dw-autopilot.md +64 -5
  15. package/scaffold/en/commands/dw-bugfix.md +124 -26
  16. package/scaffold/en/commands/dw-install-aws-skills.md +166 -0
  17. package/scaffold/en/commands/dw-install-azure-skills.md +138 -0
  18. package/scaffold/en/commands/dw-intel.md +30 -3
  19. package/scaffold/en/commands/dw-pause.md +92 -0
  20. package/scaffold/en/commands/dw-qa.md +87 -11
  21. package/scaffold/en/commands/dw-resume.md +90 -0
  22. package/scaffold/en/commands/dw-review.md +22 -12
  23. package/scaffold/en/templates/bugfix-summary-template.md +66 -0
  24. package/scaffold/en/templates/concerns-template.md +59 -0
  25. package/scaffold/en/templates/state-template.md +59 -0
  26. package/scaffold/pt-br/agent-instructions.md +23 -0
  27. package/scaffold/pt-br/commands/dw-analyze-project.md +64 -0
  28. package/scaffold/pt-br/commands/dw-autopilot.md +64 -5
  29. package/scaffold/pt-br/commands/dw-bugfix.md +134 -18
  30. package/scaffold/pt-br/commands/dw-install-aws-skills.md +166 -0
  31. package/scaffold/pt-br/commands/dw-install-azure-skills.md +138 -0
  32. package/scaffold/pt-br/commands/dw-intel.md +30 -3
  33. package/scaffold/pt-br/commands/dw-pause.md +92 -0
  34. package/scaffold/pt-br/commands/dw-qa.md +87 -11
  35. package/scaffold/pt-br/commands/dw-resume.md +90 -0
  36. package/scaffold/pt-br/commands/dw-review.md +22 -12
  37. package/scaffold/pt-br/templates/bugfix-summary-template.md +66 -0
  38. package/scaffold/pt-br/templates/concerns-template.md +59 -0
  39. package/scaffold/pt-br/templates/state-template.md +59 -0
  40. package/scaffold/skills/dw-codebase-intel/SKILL.md +9 -5
  41. package/scaffold/skills/dw-codebase-intel/references/query-patterns.md +52 -0
  42. package/scaffold/skills/dw-memory/SKILL.md +26 -1
  43. package/scaffold/skills/dw-memory/references/context-budget.md +63 -0
@@ -0,0 +1,345 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+ const { ensureDir, writeFile } = require('./utils');
6
+ const { multiSelect } = require('./prompts');
7
+ const { addMcpServer } = require('./mcp');
8
+ const { CATEGORIES, listCategories, resolveDirs } = require('./aws-categories');
9
+
10
+ const UPSTREAM_REPO = 'https://github.com/aws/agent-toolkit-for-aws.git';
11
+ const MCP_NAME = 'aws-mcp';
12
+ const DEFAULT_REGION = 'us-east-1';
13
+
14
+ // AWS MCP Server endpoints — see https://docs.aws.amazon.com/aws-mcp/.
15
+ // Knowledge MCP (https://knowledge-mcp.global.api.aws) is deprecated by AWS in
16
+ // favor of the unified server. We intentionally only register the unified one.
17
+ const REGIONAL_ENDPOINTS = {
18
+ 'us-east-1': 'https://aws-mcp.us-east-1.api.aws/mcp',
19
+ 'eu-central-1': 'https://aws-mcp.eu-central-1.api.aws/mcp',
20
+ };
21
+
22
+ function buildMcpConfig(region) {
23
+ const endpoint = REGIONAL_ENDPOINTS[region] || REGIONAL_ENDPOINTS[DEFAULT_REGION];
24
+ return {
25
+ command: 'uvx',
26
+ args: [
27
+ 'mcp-proxy-for-aws@latest',
28
+ endpoint,
29
+ '--metadata',
30
+ `AWS_REGION=${region}`,
31
+ ],
32
+ transport: 'stdio',
33
+ timeout: 100000,
34
+ };
35
+ }
36
+
37
+ const AWS_MCP_INSTRUCTIONS = `# AWS MCP Server — How the agent should use it
38
+
39
+ Installed by \`/dw-install-aws-skills\` (or \`npx @brunosps00/dev-workflow install-aws-skills\`). Lives at \`.dw/references/aws-mcp-instructions.md\` and is automatically discoverable by any \`dw-*\` command that touches AWS code paths.
40
+
41
+ ## Available MCP tools (from \`aws-mcp\` server)
42
+
43
+ | Tool | When to call it | Destructive? |
44
+ |------|-----------------|---------------|
45
+ | \`aws___search_documentation\` | The user asks "how does X work in AWS", "what's the limit of Y", "best practice for Z" — anything conceptual or capability-oriented. Returns indexed hits from docs.aws.amazon.com. | No (read-only) |
46
+ | \`aws___read_documentation\` | A specific docs.aws.amazon.com URL is already in context (cited by the user, returned by a previous search, in an error message) and you need the full page content. | No (read-only) |
47
+ | \`aws___retrieve_skill\` | The user is starting a specific AWS task and you want a curated skill (CDK setup, Bedrock invocation pattern, etc.). Pulls from the bundled skill catalog. | No (read-only) |
48
+ | \`aws___call_aws\` | The user explicitly asked for an AWS API operation — list, describe, create, modify, delete. Executes against the user's IAM credentials. | **YES — can mutate cloud state** |
49
+ | \`aws___run_script\` | Heavier analysis or orchestration that needs Python: parsing JSON returned by \`call_aws\`, aggregating across services, running an ad-hoc query. | **YES — can call APIs from the script** |
50
+
51
+ ## Destructive operations — required behavior
52
+
53
+ <critical>
54
+ Before invoking \`aws___call_aws\` for any of the following, STATE the operation in chat, the target resource (account ID, region, resource ID/name), and the expected effect. WAIT for explicit user confirmation:
55
+
56
+ - Any \`create*\`, \`put*\`, \`update*\`, \`modify*\`, \`delete*\`, \`terminate*\`, \`stop*\` API
57
+ - Any IAM change (role, policy, user, group, identity provider, permission boundary)
58
+ - Any operation that affects billing (provisioning, scaling, premium support, marketplace subscriptions)
59
+ - Any operation in production accounts (detect via account ID or tags if available)
60
+
61
+ For read-only operations (\`list*\`, \`describe*\`, \`get*\`), proceed without confirmation — but cite the call in the report.
62
+
63
+ The user's IAM principal determines what the call CAN do. dev-workflow does NOT validate scope. If a destructive call fails with AccessDenied, that is by design — surface the error and stop.
64
+ </critical>
65
+
66
+ ## Region behavior
67
+
68
+ The MCP server endpoint is regional (currently \`us-east-1\` and \`eu-central-1\`). The AWS operations the server performs default to the region encoded in the proxy \`--metadata AWS_REGION=<x>\` argument inside \`.claude/settings.json\`. Override per-query by saying "list EC2 instances in eu-west-1" — the server honors regions in the prompt.
69
+
70
+ To change the default permanently, edit the args array in \`.claude/settings.json\` (\`AWS_REGION=<x>\`) and pick the corresponding endpoint URL, or re-run \`/dw-install-aws-skills --region=<x>\`.
71
+
72
+ ## When NOT to call
73
+
74
+ - Pure code review with no AWS surface. Use \`dw-review\` rules + project conventions.
75
+ - Questions about non-AWS clouds (Azure, GCP, vendor SaaS). The AWS MCP only returns AWS content; calling it on unrelated topics wastes budget.
76
+ - Trivial syntactic questions answerable from the codebase or current model knowledge — fetching docs is overkill.
77
+
78
+ ## Source-grounding discipline
79
+
80
+ Every claim about AWS that comes from these tools MUST be cited per the \`dw-source-grounding\` skill format:
81
+
82
+ \`\`\`
83
+ [source: <docs.aws.amazon.com URL>, version: <as displayed>, retrieved: YYYY-MM-DD]
84
+ \`\`\`
85
+
86
+ For \`aws___call_aws\` results, cite the API name + region + a one-line summary of what was returned (do not paste full JSON blobs into the response unless the user asks).
87
+
88
+ ## Companion skills
89
+
90
+ The \`.agents/skills/aws/\` directory holds skills installed by \`/dw-install-aws-skills\`. Skills under \`aws-cdk\`, \`aws-cloudformation\`, \`aws-iam\`, etc. encode best practices that often answer the question without a network round-trip. Prefer loading the matching skill BEFORE calling the MCP tools.
91
+
92
+ ## Prerequisites the user maintains
93
+
94
+ - \`uv\` (Python tool runner) — used by \`uvx\` to invoke \`mcp-proxy-for-aws\`.
95
+ - \`aws cli\` ≥ 2.32.0 — proxy uses it for credential discovery.
96
+ - Valid AWS credentials — typically via \`aws login\`, IAM access keys, or SSO profile. Verify with \`aws sts get-caller-identity\`.
97
+
98
+ If credentials expire (most commonly with short-lived session tokens), the agent's tool calls will fail with \`ExpiredTokenException\`. Tell the user to refresh and re-try; do not silently swallow the error.
99
+
100
+ ## Refresh / uninstall
101
+
102
+ - **Refresh:** re-run \`/dw-install-aws-skills\` (or \`npx @brunosps00/dev-workflow install-aws-skills\`). The command clears \`.agents/skills/aws/\` and pulls upstream fresh.
103
+ - **Uninstall:** delete \`.agents/skills/aws/\` and remove the \`aws-mcp\` entry from \`.claude/settings.json\` \`mcpServers\`.
104
+
105
+ ## Attribution
106
+
107
+ Skills are sourced from [\`aws/agent-toolkit-for-aws\`](https://github.com/aws/agent-toolkit-for-aws) (Apache 2.0). The MCP server is documented at [docs.aws.amazon.com/aws-mcp/](https://docs.aws.amazon.com/aws-mcp/). The proxy is [\`aws/mcp-proxy-for-aws\`](https://github.com/aws/mcp-proxy-for-aws).
108
+ `;
109
+
110
+ function check(cmd) {
111
+ try {
112
+ execSync(cmd, { stdio: 'pipe', timeout: 10000 });
113
+ return true;
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ function printPrereqInstructions(missing) {
120
+ console.log('\n Missing prerequisites — install these before re-running:\n');
121
+ if (missing.has('git')) {
122
+ console.log(' git:');
123
+ console.log(' macOS: brew install git (or use Xcode Command Line Tools)');
124
+ console.log(' Linux: apt install git (or distro equivalent)');
125
+ console.log(' Windows: https://git-scm.com/download/win\n');
126
+ }
127
+ if (missing.has('uv')) {
128
+ console.log(' uv (Python tool runner — runs uvx):');
129
+ console.log(' macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh');
130
+ console.log(' Windows: powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"\n');
131
+ }
132
+ if (missing.has('aws')) {
133
+ console.log(' aws cli (version 2.32.0 or later):');
134
+ console.log(' https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html');
135
+ console.log(' macOS: brew install awscli');
136
+ console.log(' Windows: winget install Amazon.AWSCLI');
137
+ console.log(' Linux: See AWS docs for arch-specific .zip\n');
138
+ }
139
+ if (missing.has('creds')) {
140
+ console.log(' AWS credentials:');
141
+ console.log(' Easiest: aws login (rotates every 15 min, valid 12h)');
142
+ console.log(' SSO: aws configure sso');
143
+ console.log(' Keys: aws configure (IAM access keys)');
144
+ console.log(' Verify: aws sts get-caller-identity\n');
145
+ }
146
+ }
147
+
148
+ function shallowClone(targetDir) {
149
+ ensureDir(path.dirname(targetDir));
150
+ execSync(`git clone --depth=1 ${UPSTREAM_REPO} "${targetDir}"`, {
151
+ stdio: 'inherit',
152
+ timeout: 180000,
153
+ });
154
+ }
155
+
156
+ function expandGlob(repoRoot, pattern) {
157
+ // Supports two forms: "core-skills/*" and "specialized-skills/<x>/*".
158
+ // Globbing kept minimal — no node-glob dependency.
159
+ const skillsRoot = path.join(repoRoot, 'skills');
160
+ const parts = pattern.split('/');
161
+ if (parts[parts.length - 1] !== '*') {
162
+ // exact path
163
+ const fullPath = path.join(skillsRoot, pattern);
164
+ return fs.existsSync(fullPath) ? [fullPath] : [];
165
+ }
166
+ const parentRel = parts.slice(0, -1).join('/');
167
+ const parent = path.join(skillsRoot, parentRel);
168
+ if (!fs.existsSync(parent)) return [];
169
+ return fs
170
+ .readdirSync(parent, { withFileTypes: true })
171
+ .filter((e) => e.isDirectory())
172
+ .map((e) => path.join(parent, e.name));
173
+ }
174
+
175
+ function collectAllSkillDirs(repoRoot) {
176
+ const out = [];
177
+ for (const pattern of ['core-skills/*', 'specialized-skills/*/*']) {
178
+ out.push(...expandGlob(repoRoot, pattern));
179
+ }
180
+ return out;
181
+ }
182
+
183
+ function copyTree(srcDir, destDir) {
184
+ ensureDir(destDir);
185
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
186
+ const srcPath = path.join(srcDir, entry.name);
187
+ const destPath = path.join(destDir, entry.name);
188
+ if (entry.isDirectory()) {
189
+ copyTree(srcPath, destPath);
190
+ } else {
191
+ fs.copyFileSync(srcPath, destPath);
192
+ }
193
+ }
194
+ }
195
+
196
+ function rmRecursive(targetPath) {
197
+ if (!fs.existsSync(targetPath)) return;
198
+ fs.rmSync(targetPath, { recursive: true, force: true });
199
+ }
200
+
201
+ function hasExecutable(skillDir) {
202
+ if (!fs.existsSync(skillDir)) return false;
203
+ for (const entry of fs.readdirSync(skillDir, { withFileTypes: true })) {
204
+ if (!entry.isFile()) continue;
205
+ if (/\.(sh|py|js|ts|mjs|cjs|ps1|bat)$/i.test(entry.name)) return true;
206
+ }
207
+ return false;
208
+ }
209
+
210
+ function parseFlag(name) {
211
+ for (const arg of process.argv.slice(2)) {
212
+ if (arg.startsWith(`--${name}=`)) {
213
+ return arg.slice(name.length + 3);
214
+ }
215
+ }
216
+ return null;
217
+ }
218
+
219
+ async function run() {
220
+ const projectRoot = process.cwd();
221
+
222
+ console.log('\n dev-workflow install-aws-skills');
223
+ console.log(` ${'='.repeat(40)}\n`);
224
+
225
+ // 1. Detect prerequisites
226
+ const missing = new Set();
227
+ if (!check('git --version')) missing.add('git');
228
+ if (!check('uv --version')) missing.add('uv');
229
+ if (!check('aws --version')) missing.add('aws');
230
+
231
+ if (!missing.has('aws')) {
232
+ // Only check creds if the CLI itself is present.
233
+ if (!check('aws sts get-caller-identity')) missing.add('creds');
234
+ }
235
+
236
+ if (missing.size > 0) {
237
+ console.log(' \x1b[31m✗ prerequisites not met\x1b[0m');
238
+ printPrereqInstructions(missing);
239
+ process.exit(1);
240
+ }
241
+
242
+ console.log(' \x1b[32m✓\x1b[0m git, uv, aws cli, and AWS credentials all detected\n');
243
+
244
+ // 2. Region
245
+ const regionFlag = parseFlag('region');
246
+ const region = regionFlag || DEFAULT_REGION;
247
+ if (!REGIONAL_ENDPOINTS[region]) {
248
+ console.log(
249
+ ` \x1b[33m! region "${region}" has no known endpoint; falling back to ${DEFAULT_REGION}.\x1b[0m`,
250
+ );
251
+ console.log(' Supported regions:', Object.keys(REGIONAL_ENDPOINTS).join(', '));
252
+ }
253
+ const effectiveRegion = REGIONAL_ENDPOINTS[region] ? region : DEFAULT_REGION;
254
+ console.log(` Region: ${effectiveRegion} (endpoint ${REGIONAL_ENDPOINTS[effectiveRegion]})\n`);
255
+
256
+ // 3. Select categories
257
+ const categories = listCategories();
258
+ const lines = categories.map((c) => `${c} — ${CATEGORIES[c].description}`);
259
+ const selected = await multiSelect('Select AWS skill categories to install:', lines);
260
+ const selectedNames = selected.map((s) => s.split(' — ')[0]);
261
+ console.log(`\n Selected: ${selectedNames.join(', ')}\n`);
262
+
263
+ // 4. Clone upstream
264
+ const tmpDir = path.join(os.tmpdir(), `.dw-aws-skills-${Date.now()}`);
265
+ try {
266
+ console.log(` Cloning ${UPSTREAM_REPO} (shallow)...`);
267
+ shallowClone(tmpDir);
268
+ console.log(` \x1b[32m✓\x1b[0m cloned to ${tmpDir}\n`);
269
+
270
+ // 5. Resolve which dirs to copy
271
+ const resolved = resolveDirs(selectedNames);
272
+ let skillDirs;
273
+ if (resolved === '__ALL__') {
274
+ skillDirs = collectAllSkillDirs(tmpDir);
275
+ } else {
276
+ skillDirs = [];
277
+ for (const pattern of resolved) {
278
+ skillDirs.push(...expandGlob(tmpDir, pattern));
279
+ }
280
+ }
281
+
282
+ if (skillDirs.length === 0) {
283
+ console.log(' \x1b[33m! No skills matched the selected categories.\x1b[0m');
284
+ console.log(' Nothing copied. The MCP server will still be registered.\n');
285
+ } else {
286
+ const destRoot = path.join(projectRoot, '.agents', 'skills', 'aws');
287
+ console.log(` Refreshing ${destRoot} (existing content removed)...`);
288
+ rmRecursive(destRoot);
289
+ ensureDir(destRoot);
290
+
291
+ let copied = 0;
292
+ let skipped = 0;
293
+ for (const srcDir of skillDirs) {
294
+ const skillName = path.basename(srcDir);
295
+ if (hasExecutable(srcDir)) {
296
+ skipped++;
297
+ continue;
298
+ }
299
+ // Flatten — destination has no core-skills/ vs specialized-skills/ split.
300
+ const destDir = path.join(destRoot, skillName);
301
+ copyTree(srcDir, destDir);
302
+ copied++;
303
+ }
304
+ console.log(` \x1b[32m✓\x1b[0m copied ${copied} skill(s) to .agents/skills/aws/`);
305
+ if (skipped > 0) {
306
+ console.log(` \x1b[33m! skipped ${skipped} skill(s) that ship executable scripts\x1b[0m`);
307
+ }
308
+ console.log();
309
+ }
310
+
311
+ // 6. Register MCP
312
+ const mcpConfig = buildMcpConfig(effectiveRegion);
313
+ const mcpStatus = addMcpServer(projectRoot, MCP_NAME, mcpConfig);
314
+ if (mcpStatus === 'added') {
315
+ console.log(` \x1b[32m✓\x1b[0m registered ${MCP_NAME} MCP in .claude/settings.json (region ${effectiveRegion})`);
316
+ } else {
317
+ console.log(` \x1b[33m—\x1b[0m ${MCP_NAME} MCP already present, left unchanged`);
318
+ console.log(' To change region, edit .claude/settings.json directly or remove the entry and re-run.');
319
+ }
320
+
321
+ // 7. Write instructions
322
+ const instructionsPath = path.join(projectRoot, '.dw', 'references', 'aws-mcp-instructions.md');
323
+ const status = writeFile(instructionsPath, AWS_MCP_INSTRUCTIONS, true);
324
+ console.log(` \x1b[32m✓\x1b[0m ${status} ${instructionsPath}`);
325
+ } finally {
326
+ rmRecursive(tmpDir);
327
+ }
328
+
329
+ console.log(`\n ${'='.repeat(40)}`);
330
+ console.log(' Done.');
331
+ console.log();
332
+ console.log(' Next steps:');
333
+ console.log(' 1. Restart Claude Code (or Codex / Copilot / OpenCode) so the MCP loads.');
334
+ console.log(' 2. Ask something AWS-specific to validate, e.g.');
335
+ console.log(' "What\'s the limit on Lambda concurrent executions?"');
336
+ console.log(' 3. The agent can now call AWS APIs via aws___call_aws — review');
337
+ console.log(' .dw/references/aws-mcp-instructions.md for destructive-op protocol.');
338
+ console.log(' 4. To refresh from upstream, re-run this command.');
339
+ console.log(` 5. Region override: re-run with --region=<aws-region> (currently: ${effectiveRegion}).`);
340
+ console.log(' 6. To uninstall: rm -rf .agents/skills/aws/ and remove the');
341
+ console.log(' "aws-mcp" entry from .claude/settings.json mcpServers.');
342
+ console.log();
343
+ }
344
+
345
+ module.exports = { run };
@@ -0,0 +1,231 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+ const { ensureDir, writeFile } = require('./utils');
6
+ const { multiSelect } = require('./prompts');
7
+ const { addMcpServer } = require('./mcp');
8
+ const { CATEGORIES, listCategories, resolvePrefixes, matchesPrefixes } = require('./azure-categories');
9
+
10
+ const UPSTREAM_REPO = 'https://github.com/MicrosoftDocs/Agent-Skills.git';
11
+ const MCP_NAME = 'microsoft-learn';
12
+ const MCP_CONFIG = {
13
+ type: 'http',
14
+ url: 'https://learn.microsoft.com/api/mcp',
15
+ };
16
+
17
+ const AZURE_MCP_INSTRUCTIONS = `# Azure Documentation MCP — How the agent should use it
18
+
19
+ Installed by \`/dw-install-azure-skills\` (or \`npx @brunosps00/dev-workflow install-azure-skills\`). Lives at \`.dw/references/azure-mcp-instructions.md\` and is automatically discoverable by any \`dw-*\` command that touches Azure code paths.
20
+
21
+ ## Available MCP tools (from \`microsoft-learn\` server)
22
+
23
+ | Tool | When to call it |
24
+ |------|-----------------|
25
+ | \`microsoft_docs_search\` | The user asks "how does X work in Azure", "what's the limit of Y", "best practice for Z" — anything conceptual or capability-oriented. Returns indexed hits from learn.microsoft.com. |
26
+ | \`microsoft_docs_fetch\` | A specific Learn URL is already in context (cited by the user, returned by a previous search, in an error message) and you need the full page content. Pass the URL. |
27
+ | \`microsoft_code_sample_search\` | The user wants an official code snippet — \`az cli\` invocation, .NET / Python / TypeScript SDK call, ARM/Bicep template, Terraform AzureRM example. Returns first-party samples. |
28
+
29
+ ## When NOT to call
30
+
31
+ - Pure code review with no Azure surface. Use \`dw-review\` rules + project conventions.
32
+ - Questions about non-Microsoft tech (AWS, GCP, vendor SaaS). The Microsoft MCP only returns Learn content; calling it on unrelated topics wastes budget.
33
+ - Trivial syntactic questions answerable from the codebase or from current model knowledge — fetching docs is overkill.
34
+
35
+ ## Source-grounding discipline
36
+
37
+ Every claim about Azure that comes from these tools MUST be cited per the \`dw-source-grounding\` skill format:
38
+
39
+ \`\`\`
40
+ [source: <Learn URL>, version: <as displayed on the doc>, retrieved: YYYY-MM-DD]
41
+ \`\`\`
42
+
43
+ If a tool call returns no result, fall back to web search and clearly label the answer as uncertain. Do not invent Azure API surface — Azure surface drifts and a wrong claim has cascading consequences in TechSpec → Tasks → implementation.
44
+
45
+ ## Companion skills
46
+
47
+ The \`.agents/skills/azure/\` directory holds per-service skills installed by \`/dw-install-azure-skills\`. They contain curated guidance per service (e.g., \`azure-container-apps\`, \`azure-openai\`). When the user's task touches a service, prefer loading the matching skill BEFORE calling the MCP tools — the skill often answers without a network round-trip.
48
+
49
+ ## Refresh / uninstall
50
+
51
+ - **Refresh:** re-run \`/dw-install-azure-skills\` (or \`npx @brunosps00/dev-workflow install-azure-skills\`). The command clears \`.agents/skills/azure/\` and pulls upstream fresh.
52
+ - **Uninstall:** delete \`.agents/skills/azure/\` and remove the \`microsoft-learn\` entry from \`.claude/settings.json\` \`mcpServers\`.
53
+
54
+ ## Attribution
55
+
56
+ Skills are sourced from [\`MicrosoftDocs/Agent-Skills\`](https://github.com/MicrosoftDocs/Agent-Skills) (CC-BY-4.0). The MCP server is documented at [Microsoft Learn](https://learn.microsoft.com/en-us/training/support/mcp-get-started).
57
+ `;
58
+
59
+ function checkGit() {
60
+ try {
61
+ execSync('git --version', { stdio: 'pipe', timeout: 5000 });
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ function shallowClone(targetDir) {
69
+ ensureDir(path.dirname(targetDir));
70
+ execSync(`git clone --depth=1 ${UPSTREAM_REPO} "${targetDir}"`, {
71
+ stdio: 'inherit',
72
+ timeout: 180000,
73
+ });
74
+ }
75
+
76
+ function listSkillDirs(repoRoot) {
77
+ const skillsDir = path.join(repoRoot, 'skills');
78
+ if (!fs.existsSync(skillsDir)) return [];
79
+ return fs
80
+ .readdirSync(skillsDir, { withFileTypes: true })
81
+ .filter((entry) => entry.isDirectory())
82
+ .map((entry) => entry.name);
83
+ }
84
+
85
+ function copySkillTree(srcDir, destDir) {
86
+ ensureDir(destDir);
87
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
88
+ for (const entry of entries) {
89
+ const srcPath = path.join(srcDir, entry.name);
90
+ const destPath = path.join(destDir, entry.name);
91
+ if (entry.isDirectory()) {
92
+ copySkillTree(srcPath, destPath);
93
+ } else {
94
+ fs.copyFileSync(srcPath, destPath);
95
+ }
96
+ }
97
+ }
98
+
99
+ function rmRecursive(targetPath) {
100
+ if (!fs.existsSync(targetPath)) return;
101
+ fs.rmSync(targetPath, { recursive: true, force: true });
102
+ }
103
+
104
+ // Parse --products=csv override from process.argv. Returns array of slugs or null.
105
+ function parseProductsFlag() {
106
+ for (const arg of process.argv.slice(2)) {
107
+ if (arg.startsWith('--products=')) {
108
+ const csv = arg.slice('--products='.length);
109
+ return csv.split(',').map((s) => s.trim()).filter(Boolean);
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+
115
+ async function run() {
116
+ const projectRoot = process.cwd();
117
+
118
+ console.log('\n dev-workflow install-azure-skills');
119
+ console.log(` ${'='.repeat(40)}\n`);
120
+
121
+ if (!checkGit()) {
122
+ console.error(' \x1b[31m✗ git not found.\x1b[0m');
123
+ console.error(' This command needs git to clone the upstream skills repo.');
124
+ console.error(' Install git and re-run.');
125
+ process.exit(1);
126
+ }
127
+
128
+ // 1. Decide what to install
129
+ let prefixes;
130
+ const explicitProducts = parseProductsFlag();
131
+ if (explicitProducts) {
132
+ console.log(` Explicit --products override: ${explicitProducts.join(', ')}\n`);
133
+ prefixes = explicitProducts;
134
+ } else {
135
+ const categories = listCategories();
136
+ const lines = [];
137
+ for (const cat of categories) {
138
+ const desc = CATEGORIES[cat].description;
139
+ lines.push(`${cat} — ${desc}`);
140
+ }
141
+ const selected = await multiSelect('Select Azure skill categories to install:', lines);
142
+ // map back from "Category — description" → category name
143
+ const selectedNames = selected.map((s) => s.split(' — ')[0]);
144
+ console.log(`\n Selected: ${selectedNames.join(', ')}\n`);
145
+ prefixes = resolvePrefixes(selectedNames);
146
+ }
147
+
148
+ // 2. Shallow clone upstream
149
+ const tmpDir = path.join(os.tmpdir(), `.dw-azure-skills-${Date.now()}`);
150
+ try {
151
+ console.log(` Cloning ${UPSTREAM_REPO} (shallow)...`);
152
+ shallowClone(tmpDir);
153
+ console.log(` \x1b[32m✓\x1b[0m cloned to ${tmpDir}\n`);
154
+
155
+ // 3. Filter + copy
156
+ const allSkills = listSkillDirs(tmpDir);
157
+ const matched = allSkills.filter((name) => matchesPrefixes(name, prefixes));
158
+
159
+ if (matched.length === 0) {
160
+ console.log(' \x1b[33m! No skills matched the selected categories.\x1b[0m');
161
+ console.log(' Nothing copied. The MCP server will still be registered.\n');
162
+ } else {
163
+ const destRoot = path.join(projectRoot, '.agents', 'skills', 'azure');
164
+ console.log(` Refreshing ${destRoot} (existing content removed)...`);
165
+ rmRecursive(destRoot);
166
+ ensureDir(destRoot);
167
+
168
+ let copied = 0;
169
+ let skipped = 0;
170
+ for (const skillName of matched) {
171
+ const srcDir = path.join(tmpDir, 'skills', skillName);
172
+ const destDir = path.join(destRoot, skillName);
173
+ // Skip skills that ship executable scripts outside markdown — limit scope per plan.
174
+ const hasShell = fs.existsSync(path.join(srcDir, 'scripts')) || hasExecutable(srcDir);
175
+ if (hasShell) {
176
+ skipped++;
177
+ continue;
178
+ }
179
+ copySkillTree(srcDir, destDir);
180
+ copied++;
181
+ }
182
+ console.log(` \x1b[32m✓\x1b[0m copied ${copied} skill(s) to .agents/skills/azure/`);
183
+ if (skipped > 0) {
184
+ console.log(` \x1b[33m! skipped ${skipped} skill(s) that ship executable scripts\x1b[0m`);
185
+ }
186
+ console.log();
187
+ }
188
+
189
+ // 4. Register MCP
190
+ const mcpStatus = addMcpServer(projectRoot, MCP_NAME, MCP_CONFIG);
191
+ if (mcpStatus === 'added') {
192
+ console.log(` \x1b[32m✓\x1b[0m registered ${MCP_NAME} MCP in .claude/settings.json`);
193
+ } else {
194
+ console.log(` \x1b[33m—\x1b[0m ${MCP_NAME} MCP already present, left unchanged`);
195
+ }
196
+
197
+ // 5. Write instructions
198
+ const instructionsPath = path.join(projectRoot, '.dw', 'references', 'azure-mcp-instructions.md');
199
+ const status = writeFile(instructionsPath, AZURE_MCP_INSTRUCTIONS, true);
200
+ console.log(` \x1b[32m✓\x1b[0m ${status} ${instructionsPath}`);
201
+ } finally {
202
+ // 6. Cleanup
203
+ rmRecursive(tmpDir);
204
+ }
205
+
206
+ console.log(`\n ${'='.repeat(40)}`);
207
+ console.log(' Done.');
208
+ console.log();
209
+ console.log(' Next steps:');
210
+ console.log(' 1. Restart Claude Code (or Codex / Copilot / OpenCode) so the MCP loads.');
211
+ console.log(' 2. Ask something Azure-specific to validate, e.g.');
212
+ console.log(' "How do I deploy a containerized app to Azure Container Apps?"');
213
+ console.log(' 3. To refresh from upstream, re-run this command.');
214
+ console.log(' 4. To uninstall: rm -rf .agents/skills/azure/ and remove the');
215
+ console.log(' "microsoft-learn" entry from .claude/settings.json mcpServers.');
216
+ console.log();
217
+ }
218
+
219
+ // Heuristic: any .sh / .py / .js / .ts file at the skill root counts as executable.
220
+ // SKILL.md + references/ + assets/ stays text-only and is safe to copy.
221
+ function hasExecutable(skillDir) {
222
+ if (!fs.existsSync(skillDir)) return false;
223
+ const entries = fs.readdirSync(skillDir, { withFileTypes: true });
224
+ for (const entry of entries) {
225
+ if (!entry.isFile()) continue;
226
+ if (/\.(sh|py|js|ts|mjs|cjs|ps1|bat)$/i.test(entry.name)) return true;
227
+ }
228
+ return false;
229
+ }
230
+
231
+ module.exports = { run };
package/lib/mcp.js CHANGED
@@ -3,38 +3,49 @@ const path = require('path');
3
3
  const { ensureDir } = require('./utils');
4
4
  const { MCP_SERVERS } = require('./constants');
5
5
 
6
- function installMCPs(projectRoot) {
6
+ // Read .claude/settings.json (or return empty object if missing/malformed).
7
+ function readSettings(projectRoot) {
7
8
  const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
8
- let settings = {};
9
-
10
- if (fs.existsSync(settingsPath)) {
11
- try {
12
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
13
- } catch {
14
- settings = {};
15
- }
9
+ if (!fs.existsSync(settingsPath)) return {};
10
+ try {
11
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
12
+ } catch {
13
+ return {};
16
14
  }
15
+ }
17
16
 
18
- if (!settings.mcpServers) {
19
- settings.mcpServers = {};
17
+ // Write .claude/settings.json, ensuring directory exists and JSON ends with newline.
18
+ function writeSettings(projectRoot, settings) {
19
+ const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
20
+ ensureDir(path.dirname(settingsPath));
21
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
22
+ }
23
+
24
+ // Upsert a single MCP server entry into .claude/settings.json. Returns "added" or
25
+ // "skipped". Existing entries are NEVER overwritten — the user's config wins.
26
+ // Shared by installMCPs (default init/update) and install-azure-skills.
27
+ function addMcpServer(projectRoot, name, config) {
28
+ const settings = readSettings(projectRoot);
29
+ if (!settings.mcpServers) settings.mcpServers = {};
30
+ if (settings.mcpServers[name]) {
31
+ return 'skipped';
20
32
  }
33
+ settings.mcpServers[name] = config;
34
+ writeSettings(projectRoot, settings);
35
+ return 'added';
36
+ }
21
37
 
38
+ function installMCPs(projectRoot) {
22
39
  let added = 0;
23
40
  let skipped = 0;
24
41
 
25
42
  for (const [name, config] of Object.entries(MCP_SERVERS)) {
26
- if (settings.mcpServers[name]) {
27
- skipped++;
28
- } else {
29
- settings.mcpServers[name] = config;
30
- added++;
31
- }
43
+ const status = addMcpServer(projectRoot, name, config);
44
+ if (status === 'added') added++;
45
+ else skipped++;
32
46
  }
33
47
 
34
- ensureDir(path.dirname(settingsPath));
35
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
36
-
37
48
  return { added, skipped };
38
49
  }
39
50
 
40
- module.exports = { installMCPs };
51
+ module.exports = { installMCPs, addMcpServer, readSettings, writeSettings };