frontmcp 0.1.3 → 0.1.5
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/dist/cli.js +105 -61
- package/package.json +1 -1
- package/src/cli.ts +111 -74
package/dist/cli.js
CHANGED
|
@@ -76,7 +76,7 @@ ${c('bold', 'Commands')}
|
|
|
76
76
|
init Create or fix a tsconfig.json suitable for FrontMCP
|
|
77
77
|
doctor Check Node/npm versions and tsconfig requirements
|
|
78
78
|
inspector Launch MCP Inspector (npx @modelcontextprotocol/inspector)
|
|
79
|
-
create
|
|
79
|
+
create <name> Scaffold a new FrontMCP project in ./<name>
|
|
80
80
|
help Show this help message
|
|
81
81
|
|
|
82
82
|
${c('bold', 'Options')}
|
|
@@ -89,7 +89,7 @@ ${c('bold', 'Examples')}
|
|
|
89
89
|
frontmcp init
|
|
90
90
|
frontmcp doctor
|
|
91
91
|
frontmcp inspector
|
|
92
|
-
npx frontmcp create
|
|
92
|
+
npx frontmcp create my-mcp
|
|
93
93
|
`);
|
|
94
94
|
}
|
|
95
95
|
function parseArgs(argv) {
|
|
@@ -146,30 +146,24 @@ function resolveEntry(cwd, explicit) {
|
|
|
146
146
|
return full;
|
|
147
147
|
throw new Error(`Entry override not found: ${explicit}`);
|
|
148
148
|
}
|
|
149
|
-
// 1) package.json main
|
|
150
149
|
const pkgPath = path.join(cwd, 'package.json');
|
|
151
150
|
if (yield fileExists(pkgPath)) {
|
|
152
151
|
const pkg = yield readJSON(pkgPath);
|
|
153
152
|
if (pkg && typeof pkg.main === 'string' && pkg.main.trim()) {
|
|
154
153
|
const mainCandidates = tryCandidates(path.resolve(cwd, pkg.main));
|
|
155
|
-
for (const p of mainCandidates)
|
|
154
|
+
for (const p of mainCandidates)
|
|
156
155
|
if (yield fileExists(p))
|
|
157
156
|
return p;
|
|
158
|
-
}
|
|
159
|
-
// If "main" is a directory-like path, try index.* within it
|
|
160
157
|
const asDir = path.resolve(cwd, pkg.main);
|
|
161
158
|
const idxCandidates = tryCandidates(path.join(asDir, 'index'));
|
|
162
|
-
for (const p of idxCandidates)
|
|
159
|
+
for (const p of idxCandidates)
|
|
163
160
|
if (yield fileExists(p))
|
|
164
161
|
return p;
|
|
165
|
-
}
|
|
166
162
|
}
|
|
167
163
|
}
|
|
168
|
-
// 2) src/main.ts
|
|
169
164
|
const fallback = path.join(cwd, 'src', 'main.ts');
|
|
170
165
|
if (yield fileExists(fallback))
|
|
171
166
|
return fallback;
|
|
172
|
-
// 3) Not found
|
|
173
167
|
const msg = [
|
|
174
168
|
c('red', 'No entry file found.'),
|
|
175
169
|
'',
|
|
@@ -191,6 +185,43 @@ function runCmd(cmd, args, opts = {}) {
|
|
|
191
185
|
child.on('error', reject);
|
|
192
186
|
});
|
|
193
187
|
}
|
|
188
|
+
function ensureDir(p) {
|
|
189
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
190
|
+
yield fs_1.promises.mkdir(p, { recursive: true });
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function isDirEmpty(dir) {
|
|
194
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
195
|
+
try {
|
|
196
|
+
const items = yield fs_1.promises.readdir(dir);
|
|
197
|
+
return items.length === 0;
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
if ((e === null || e === void 0 ? void 0 : e.code) === 'ENOENT')
|
|
201
|
+
return true;
|
|
202
|
+
throw e;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function sanitizeForFolder(name) {
|
|
207
|
+
const seg = name.startsWith('@') && name.includes('/') ? name.split('/')[1] : name;
|
|
208
|
+
return seg.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').toLowerCase() || 'frontmcp-app';
|
|
209
|
+
}
|
|
210
|
+
function sanitizeForNpm(name) {
|
|
211
|
+
if (name.startsWith('@') && name.includes('/')) {
|
|
212
|
+
const [scope, pkg] = name.split('/');
|
|
213
|
+
const s = scope.replace(/[^a-z0-9-]/gi, '').toLowerCase();
|
|
214
|
+
const p = pkg.replace(/[^a-z0-9._-]/gi, '-').toLowerCase();
|
|
215
|
+
return `@${s}/${p || 'frontmcp-app'}`;
|
|
216
|
+
}
|
|
217
|
+
return name.replace(/[^a-z0-9._-]/gi, '-').toLowerCase() || 'frontmcp-app';
|
|
218
|
+
}
|
|
219
|
+
/* ------------------------ Self-version detection -------------------------- */
|
|
220
|
+
function getSelfVersion() {
|
|
221
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
222
|
+
return '0.1.5';
|
|
223
|
+
});
|
|
224
|
+
}
|
|
194
225
|
/* --------------------------------- Actions -------------------------------- */
|
|
195
226
|
function runDev(opts) {
|
|
196
227
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -201,11 +232,6 @@ function runDev(opts) {
|
|
|
201
232
|
yield runCmd('npx', ['-y', 'tsx', '--watch', entry]);
|
|
202
233
|
});
|
|
203
234
|
}
|
|
204
|
-
function ensureDir(p) {
|
|
205
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
206
|
-
yield fs_1.promises.mkdir(p, { recursive: true });
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
235
|
function isTsLike(p) {
|
|
210
236
|
return /\.tsx?$/i.test(p);
|
|
211
237
|
}
|
|
@@ -229,7 +255,6 @@ function runBuild(opts) {
|
|
|
229
255
|
if (yield fileExists(tsconfigPath)) {
|
|
230
256
|
console.log(c('gray', `[build] tsconfig.json detected (project options will be respected where applicable)`));
|
|
231
257
|
}
|
|
232
|
-
// Compile the single entry file
|
|
233
258
|
yield runCmd('npx', ['-y', 'tsc', entry, ...args]);
|
|
234
259
|
console.log(c('green', '✅ Build completed.'));
|
|
235
260
|
console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
|
|
@@ -248,7 +273,7 @@ const RECOMMENDED_TSCONFIG = {
|
|
|
248
273
|
module: REQUIRED_DECORATOR_FIELDS.module,
|
|
249
274
|
emitDecoratorMetadata: REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata,
|
|
250
275
|
experimentalDecorators: REQUIRED_DECORATOR_FIELDS.experimentalDecorators,
|
|
251
|
-
moduleResolution: '
|
|
276
|
+
moduleResolution: 'node',
|
|
252
277
|
strict: true,
|
|
253
278
|
esModuleInterop: true,
|
|
254
279
|
resolveJsonModule: true,
|
|
@@ -276,7 +301,6 @@ function deepMerge(base, patch) {
|
|
|
276
301
|
function ensureRequiredTsOptions(obj) {
|
|
277
302
|
const next = Object.assign({}, obj);
|
|
278
303
|
next.compilerOptions = Object.assign({}, (next.compilerOptions || {}));
|
|
279
|
-
// Force the required values
|
|
280
304
|
next.compilerOptions.target = REQUIRED_DECORATOR_FIELDS.target;
|
|
281
305
|
next.compilerOptions.module = REQUIRED_DECORATOR_FIELDS.module;
|
|
282
306
|
next.compilerOptions.emitDecoratorMetadata = REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata;
|
|
@@ -311,18 +335,17 @@ function checkRequiredTsOptions(compilerOptions) {
|
|
|
311
335
|
issues.push(`compilerOptions.experimentalDecorators should be true`);
|
|
312
336
|
return { ok, issues };
|
|
313
337
|
}
|
|
314
|
-
function runInit() {
|
|
338
|
+
function runInit(baseDir) {
|
|
315
339
|
return __awaiter(this, void 0, void 0, function* () {
|
|
316
|
-
const cwd = process.cwd();
|
|
340
|
+
const cwd = baseDir !== null && baseDir !== void 0 ? baseDir : process.cwd();
|
|
317
341
|
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
318
342
|
const existing = yield readJSON(tsconfigPath);
|
|
319
343
|
if (!existing) {
|
|
320
|
-
console.log(c('yellow',
|
|
344
|
+
console.log(c('yellow', `tsconfig.json not found — creating one in ${path.relative(process.cwd(), cwd) || '.'}.`));
|
|
321
345
|
yield writeJSON(tsconfigPath, RECOMMENDED_TSCONFIG);
|
|
322
346
|
console.log(c('green', '✅ Created tsconfig.json with required decorator settings.'));
|
|
323
347
|
return;
|
|
324
348
|
}
|
|
325
|
-
// Merge user config on top of recommended, then force required decorator options
|
|
326
349
|
let merged = deepMerge(RECOMMENDED_TSCONFIG, existing);
|
|
327
350
|
merged = ensureRequiredTsOptions(merged);
|
|
328
351
|
yield writeJSON(tsconfigPath, merged);
|
|
@@ -347,7 +370,6 @@ function runDoctor() {
|
|
|
347
370
|
const MIN_NPM = '10.0.0';
|
|
348
371
|
const cwd = process.cwd();
|
|
349
372
|
let ok = true;
|
|
350
|
-
// Node
|
|
351
373
|
const nodeVer = process.versions.node;
|
|
352
374
|
if (cmpSemver(nodeVer, MIN_NODE) >= 0) {
|
|
353
375
|
console.log(`✅ Node ${nodeVer} (min ${MIN_NODE})`);
|
|
@@ -356,7 +378,6 @@ function runDoctor() {
|
|
|
356
378
|
ok = false;
|
|
357
379
|
console.log(`❌ Node ${nodeVer} — please upgrade to >= ${MIN_NODE}`);
|
|
358
380
|
}
|
|
359
|
-
// npm
|
|
360
381
|
let npmVer = 'unknown';
|
|
361
382
|
try {
|
|
362
383
|
npmVer = yield new Promise((resolve, reject) => {
|
|
@@ -379,7 +400,6 @@ function runDoctor() {
|
|
|
379
400
|
ok = false;
|
|
380
401
|
console.log('❌ npm not found in PATH');
|
|
381
402
|
}
|
|
382
|
-
// tsconfig.json presence + required fields
|
|
383
403
|
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
384
404
|
if (yield fileExists(tsconfigPath)) {
|
|
385
405
|
console.log(`✅ tsconfig.json found`);
|
|
@@ -398,7 +418,6 @@ function runDoctor() {
|
|
|
398
418
|
ok = false;
|
|
399
419
|
console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
|
|
400
420
|
}
|
|
401
|
-
// Entry check (nice to have)
|
|
402
421
|
try {
|
|
403
422
|
const entry = yield resolveEntry(cwd);
|
|
404
423
|
console.log(`✅ entry detected: ${path.relative(cwd, entry)}`);
|
|
@@ -407,12 +426,10 @@ function runDoctor() {
|
|
|
407
426
|
const firstLine = (_c = (_b = (_a = e === null || e === void 0 ? void 0 : e.message) === null || _a === void 0 ? void 0 : _a.split('\n')) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : 'entry not found';
|
|
408
427
|
console.log(`❌ entry not detected — ${firstLine}`);
|
|
409
428
|
}
|
|
410
|
-
if (ok)
|
|
429
|
+
if (ok)
|
|
411
430
|
console.log(c('green', '\nAll checks passed. You are ready to go!'));
|
|
412
|
-
|
|
413
|
-
else {
|
|
431
|
+
else
|
|
414
432
|
console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
|
|
415
|
-
}
|
|
416
433
|
});
|
|
417
434
|
}
|
|
418
435
|
/* ------------------------------- Inspector -------------------------------- */
|
|
@@ -426,13 +443,13 @@ function runInspector() {
|
|
|
426
443
|
function pkgNameFromCwd(cwd) {
|
|
427
444
|
return path.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, '-').toLowerCase() || 'frontmcp-app';
|
|
428
445
|
}
|
|
429
|
-
function upsertPackageJson(cwd) {
|
|
446
|
+
function upsertPackageJson(cwd, nameOverride, selfVersion) {
|
|
430
447
|
return __awaiter(this, void 0, void 0, function* () {
|
|
431
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k
|
|
448
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
432
449
|
const pkgPath = path.join(cwd, 'package.json');
|
|
433
450
|
const existing = yield readJSON(pkgPath);
|
|
434
451
|
const base = {
|
|
435
|
-
name: pkgNameFromCwd(cwd),
|
|
452
|
+
name: nameOverride !== null && nameOverride !== void 0 ? nameOverride : pkgNameFromCwd(cwd),
|
|
436
453
|
version: '0.1.0',
|
|
437
454
|
private: true,
|
|
438
455
|
type: 'module',
|
|
@@ -449,39 +466,45 @@ function upsertPackageJson(cwd) {
|
|
|
449
466
|
},
|
|
450
467
|
dependencies: {
|
|
451
468
|
'@frontmcp/sdk': 'latest',
|
|
452
|
-
zod: '
|
|
469
|
+
zod: '^3.23.8',
|
|
470
|
+
'reflect-metadata': '^0.2.2',
|
|
453
471
|
},
|
|
454
472
|
devDependencies: {
|
|
455
|
-
|
|
456
|
-
tsx: '
|
|
473
|
+
frontmcp: selfVersion, // exact version used by npx
|
|
474
|
+
tsx: '^4.20.6',
|
|
475
|
+
typescript: '^5.5.3',
|
|
457
476
|
},
|
|
458
477
|
};
|
|
459
478
|
if (!existing) {
|
|
460
479
|
yield writeJSON(pkgPath, base);
|
|
461
|
-
console.log(c('green', '✅ Created package.json (
|
|
480
|
+
console.log(c('green', '✅ Created package.json (pinned tsx/typescript/zod/reflect-metadata, exact frontmcp)'));
|
|
462
481
|
return;
|
|
463
482
|
}
|
|
464
|
-
// Merge, preserving user fields; ensure our requirements exist
|
|
465
483
|
const merged = Object.assign(Object.assign({}, base), existing);
|
|
484
|
+
merged.name = existing.name || base.name;
|
|
466
485
|
merged.main = existing.main || base.main;
|
|
467
486
|
merged.type = existing.type || base.type;
|
|
468
|
-
merged.scripts = Object.assign(Object.assign(Object.assign({}, base.scripts), (existing.scripts || {})), { dev: (
|
|
487
|
+
merged.scripts = Object.assign(Object.assign(Object.assign({}, base.scripts), (existing.scripts || {})), { dev: (_b = (_a = existing.scripts) === null || _a === void 0 ? void 0 : _a.dev) !== null && _b !== void 0 ? _b : base.scripts.dev, build: (_d = (_c = existing.scripts) === null || _c === void 0 ? void 0 : _c.build) !== null && _d !== void 0 ? _d : base.scripts.build, inspect: (_f = (_e = existing.scripts) === null || _e === void 0 ? void 0 : _e.inspect) !== null && _f !== void 0 ? _f : base.scripts.inspect, doctor: (_h = (_g = existing.scripts) === null || _g === void 0 ? void 0 : _g.doctor) !== null && _h !== void 0 ? _h : base.scripts.doctor });
|
|
469
488
|
merged.engines = Object.assign(Object.assign({}, (existing.engines || {})), { node: ((_j = existing.engines) === null || _j === void 0 ? void 0 : _j.node) || base.engines.node, npm: ((_k = existing.engines) === null || _k === void 0 ? void 0 : _k.npm) || base.engines.npm });
|
|
470
|
-
merged.dependencies = Object.assign(Object.assign({},
|
|
471
|
-
|
|
489
|
+
merged.dependencies = Object.assign(Object.assign(Object.assign({}, base.dependencies), (existing.dependencies || {})), {
|
|
490
|
+
// ensure pins
|
|
491
|
+
zod: '^3.23.8', 'reflect-metadata': '^0.2.2' });
|
|
492
|
+
merged.devDependencies = Object.assign(Object.assign(Object.assign({}, base.devDependencies), (existing.devDependencies || {})), {
|
|
493
|
+
// ensure pins
|
|
494
|
+
frontmcp: selfVersion, tsx: '^4.20.6', typescript: '^5.5.3' });
|
|
472
495
|
yield writeJSON(pkgPath, merged);
|
|
473
|
-
console.log(c('green', '✅ Updated package.json (ensured
|
|
496
|
+
console.log(c('green', '✅ Updated package.json (ensured exact frontmcp and pinned versions)'));
|
|
474
497
|
});
|
|
475
498
|
}
|
|
476
|
-
function scaffoldFileIfMissing(p, content) {
|
|
499
|
+
function scaffoldFileIfMissing(baseDir, p, content) {
|
|
477
500
|
return __awaiter(this, void 0, void 0, function* () {
|
|
478
501
|
if (yield fileExists(p)) {
|
|
479
|
-
console.log(c('gray', `skip: ${path.relative(
|
|
502
|
+
console.log(c('gray', `skip: ${path.relative(baseDir, p)} already exists`));
|
|
480
503
|
return;
|
|
481
504
|
}
|
|
482
505
|
yield ensureDir(path.dirname(p));
|
|
483
506
|
yield fs_1.promises.writeFile(p, content.replace(/^\n/, ''), 'utf8');
|
|
484
|
-
console.log(c('green', `✓ created ${path.relative(
|
|
507
|
+
console.log(c('green', `✓ created ${path.relative(baseDir, p)}`));
|
|
485
508
|
});
|
|
486
509
|
}
|
|
487
510
|
const TEMPLATE_MAIN_TS = `
|
|
@@ -526,24 +549,43 @@ const AddTool = tool({
|
|
|
526
549
|
|
|
527
550
|
export default AddTool;
|
|
528
551
|
`;
|
|
529
|
-
function runCreate() {
|
|
552
|
+
function runCreate(projectArg) {
|
|
530
553
|
return __awaiter(this, void 0, void 0, function* () {
|
|
531
|
-
|
|
532
|
-
|
|
554
|
+
if (!projectArg) {
|
|
555
|
+
console.error(c('red', 'Error: project name is required.\n'));
|
|
556
|
+
console.log(`Usage: ${c('bold', 'npx frontmcp create <project-name>')}`);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
const folder = sanitizeForFolder(projectArg);
|
|
560
|
+
const pkgName = sanitizeForNpm(projectArg);
|
|
561
|
+
const targetDir = path.resolve(process.cwd(), folder);
|
|
562
|
+
if (yield fileExists(targetDir)) {
|
|
563
|
+
if (!(yield isDirEmpty(targetDir))) {
|
|
564
|
+
console.error(c('red', `Refusing to scaffold into non-empty directory: ${path.relative(process.cwd(), targetDir)}`));
|
|
565
|
+
console.log(c('gray', 'Pick a different name or start with an empty folder.'));
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
yield ensureDir(targetDir);
|
|
571
|
+
}
|
|
572
|
+
console.log(`${c('cyan', '[create]')} Creating project in ${c('bold', './' + path.relative(process.cwd(), targetDir))}`);
|
|
573
|
+
process.chdir(targetDir);
|
|
533
574
|
// 1) tsconfig
|
|
534
|
-
yield runInit();
|
|
535
|
-
// 2) package.json
|
|
536
|
-
yield
|
|
575
|
+
yield runInit(targetDir);
|
|
576
|
+
// 2) package.json (with pinned deps and exact frontmcp version)
|
|
577
|
+
const selfVersion = yield getSelfVersion();
|
|
578
|
+
yield upsertPackageJson(targetDir, pkgName, selfVersion);
|
|
537
579
|
// 3) files
|
|
538
|
-
yield scaffoldFileIfMissing(path.join(
|
|
539
|
-
yield scaffoldFileIfMissing(path.join(
|
|
540
|
-
yield scaffoldFileIfMissing(path.join(
|
|
541
|
-
// 4) final tips
|
|
580
|
+
yield scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'main.ts'), TEMPLATE_MAIN_TS);
|
|
581
|
+
yield scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'calc.app.ts'), TEMPLATE_CALC_APP_TS);
|
|
582
|
+
yield scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'tools', 'add.tool.ts'), TEMPLATE_ADD_TOOL_TS);
|
|
542
583
|
console.log('\nNext steps:');
|
|
543
|
-
console.log(
|
|
544
|
-
console.log(' 2) npm
|
|
545
|
-
console.log(' 3) npm run
|
|
546
|
-
console.log(' 4) npm run
|
|
584
|
+
console.log(` 1) cd ${folder}`);
|
|
585
|
+
console.log(' 2) npm install');
|
|
586
|
+
console.log(' 3) npm run dev ', c('gray', '# starts tsx watcher via frontmcp dev'));
|
|
587
|
+
console.log(' 4) npm run inspect ', c('gray', '# launch MCP Inspector'));
|
|
588
|
+
console.log(' 5) npm run build ', c('gray', '# compile with tsc via frontmcp build'));
|
|
547
589
|
});
|
|
548
590
|
}
|
|
549
591
|
/* --------------------------------- Main ----------------------------------- */
|
|
@@ -574,9 +616,11 @@ function main() {
|
|
|
574
616
|
case 'inspector':
|
|
575
617
|
yield runInspector();
|
|
576
618
|
break;
|
|
577
|
-
case 'create':
|
|
578
|
-
|
|
619
|
+
case 'create': {
|
|
620
|
+
const projectName = parsed._[1]; // require a name
|
|
621
|
+
yield runCreate(projectName);
|
|
579
622
|
break;
|
|
623
|
+
}
|
|
580
624
|
case 'help':
|
|
581
625
|
showHelp();
|
|
582
626
|
break;
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -47,7 +47,7 @@ ${c('bold', 'Commands')}
|
|
|
47
47
|
init Create or fix a tsconfig.json suitable for FrontMCP
|
|
48
48
|
doctor Check Node/npm versions and tsconfig requirements
|
|
49
49
|
inspector Launch MCP Inspector (npx @modelcontextprotocol/inspector)
|
|
50
|
-
create
|
|
50
|
+
create <name> Scaffold a new FrontMCP project in ./<name>
|
|
51
51
|
help Show this help message
|
|
52
52
|
|
|
53
53
|
${c('bold', 'Options')}
|
|
@@ -60,7 +60,7 @@ ${c('bold', 'Examples')}
|
|
|
60
60
|
frontmcp init
|
|
61
61
|
frontmcp doctor
|
|
62
62
|
frontmcp inspector
|
|
63
|
-
npx frontmcp create
|
|
63
|
+
npx frontmcp create my-mcp
|
|
64
64
|
`);
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -109,30 +109,20 @@ async function resolveEntry(cwd: string, explicit?: string): Promise<string> {
|
|
|
109
109
|
if (await fileExists(full)) return full;
|
|
110
110
|
throw new Error(`Entry override not found: ${explicit}`);
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
// 1) package.json main
|
|
114
112
|
const pkgPath = path.join(cwd, 'package.json');
|
|
115
113
|
if (await fileExists(pkgPath)) {
|
|
116
114
|
const pkg = await readJSON<any>(pkgPath);
|
|
117
115
|
if (pkg && typeof pkg.main === 'string' && pkg.main.trim()) {
|
|
118
116
|
const mainCandidates = tryCandidates(path.resolve(cwd, pkg.main));
|
|
119
|
-
for (const p of mainCandidates)
|
|
120
|
-
if (await fileExists(p)) return p;
|
|
121
|
-
}
|
|
122
|
-
// If "main" is a directory-like path, try index.* within it
|
|
117
|
+
for (const p of mainCandidates) if (await fileExists(p)) return p;
|
|
123
118
|
const asDir = path.resolve(cwd, pkg.main);
|
|
124
119
|
const idxCandidates = tryCandidates(path.join(asDir, 'index'));
|
|
125
|
-
for (const p of idxCandidates)
|
|
126
|
-
if (await fileExists(p)) return p;
|
|
127
|
-
}
|
|
120
|
+
for (const p of idxCandidates) if (await fileExists(p)) return p;
|
|
128
121
|
}
|
|
129
122
|
}
|
|
130
|
-
|
|
131
|
-
// 2) src/main.ts
|
|
132
123
|
const fallback = path.join(cwd, 'src', 'main.ts');
|
|
133
124
|
if (await fileExists(fallback)) return fallback;
|
|
134
125
|
|
|
135
|
-
// 3) Not found
|
|
136
126
|
const msg = [
|
|
137
127
|
c('red', 'No entry file found.'),
|
|
138
128
|
'',
|
|
@@ -155,6 +145,41 @@ function runCmd(cmd: string, args: string[], opts: { cwd?: string } = {}): Promi
|
|
|
155
145
|
});
|
|
156
146
|
}
|
|
157
147
|
|
|
148
|
+
async function ensureDir(p: string): Promise<void> {
|
|
149
|
+
await fsp.mkdir(p, {recursive: true});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function isDirEmpty(dir: string): Promise<boolean> {
|
|
153
|
+
try {
|
|
154
|
+
const items = await fsp.readdir(dir);
|
|
155
|
+
return items.length === 0;
|
|
156
|
+
} catch (e: any) {
|
|
157
|
+
if (e?.code === 'ENOENT') return true;
|
|
158
|
+
throw e;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sanitizeForFolder(name: string): string {
|
|
163
|
+
const seg = name.startsWith('@') && name.includes('/') ? name.split('/')[1] : name;
|
|
164
|
+
return seg.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').toLowerCase() || 'frontmcp-app';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sanitizeForNpm(name: string): string {
|
|
168
|
+
if (name.startsWith('@') && name.includes('/')) {
|
|
169
|
+
const [scope, pkg] = name.split('/');
|
|
170
|
+
const s = scope.replace(/[^a-z0-9-]/gi, '').toLowerCase();
|
|
171
|
+
const p = pkg.replace(/[^a-z0-9._-]/gi, '-').toLowerCase();
|
|
172
|
+
return `@${s}/${p || 'frontmcp-app'}`;
|
|
173
|
+
}
|
|
174
|
+
return name.replace(/[^a-z0-9._-]/gi, '-').toLowerCase() || 'frontmcp-app';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ------------------------ Self-version detection -------------------------- */
|
|
178
|
+
|
|
179
|
+
async function getSelfVersion(): Promise<string> {
|
|
180
|
+
return '0.1.5';
|
|
181
|
+
}
|
|
182
|
+
|
|
158
183
|
/* --------------------------------- Actions -------------------------------- */
|
|
159
184
|
|
|
160
185
|
async function runDev(opts: ParsedArgs): Promise<void> {
|
|
@@ -165,10 +190,6 @@ async function runDev(opts: ParsedArgs): Promise<void> {
|
|
|
165
190
|
await runCmd('npx', ['-y', 'tsx', '--watch', entry]);
|
|
166
191
|
}
|
|
167
192
|
|
|
168
|
-
async function ensureDir(p: string): Promise<void> {
|
|
169
|
-
await fsp.mkdir(p, {recursive: true});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
193
|
function isTsLike(p: string): boolean {
|
|
173
194
|
return /\.tsx?$/i.test(p);
|
|
174
195
|
}
|
|
@@ -197,7 +218,6 @@ async function runBuild(opts: ParsedArgs): Promise<void> {
|
|
|
197
218
|
console.log(c('gray', `[build] tsconfig.json detected (project options will be respected where applicable)`));
|
|
198
219
|
}
|
|
199
220
|
|
|
200
|
-
// Compile the single entry file
|
|
201
221
|
await runCmd('npx', ['-y', 'tsc', entry, ...args]);
|
|
202
222
|
console.log(c('green', '✅ Build completed.'));
|
|
203
223
|
console.log(c('gray', `Output placed in ${path.relative(cwd, outDir)}`));
|
|
@@ -219,7 +239,7 @@ const RECOMMENDED_TSCONFIG = {
|
|
|
219
239
|
emitDecoratorMetadata: REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata,
|
|
220
240
|
experimentalDecorators: REQUIRED_DECORATOR_FIELDS.experimentalDecorators,
|
|
221
241
|
|
|
222
|
-
moduleResolution: '
|
|
242
|
+
moduleResolution: 'node',
|
|
223
243
|
strict: true,
|
|
224
244
|
esModuleInterop: true,
|
|
225
245
|
resolveJsonModule: true,
|
|
@@ -247,13 +267,10 @@ function deepMerge<T extends Record<string, any>, U extends Record<string, any>>
|
|
|
247
267
|
function ensureRequiredTsOptions(obj: Record<string, any>): Record<string, any> {
|
|
248
268
|
const next = {...obj};
|
|
249
269
|
next.compilerOptions = {...(next.compilerOptions || {})};
|
|
250
|
-
|
|
251
|
-
// Force the required values
|
|
252
270
|
next.compilerOptions.target = REQUIRED_DECORATOR_FIELDS.target;
|
|
253
271
|
next.compilerOptions.module = REQUIRED_DECORATOR_FIELDS.module;
|
|
254
272
|
next.compilerOptions.emitDecoratorMetadata = REQUIRED_DECORATOR_FIELDS.emitDecoratorMetadata;
|
|
255
273
|
next.compilerOptions.experimentalDecorators = REQUIRED_DECORATOR_FIELDS.experimentalDecorators;
|
|
256
|
-
|
|
257
274
|
return next;
|
|
258
275
|
}
|
|
259
276
|
|
|
@@ -285,19 +302,18 @@ function checkRequiredTsOptions(compilerOptions: Record<string, any> | undefined
|
|
|
285
302
|
return {ok, issues};
|
|
286
303
|
}
|
|
287
304
|
|
|
288
|
-
async function runInit(): Promise<void> {
|
|
289
|
-
const cwd = process.cwd();
|
|
305
|
+
async function runInit(baseDir?: string): Promise<void> {
|
|
306
|
+
const cwd = baseDir ?? process.cwd();
|
|
290
307
|
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
291
308
|
const existing = await readJSON<Record<string, any>>(tsconfigPath);
|
|
292
309
|
|
|
293
310
|
if (!existing) {
|
|
294
|
-
console.log(c('yellow',
|
|
311
|
+
console.log(c('yellow', `tsconfig.json not found — creating one in ${path.relative(process.cwd(), cwd) || '.'}.`));
|
|
295
312
|
await writeJSON(tsconfigPath, RECOMMENDED_TSCONFIG);
|
|
296
313
|
console.log(c('green', '✅ Created tsconfig.json with required decorator settings.'));
|
|
297
314
|
return;
|
|
298
315
|
}
|
|
299
316
|
|
|
300
|
-
// Merge user config on top of recommended, then force required decorator options
|
|
301
317
|
let merged = deepMerge(RECOMMENDED_TSCONFIG as any, existing);
|
|
302
318
|
merged = ensureRequiredTsOptions(merged);
|
|
303
319
|
|
|
@@ -322,7 +338,6 @@ async function runDoctor(): Promise<void> {
|
|
|
322
338
|
|
|
323
339
|
let ok = true;
|
|
324
340
|
|
|
325
|
-
// Node
|
|
326
341
|
const nodeVer = process.versions.node;
|
|
327
342
|
if (cmpSemver(nodeVer, MIN_NODE) >= 0) {
|
|
328
343
|
console.log(`✅ Node ${nodeVer} (min ${MIN_NODE})`);
|
|
@@ -331,7 +346,6 @@ async function runDoctor(): Promise<void> {
|
|
|
331
346
|
console.log(`❌ Node ${nodeVer} — please upgrade to >= ${MIN_NODE}`);
|
|
332
347
|
}
|
|
333
348
|
|
|
334
|
-
// npm
|
|
335
349
|
let npmVer = 'unknown';
|
|
336
350
|
try {
|
|
337
351
|
npmVer = await new Promise<string>((resolve, reject) => {
|
|
@@ -352,13 +366,11 @@ async function runDoctor(): Promise<void> {
|
|
|
352
366
|
console.log('❌ npm not found in PATH');
|
|
353
367
|
}
|
|
354
368
|
|
|
355
|
-
// tsconfig.json presence + required fields
|
|
356
369
|
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
357
370
|
if (await fileExists(tsconfigPath)) {
|
|
358
371
|
console.log(`✅ tsconfig.json found`);
|
|
359
372
|
const tsconfig = await readJSON<Record<string, any>>(tsconfigPath);
|
|
360
373
|
const {ok: oks, issues} = checkRequiredTsOptions(tsconfig?.compilerOptions);
|
|
361
|
-
|
|
362
374
|
for (const line of oks) console.log(c('green', ` ✓ ${line}`));
|
|
363
375
|
if (issues.length) {
|
|
364
376
|
ok = false;
|
|
@@ -370,7 +382,6 @@ async function runDoctor(): Promise<void> {
|
|
|
370
382
|
console.log(`❌ tsconfig.json not found — run ${c('cyan', 'frontmcp init')}`);
|
|
371
383
|
}
|
|
372
384
|
|
|
373
|
-
// Entry check (nice to have)
|
|
374
385
|
try {
|
|
375
386
|
const entry = await resolveEntry(cwd);
|
|
376
387
|
console.log(`✅ entry detected: ${path.relative(cwd, entry)}`);
|
|
@@ -379,11 +390,8 @@ async function runDoctor(): Promise<void> {
|
|
|
379
390
|
console.log(`❌ entry not detected — ${firstLine}`);
|
|
380
391
|
}
|
|
381
392
|
|
|
382
|
-
if (ok)
|
|
383
|
-
|
|
384
|
-
} else {
|
|
385
|
-
console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
|
|
386
|
-
}
|
|
393
|
+
if (ok) console.log(c('green', '\nAll checks passed. You are ready to go!'));
|
|
394
|
+
else console.log(c('yellow', '\nSome checks failed. See above for fixes.'));
|
|
387
395
|
}
|
|
388
396
|
|
|
389
397
|
/* ------------------------------- Inspector -------------------------------- */
|
|
@@ -399,12 +407,12 @@ function pkgNameFromCwd(cwd: string) {
|
|
|
399
407
|
return path.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, '-').toLowerCase() || 'frontmcp-app';
|
|
400
408
|
}
|
|
401
409
|
|
|
402
|
-
async function upsertPackageJson(cwd: string) {
|
|
410
|
+
async function upsertPackageJson(cwd: string, nameOverride: string | undefined, selfVersion: string) {
|
|
403
411
|
const pkgPath = path.join(cwd, 'package.json');
|
|
404
412
|
const existing = await readJSON<Record<string, any>>(pkgPath);
|
|
405
413
|
|
|
406
414
|
const base = {
|
|
407
|
-
name: pkgNameFromCwd(cwd),
|
|
415
|
+
name: nameOverride ?? pkgNameFromCwd(cwd),
|
|
408
416
|
version: '0.1.0',
|
|
409
417
|
private: true,
|
|
410
418
|
type: 'module',
|
|
@@ -421,33 +429,35 @@ async function upsertPackageJson(cwd: string) {
|
|
|
421
429
|
},
|
|
422
430
|
dependencies: {
|
|
423
431
|
'@frontmcp/sdk': 'latest',
|
|
424
|
-
zod: '
|
|
432
|
+
zod: '^3.23.8',
|
|
433
|
+
'reflect-metadata': '^0.2.2',
|
|
425
434
|
},
|
|
426
435
|
devDependencies: {
|
|
427
|
-
|
|
428
|
-
tsx: '
|
|
436
|
+
frontmcp: selfVersion, // exact version used by npx
|
|
437
|
+
tsx: '^4.20.6',
|
|
438
|
+
typescript: '^5.5.3',
|
|
429
439
|
},
|
|
430
440
|
};
|
|
431
441
|
|
|
432
442
|
if (!existing) {
|
|
433
443
|
await writeJSON(pkgPath, base);
|
|
434
|
-
console.log(c('green', '✅ Created package.json (
|
|
444
|
+
console.log(c('green', '✅ Created package.json (pinned tsx/typescript/zod/reflect-metadata, exact frontmcp)'));
|
|
435
445
|
return;
|
|
436
446
|
}
|
|
437
447
|
|
|
438
|
-
// Merge, preserving user fields; ensure our requirements exist
|
|
439
448
|
const merged = {...base, ...existing};
|
|
440
449
|
|
|
450
|
+
merged.name = existing.name || base.name;
|
|
441
451
|
merged.main = existing.main || base.main;
|
|
442
452
|
merged.type = existing.type || base.type;
|
|
443
453
|
|
|
444
454
|
merged.scripts = {
|
|
445
455
|
...base.scripts,
|
|
446
456
|
...(existing.scripts || {}),
|
|
447
|
-
dev:
|
|
448
|
-
build:
|
|
449
|
-
inspect:
|
|
450
|
-
doctor:
|
|
457
|
+
dev: existing.scripts?.dev ?? base.scripts.dev,
|
|
458
|
+
build: existing.scripts?.build ?? base.scripts.build,
|
|
459
|
+
inspect: existing.scripts?.inspect ?? base.scripts.inspect,
|
|
460
|
+
doctor: existing.scripts?.doctor ?? base.scripts.doctor,
|
|
451
461
|
};
|
|
452
462
|
|
|
453
463
|
merged.engines = {
|
|
@@ -457,29 +467,34 @@ async function upsertPackageJson(cwd: string) {
|
|
|
457
467
|
};
|
|
458
468
|
|
|
459
469
|
merged.dependencies = {
|
|
460
|
-
...
|
|
461
|
-
|
|
462
|
-
|
|
470
|
+
...base.dependencies,
|
|
471
|
+
...(existing.dependencies || {}),
|
|
472
|
+
// ensure pins
|
|
473
|
+
zod: '^3.23.8',
|
|
474
|
+
'reflect-metadata': '^0.2.2',
|
|
463
475
|
};
|
|
464
476
|
|
|
465
477
|
merged.devDependencies = {
|
|
466
|
-
...
|
|
467
|
-
|
|
468
|
-
|
|
478
|
+
...base.devDependencies,
|
|
479
|
+
...(existing.devDependencies || {}),
|
|
480
|
+
// ensure pins
|
|
481
|
+
frontmcp: selfVersion,
|
|
482
|
+
tsx: '^4.20.6',
|
|
483
|
+
typescript: '^5.5.3',
|
|
469
484
|
};
|
|
470
485
|
|
|
471
486
|
await writeJSON(pkgPath, merged);
|
|
472
|
-
console.log(c('green', '✅ Updated package.json (ensured
|
|
487
|
+
console.log(c('green', '✅ Updated package.json (ensured exact frontmcp and pinned versions)'));
|
|
473
488
|
}
|
|
474
489
|
|
|
475
|
-
async function scaffoldFileIfMissing(p: string, content: string) {
|
|
490
|
+
async function scaffoldFileIfMissing(baseDir: string, p: string, content: string) {
|
|
476
491
|
if (await fileExists(p)) {
|
|
477
|
-
console.log(c('gray', `skip: ${path.relative(
|
|
492
|
+
console.log(c('gray', `skip: ${path.relative(baseDir, p)} already exists`));
|
|
478
493
|
return;
|
|
479
494
|
}
|
|
480
495
|
await ensureDir(path.dirname(p));
|
|
481
496
|
await fsp.writeFile(p, content.replace(/^\n/, ''), 'utf8');
|
|
482
|
-
console.log(c('green', `✓ created ${path.relative(
|
|
497
|
+
console.log(c('green', `✓ created ${path.relative(baseDir, p)}`));
|
|
483
498
|
}
|
|
484
499
|
|
|
485
500
|
const TEMPLATE_MAIN_TS = `
|
|
@@ -527,28 +542,48 @@ const AddTool = tool({
|
|
|
527
542
|
export default AddTool;
|
|
528
543
|
`;
|
|
529
544
|
|
|
530
|
-
async function runCreate(): Promise<void> {
|
|
531
|
-
|
|
545
|
+
async function runCreate(projectArg?: string): Promise<void> {
|
|
546
|
+
if (!projectArg) {
|
|
547
|
+
console.error(c('red', 'Error: project name is required.\n'));
|
|
548
|
+
console.log(`Usage: ${c('bold', 'npx frontmcp create <project-name>')}`);
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const folder = sanitizeForFolder(projectArg);
|
|
553
|
+
const pkgName = sanitizeForNpm(projectArg);
|
|
554
|
+
const targetDir = path.resolve(process.cwd(), folder);
|
|
532
555
|
|
|
533
|
-
|
|
556
|
+
if (await fileExists(targetDir)) {
|
|
557
|
+
if (!(await isDirEmpty(targetDir))) {
|
|
558
|
+
console.error(c('red', `Refusing to scaffold into non-empty directory: ${path.relative(process.cwd(), targetDir)}`));
|
|
559
|
+
console.log(c('gray', 'Pick a different name or start with an empty folder.'));
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
} else {
|
|
563
|
+
await ensureDir(targetDir);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
console.log(`${c('cyan', '[create]')} Creating project in ${c('bold', './' + path.relative(process.cwd(), targetDir))}`);
|
|
567
|
+
process.chdir(targetDir);
|
|
534
568
|
|
|
535
569
|
// 1) tsconfig
|
|
536
|
-
await runInit();
|
|
570
|
+
await runInit(targetDir);
|
|
537
571
|
|
|
538
|
-
// 2) package.json
|
|
539
|
-
await
|
|
572
|
+
// 2) package.json (with pinned deps and exact frontmcp version)
|
|
573
|
+
const selfVersion = await getSelfVersion();
|
|
574
|
+
await upsertPackageJson(targetDir, pkgName, selfVersion);
|
|
540
575
|
|
|
541
576
|
// 3) files
|
|
542
|
-
await scaffoldFileIfMissing(path.join(
|
|
543
|
-
await scaffoldFileIfMissing(path.join(
|
|
544
|
-
await scaffoldFileIfMissing(path.join(
|
|
577
|
+
await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'main.ts'), TEMPLATE_MAIN_TS);
|
|
578
|
+
await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'calc.app.ts'), TEMPLATE_CALC_APP_TS);
|
|
579
|
+
await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'src', 'tools', 'add.tool.ts'), TEMPLATE_ADD_TOOL_TS);
|
|
545
580
|
|
|
546
|
-
// 4) final tips
|
|
547
581
|
console.log('\nNext steps:');
|
|
548
|
-
console.log(
|
|
549
|
-
console.log(' 2) npm
|
|
550
|
-
console.log(' 3) npm run
|
|
551
|
-
console.log(' 4) npm run
|
|
582
|
+
console.log(` 1) cd ${folder}`);
|
|
583
|
+
console.log(' 2) npm install');
|
|
584
|
+
console.log(' 3) npm run dev ', c('gray', '# starts tsx watcher via frontmcp dev'));
|
|
585
|
+
console.log(' 4) npm run inspect ', c('gray', '# launch MCP Inspector'));
|
|
586
|
+
console.log(' 5) npm run build ', c('gray', '# compile with tsc via frontmcp build'));
|
|
552
587
|
}
|
|
553
588
|
|
|
554
589
|
/* --------------------------------- Main ----------------------------------- */
|
|
@@ -581,9 +616,11 @@ async function main(): Promise<void> {
|
|
|
581
616
|
case 'inspector':
|
|
582
617
|
await runInspector();
|
|
583
618
|
break;
|
|
584
|
-
case 'create':
|
|
585
|
-
|
|
619
|
+
case 'create': {
|
|
620
|
+
const projectName = parsed._[1]; // require a name
|
|
621
|
+
await runCreate(projectName);
|
|
586
622
|
break;
|
|
623
|
+
}
|
|
587
624
|
case 'help':
|
|
588
625
|
showHelp();
|
|
589
626
|
break;
|