MCP Server 2.0 (#452)

* MCP Server 2.0

- Updated application version from 2.23.14 to 2.24.0 in package.json.
- MCP Server improved with new features
- Migrated functionality from Dev Tools to MCP Server
- Improved getMonitoringProvider not to crash application when misconfigured
This commit is contained in:
Giancarlo Buomprisco
2026-02-11 20:42:01 +01:00
committed by GitHub
parent 059408a70a
commit f3ac595d06
123 changed files with 17803 additions and 5265 deletions

View File

@@ -0,0 +1,431 @@
import { describe, expect, it } from 'vitest';
import {
type KitStatusDeps,
createKitStatusService,
} from '../kit-status.service';
function createDeps(overrides: Partial<KitStatusDeps> = {}): KitStatusDeps {
const readJsonMap: Record<string, unknown> = {
'package.json': {
name: 'test-project',
packageManager: 'pnpm@10.0.0',
},
'apps/web/package.json': {
dependencies: {
next: '16.1.6',
},
},
};
return {
rootPath: '/repo',
async readJsonFile(path: string) {
if (!(path in readJsonMap)) {
throw new Error(`missing file: ${path}`);
}
return readJsonMap[path];
},
async pathExists(path: string) {
return path === 'apps/web/supabase';
},
async isDirectory(path: string) {
return path === 'node_modules';
},
async executeCommand(command: string, args: string[]) {
if (command !== 'git') {
throw new Error('unsupported command');
}
if (args[0] === 'rev-parse') {
return {
stdout: 'main\n',
stderr: '',
exitCode: 0,
};
}
if (args[0] === 'status') {
return {
stdout: '',
stderr: '',
exitCode: 0,
};
}
if (args[0] === 'symbolic-ref') {
return {
stdout: 'origin/main\n',
stderr: '',
exitCode: 0,
};
}
if (args[0] === 'merge-base') {
return {
stdout: 'abc123\n',
stderr: '',
exitCode: 0,
};
}
if (args[0] === 'merge-tree') {
return {
stdout: '',
stderr: '',
exitCode: 0,
};
}
throw new Error('unsupported git args');
},
async isPortOpen(port: number) {
return port === 3000 || port === 54321 || port === 54323;
},
getNodeVersion() {
return 'v22.5.0';
},
...overrides,
};
}
describe('KitStatusService', () => {
it('returns a complete status in the happy path', async () => {
const service = createKitStatusService(createDeps());
const result = await service.getStatus({});
expect(result.project_name).toBe('test-project');
expect(result.package_manager).toBe('pnpm');
expect(result.node_version).toBe('22.5.0');
expect(result.git_branch).toBe('main');
expect(result.git_clean).toBe(true);
expect(result.deps_installed).toBe(true);
expect(result.variant).toBe('next-supabase');
expect(result.services.app.running).toBe(true);
expect(result.services.app.port).toBe(3000);
expect(result.services.supabase.running).toBe(true);
expect(result.services.supabase.api_port).toBe(54321);
expect(result.services.supabase.studio_port).toBe(54323);
expect(result.git_modified_files).toHaveLength(0);
expect(result.git_untracked_files).toHaveLength(0);
expect(result.git_merge_check.target_branch).toBe('main');
expect(result.git_merge_check.has_conflicts).toBe(false);
expect(result.diagnostics).toHaveLength(5);
});
it('falls back when git commands fail', async () => {
const service = createKitStatusService(
createDeps({
async executeCommand() {
throw new Error('git not found');
},
}),
);
const result = await service.getStatus({});
expect(result.git_branch).toBe('unknown');
expect(result.git_clean).toBe(false);
expect(result.git_merge_check.detectable).toBe(false);
expect(result.diagnostics.find((item) => item.id === 'git')?.status).toBe(
'warn',
);
});
it('collects modified files from git status output', async () => {
const service = createKitStatusService(
createDeps({
async executeCommand(command: string, args: string[]) {
if (command !== 'git') {
throw new Error('unsupported command');
}
if (args[0] === 'rev-parse') {
return {
stdout: 'feature/status\n',
stderr: '',
exitCode: 0,
};
}
if (args[0] === 'status') {
return {
stdout: ' M apps/web/page.tsx\n?? new-file.ts\n',
stderr: '',
exitCode: 0,
};
}
if (args[0] === 'symbolic-ref') {
return {
stdout: 'origin/main\n',
stderr: '',
exitCode: 0,
};
}
if (args[0] === 'merge-base') {
return {
stdout: 'abc123\n',
stderr: '',
exitCode: 0,
};
}
if (args[0] === 'merge-tree') {
return {
stdout: '',
stderr: '',
exitCode: 0,
};
}
throw new Error('unsupported git args');
},
}),
);
const result = await service.getStatus({});
expect(result.git_clean).toBe(false);
expect(result.git_modified_files).toEqual(['apps/web/page.tsx']);
expect(result.git_untracked_files).toEqual(['new-file.ts']);
});
it('detects merge conflicts against default branch', async () => {
const service = createKitStatusService(
createDeps({
async executeCommand(command: string, args: string[]) {
if (command !== 'git') {
throw new Error('unsupported command');
}
if (args[0] === 'rev-parse') {
return {
stdout: 'feature/conflicts\n',
stderr: '',
exitCode: 0,
};
}
if (args[0] === 'status') {
return {
stdout: '',
stderr: '',
exitCode: 0,
};
}
if (args[0] === 'symbolic-ref') {
return {
stdout: 'origin/main\n',
stderr: '',
exitCode: 0,
};
}
if (args[0] === 'merge-base') {
return {
stdout: 'abc123\n',
stderr: '',
exitCode: 0,
};
}
if (args[0] === 'merge-tree') {
return {
stdout:
'CONFLICT (content): Merge conflict in apps/dev-tool/app/page.tsx\n',
stderr: '',
exitCode: 0,
};
}
throw new Error('unsupported git args');
},
}),
);
const result = await service.getStatus({});
expect(result.git_merge_check.target_branch).toBe('main');
expect(result.git_merge_check.detectable).toBe(true);
expect(result.git_merge_check.has_conflicts).toBe(true);
expect(result.git_merge_check.conflict_files).toEqual([
'apps/dev-tool/app/page.tsx',
]);
expect(
result.diagnostics.find((item) => item.id === 'merge_conflicts')?.status,
).toBe('warn');
});
it('uses unknown package manager when packageManager is missing', async () => {
const service = createKitStatusService(
createDeps({
async readJsonFile(path: string) {
if (path === 'package.json') {
return { name: 'test-project' };
}
if (path === 'apps/web/package.json') {
return {
dependencies: {
next: '16.1.6',
},
};
}
throw new Error(`missing file: ${path}`);
},
}),
);
const result = await service.getStatus({});
expect(result.package_manager).toBe('unknown');
});
it('provides remedies when services are not running', async () => {
const service = createKitStatusService(
createDeps({
async isPortOpen() {
return false;
},
}),
);
const result = await service.getStatus({});
expect(result.services.app.running).toBe(false);
expect(result.services.supabase.running).toBe(false);
const devServerDiagnostic = result.diagnostics.find(
(item) => item.id === 'dev_server',
);
const supabaseDiagnostic = result.diagnostics.find(
(item) => item.id === 'supabase',
);
expect(devServerDiagnostic?.status).toBe('fail');
expect(devServerDiagnostic?.remedies).toEqual(['Run pnpm dev']);
expect(supabaseDiagnostic?.status).toBe('fail');
expect(supabaseDiagnostic?.remedies).toEqual([
'Run pnpm supabase:web:start',
]);
});
it('maps variant from .makerkit/config.json when present', async () => {
const service = createKitStatusService(
createDeps({
async pathExists(path: string) {
return path === '.makerkit/config.json';
},
async readJsonFile(path: string) {
if (path === '.makerkit/config.json') {
return {
variant: 'next-prisma',
};
}
if (path === 'package.json') {
return {
name: 'test-project',
packageManager: 'pnpm@10.0.0',
};
}
throw new Error(`missing file: ${path}`);
},
}),
);
const result = await service.getStatus({});
expect(result.variant).toBe('next-prisma');
expect(result.variant_family).toBe('orm');
expect(result.database).toBe('postgresql');
expect(result.auth).toBe('better-auth');
});
it('reads variant from the template key when present', async () => {
const service = createKitStatusService(
createDeps({
async pathExists(path: string) {
return path === '.makerkit/config.json';
},
async readJsonFile(path: string) {
if (path === '.makerkit/config.json') {
return {
template: 'react-router-supabase',
};
}
if (path === 'package.json') {
return {
name: 'test-project',
packageManager: 'pnpm@10.0.0',
};
}
throw new Error(`missing file: ${path}`);
},
}),
);
const result = await service.getStatus({});
expect(result.variant).toBe('react-router-supabase');
expect(result.framework).toBe('react-router');
});
it('reads variant from kitVariant key and preserves unknown names', async () => {
const service = createKitStatusService(
createDeps({
async pathExists(path: string) {
return path === '.makerkit/config.json';
},
async readJsonFile(path: string) {
if (path === '.makerkit/config.json') {
return {
kitVariant: 'custom-enterprise-kit',
};
}
if (path === 'package.json') {
return {
name: 'test-project',
packageManager: 'pnpm@10.0.0',
};
}
throw new Error(`missing file: ${path}`);
},
}),
);
const result = await service.getStatus({});
expect(result.variant).toBe('custom-enterprise-kit');
expect(result.variant_family).toBe('supabase');
expect(result.framework).toBe('nextjs');
});
it('uses heuristic variant fallback when config is absent', async () => {
const service = createKitStatusService(
createDeps({
async pathExists(path: string) {
return path === 'apps/web/supabase';
},
}),
);
const result = await service.getStatus({});
expect(result.variant).toBe('next-supabase');
expect(result.framework).toBe('nextjs');
expect(result.database).toBe('supabase');
expect(result.auth).toBe('supabase');
});
});

View File

@@ -0,0 +1,144 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { execFile } from 'node:child_process';
import { access, readFile, stat } from 'node:fs/promises';
import { Socket } from 'node:net';
import { join } from 'node:path';
import { promisify } from 'node:util';
import {
type KitStatusDeps,
createKitStatusService,
} from './kit-status.service';
import { KitStatusInputSchema, KitStatusOutputSchema } from './schema';
const execFileAsync = promisify(execFile);
export function registerKitStatusTool(server: McpServer) {
return server.registerTool(
'kit_status',
{
description: 'Project status with variant context',
inputSchema: KitStatusInputSchema,
outputSchema: KitStatusOutputSchema,
},
async (input) => {
const parsedInput = KitStatusInputSchema.parse(input);
try {
const service = createKitStatusService(createKitStatusDeps());
const status = await service.getStatus(parsedInput);
return {
structuredContent: status,
content: [
{
type: 'text',
text: JSON.stringify(status),
},
],
};
} catch (error) {
const message = toErrorMessage(error);
return {
isError: true,
content: [
{
type: 'text',
text: `kit_status failed: ${message}`,
},
],
};
}
},
);
}
function createKitStatusDeps(): KitStatusDeps {
const rootPath = process.cwd();
return {
rootPath,
async readJsonFile(path: string): Promise<unknown> {
const filePath = join(rootPath, path);
const content = await readFile(filePath, 'utf8');
return JSON.parse(content) as unknown;
},
async pathExists(path: string) {
const fullPath = join(rootPath, path);
try {
await access(fullPath);
return true;
} catch {
return false;
}
},
async isDirectory(path: string) {
const fullPath = join(rootPath, path);
try {
const stats = await stat(fullPath);
return stats.isDirectory();
} catch {
return false;
}
},
async executeCommand(command: string, args: string[]) {
const result = await execFileAsync(command, args, {
cwd: rootPath,
});
return {
stdout: result.stdout,
stderr: result.stderr,
exitCode: 0,
};
},
async isPortOpen(port: number) {
return checkPort(port);
},
getNodeVersion() {
return process.version;
},
};
}
async function checkPort(port: number) {
return new Promise<boolean>((resolve) => {
const socket = new Socket();
socket.setTimeout(200);
socket.once('connect', () => {
socket.destroy();
resolve(true);
});
socket.once('timeout', () => {
socket.destroy();
resolve(false);
});
socket.once('error', () => {
socket.destroy();
resolve(false);
});
socket.connect(port, '127.0.0.1');
});
}
function toErrorMessage(error: unknown) {
if (error instanceof Error) {
return error.message;
}
return 'Unknown error';
}
export {
createKitStatusService,
type KitStatusDeps,
} from './kit-status.service';
export type { KitStatusOutput } from './schema';

View File

@@ -0,0 +1,549 @@
import { join } from 'node:path';
import type { KitStatusInput, KitStatusOutput } from './schema';
interface VariantDescriptor {
variant: string;
variant_family: string;
framework: string;
database: string;
auth: string;
}
interface CommandResult {
stdout: string;
stderr: string;
exitCode: number;
}
interface ServicesStatus {
app: {
running: boolean;
port: number | null;
};
supabase: {
running: boolean;
api_port: number | null;
studio_port: number | null;
};
}
interface MergeCheckStatus {
target_branch: string | null;
detectable: boolean;
has_conflicts: boolean | null;
conflict_files: string[];
message: string;
}
export interface KitStatusDeps {
rootPath: string;
readJsonFile(path: string): Promise<unknown>;
pathExists(path: string): Promise<boolean>;
isDirectory(path: string): Promise<boolean>;
executeCommand(command: string, args: string[]): Promise<CommandResult>;
isPortOpen(port: number): Promise<boolean>;
getNodeVersion(): string;
}
export function createKitStatusService(deps: KitStatusDeps) {
return new KitStatusService(deps);
}
export class KitStatusService {
constructor(private readonly deps: KitStatusDeps) {}
async getStatus(_input: KitStatusInput): Promise<KitStatusOutput> {
const packageJson = await this.readObject('package.json');
const projectName = this.readString(packageJson, 'name') ?? 'unknown';
const packageManager = this.getPackageManager(packageJson);
const depsInstalled = await this.deps.isDirectory('node_modules');
const { gitBranch, gitClean, modifiedFiles, untrackedFiles, mergeCheck } =
await this.getGitStatus();
const variant = await this.resolveVariant();
const services = await this.getServicesStatus();
const diagnostics = this.buildDiagnostics({
depsInstalled,
gitBranch,
gitClean,
mergeCheck,
services,
});
return {
...variant,
project_name: projectName,
node_version: this.deps.getNodeVersion().replace(/^v/, ''),
package_manager: packageManager,
deps_installed: depsInstalled,
git_clean: gitClean,
git_branch: gitBranch,
git_modified_files: modifiedFiles,
git_untracked_files: untrackedFiles,
git_merge_check: mergeCheck,
services,
diagnostics,
};
}
private async getServicesStatus(): Promise<ServicesStatus> {
const app = await this.detectAppService();
const supabase = await this.detectSupabaseService();
return {
app,
supabase,
};
}
private async detectAppService() {
const commonDevPorts = [3000, 3001, 3002, 3003];
for (const port of commonDevPorts) {
if (await this.deps.isPortOpen(port)) {
return {
running: true,
port,
};
}
}
return {
running: false,
port: null,
};
}
private async detectSupabaseService() {
const apiPort = 54321;
const studioPort = 54323;
const [apiRunning, studioRunning] = await Promise.all([
this.deps.isPortOpen(apiPort),
this.deps.isPortOpen(studioPort),
]);
return {
running: apiRunning || studioRunning,
api_port: apiRunning ? apiPort : null,
studio_port: studioRunning ? studioPort : null,
};
}
private buildDiagnostics(params: {
depsInstalled: boolean;
gitBranch: string;
gitClean: boolean;
mergeCheck: MergeCheckStatus;
services: ServicesStatus;
}) {
const diagnostics: KitStatusOutput['diagnostics'] = [];
diagnostics.push({
id: 'dependencies',
status: params.depsInstalled ? 'pass' : 'fail',
message: params.depsInstalled
? 'Dependencies are installed.'
: 'Dependencies are missing.',
remedies: params.depsInstalled ? [] : ['Run pnpm install'],
});
diagnostics.push({
id: 'dev_server',
status: params.services.app.running ? 'pass' : 'fail',
message: params.services.app.running
? `Dev server is running on port ${params.services.app.port}.`
: 'Dev server is not running.',
remedies: params.services.app.running ? [] : ['Run pnpm dev'],
});
diagnostics.push({
id: 'supabase',
status: params.services.supabase.running ? 'pass' : 'fail',
message: params.services.supabase.running
? `Supabase is running${params.services.supabase.api_port ? ` (API ${params.services.supabase.api_port})` : ''}${params.services.supabase.studio_port ? ` (Studio ${params.services.supabase.studio_port})` : ''}.`
: 'Supabase is not running.',
remedies: params.services.supabase.running
? []
: ['Run pnpm supabase:web:start'],
});
diagnostics.push({
id: 'git',
status:
params.gitBranch === 'unknown'
? 'warn'
: params.gitClean
? 'pass'
: 'warn',
message:
params.gitBranch === 'unknown'
? 'Git status unavailable.'
: `Current branch ${params.gitBranch} is ${params.gitClean ? 'clean' : 'dirty'}.`,
remedies:
params.gitBranch === 'unknown' || params.gitClean
? []
: ['Commit or stash changes when you need a clean workspace'],
});
diagnostics.push({
id: 'merge_conflicts',
status:
params.mergeCheck.has_conflicts === true
? 'warn'
: params.mergeCheck.detectable
? 'pass'
: 'warn',
message: params.mergeCheck.message,
remedies:
params.mergeCheck.has_conflicts === true
? [
`Rebase or merge ${params.mergeCheck.target_branch} and resolve conflicts`,
]
: [],
});
return diagnostics;
}
private async getGitStatus() {
try {
const branchResult = await this.deps.executeCommand('git', [
'rev-parse',
'--abbrev-ref',
'HEAD',
]);
const statusResult = await this.deps.executeCommand('git', [
'status',
'--porcelain',
]);
const parsedStatus = this.parseGitStatus(statusResult.stdout);
const mergeCheck = await this.getMergeCheck();
return {
gitBranch: branchResult.stdout.trim() || 'unknown',
gitClean:
parsedStatus.modifiedFiles.length === 0 &&
parsedStatus.untrackedFiles.length === 0,
modifiedFiles: parsedStatus.modifiedFiles,
untrackedFiles: parsedStatus.untrackedFiles,
mergeCheck,
};
} catch {
return {
gitBranch: 'unknown',
gitClean: false,
modifiedFiles: [],
untrackedFiles: [],
mergeCheck: {
target_branch: null,
detectable: false,
has_conflicts: null,
conflict_files: [],
message: 'Git metadata unavailable.',
} satisfies MergeCheckStatus,
};
}
}
private parseGitStatus(output: string) {
const modifiedFiles: string[] = [];
const untrackedFiles: string[] = [];
const lines = output.split('\n').filter((line) => line.trim().length > 0);
for (const line of lines) {
if (line.startsWith('?? ')) {
const path = line.slice(3).trim();
if (path) {
untrackedFiles.push(path);
}
continue;
}
if (line.length >= 4) {
const path = line.slice(3).trim();
if (path) {
modifiedFiles.push(path);
}
}
}
return {
modifiedFiles,
untrackedFiles,
};
}
private async getMergeCheck(): Promise<MergeCheckStatus> {
const targetBranch = await this.resolveMergeTargetBranch();
if (!targetBranch) {
return {
target_branch: null,
detectable: false,
has_conflicts: null,
conflict_files: [],
message: 'No default target branch found for merge conflict checks.',
};
}
try {
const mergeBaseResult = await this.deps.executeCommand('git', [
'merge-base',
'HEAD',
targetBranch,
]);
const mergeBase = mergeBaseResult.stdout.trim();
if (!mergeBase) {
return {
target_branch: targetBranch,
detectable: false,
has_conflicts: null,
conflict_files: [],
message: 'Unable to compute merge base.',
};
}
const mergeTreeResult = await this.deps.executeCommand('git', [
'merge-tree',
mergeBase,
'HEAD',
targetBranch,
]);
const rawOutput = `${mergeTreeResult.stdout}\n${mergeTreeResult.stderr}`;
const conflictFiles = this.extractConflictFiles(rawOutput);
const hasConflictMarkers =
/CONFLICT|changed in both|both modified|both added/i.test(rawOutput);
const hasConflicts = conflictFiles.length > 0 || hasConflictMarkers;
return {
target_branch: targetBranch,
detectable: true,
has_conflicts: hasConflicts,
conflict_files: conflictFiles,
message: hasConflicts
? `Potential merge conflicts detected against ${targetBranch}.`
: `No merge conflicts detected against ${targetBranch}.`,
};
} catch {
return {
target_branch: targetBranch,
detectable: false,
has_conflicts: null,
conflict_files: [],
message: 'Merge conflict detection is not available in this git setup.',
};
}
}
private extractConflictFiles(rawOutput: string) {
const files = new Set<string>();
const lines = rawOutput.split('\n');
for (const line of lines) {
const conflictMatch = line.match(/CONFLICT .* in (.+)$/);
if (conflictMatch?.[1]) {
files.add(conflictMatch[1].trim());
}
}
return Array.from(files).sort((a, b) => a.localeCompare(b));
}
private async resolveMergeTargetBranch() {
try {
const originHead = await this.deps.executeCommand('git', [
'symbolic-ref',
'--quiet',
'--short',
'refs/remotes/origin/HEAD',
]);
const value = originHead.stdout.trim();
if (value) {
return value.replace(/^origin\//, '');
}
} catch {
// Fallback candidates below.
}
for (const candidate of ['main', 'master']) {
try {
await this.deps.executeCommand('git', [
'rev-parse',
'--verify',
candidate,
]);
return candidate;
} catch {
// Try next.
}
}
return null;
}
private async resolveVariant(): Promise<VariantDescriptor> {
const explicitVariant = await this.resolveConfiguredVariant();
if (explicitVariant) {
return explicitVariant;
}
const heuristicVariant = await this.resolveHeuristicVariant();
if (heuristicVariant) {
return heuristicVariant;
}
return this.mapVariant('next-supabase');
}
private async resolveConfiguredVariant(): Promise<VariantDescriptor | null> {
const configPath = '.makerkit/config.json';
if (!(await this.deps.pathExists(configPath))) {
return null;
}
const config = await this.readObject(configPath);
const value =
this.readString(config, 'variant') ??
this.readString(config, 'template') ??
this.readString(config, 'kitVariant');
if (!value) {
return null;
}
return this.mapVariant(value, {
preserveVariant: true,
});
}
private async resolveHeuristicVariant(): Promise<VariantDescriptor | null> {
const hasSupabaseFolder = await this.deps.pathExists('apps/web/supabase');
if (!hasSupabaseFolder) {
return null;
}
const appPackage = await this.readObject(
join('apps', 'web', 'package.json'),
);
const hasNextDependency = this.hasDependency(appPackage, 'next');
if (hasNextDependency) {
return this.mapVariant('next-supabase');
}
return null;
}
private hasDependency(json: Record<string, unknown>, dependency: string) {
const dependencies = this.readObjectValue(json, 'dependencies');
const devDependencies = this.readObjectValue(json, 'devDependencies');
return Boolean(
this.readString(dependencies, dependency) ||
this.readString(devDependencies, dependency),
);
}
private mapVariant(
variant: string,
options: {
preserveVariant?: boolean;
} = {},
): VariantDescriptor {
if (variant === 'next-drizzle') {
return {
variant,
variant_family: 'orm',
framework: 'nextjs',
database: 'postgresql',
auth: 'better-auth',
};
}
if (variant === 'next-prisma') {
return {
variant,
variant_family: 'orm',
framework: 'nextjs',
database: 'postgresql',
auth: 'better-auth',
};
}
if (variant === 'react-router-supabase') {
return {
variant,
variant_family: 'supabase',
framework: 'react-router',
database: 'supabase',
auth: 'supabase',
};
}
return {
variant: options.preserveVariant ? variant : 'next-supabase',
variant_family: 'supabase',
framework: 'nextjs',
database: 'supabase',
auth: 'supabase',
};
}
private async readObject(path: string): Promise<Record<string, unknown>> {
try {
const value = await this.deps.readJsonFile(path);
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return {};
}
return value as Record<string, unknown>;
} catch {
return {};
}
}
private readString(obj: Record<string, unknown>, key: string) {
const value = obj[key];
return typeof value === 'string' && value.length > 0 ? value : null;
}
private readObjectValue(obj: Record<string, unknown>, key: string) {
const value = obj[key];
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return {};
}
return value as Record<string, unknown>;
}
private getPackageManager(packageJson: Record<string, unknown>) {
const packageManager = this.readString(packageJson, 'packageManager');
if (!packageManager) {
return 'unknown';
}
const [name] = packageManager.split('@');
return name || 'unknown';
}
}

View File

@@ -0,0 +1,48 @@
import { z } from 'zod/v3';
export const KitStatusInputSchema = z.object({});
export const KitStatusOutputSchema = z.object({
variant: z.string(),
variant_family: z.string(),
framework: z.string(),
database: z.string(),
auth: z.string(),
project_name: z.string(),
node_version: z.string(),
package_manager: z.string(),
deps_installed: z.boolean(),
git_clean: z.boolean(),
git_branch: z.string(),
git_modified_files: z.array(z.string()),
git_untracked_files: z.array(z.string()),
git_merge_check: z.object({
target_branch: z.string().nullable(),
detectable: z.boolean(),
has_conflicts: z.boolean().nullable(),
conflict_files: z.array(z.string()),
message: z.string(),
}),
services: z.object({
app: z.object({
running: z.boolean(),
port: z.number().nullable(),
}),
supabase: z.object({
running: z.boolean(),
api_port: z.number().nullable(),
studio_port: z.number().nullable(),
}),
}),
diagnostics: z.array(
z.object({
id: z.string(),
status: z.enum(['pass', 'warn', 'fail']),
message: z.string(),
remedies: z.array(z.string()).default([]),
}),
),
});
export type KitStatusInput = z.infer<typeof KitStatusInputSchema>;
export type KitStatusOutput = z.infer<typeof KitStatusOutputSchema>;