@programinglive/dev-workflow-mcp-server 1.5.2 → 1.5.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 (78) 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/github-release.yml +56 -0
  9. package/.github/workflows/npm-publish.yml +50 -50
  10. package/.husky/commit-msg +0 -0
  11. package/CHANGELOG.md +759 -741
  12. package/CODE_OF_CONDUCT.md +79 -79
  13. package/CONTRIBUTING.md +60 -60
  14. package/LICENSE +21 -21
  15. package/README.md +632 -632
  16. package/SECURITY.md +20 -20
  17. package/db/index.js +204 -204
  18. package/docs/NETLIFY_DEPLOYMENT.md +125 -125
  19. package/docs/PLESK_DEPLOYMENT.md +170 -170
  20. package/docs/WORKFLOW-BEST-PRACTICES.md +58 -58
  21. package/docs/antigravity/EXAMPLES.md +535 -535
  22. package/docs/antigravity/GETTING-STARTED.md +292 -292
  23. package/docs/antigravity/TROUBLESHOOTING.md +742 -742
  24. package/docs/antigravity/WALKTHROUGH.md +25 -25
  25. package/docs/antigravity/WORKFLOW-TEMPLATES.md +670 -670
  26. package/docs/product/PRD.md +67 -67
  27. package/docs/release-notes/RELEASE_NOTES.md +233 -226
  28. package/docs/web-dashboard.md +175 -175
  29. package/exec.js +49 -49
  30. package/index.js +279 -279
  31. package/install.js +73 -73
  32. package/package.json +4 -4
  33. package/release-wrapper.js +192 -192
  34. package/scripts/build-docs.js +63 -63
  35. package/scripts/extract-release-notes.cjs +78 -0
  36. package/scripts/hello-world.js +1 -1
  37. package/scripts/release.cjs +252 -252
  38. package/scripts/release.js +252 -0
  39. package/scripts/update-release-notes.cjs +178 -178
  40. package/scripts/update-release-notes.js +182 -0
  41. package/server.json +23 -23
  42. package/tailwind.config.js +8 -8
  43. package/tests/build.test.js +83 -83
  44. package/tests/exec.test.js +35 -35
  45. package/tests/handlers.test.js +1058 -1058
  46. package/tests/hello-world.test.js +13 -13
  47. package/tests/mcpRegistry.test.js +65 -65
  48. package/tests/patch-release.test.js +7 -0
  49. package/tests/release-wrapper.test.js +107 -107
  50. package/tests/workflowState.test.js +505 -505
  51. package/tools/definitions.js +332 -332
  52. package/tools/handlers.js +1019 -1019
  53. package/vite.config.js +43 -43
  54. package/vite.web.config.js +23 -23
  55. package/web/README.md +36 -36
  56. package/web/app/docs/page.tsx +202 -202
  57. package/web/app/globals.css +90 -90
  58. package/web/app/layout.tsx +36 -36
  59. package/web/app/page.tsx +15 -15
  60. package/web/components/ApiReference.tsx +20 -20
  61. package/web/components/Features.tsx +61 -61
  62. package/web/components/Footer.tsx +9 -9
  63. package/web/components/Hero.tsx +96 -96
  64. package/web/components/History.tsx +143 -143
  65. package/web/components/Navbar.tsx +64 -64
  66. package/web/components/Terminal.tsx +114 -114
  67. package/web/eslint.config.mjs +18 -18
  68. package/web/lib/db.js +131 -131
  69. package/web/lib/docs.js +73 -73
  70. package/web/netlify.toml +3 -3
  71. package/web/next.config.ts +12 -12
  72. package/web/package-lock.json +6560 -6560
  73. package/web/package.json +28 -28
  74. package/web/postcss.config.mjs +9 -9
  75. package/web/server.js +31 -31
  76. package/web/tailwind.config.ts +19 -19
  77. package/web/tsconfig.json +42 -42
  78. 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
+ });