@programinglive/dev-workflow-mcp-server 1.5.2 → 1.5.3

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 (76) hide show
  1. package/.agent/workflows/dev-workflow-bugfix.md +250 -250
  2. package/.agent/workflows/dev-workflow-feature.md +201 -201
  3. package/.agent/workflows/dev-workflow-full-cycle.md +400 -400
  4. package/.agent/workflows/dev-workflow-refactor.md +238 -238
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +34 -34
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +25 -25
  7. package/.github/pull_request_template.md +22 -22
  8. package/.github/workflows/npm-publish.yml +50 -50
  9. package/.husky/commit-msg +0 -0
  10. package/CHANGELOG.md +748 -741
  11. package/CODE_OF_CONDUCT.md +79 -79
  12. package/CONTRIBUTING.md +60 -60
  13. package/LICENSE +21 -21
  14. package/README.md +632 -632
  15. package/SECURITY.md +20 -20
  16. package/db/index.js +204 -204
  17. package/docs/NETLIFY_DEPLOYMENT.md +125 -125
  18. package/docs/PLESK_DEPLOYMENT.md +170 -170
  19. package/docs/WORKFLOW-BEST-PRACTICES.md +58 -58
  20. package/docs/antigravity/EXAMPLES.md +535 -535
  21. package/docs/antigravity/GETTING-STARTED.md +292 -292
  22. package/docs/antigravity/TROUBLESHOOTING.md +742 -742
  23. package/docs/antigravity/WALKTHROUGH.md +25 -25
  24. package/docs/antigravity/WORKFLOW-TEMPLATES.md +670 -670
  25. package/docs/product/PRD.md +67 -67
  26. package/docs/release-notes/RELEASE_NOTES.md +226 -226
  27. package/docs/web-dashboard.md +175 -175
  28. package/exec.js +49 -49
  29. package/index.js +279 -279
  30. package/install.js +73 -73
  31. package/package.json +8 -8
  32. package/release-wrapper.js +192 -192
  33. package/scripts/build-docs.js +63 -63
  34. package/scripts/hello-world.js +1 -1
  35. package/scripts/release.cjs +252 -252
  36. package/scripts/release.js +252 -0
  37. package/scripts/update-release-notes.cjs +178 -178
  38. package/scripts/update-release-notes.js +182 -0
  39. package/server.json +23 -23
  40. package/tailwind.config.js +8 -8
  41. package/tests/build.test.js +83 -83
  42. package/tests/exec.test.js +35 -35
  43. package/tests/handlers.test.js +1058 -1058
  44. package/tests/hello-world.test.js +13 -13
  45. package/tests/mcpRegistry.test.js +65 -65
  46. package/tests/patch-release.test.js +7 -0
  47. package/tests/release-wrapper.test.js +107 -107
  48. package/tests/workflowState.test.js +505 -505
  49. package/tools/definitions.js +332 -332
  50. package/tools/handlers.js +1019 -1019
  51. package/vite.config.js +43 -43
  52. package/vite.web.config.js +23 -23
  53. package/web/README.md +36 -36
  54. package/web/app/docs/page.tsx +202 -202
  55. package/web/app/globals.css +90 -90
  56. package/web/app/layout.tsx +36 -36
  57. package/web/app/page.tsx +15 -15
  58. package/web/components/ApiReference.tsx +20 -20
  59. package/web/components/Features.tsx +61 -61
  60. package/web/components/Footer.tsx +9 -9
  61. package/web/components/Hero.tsx +96 -96
  62. package/web/components/History.tsx +143 -143
  63. package/web/components/Navbar.tsx +64 -64
  64. package/web/components/Terminal.tsx +114 -114
  65. package/web/eslint.config.mjs +18 -18
  66. package/web/lib/db.js +131 -131
  67. package/web/lib/docs.js +73 -73
  68. package/web/netlify.toml +3 -3
  69. package/web/next.config.ts +12 -12
  70. package/web/package-lock.json +6560 -6560
  71. package/web/package.json +28 -28
  72. package/web/postcss.config.mjs +9 -9
  73. package/web/server.js +31 -31
  74. package/web/tailwind.config.ts +19 -19
  75. package/web/tsconfig.json +42 -42
  76. package/workflow-state.js +612 -612
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function updateReleaseNotes({
7
+ rootDir = process.cwd(),
8
+ releaseNotesRelativePath = path.join('docs', 'release-notes', 'RELEASE_NOTES.md'),
9
+ changelogRelativePath = 'CHANGELOG.md',
10
+ packageJsonRelativePath = 'package.json',
11
+ now = () => new Date()
12
+ } = {}) {
13
+ const releaseNotesPath = path.join(rootDir, releaseNotesRelativePath);
14
+ if (!fs.existsSync(releaseNotesPath)) {
15
+ console.log(`ℹ️ Release notes file not found at ${releaseNotesPath}; skipping update.`);
16
+ return false;
17
+ }
18
+
19
+ const packageJsonPath = path.join(rootDir, packageJsonRelativePath);
20
+ if (!fs.existsSync(packageJsonPath)) {
21
+ console.log(`ℹ️ package.json not found at ${packageJsonPath}; skipping release notes update.`);
22
+ return false;
23
+ }
24
+
25
+ const changelogPath = path.join(rootDir, changelogRelativePath);
26
+ if (!fs.existsSync(changelogPath)) {
27
+ console.log(`ℹ️ CHANGELOG not found at ${changelogPath}; skipping release notes update.`);
28
+ return false;
29
+ }
30
+
31
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
32
+ const version = packageJson.version;
33
+ if (!version) {
34
+ console.log('ℹ️ package.json version is missing; skipping release notes update.');
35
+ return false;
36
+ }
37
+
38
+ const releaseNotesContent = fs.readFileSync(releaseNotesPath, 'utf8');
39
+ if (releaseNotesContent.includes(`| ${version} `) || releaseNotesContent.includes(`## ${version}`)) {
40
+ console.log(`ℹ️ Release notes already contain version ${version}; skipping.`);
41
+ return false;
42
+ }
43
+
44
+ const changelogContent = fs.readFileSync(changelogPath, 'utf8');
45
+ const changelogInfo = extractChangelogInfo({ changelogContent, version });
46
+
47
+ const releaseDate = changelogInfo.date || formatDate(now());
48
+ const highlight = changelogInfo.highlight || changelogInfo.heading || 'See CHANGELOG for details.';
49
+ const sectionHeading = changelogInfo.heading || '';
50
+ const detailBullets = Array.isArray(changelogInfo.bullets) && changelogInfo.bullets.length > 0
51
+ ? changelogInfo.bullets
52
+ : [highlight];
53
+
54
+ const updatedContent = insertReleaseNotesEntry({
55
+ content: releaseNotesContent,
56
+ version,
57
+ releaseDate,
58
+ highlight,
59
+ sectionHeading,
60
+ detailBullets
61
+ });
62
+
63
+ fs.writeFileSync(releaseNotesPath, updatedContent);
64
+ console.log(`✅ Added release notes entry for version ${version}.`);
65
+ return true;
66
+ }
67
+
68
+ function insertReleaseNotesEntry({ content, version, releaseDate, highlight, sectionHeading, detailBullets }) {
69
+ const lines = content.split('\n');
70
+ const headerSeparatorIndex = lines.findIndex((line) => line.trim().startsWith('|---------'));
71
+ if (headerSeparatorIndex === -1) {
72
+ throw new Error('Unable to locate release notes table header.');
73
+ }
74
+
75
+ const newRow = `| ${version} | ${releaseDate} | ${escapePipes(highlight)} |`;
76
+ lines.splice(headerSeparatorIndex + 1, 0, newRow);
77
+
78
+ const sectionLines = [];
79
+ sectionLines.push('');
80
+ const trimmedHeading = typeof sectionHeading === 'string' ? sectionHeading.trim() : '';
81
+ const headingSuffix = trimmedHeading ? ` – ${trimmedHeading}` : '';
82
+ sectionLines.push(`## ${version}${headingSuffix}`);
83
+ sectionLines.push('');
84
+ sectionLines.push(`Released on **${releaseDate}**.`);
85
+ sectionLines.push('');
86
+ for (const bullet of detailBullets) {
87
+ sectionLines.push(`- ${bullet}`);
88
+ }
89
+ sectionLines.push('');
90
+
91
+ const firstSectionIndex = lines.findIndex((line, idx) => idx > headerSeparatorIndex && line.startsWith('## '));
92
+ if (firstSectionIndex === -1) {
93
+ lines.push(...sectionLines);
94
+ } else {
95
+ lines.splice(firstSectionIndex, 0, ...sectionLines);
96
+ }
97
+
98
+ return lines.join('\n');
99
+ }
100
+
101
+ function extractChangelogInfo({ changelogContent, version }) {
102
+ const versionHeadingRegex = new RegExp(`^### \\[${escapeRegExp(version)}\\][^\n]*`, 'm');
103
+ const match = changelogContent.match(versionHeadingRegex);
104
+ if (!match) {
105
+ return {};
106
+ }
107
+
108
+ const headingIndex = match.index;
109
+ const rest = changelogContent.slice(headingIndex + match[0].length);
110
+ const nextVersionRegex = /^### \[[^\n]*\]/m;
111
+ const nextVersionMatch = nextVersionRegex.exec(rest);
112
+ const section = nextVersionMatch ? rest.slice(0, nextVersionMatch.index) : rest;
113
+
114
+ const dateMatch = match[0].match(/\((\d{4}-\d{2}-\d{2})\)/);
115
+ const date = dateMatch ? dateMatch[1] : undefined;
116
+
117
+ const categoryRegex = /^###\s+([^\n]+)$/m;
118
+ const categoryMatch = section.match(categoryRegex);
119
+ let heading;
120
+ if (categoryMatch) {
121
+ heading = sanitizeText(categoryMatch[1]);
122
+ }
123
+
124
+ const bullets = [];
125
+ const bulletRegex = /^\*\s+([^\n]+)$/gm;
126
+ let bulletMatch;
127
+ while ((bulletMatch = bulletRegex.exec(section)) !== null) {
128
+ const sanitized = sanitizeText(bulletMatch[1]);
129
+ if (!bullets.includes(sanitized)) {
130
+ bullets.push(sanitized);
131
+ }
132
+ }
133
+
134
+ const highlight = bullets.length > 0 ? bullets[0] : undefined;
135
+
136
+ return {
137
+ date,
138
+ highlight,
139
+ heading,
140
+ bullets
141
+ };
142
+ }
143
+
144
+ function sanitizeText(text) {
145
+ return text
146
+ .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
147
+ .replace(/\s+/g, ' ')
148
+ .trim();
149
+ }
150
+
151
+ function escapeRegExp(string) {
152
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
153
+ }
154
+
155
+ function escapePipes(text) {
156
+ return text.replace(/\|/g, '\\|');
157
+ }
158
+
159
+ function formatDate(date) {
160
+ const year = date.getFullYear();
161
+ const month = String(date.getMonth() + 1).padStart(2, '0');
162
+ const day = String(date.getDate()).padStart(2, '0');
163
+ return `${year}-${month}-${day}`;
164
+ }
165
+
166
+ module.exports = {
167
+ updateReleaseNotes,
168
+ insertReleaseNotesEntry,
169
+ extractChangelogInfo,
170
+ sanitizeText,
171
+ escapePipes,
172
+ formatDate
173
+ };
174
+
175
+ if (require.main === module) {
176
+ try {
177
+ updateReleaseNotes();
178
+ } catch (error) {
179
+ console.error(`❌ Failed to update release notes: ${error.message}`);
180
+ process.exit(1);
181
+ }
182
+ }
package/server.json CHANGED
@@ -1,23 +1,23 @@
1
- {
2
- "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
3
- "name": "io.github.programinglive/dev-workflow-mcp-server",
4
- "description": "MCP server to enforce development workflow discipline",
5
- "title": "Dev Workflow MCP Server",
6
- "websiteUrl": "https://devworkflow.programinglive.com/",
7
- "repository": {
8
- "url": "https://github.com/programinglive/dev-workflow-mcp-server",
9
- "source": "github"
10
- },
11
- "version": "1.4.6",
12
- "packages": [
13
- {
14
- "registryType": "npm",
15
- "registryBaseUrl": "https://registry.npmjs.org",
16
- "identifier": "@programinglive/dev-workflow-mcp-server",
17
- "version": "1.4.6",
18
- "transport": {
19
- "type": "stdio"
20
- }
21
- }
22
- ]
23
- }
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
3
+ "name": "io.github.programinglive/dev-workflow-mcp-server",
4
+ "description": "MCP server to enforce development workflow discipline",
5
+ "title": "Dev Workflow MCP Server",
6
+ "websiteUrl": "https://devworkflow.programinglive.com/",
7
+ "repository": {
8
+ "url": "https://github.com/programinglive/dev-workflow-mcp-server",
9
+ "source": "github"
10
+ },
11
+ "version": "1.4.6",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "registryBaseUrl": "https://registry.npmjs.org",
16
+ "identifier": "@programinglive/dev-workflow-mcp-server",
17
+ "version": "1.4.6",
18
+ "transport": {
19
+ "type": "stdio"
20
+ }
21
+ }
22
+ ]
23
+ }
@@ -1,8 +1,8 @@
1
- /** @type {import('tailwindcss').Config} */
2
- module.exports = {
3
- content: ["./web/public/**/*.{html,js}"],
4
- theme: {
5
- extend: {},
6
- },
7
- plugins: [],
8
- }
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: ["./web/public/**/*.{html,js}"],
4
+ theme: {
5
+ extend: {},
6
+ },
7
+ plugins: [],
8
+ }
@@ -1,83 +1,83 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert';
3
- import { readFile, access } from 'fs/promises';
4
- import { exec as execCallback } from 'child_process';
5
- import { promisify } from 'util';
6
- import path from 'path';
7
-
8
- const exec = promisify(execCallback);
9
-
10
- test('build generates dist/index.mjs file', async () => {
11
- const { stderr } = await exec('npm run build:server && npm run postbuild');
12
- // Allow the known vite warning about node:process
13
- const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
14
- const tailwindOutput = '≈ tailwindcss';
15
- const browserslistWarning = 'Browserslist: caniuse-lite is outdated';
16
- const actualStderr = stderr.replace(/\x1B\[[0-9;]*m/g, ''); // Remove ANSI codes
17
- assert(
18
- actualStderr === '' || actualStderr.includes(knownWarning) || actualStderr.includes(tailwindOutput) || actualStderr.includes(browserslistWarning),
19
- `Build should not have unexpected errors: ${actualStderr}`
20
- );
21
-
22
- // Verify the file exists
23
- const distPath = path.join(process.cwd(), 'dist', 'index.mjs');
24
- await access(distPath);
25
-
26
- // Verify it contains expected content
27
- const content = await readFile(distPath, 'utf-8');
28
- assert(content.includes('main'), 'Bundle should contain main function');
29
- assert(content.length > 1000, 'Bundle should be substantial');
30
-
31
- const docDir = path.join(process.cwd(), 'dist', 'docs');
32
- const docFiles = ['README.html', 'web-dashboard.html'];
33
- for (const file of docFiles) {
34
- const fullPath = path.join(docDir, file);
35
- await access(fullPath);
36
- const docContent = await readFile(fullPath, 'utf-8');
37
- assert(docContent.includes('<h1'), `${file} should contain HTML output`);
38
- }
39
- });
40
-
41
- test('build script exists in package.json', async () => {
42
- const packagePath = path.join(process.cwd(), 'package.json');
43
- const content = await readFile(packagePath, 'utf-8');
44
- const pkg = JSON.parse(content);
45
-
46
- assert.strictEqual(typeof pkg.scripts['build:server'], 'string');
47
- assert(pkg.scripts['build:server'].includes('vite build'));
48
- });
49
-
50
- test('Hero component fetches npm downloads directly', async () => {
51
- const heroPath = path.join(process.cwd(), 'web', 'components', 'Hero.tsx');
52
- const heroContent = await readFile(heroPath, 'utf-8');
53
-
54
- // Verify it fetches npm downloads directly from npm API
55
- assert(heroContent.includes('api.npmjs.org'), 'Hero should fetch downloads from npm API');
56
- assert(heroContent.includes('useState<string | null>(null)'), 'Hero should have version state');
57
- });
58
-
59
- test('Netlify deployment configuration exists for static export', async () => {
60
- const netlifyTomlPath = path.join(process.cwd(), 'web', 'netlify.toml');
61
- await access(netlifyTomlPath);
62
-
63
- const content = await readFile(netlifyTomlPath, 'utf-8');
64
- assert(content.includes('[build]'), 'netlify.toml should have build section');
65
- assert(content.includes('npm run build'), 'netlify.toml should have build command');
66
- assert(content.includes('publish = "out"'), 'netlify.toml should publish out directory for static export');
67
- });
68
-
69
- test('Web package.json does not include better-sqlite3', async () => {
70
- const webPackagePath = path.join(process.cwd(), 'web', 'package.json');
71
- const content = await readFile(webPackagePath, 'utf-8');
72
- const pkg = JSON.parse(content);
73
-
74
- assert(!pkg.dependencies['better-sqlite3'], 'Web should not depend on better-sqlite3 for Netlify compatibility');
75
- });
76
-
77
- test('Next.js config uses static export for Netlify', async () => {
78
- const nextConfigPath = path.join(process.cwd(), 'web', 'next.config.ts');
79
- const content = await readFile(nextConfigPath, 'utf-8');
80
-
81
- assert(content.includes("output: 'export'"), 'next.config.ts should use static export');
82
- assert(content.includes('unoptimized: true'), 'next.config.ts should disable image optimization for static export');
83
- });
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { readFile, access } from 'fs/promises';
4
+ import { exec as execCallback } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import path from 'path';
7
+
8
+ const exec = promisify(execCallback);
9
+
10
+ test('build generates dist/index.mjs file', async () => {
11
+ const { stderr } = await exec('npm run build:server && npm run postbuild');
12
+ // Allow the known vite warning about node:process
13
+ const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
14
+ const tailwindOutput = '≈ tailwindcss';
15
+ const browserslistWarning = 'Browserslist: caniuse-lite is outdated';
16
+ const actualStderr = stderr.replace(/\x1B\[[0-9;]*m/g, ''); // Remove ANSI codes
17
+ assert(
18
+ actualStderr === '' || actualStderr.includes(knownWarning) || actualStderr.includes(tailwindOutput) || actualStderr.includes(browserslistWarning),
19
+ `Build should not have unexpected errors: ${actualStderr}`
20
+ );
21
+
22
+ // Verify the file exists
23
+ const distPath = path.join(process.cwd(), 'dist', 'index.mjs');
24
+ await access(distPath);
25
+
26
+ // Verify it contains expected content
27
+ const content = await readFile(distPath, 'utf-8');
28
+ assert(content.includes('main'), 'Bundle should contain main function');
29
+ assert(content.length > 1000, 'Bundle should be substantial');
30
+
31
+ const docDir = path.join(process.cwd(), 'dist', 'docs');
32
+ const docFiles = ['README.html', 'web-dashboard.html'];
33
+ for (const file of docFiles) {
34
+ const fullPath = path.join(docDir, file);
35
+ await access(fullPath);
36
+ const docContent = await readFile(fullPath, 'utf-8');
37
+ assert(docContent.includes('<h1'), `${file} should contain HTML output`);
38
+ }
39
+ });
40
+
41
+ test('build script exists in package.json', async () => {
42
+ const packagePath = path.join(process.cwd(), 'package.json');
43
+ const content = await readFile(packagePath, 'utf-8');
44
+ const pkg = JSON.parse(content);
45
+
46
+ assert.strictEqual(typeof pkg.scripts['build:server'], 'string');
47
+ assert(pkg.scripts['build:server'].includes('vite build'));
48
+ });
49
+
50
+ test('Hero component fetches npm downloads directly', async () => {
51
+ const heroPath = path.join(process.cwd(), 'web', 'components', 'Hero.tsx');
52
+ const heroContent = await readFile(heroPath, 'utf-8');
53
+
54
+ // Verify it fetches npm downloads directly from npm API
55
+ assert(heroContent.includes('api.npmjs.org'), 'Hero should fetch downloads from npm API');
56
+ assert(heroContent.includes('useState<string | null>(null)'), 'Hero should have version state');
57
+ });
58
+
59
+ test('Netlify deployment configuration exists for static export', async () => {
60
+ const netlifyTomlPath = path.join(process.cwd(), 'web', 'netlify.toml');
61
+ await access(netlifyTomlPath);
62
+
63
+ const content = await readFile(netlifyTomlPath, 'utf-8');
64
+ assert(content.includes('[build]'), 'netlify.toml should have build section');
65
+ assert(content.includes('npm run build'), 'netlify.toml should have build command');
66
+ assert(content.includes('publish = "out"'), 'netlify.toml should publish out directory for static export');
67
+ });
68
+
69
+ test('Web package.json does not include better-sqlite3', async () => {
70
+ const webPackagePath = path.join(process.cwd(), 'web', 'package.json');
71
+ const content = await readFile(webPackagePath, 'utf-8');
72
+ const pkg = JSON.parse(content);
73
+
74
+ assert(!pkg.dependencies['better-sqlite3'], 'Web should not depend on better-sqlite3 for Netlify compatibility');
75
+ });
76
+
77
+ test('Next.js config uses static export for Netlify', async () => {
78
+ const nextConfigPath = path.join(process.cwd(), 'web', 'next.config.ts');
79
+ const content = await readFile(nextConfigPath, 'utf-8');
80
+
81
+ assert(content.includes("output: 'export'"), 'next.config.ts should use static export');
82
+ assert(content.includes('unoptimized: true'), 'next.config.ts should disable image optimization for static export');
83
+ });
@@ -1,35 +1,35 @@
1
- import { test } from 'node:test';
2
- import assert from 'node:assert';
3
- import { exec } from '../exec.js';
4
- import { tmpdir } from 'os';
5
- import { mkdtemp, rm } from 'fs/promises';
6
- import path from 'path';
7
-
8
- test('exec runs commands successfully', async () => {
9
- const echoCommand = process.platform === 'win32' ? 'echo hello world' : 'echo "hello world"';
10
- const { stdout } = await exec(echoCommand);
11
- const normalized = stdout.trim().replace(/^"(.*)"$/, '$1');
12
- assert.strictEqual(normalized, 'hello world');
13
- });
14
-
15
- test('exec respects explicit cwd option', async () => {
16
- const tempDir = await mkdtemp(path.join(tmpdir(), 'mcp-test-'));
17
-
18
- try {
19
- const dirCommand = process.platform === 'win32' ? 'cd' : 'pwd';
20
- const { stdout } = await exec(dirCommand, { cwd: tempDir });
21
- // Just verify it runs in the specified directory (path may vary by platform)
22
- assert(stdout.trim().toLowerCase().includes('mcp-test-'));
23
- } finally {
24
- await rm(tempDir, { recursive: true, force: true });
25
- }
26
- });
27
-
28
- test('exec handles command errors', async () => {
29
- try {
30
- await exec('command-that-does-not-exist');
31
- assert.fail('Should have thrown an error');
32
- } catch (error) {
33
- assert(error.code !== undefined || error.message.includes('command-not-found'));
34
- }
35
- });
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { exec } from '../exec.js';
4
+ import { tmpdir } from 'os';
5
+ import { mkdtemp, rm } from 'fs/promises';
6
+ import path from 'path';
7
+
8
+ test('exec runs commands successfully', async () => {
9
+ const echoCommand = process.platform === 'win32' ? 'echo hello world' : 'echo "hello world"';
10
+ const { stdout } = await exec(echoCommand);
11
+ const normalized = stdout.trim().replace(/^"(.*)"$/, '$1');
12
+ assert.strictEqual(normalized, 'hello world');
13
+ });
14
+
15
+ test('exec respects explicit cwd option', async () => {
16
+ const tempDir = await mkdtemp(path.join(tmpdir(), 'mcp-test-'));
17
+
18
+ try {
19
+ const dirCommand = process.platform === 'win32' ? 'cd' : 'pwd';
20
+ const { stdout } = await exec(dirCommand, { cwd: tempDir });
21
+ // Just verify it runs in the specified directory (path may vary by platform)
22
+ assert(stdout.trim().toLowerCase().includes('mcp-test-'));
23
+ } finally {
24
+ await rm(tempDir, { recursive: true, force: true });
25
+ }
26
+ });
27
+
28
+ test('exec handles command errors', async () => {
29
+ try {
30
+ await exec('command-that-does-not-exist');
31
+ assert.fail('Should have thrown an error');
32
+ } catch (error) {
33
+ assert(error.code !== undefined || error.message.includes('command-not-found'));
34
+ }
35
+ });