1- import { describe , it , expect , beforeEach , afterEach } from '@jest/globals' ;
2- import { mkdtempSync , rmSync , existsSync , readFileSync } from 'fs' ;
1+ import { describe , it , expect , beforeEach , afterEach , jest } from '@jest/globals' ;
2+ import { mkdtempSync , mkdirSync , rmSync , existsSync , readFileSync , writeFileSync } from 'fs' ;
33import { join } from 'path' ;
44import { tmpdir } from 'os' ;
5+ import { spawnSync } from 'child_process' ;
56
6- // Import createProject directly for unit testing
77import { createProject } from '../../src/cli/project/create.js' ;
88
99describe ( 'mcp create . (current directory)' , ( ) => {
@@ -13,7 +13,6 @@ describe('mcp create . (current directory)', () => {
1313
1414 beforeEach ( ( ) => {
1515 tempDir = mkdtempSync ( join ( tmpdir ( ) , 'mcp-test-' ) ) ;
16- process . chdir ( tempDir ) ;
1716 } ) ;
1817
1918 afterEach ( ( ) => {
@@ -22,60 +21,205 @@ describe('mcp create . (current directory)', () => {
2221 rmSync ( tempDir , { recursive : true , force : true } ) ;
2322 } ) ;
2423
25- it ( 'should scaffold project in the current directory when name is "."' , async ( ) => {
26- // Create a specifically named subdirectory to control the derived name
27- const projectDir = join ( tempDir , 'my-test-server' ) ;
28- const { mkdirSync } = await import ( 'fs' ) ;
29- mkdirSync ( projectDir ) ;
30- process . chdir ( projectDir ) ;
31-
32- await createProject ( '.' , { install : false , example : true } ) ;
33-
34- // Verify files were created in the current directory (not a subdirectory)
35- expect ( existsSync ( join ( projectDir , 'package.json' ) ) ) . toBe ( true ) ;
36- expect ( existsSync ( join ( projectDir , 'tsconfig.json' ) ) ) . toBe ( true ) ;
37- expect ( existsSync ( join ( projectDir , 'src' , 'index.ts' ) ) ) . toBe ( true ) ;
38- expect ( existsSync ( join ( projectDir , 'src' , 'tools' , 'ExampleTool.ts' ) ) ) . toBe ( true ) ;
39- expect ( existsSync ( join ( projectDir , '.gitignore' ) ) ) . toBe ( true ) ;
40- expect ( existsSync ( join ( projectDir , 'README.md' ) ) ) . toBe ( true ) ;
41-
42- // No subdirectory should have been created
43- expect ( existsSync ( join ( projectDir , '.' ) ) ) . toBe ( true ) ;
44-
45- // Verify project name was derived from directory name
46- const pkg = JSON . parse ( readFileSync ( join ( projectDir , 'package.json' ) , 'utf-8' ) ) ;
47- expect ( pkg . name ) . toBe ( 'my-test-server' ) ;
48- expect ( pkg . description ) . toBe ( 'my-test-server MCP server' ) ;
49- expect ( pkg . bin [ 'my-test-server' ] ) . toBe ( './dist/index.js' ) ;
24+ function mockProcessExit ( ) : { getExitCode : ( ) => number | undefined } {
25+ let exitCode : number | undefined ;
26+ process . exit = ( ( code ?: number ) => {
27+ exitCode = code ;
28+ throw new Error ( 'process.exit called' ) ;
29+ } ) as never ;
30+ return { getExitCode : ( ) => exitCode } ;
31+ }
32+
33+ describe ( 'scaffolding' , ( ) => {
34+ it ( 'should create all project files in the current directory' , async ( ) => {
35+ const projectDir = join ( tempDir , 'my-test-server' ) ;
36+ mkdirSync ( projectDir ) ;
37+ process . chdir ( projectDir ) ;
38+
39+ await createProject ( '.' , { install : false , example : true } ) ;
40+
41+ expect ( existsSync ( join ( projectDir , 'package.json' ) ) ) . toBe ( true ) ;
42+ expect ( existsSync ( join ( projectDir , 'tsconfig.json' ) ) ) . toBe ( true ) ;
43+ expect ( existsSync ( join ( projectDir , 'src' , 'index.ts' ) ) ) . toBe ( true ) ;
44+ expect ( existsSync ( join ( projectDir , 'src' , 'tools' , 'ExampleTool.ts' ) ) ) . toBe ( true ) ;
45+ expect ( existsSync ( join ( projectDir , '.gitignore' ) ) ) . toBe ( true ) ;
46+ expect ( existsSync ( join ( projectDir , 'README.md' ) ) ) . toBe ( true ) ;
47+ } ) ;
48+
49+ it ( 'should not create a subdirectory' , async ( ) => {
50+ const projectDir = join ( tempDir , 'test-server' ) ;
51+ mkdirSync ( projectDir ) ;
52+ process . chdir ( projectDir ) ;
53+
54+ await createProject ( '.' , { install : false , example : false } ) ;
55+
56+ // No nested subdirectory should exist with the project name
57+ expect ( existsSync ( join ( projectDir , 'test-server' ) ) ) . toBe ( false ) ;
58+ } ) ;
59+
60+ it ( 'should skip example tool when --no-example is used' , async ( ) => {
61+ const projectDir = join ( tempDir , 'no-example' ) ;
62+ mkdirSync ( projectDir ) ;
63+ process . chdir ( projectDir ) ;
64+
65+ await createProject ( '.' , { install : false , example : false } ) ;
66+
67+ expect ( existsSync ( join ( projectDir , 'src' , 'tools' , 'ExampleTool.ts' ) ) ) . toBe ( false ) ;
68+ expect ( existsSync ( join ( projectDir , 'src' , 'index.ts' ) ) ) . toBe ( true ) ;
69+ } ) ;
70+
71+ it ( 'should generate HTTP transport config when --http is used' , async ( ) => {
72+ const projectDir = join ( tempDir , 'http-server' ) ;
73+ mkdirSync ( projectDir ) ;
74+ process . chdir ( projectDir ) ;
75+
76+ await createProject ( '.' , { install : false , http : true , port : 3000 } ) ;
77+
78+ const indexTs = readFileSync ( join ( projectDir , 'src' , 'index.ts' ) , 'utf-8' ) ;
79+ expect ( indexTs ) . toContain ( 'http-stream' ) ;
80+ expect ( indexTs ) . toContain ( '3000' ) ;
81+ } ) ;
5082 } ) ;
5183
52- it ( 'should derive a valid npm name from directory with uppercase/special chars' , async ( ) => {
53- const projectDir = join ( tempDir , 'My_Cool.Server' ) ;
54- const { mkdirSync } = await import ( 'fs' ) ;
55- mkdirSync ( projectDir ) ;
56- process . chdir ( projectDir ) ;
84+ describe ( 'project name derivation' , ( ) => {
85+ it ( 'should derive name from directory name' , async ( ) => {
86+ const projectDir = join ( tempDir , 'my-test-server' ) ;
87+ mkdirSync ( projectDir ) ;
88+ process . chdir ( projectDir ) ;
89+
90+ await createProject ( '.' , { install : false , example : false } ) ;
91+
92+ const pkg = JSON . parse ( readFileSync ( join ( projectDir , 'package.json' ) , 'utf-8' ) ) ;
93+ expect ( pkg . name ) . toBe ( 'my-test-server' ) ;
94+ expect ( pkg . description ) . toBe ( 'my-test-server MCP server' ) ;
95+ expect ( pkg . bin [ 'my-test-server' ] ) . toBe ( './dist/index.js' ) ;
96+ } ) ;
97+
98+ it ( 'should lowercase uppercase directory names' , async ( ) => {
99+ const projectDir = join ( tempDir , 'MyServer' ) ;
100+ mkdirSync ( projectDir ) ;
101+ process . chdir ( projectDir ) ;
102+
103+ await createProject ( '.' , { install : false , example : false } ) ;
104+
105+ const pkg = JSON . parse ( readFileSync ( join ( projectDir , 'package.json' ) , 'utf-8' ) ) ;
106+ expect ( pkg . name ) . toBe ( 'myserver' ) ;
107+ } ) ;
108+
109+ it ( 'should replace special characters with hyphens' , async ( ) => {
110+ const projectDir = join ( tempDir , 'My_Cool.Server' ) ;
111+ mkdirSync ( projectDir ) ;
112+ process . chdir ( projectDir ) ;
113+
114+ await createProject ( '.' , { install : false , example : false } ) ;
115+
116+ const pkg = JSON . parse ( readFileSync ( join ( projectDir , 'package.json' ) , 'utf-8' ) ) ;
117+ expect ( pkg . name ) . toBe ( 'my-cool-server' ) ;
118+ } ) ;
119+
120+ it ( 'should collapse consecutive hyphens' , async ( ) => {
121+ const projectDir = join ( tempDir , 'my___server' ) ;
122+ mkdirSync ( projectDir ) ;
123+ process . chdir ( projectDir ) ;
124+
125+ await createProject ( '.' , { install : false , example : false } ) ;
57126
58- await createProject ( '.' , { install : false , example : false } ) ;
127+ const pkg = JSON . parse ( readFileSync ( join ( projectDir , 'package.json' ) , 'utf-8' ) ) ;
128+ expect ( pkg . name ) . toBe ( 'my-server' ) ;
129+ } ) ;
59130
60- const pkg = JSON . parse ( readFileSync ( join ( projectDir , 'package.json' ) , 'utf-8' ) ) ;
61- // Uppercase → lowercase, underscores/dots → hyphens
62- expect ( pkg . name ) . toBe ( 'my-cool-server' ) ;
131+ it ( 'should strip leading and trailing hyphens' , async ( ) => {
132+ const projectDir = join ( tempDir , '-my-server-' ) ;
133+ mkdirSync ( projectDir ) ;
134+ process . chdir ( projectDir ) ;
135+
136+ await createProject ( '.' , { install : false , example : false } ) ;
137+
138+ const pkg = JSON . parse ( readFileSync ( join ( projectDir , 'package.json' ) , 'utf-8' ) ) ;
139+ expect ( pkg . name ) . toBe ( 'my-server' ) ;
140+ } ) ;
63141 } ) ;
64142
65- it ( 'should reject when current directory has conflicting files' , async ( ) => {
66- const projectDir = join ( tempDir , 'existing-project' ) ;
67- const { mkdirSync , writeFileSync } = await import ( 'fs ') ;
68- mkdirSync ( projectDir ) ;
69- writeFileSync ( join ( projectDir , 'package.json' ) , '{}' ) ;
70- process . chdir ( projectDir ) ;
143+ describe ( 'conflict detection' , ( ) => {
144+ it ( 'should reject when package.json exists' , async ( ) => {
145+ const projectDir = join ( tempDir , 'has-pkg ') ;
146+ mkdirSync ( projectDir ) ;
147+ writeFileSync ( join ( projectDir , 'package.json' ) , '{}' ) ;
148+ process . chdir ( projectDir ) ;
71149
72- let exitCode : number | undefined ;
73- process . exit = ( ( code ?: number ) => {
74- exitCode = code ;
75- throw new Error ( 'process.exit called' ) ;
76- } ) as never ;
150+ const { getExitCode } = mockProcessExit ( ) ;
151+
152+ await expect ( createProject ( '.' , { install : false } ) ) . rejects . toThrow ( 'process.exit called' ) ;
153+ expect ( getExitCode ( ) ) . toBe ( 1 ) ;
154+ } ) ;
155+
156+ it ( 'should reject when tsconfig.json exists' , async ( ) => {
157+ const projectDir = join ( tempDir , 'has-tsconfig' ) ;
158+ mkdirSync ( projectDir ) ;
159+ writeFileSync ( join ( projectDir , 'tsconfig.json' ) , '{}' ) ;
160+ process . chdir ( projectDir ) ;
161+
162+ const { getExitCode } = mockProcessExit ( ) ;
163+
164+ await expect ( createProject ( '.' , { install : false } ) ) . rejects . toThrow ( 'process.exit called' ) ;
165+ expect ( getExitCode ( ) ) . toBe ( 1 ) ;
166+ } ) ;
167+
168+ it ( 'should reject when src directory exists' , async ( ) => {
169+ const projectDir = join ( tempDir , 'has-src' ) ;
170+ mkdirSync ( projectDir ) ;
171+ mkdirSync ( join ( projectDir , 'src' ) ) ;
172+ process . chdir ( projectDir ) ;
173+
174+ const { getExitCode } = mockProcessExit ( ) ;
175+
176+ await expect ( createProject ( '.' , { install : false } ) ) . rejects . toThrow ( 'process.exit called' ) ;
177+ expect ( getExitCode ( ) ) . toBe ( 1 ) ;
178+ } ) ;
179+
180+ it ( 'should allow directories with other non-conflicting files' , async ( ) => {
181+ const projectDir = join ( tempDir , 'has-readme' ) ;
182+ mkdirSync ( projectDir ) ;
183+ writeFileSync ( join ( projectDir , 'notes.txt' ) , 'hello' ) ;
184+ process . chdir ( projectDir ) ;
185+
186+ await createProject ( '.' , { install : false , example : false } ) ;
187+
188+ expect ( existsSync ( join ( projectDir , 'package.json' ) ) ) . toBe ( true ) ;
189+ } ) ;
190+ } ) ;
191+
192+ describe ( 'git init behavior' , ( ) => {
193+ it ( 'should skip git init when .git already exists' , async ( ) => {
194+ const projectDir = join ( tempDir , 'git-exists' ) ;
195+ mkdirSync ( projectDir ) ;
196+ process . chdir ( projectDir ) ;
197+
198+ // Initialize a git repo before running create
199+ spawnSync ( 'git' , [ 'init' ] , { cwd : projectDir , stdio : 'ignore' } ) ;
200+ expect ( existsSync ( join ( projectDir , '.git' ) ) ) . toBe ( true ) ;
201+
202+ // Capture console output to verify git init was skipped
203+ const logs : string [ ] = [ ] ;
204+ const originalLog = console . log ;
205+ console . log = ( ...args : unknown [ ] ) => logs . push ( args . join ( ' ' ) ) ;
206+
207+ await createProject ( '.' , { install : false , example : false } ) ;
208+
209+ console . log = originalLog ;
210+
211+ expect ( logs . some ( ( l ) => l . includes ( 'Initializing git' ) ) ) . toBe ( false ) ;
212+ expect ( existsSync ( join ( projectDir , 'package.json' ) ) ) . toBe ( true ) ;
213+ } ) ;
214+
215+ it ( 'should run git init when no .git exists' , async ( ) => {
216+ const projectDir = join ( tempDir , 'no-git' ) ;
217+ mkdirSync ( projectDir ) ;
218+ process . chdir ( projectDir ) ;
219+
220+ await createProject ( '.' , { install : false , example : false } ) ;
77221
78- await expect ( createProject ( '.' , { install : false } ) ) . rejects . toThrow ( 'process.exit called' ) ;
79- expect ( exitCode ) . toBe ( 1 ) ;
222+ expect ( existsSync ( join ( projectDir , '.git' ) ) ) . toBe ( true ) ;
223+ } ) ;
80224 } ) ;
81225} ) ;
0 commit comments