2.24.1 (#453)
* 2.24.1 - Updated dependencies - MCP Server: better compatibility with Windows - MCP Server: allow using a custom root for better flexibility - Version Check: use package.json version instead of number of commits - Prettier: reformatted some files - Add SSH_AUTH_SOCK to dev passThroughEnv to solve SSH issues; handle execSync errors - Use GIT_SSH to fix SSH issues on Windows - Updated Stripe version - Updated application version from 2.24.0 to 2.24.1 in package.json. - Enhanced error handling in billing services to include error causes for better debugging.
This commit is contained in:
committed by
GitHub
parent
f3ac595d06
commit
ca585e09be
@@ -27,7 +27,7 @@
|
|||||||
"@kit/shared": "workspace:*",
|
"@kit/shared": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@types/nodemailer": "catalog:",
|
"@types/nodemailer": "catalog:",
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
"@supabase/supabase-js": "catalog:",
|
"@supabase/supabase-js": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"dotenv": "17.2.4",
|
"dotenv": "17.3.1",
|
||||||
"node-html-parser": "^7.0.2",
|
"node-html-parser": "^7.0.2",
|
||||||
"totp-generator": "^2.0.1"
|
"totp-generator": "^2.0.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class UserBillingService {
|
|||||||
`Checkout session not created due to an error`,
|
`Checkout session not created due to an error`,
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new Error(`Failed to create a checkout session`);
|
throw new Error(`Failed to create a checkout session`, { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +177,7 @@ class UserBillingService {
|
|||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Encountered an error creating the Billing Portal session`,
|
`Encountered an error creating the Billing Portal session`,
|
||||||
|
{ cause: error },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class TeamBillingService {
|
|||||||
`Error creating the checkout session`,
|
`Error creating the checkout session`,
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new Error(`Checkout not created`);
|
throw new Error(`Checkout not created`, { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ class TeamBillingService {
|
|||||||
`Billing Portal session was not created`,
|
`Billing Portal session was not created`,
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new Error(`Error creating Billing Portal`);
|
throw new Error(`Error creating Billing Portal`, { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
"react-hook-form": "catalog:",
|
"react-hook-form": "catalog:",
|
||||||
"react-i18next": "catalog:",
|
"react-i18next": "catalog:",
|
||||||
"recharts": "2.15.3",
|
"recharts": "2.15.3",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tw-animate-css": "catalog:",
|
"tw-animate-css": "catalog:",
|
||||||
"urlpattern-polyfill": "^10.1.0",
|
"urlpattern-polyfill": "^10.1.0",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-supabase-saas-kit-turbo",
|
"name": "next-supabase-saas-kit-turbo",
|
||||||
"version": "2.24.0",
|
"version": "2.24.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -45,10 +45,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@manypkg/cli": "^0.25.1",
|
"@manypkg/cli": "^0.25.1",
|
||||||
"@turbo/gen": "^2.7.6",
|
"@turbo/gen": "^2.8.11",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"turbo": "2.8.5",
|
"turbo": "2.8.11",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ export class LemonSqueezyBillingStrategyService implements BillingStrategyProvid
|
|||||||
'Failed to create billing portal session',
|
'Failed to create billing portal session',
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new Error('Failed to create billing portal session');
|
throw new Error('Failed to create billing portal session', {
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(ctx, 'Billing portal session created successfully');
|
logger.info(ctx, 'Billing portal session created successfully');
|
||||||
@@ -138,7 +140,7 @@ export class LemonSqueezyBillingStrategyService implements BillingStrategyProvid
|
|||||||
'Failed to cancel subscription',
|
'Failed to cancel subscription',
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new Error('Failed to cancel subscription');
|
throw new Error('Failed to cancel subscription', { cause: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(ctx, 'Subscription cancelled successfully');
|
logger.info(ctx, 'Subscription cancelled successfully');
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
|||||||
'Failed to retrieve checkout session',
|
'Failed to retrieve checkout session',
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new Error('Failed to retrieve checkout session');
|
throw new Error('Failed to retrieve checkout session', { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
|||||||
'Failed to report usage',
|
'Failed to report usage',
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new Error('Failed to report usage');
|
throw new Error('Failed to report usage', { cause: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -277,7 +277,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
|||||||
'Failed to report usage',
|
'Failed to report usage',
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new Error('Failed to report usage');
|
throw new Error('Failed to report usage', { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ ...ctx, error }, 'Failed to update subscription');
|
logger.error({ ...ctx, error }, 'Failed to update subscription');
|
||||||
|
|
||||||
throw new Error('Failed to update subscription');
|
throw new Error('Failed to update subscription', { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +357,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ ...ctx, error }, 'Failed to retrieve plan');
|
logger.error({ ...ctx, error }, 'Failed to retrieve plan');
|
||||||
|
|
||||||
throw new Error('Failed to retrieve plan');
|
throw new Error('Failed to retrieve plan', { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +413,7 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ ...ctx, error }, 'Failed to retrieve subscription');
|
logger.error({ ...ctx, error }, 'Failed to retrieve subscription');
|
||||||
|
|
||||||
throw new Error('Failed to retrieve subscription');
|
throw new Error('Failed to retrieve subscription', { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'server-only';
|
|||||||
|
|
||||||
import { StripeServerEnvSchema } from '../schema/stripe-server-env.schema';
|
import { StripeServerEnvSchema } from '../schema/stripe-server-env.schema';
|
||||||
|
|
||||||
const STRIPE_API_VERSION = '2026-01-28.clover';
|
const STRIPE_API_VERSION = '2026-02-25.clover';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description returns a Stripe instance
|
* @description returns a Stripe instance
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class DeletePersonalAccountService {
|
|||||||
'Encountered an error deleting user',
|
'Encountered an error deleting user',
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new Error('Error deleting user');
|
throw new Error('Error deleting user', { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"@kit/email-templates": "workspace:*",
|
"@kit/email-templates": "workspace:*",
|
||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@modelcontextprotocol/sdk": "1.26.0",
|
"@modelcontextprotocol/sdk": "1.27.1",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"postgres": "3.4.8",
|
"postgres": "3.4.8",
|
||||||
"tsup": "catalog:",
|
"tsup": "catalog:",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
|
||||||
|
import { resolveProjectRoot } from './resolve-root';
|
||||||
import { registerComponentsTools } from './tools/components';
|
import { registerComponentsTools } from './tools/components';
|
||||||
import {
|
import {
|
||||||
registerDatabaseResources,
|
registerDatabaseResources,
|
||||||
@@ -22,30 +23,30 @@ import { registerKitStatusTool } from './tools/status/index';
|
|||||||
import { registerKitTranslationsTools } from './tools/translations/index';
|
import { registerKitTranslationsTools } from './tools/translations/index';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Create server instance
|
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
name: 'makerkit',
|
name: 'makerkit',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
|
const rootPath = resolveProjectRoot();
|
||||||
|
|
||||||
registerGetMigrationsTools(server);
|
registerGetMigrationsTools(server, rootPath);
|
||||||
registerKitStatusTool(server);
|
registerKitStatusTool(server, rootPath);
|
||||||
registerKitPrerequisitesTool(server);
|
registerKitPrerequisitesTool(server, rootPath);
|
||||||
registerKitEnvTools(server);
|
registerKitEnvTools(server, rootPath);
|
||||||
registerKitDevTools(server);
|
registerKitDevTools(server, rootPath);
|
||||||
registerKitDbTools(server);
|
registerKitDbTools(server, rootPath);
|
||||||
registerKitEmailsTools(server);
|
registerKitEmailsTools(server, rootPath);
|
||||||
registerKitEmailTemplatesTools(server);
|
registerKitEmailTemplatesTools(server, rootPath);
|
||||||
registerKitTranslationsTools(server);
|
registerKitTranslationsTools(server, rootPath);
|
||||||
registerDatabaseTools(server);
|
registerDatabaseTools(server, rootPath);
|
||||||
registerDatabaseResources(server);
|
registerDatabaseResources(server, rootPath);
|
||||||
registerComponentsTools(server);
|
registerComponentsTools(server, rootPath);
|
||||||
registerScriptsTools(server);
|
registerScriptsTools(server, rootPath);
|
||||||
registerRunChecksTool(server);
|
registerRunChecksTool(server, rootPath);
|
||||||
registerDepsUpgradeAdvisorTool(server);
|
registerDepsUpgradeAdvisorTool(server, rootPath);
|
||||||
registerPRDTools(server);
|
registerPRDTools(server, rootPath);
|
||||||
registerPromptsSystem(server);
|
registerPromptsSystem(server);
|
||||||
|
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
|
|||||||
291
packages/mcp-server/src/lib/__tests__/process-utils.test.ts
Normal file
291
packages/mcp-server/src/lib/__tests__/process-utils.test.ts
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import { createServer } from 'node:net';
|
||||||
|
import { afterAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IS_WINDOWS,
|
||||||
|
crossExecFileSync,
|
||||||
|
execFileAsync,
|
||||||
|
findProcessesByName,
|
||||||
|
getPortProcess,
|
||||||
|
killProcess,
|
||||||
|
spawnDetached,
|
||||||
|
} from '../process-utils';
|
||||||
|
|
||||||
|
const pidsToCleanup: number[] = [];
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
for (const pid of pidsToCleanup) {
|
||||||
|
try {
|
||||||
|
await killProcess(pid);
|
||||||
|
} catch {
|
||||||
|
// already dead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function isCommandAvailable(cmd: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await execFileAsync(cmd, ['--version']);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number) {
|
||||||
|
return new Promise((r) => setTimeout(r, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// pnpm — the command that triggered this whole cross-platform fix.
|
||||||
|
// The MCP tools call pnpm for migrations, seeding, checks, and dev server.
|
||||||
|
// On Windows pnpm is a .cmd file that requires shell: true.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('pnpm commands', () => {
|
||||||
|
it('pnpm --version (used by kit_prerequisites)', async () => {
|
||||||
|
const result = await execFileAsync('pnpm', ['--version']);
|
||||||
|
expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pnpm --version sync (used by migrations tool)', () => {
|
||||||
|
const result = crossExecFileSync('pnpm', ['--version'], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(String(result).trim()).toMatch(/^\d+\.\d+\.\d+/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pnpm run with unknown script returns non-zero (used by run_checks)', async () => {
|
||||||
|
// run_checks calls: pnpm run <script>
|
||||||
|
// Verify the cross-platform wrapper surfaces the error correctly.
|
||||||
|
await expect(
|
||||||
|
execFileAsync('pnpm', ['run', '__nonexistent_script_xyz__']),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pnpm ls --json (read-only, similar to deps-advisor outdated)', async () => {
|
||||||
|
const result = await execFileAsync('pnpm', ['ls', '--json', '--depth=0']);
|
||||||
|
// pnpm ls --json returns valid JSON
|
||||||
|
expect(() => JSON.parse(result.stdout)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// git — used by kit_status for branch, modified files, merge checks.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('git commands', () => {
|
||||||
|
it('git --version (used by kit_prerequisites)', async () => {
|
||||||
|
const result = await execFileAsync('git', ['--version']);
|
||||||
|
expect(result.stdout).toContain('git version');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('git rev-parse --abbrev-ref HEAD (used by kit_status)', async () => {
|
||||||
|
const result = await execFileAsync('git', [
|
||||||
|
'rev-parse',
|
||||||
|
'--abbrev-ref',
|
||||||
|
'HEAD',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.stdout.trim().length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('git status --porcelain (used by kit_status)', async () => {
|
||||||
|
const result = await execFileAsync('git', ['status', '--porcelain']);
|
||||||
|
// Can be empty (clean) or have entries — just shouldn't throw
|
||||||
|
expect(result.stderr).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('git log --oneline -1 (common git operation)', async () => {
|
||||||
|
const result = await execFileAsync('git', ['log', '--oneline', '-1']);
|
||||||
|
expect(result.stdout.trim().length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// node — used by kit_prerequisites.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('node commands', () => {
|
||||||
|
it('node --version (used by kit_prerequisites)', async () => {
|
||||||
|
const result = await execFileAsync('node', ['--version']);
|
||||||
|
expect(result.stdout.trim()).toMatch(/^v\d+\.\d+\.\d+$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('node -e (used for spawning scripts)', async () => {
|
||||||
|
const result = await execFileAsync('node', ['-e', 'console.log("hello")']);
|
||||||
|
|
||||||
|
expect(result.stdout.trim()).toBe('hello');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// docker — used by kit_dev_start/stop for database and mailbox containers.
|
||||||
|
// Skipped if docker is not installed.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('docker commands', async () => {
|
||||||
|
const hasDocker = await isCommandAvailable('docker');
|
||||||
|
|
||||||
|
it.skipIf(!hasDocker)(
|
||||||
|
'docker --version (used by kit_prerequisites)',
|
||||||
|
async () => {
|
||||||
|
const result = await execFileAsync('docker', ['--version']);
|
||||||
|
expect(result.stdout).toContain('Docker');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.skipIf(!hasDocker)(
|
||||||
|
'docker compose version (used before compose up/stop)',
|
||||||
|
async () => {
|
||||||
|
const result = await execFileAsync('docker', ['compose', 'version']);
|
||||||
|
expect(result.stdout).toContain('Docker Compose');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Port operations — TCP socket check + getPortProcess.
|
||||||
|
// Used by kit_dev_status, kit_db_status, mailbox status to detect running
|
||||||
|
// services on specific ports (3000, 54333, 8025).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('port operations', () => {
|
||||||
|
it('getPortProcess finds listener on a bound port', async () => {
|
||||||
|
const server = createServer();
|
||||||
|
|
||||||
|
const port = await new Promise<number>((resolve) => {
|
||||||
|
server.listen(0, '127.0.0.1', () => {
|
||||||
|
const addr = server.address();
|
||||||
|
resolve(typeof addr === 'object' && addr ? addr.port : 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const proc = await getPortProcess(port, process.cwd());
|
||||||
|
|
||||||
|
expect(proc).not.toBeNull();
|
||||||
|
expect(proc!.pid).toBeGreaterThan(0);
|
||||||
|
} finally {
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getPortProcess returns null for port with no listener', async () => {
|
||||||
|
// Use a high port unlikely to be in use
|
||||||
|
const proc = await getPortProcess(59998, process.cwd());
|
||||||
|
expect(proc).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Process lifecycle — spawnDetached + findProcessesByName + killProcess.
|
||||||
|
// The dev tools spawn pnpm and stripe as detached processes, find them by
|
||||||
|
// pattern (e.g. "stripe.*listen"), and kill them via PID/group.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('process lifecycle', () => {
|
||||||
|
it('spawn a long-running node process, find it, then kill it', async () => {
|
||||||
|
// Spawn a detached node process (mirrors how pnpm exec next dev works)
|
||||||
|
const marker = `__test_marker_${Date.now()}__`;
|
||||||
|
|
||||||
|
const child = spawnDetached('node', [
|
||||||
|
'-e',
|
||||||
|
`process.title = "${marker}"; setTimeout(() => {}, 60000)`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(child.pid).toBeGreaterThan(0);
|
||||||
|
pidsToCleanup.push(child.pid!);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
// Verify it's alive (process.kill(pid, 0) is cross-platform)
|
||||||
|
expect(() => process.kill(child.pid!, 0)).not.toThrow();
|
||||||
|
|
||||||
|
// findProcessesByName should find it (mirrors "stripe.*listen" pattern)
|
||||||
|
const found = await findProcessesByName(marker, process.cwd());
|
||||||
|
expect(found.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Kill it (mirrors kit_dev_stop stopping services)
|
||||||
|
await killProcess(child.pid!);
|
||||||
|
await sleep(300);
|
||||||
|
|
||||||
|
// Verify it's dead
|
||||||
|
expect(() => process.kill(child.pid!, 0)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('spawn a pnpm process via spawnDetached', async () => {
|
||||||
|
// This is exactly how kit_dev_start spawns the Next.js dev server:
|
||||||
|
// spawnDetached('pnpm', ['exec', 'node', '-e', '...'])
|
||||||
|
const child = spawnDetached('pnpm', [
|
||||||
|
'exec',
|
||||||
|
'node',
|
||||||
|
'-e',
|
||||||
|
'setTimeout(() => {}, 60000)',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(child.pid).toBeGreaterThan(0);
|
||||||
|
pidsToCleanup.push(child.pid!);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
// Verify pnpm-spawned process is alive
|
||||||
|
expect(() => process.kill(child.pid!, 0)).not.toThrow();
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await killProcess(child.pid!);
|
||||||
|
await sleep(300);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('findProcessesByName returns empty for nonexistent pattern', async () => {
|
||||||
|
const procs = await findProcessesByName(
|
||||||
|
'zzz_no_such_process_12345',
|
||||||
|
process.cwd(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(procs).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('killProcess does not throw for already-dead PID', async () => {
|
||||||
|
const child = spawnDetached('node', ['-e', 'process.exit(0)']);
|
||||||
|
|
||||||
|
pidsToCleanup.push(child.pid!);
|
||||||
|
await sleep(300);
|
||||||
|
|
||||||
|
// Process already exited — killProcess should not throw
|
||||||
|
await expect(killProcess(child.pid!)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Error handling — unknown commands should reject cleanly.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('execFileAsync rejects for unknown command', async () => {
|
||||||
|
await expect(
|
||||||
|
execFileAsync('__nonexistent_cmd_xyz__', ['--help']),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('crossExecFileSync throws for unknown command', () => {
|
||||||
|
expect(() =>
|
||||||
|
crossExecFileSync('__nonexistent_cmd_xyz__', ['--help']),
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Platform constant
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('IS_WINDOWS', () => {
|
||||||
|
it('matches process.platform', () => {
|
||||||
|
expect(IS_WINDOWS).toBe(process.platform === 'win32');
|
||||||
|
});
|
||||||
|
});
|
||||||
263
packages/mcp-server/src/lib/process-utils.ts
Normal file
263
packages/mcp-server/src/lib/process-utils.ts
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import {
|
||||||
|
type ExecFileOptions,
|
||||||
|
type ExecFileSyncOptions,
|
||||||
|
type SpawnOptions,
|
||||||
|
execFile,
|
||||||
|
execFileSync,
|
||||||
|
spawn,
|
||||||
|
} from 'node:child_process';
|
||||||
|
import { platform } from 'node:os';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
|
||||||
|
const rawExecFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
export const IS_WINDOWS = platform() === 'win32';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-platform execFile for `.cmd` / `.bat` commands (pnpm, npm, etc.).
|
||||||
|
*
|
||||||
|
* On Windows, `.cmd`/`.bat` files cannot be executed without a shell
|
||||||
|
* (see https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows).
|
||||||
|
* Adds `shell: true` + `windowsHide: true` on Windows to resolve them
|
||||||
|
* without opening a visible console window.
|
||||||
|
*
|
||||||
|
* For native executables (git, node, docker) prefer `execFileSync` / `execFile`
|
||||||
|
* directly — they don't need a shell.
|
||||||
|
*/
|
||||||
|
export function execFileAsync(
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
options: ExecFileOptions = {},
|
||||||
|
) {
|
||||||
|
return rawExecFileAsync(command, args, withShell(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-platform execFileSync.
|
||||||
|
*/
|
||||||
|
export function crossExecFileSync(
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
options: ExecFileSyncOptions = {},
|
||||||
|
) {
|
||||||
|
return execFileSync(command, args, withShell(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn a long-running detached process.
|
||||||
|
*
|
||||||
|
* - **Unix**: uses `detached: true` so the child becomes a process-group
|
||||||
|
* leader (allows group-kill via negative PID).
|
||||||
|
* - **Windows**: omits `detached` to avoid opening a visible console window.
|
||||||
|
* The child stays alive as long as the MCP-server process does.
|
||||||
|
*/
|
||||||
|
export function spawnDetached(
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
options: SpawnOptions = {},
|
||||||
|
) {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
...options,
|
||||||
|
...(IS_WINDOWS ? { shell: true, windowsHide: true } : {}),
|
||||||
|
detached: !IS_WINDOWS,
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
|
||||||
|
child.unref();
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kill a process (and its tree on Windows).
|
||||||
|
*
|
||||||
|
* - **Unix**: kills the process group via `process.kill(-pid)`, falling back
|
||||||
|
* to the individual process.
|
||||||
|
* - **Windows**: uses `taskkill /T /F /PID` which terminates the whole tree.
|
||||||
|
*/
|
||||||
|
export async function killProcess(
|
||||||
|
pid: number,
|
||||||
|
signal: string = 'SIGTERM',
|
||||||
|
): Promise<void> {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
try {
|
||||||
|
await rawExecFileAsync('taskkill', ['/T', '/F', '/PID', String(pid)], {
|
||||||
|
windowsHide: true,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Process may already be dead.
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.kill(-pid, signal);
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
process.kill(pid, signal);
|
||||||
|
} catch {
|
||||||
|
// Process may already be dead.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find which process is listening on a TCP port.
|
||||||
|
*
|
||||||
|
* - **Unix**: parses `lsof` output.
|
||||||
|
* - **Windows**: parses `netstat -ano` output.
|
||||||
|
*/
|
||||||
|
export async function getPortProcess(
|
||||||
|
port: number,
|
||||||
|
cwd: string,
|
||||||
|
): Promise<{ pid: number; command: string } | null> {
|
||||||
|
try {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
return await getPortProcessWindows(port, cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getPortProcessUnix(port, cwd);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find running processes whose command line matches a pattern.
|
||||||
|
*
|
||||||
|
* - **Unix**: uses `pgrep -fl`.
|
||||||
|
* - **Windows**: uses PowerShell `Get-CimInstance Win32_Process`.
|
||||||
|
*/
|
||||||
|
export async function findProcessesByName(
|
||||||
|
pattern: string,
|
||||||
|
cwd: string,
|
||||||
|
): Promise<Array<{ pid: number; command: string }>> {
|
||||||
|
try {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
return await findProcessesByNameWindows(pattern, cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await findProcessesByNameUnix(pattern, cwd);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function withShell<T extends SpawnOptions | ExecFileOptions>(options: T): T {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
return { ...options, shell: true, windowsHide: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPortProcessUnix(port: number, cwd: string) {
|
||||||
|
const result = await rawExecFileAsync(
|
||||||
|
'lsof',
|
||||||
|
['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fpc'],
|
||||||
|
{ cwd },
|
||||||
|
);
|
||||||
|
|
||||||
|
const lines = result.stdout.split('\n').map((l) => l.trim());
|
||||||
|
const pidLine = lines.find((l) => l.startsWith('p'));
|
||||||
|
const commandLine = lines.find((l) => l.startsWith('c'));
|
||||||
|
|
||||||
|
if (!pidLine || !commandLine) return null;
|
||||||
|
|
||||||
|
const pid = Number(pidLine.slice(1));
|
||||||
|
|
||||||
|
if (!Number.isFinite(pid)) return null;
|
||||||
|
|
||||||
|
return { pid, command: commandLine.slice(1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPortProcessWindows(port: number, cwd: string) {
|
||||||
|
const result = await rawExecFileAsync('netstat', ['-ano'], {
|
||||||
|
cwd,
|
||||||
|
windowsHide: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const portStr = `:${port}`;
|
||||||
|
|
||||||
|
for (const line of result.stdout.split('\n')) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
if (!trimmed.includes('LISTENING')) continue;
|
||||||
|
if (!trimmed.includes(portStr)) continue;
|
||||||
|
|
||||||
|
// Netstat format: TCP 0.0.0.0:PORT 0.0.0.0:0 LISTENING PID
|
||||||
|
const parts = trimmed.split(/\s+/);
|
||||||
|
const pid = Number(parts[parts.length - 1]);
|
||||||
|
|
||||||
|
if (!Number.isFinite(pid) || pid === 0) continue;
|
||||||
|
|
||||||
|
// Verify the port column actually matches (avoid partial matches)
|
||||||
|
const localAddr = parts[1] ?? '';
|
||||||
|
|
||||||
|
if (localAddr.endsWith(portStr)) {
|
||||||
|
return { pid, command: 'unknown' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findProcessesByNameUnix(pattern: string, cwd: string) {
|
||||||
|
const result = await rawExecFileAsync('pgrep', ['-fl', pattern], { cwd });
|
||||||
|
|
||||||
|
return result.stdout
|
||||||
|
.split('\n')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => {
|
||||||
|
const spaceIdx = line.indexOf(' ');
|
||||||
|
|
||||||
|
if (spaceIdx <= 0) return null;
|
||||||
|
|
||||||
|
const pid = Number(line.slice(0, spaceIdx));
|
||||||
|
const command = line.slice(spaceIdx + 1).trim();
|
||||||
|
|
||||||
|
if (!Number.isFinite(pid)) return null;
|
||||||
|
|
||||||
|
return { pid, command };
|
||||||
|
})
|
||||||
|
.filter((p): p is { pid: number; command: string } => p !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findProcessesByNameWindows(pattern: string, cwd: string) {
|
||||||
|
// Convert simple regex-like pattern to a PowerShell -match pattern.
|
||||||
|
// The patterns used in the codebase (e.g. "stripe.*listen") are already
|
||||||
|
// valid PowerShell regex.
|
||||||
|
const psCommand = [
|
||||||
|
`Get-CimInstance Win32_Process`,
|
||||||
|
`| Where-Object { $_.CommandLine -match '${pattern.replace(/'/g, "''")}' }`,
|
||||||
|
`| ForEach-Object { "$($_.ProcessId) $($_.CommandLine)" }`,
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
const result = await rawExecFileAsync(
|
||||||
|
'powershell',
|
||||||
|
['-NoProfile', '-Command', psCommand],
|
||||||
|
{ cwd, windowsHide: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.stdout
|
||||||
|
.split('\n')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const spaceIdx = trimmed.indexOf(' ');
|
||||||
|
|
||||||
|
if (spaceIdx <= 0) return null;
|
||||||
|
|
||||||
|
const pid = Number(trimmed.slice(0, spaceIdx));
|
||||||
|
const command = trimmed.slice(spaceIdx + 1).trim();
|
||||||
|
|
||||||
|
if (!Number.isFinite(pid)) return null;
|
||||||
|
|
||||||
|
return { pid, command };
|
||||||
|
})
|
||||||
|
.filter((p): p is { pid: number; command: string } => p !== null);
|
||||||
|
}
|
||||||
18
packages/mcp-server/src/resolve-root.ts
Normal file
18
packages/mcp-server/src/resolve-root.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { findWorkspaceRoot } from './tools/env/public-api';
|
||||||
|
|
||||||
|
export function resolveProjectRoot(argv: string[] = process.argv): string {
|
||||||
|
// 1. CLI flag: --root /path
|
||||||
|
const rootIdx = argv.indexOf('--root');
|
||||||
|
|
||||||
|
if (rootIdx !== -1 && argv[rootIdx + 1]) {
|
||||||
|
return argv[rootIdx + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Env var
|
||||||
|
if (process.env.MAKERKIT_PROJECT_ROOT) {
|
||||||
|
return process.env.MAKERKIT_PROJECT_ROOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Auto-discovery (traverse up from cwd looking for pnpm-workspace.yaml)
|
||||||
|
return findWorkspaceRoot(process.cwd());
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { MigrationsTool } from '../migrations';
|
||||||
|
|
||||||
|
const { crossExecFileSyncMock } = vi.hoisted(() => ({
|
||||||
|
crossExecFileSyncMock: vi.fn(() => Buffer.from('ok')),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../lib/process-utils', () => ({
|
||||||
|
crossExecFileSync: crossExecFileSyncMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('MigrationsTool', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
crossExecFileSyncMock.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Reset to default
|
||||||
|
MigrationsTool.setRootPath(process.cwd());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses crossExecFileSync args for CreateMigration with safe name', () => {
|
||||||
|
MigrationsTool.CreateMigration('add_users_table');
|
||||||
|
|
||||||
|
expect(crossExecFileSyncMock).toHaveBeenCalledWith(
|
||||||
|
'pnpm',
|
||||||
|
['--filter', 'web', 'supabase', 'migrations', 'new', 'add_users_table'],
|
||||||
|
{ cwd: process.cwd() },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unsafe migration names', () => {
|
||||||
|
expect(() => MigrationsTool.CreateMigration('foo && rm -rf /')).toThrow(
|
||||||
|
'Migration name must contain only letters, numbers, hyphens, or underscores',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(crossExecFileSyncMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses crossExecFileSync args for Diff', () => {
|
||||||
|
MigrationsTool.Diff();
|
||||||
|
|
||||||
|
expect(crossExecFileSyncMock).toHaveBeenCalledWith(
|
||||||
|
'pnpm',
|
||||||
|
['--filter', 'web', 'supabase', 'db', 'diff'],
|
||||||
|
{ cwd: process.cwd() },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses custom rootPath after setRootPath', () => {
|
||||||
|
MigrationsTool.setRootPath('/custom/project');
|
||||||
|
|
||||||
|
MigrationsTool.CreateMigration('test_migration');
|
||||||
|
|
||||||
|
expect(crossExecFileSyncMock).toHaveBeenCalledWith(
|
||||||
|
'pnpm',
|
||||||
|
['--filter', 'web', 'supabase', 'migrations', 'new', 'test_migration'],
|
||||||
|
{ cwd: '/custom/project' },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses custom rootPath for Diff after setRootPath', () => {
|
||||||
|
MigrationsTool.setRootPath('/custom/project');
|
||||||
|
|
||||||
|
MigrationsTool.Diff();
|
||||||
|
|
||||||
|
expect(crossExecFileSyncMock).toHaveBeenCalledWith(
|
||||||
|
'pnpm',
|
||||||
|
['--filter', 'web', 'supabase', 'db', 'diff'],
|
||||||
|
{ cwd: '/custom/project' },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,9 +12,15 @@ interface ComponentInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ComponentsTool {
|
export class ComponentsTool {
|
||||||
|
private static _rootPath = process.cwd();
|
||||||
|
|
||||||
|
static setRootPath(path: string) {
|
||||||
|
this._rootPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
static async getComponents(): Promise<ComponentInfo[]> {
|
static async getComponents(): Promise<ComponentInfo[]> {
|
||||||
const packageJsonPath = join(
|
const packageJsonPath = join(
|
||||||
process.cwd(),
|
this._rootPath,
|
||||||
'packages',
|
'packages',
|
||||||
'ui',
|
'ui',
|
||||||
'package.json',
|
'package.json',
|
||||||
@@ -179,7 +185,7 @@ export class ComponentsTool {
|
|||||||
|
|
||||||
static async getComponentContent(componentName: string): Promise<string> {
|
static async getComponentContent(componentName: string): Promise<string> {
|
||||||
const packageJsonPath = join(
|
const packageJsonPath = join(
|
||||||
process.cwd(),
|
this._rootPath,
|
||||||
'packages',
|
'packages',
|
||||||
'ui',
|
'ui',
|
||||||
'package.json',
|
'package.json',
|
||||||
@@ -193,7 +199,7 @@ export class ComponentsTool {
|
|||||||
throw new Error(`Component "${componentName}" not found in exports`);
|
throw new Error(`Component "${componentName}" not found in exports`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPath = join(process.cwd(), 'packages', 'ui', filePath);
|
const fullPath = join(this._rootPath, 'packages', 'ui', filePath);
|
||||||
return readFile(fullPath, 'utf8');
|
return readFile(fullPath, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +343,11 @@ export class ComponentsTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerComponentsTools(server: McpServer) {
|
export function registerComponentsTools(server: McpServer, rootPath?: string) {
|
||||||
|
if (rootPath) {
|
||||||
|
ComponentsTool.setRootPath(rootPath);
|
||||||
|
}
|
||||||
|
|
||||||
createGetComponentsTool(server);
|
createGetComponentsTool(server);
|
||||||
createGetComponentContentTool(server);
|
createGetComponentContentTool(server);
|
||||||
createComponentsSearchTool(server);
|
createComponentsSearchTool(server);
|
||||||
|
|||||||
@@ -1116,7 +1116,11 @@ export class DatabaseTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerDatabaseTools(server: McpServer) {
|
export function registerDatabaseTools(server: McpServer, rootPath?: string) {
|
||||||
|
if (rootPath) {
|
||||||
|
DatabaseTool.ROOT_PATH = rootPath;
|
||||||
|
}
|
||||||
|
|
||||||
createGetSchemaFilesTool(server);
|
createGetSchemaFilesTool(server);
|
||||||
createGetSchemaContentTool(server);
|
createGetSchemaContentTool(server);
|
||||||
createGetSchemasByTopicTool(server);
|
createGetSchemasByTopicTool(server);
|
||||||
@@ -1126,7 +1130,10 @@ export function registerDatabaseTools(server: McpServer) {
|
|||||||
createSearchFunctionsTool(server);
|
createSearchFunctionsTool(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerDatabaseResources(server: McpServer) {
|
export function registerDatabaseResources(
|
||||||
|
server: McpServer,
|
||||||
|
_rootPath?: string,
|
||||||
|
) {
|
||||||
createDatabaseSummaryTool(server);
|
createDatabaseSummaryTool(server);
|
||||||
createDatabaseTablesListTool(server);
|
createDatabaseTablesListTool(server);
|
||||||
createGetTableInfoTool(server);
|
createGetTableInfoTool(server);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
import { execFile } from 'node:child_process';
|
|
||||||
import { access, readFile, readdir } from 'node:fs/promises';
|
import { access, readFile, readdir } from 'node:fs/promises';
|
||||||
import { Socket } from 'node:net';
|
import { Socket } from 'node:net';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { promisify } from 'node:util';
|
|
||||||
|
|
||||||
|
import { execFileAsync } from '../../lib/process-utils';
|
||||||
import { type KitDbServiceDeps, createKitDbService } from './kit-db.service';
|
import { type KitDbServiceDeps, createKitDbService } from './kit-db.service';
|
||||||
import {
|
import {
|
||||||
KitDbMigrateInputSchema,
|
KitDbMigrateInputSchema,
|
||||||
@@ -17,15 +16,13 @@ import {
|
|||||||
KitDbStatusOutputSchema,
|
KitDbStatusOutputSchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
|
||||||
|
|
||||||
type TextContent = {
|
type TextContent = {
|
||||||
type: 'text';
|
type: 'text';
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function registerKitDbTools(server: McpServer) {
|
export function registerKitDbTools(server: McpServer, rootPath?: string) {
|
||||||
const service = createKitDbService(createKitDbDeps());
|
const service = createKitDbService(createKitDbDeps(rootPath));
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'kit_db_status',
|
'kit_db_status',
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
import { execFile } from 'node:child_process';
|
|
||||||
import { promisify } from 'node:util';
|
|
||||||
|
|
||||||
|
import { execFileAsync } from '../../lib/process-utils';
|
||||||
import {
|
import {
|
||||||
type DepsUpgradeAdvisorDeps,
|
type DepsUpgradeAdvisorDeps,
|
||||||
createDepsUpgradeAdvisorService,
|
createDepsUpgradeAdvisorService,
|
||||||
@@ -11,12 +10,13 @@ import {
|
|||||||
DepsUpgradeAdvisorOutputSchema,
|
DepsUpgradeAdvisorOutputSchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
export function registerDepsUpgradeAdvisorTool(
|
||||||
|
server: McpServer,
|
||||||
export function registerDepsUpgradeAdvisorTool(server: McpServer) {
|
rootPath?: string,
|
||||||
|
) {
|
||||||
return registerDepsUpgradeAdvisorToolWithDeps(
|
return registerDepsUpgradeAdvisorToolWithDeps(
|
||||||
server,
|
server,
|
||||||
createDepsUpgradeAdvisorDeps(),
|
createDepsUpgradeAdvisorDeps(rootPath),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,9 +63,9 @@ export function registerDepsUpgradeAdvisorToolWithDeps(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDepsUpgradeAdvisorDeps(): DepsUpgradeAdvisorDeps {
|
function createDepsUpgradeAdvisorDeps(
|
||||||
const rootPath = process.cwd();
|
rootPath = process.cwd(),
|
||||||
|
): DepsUpgradeAdvisorDeps {
|
||||||
return {
|
return {
|
||||||
async executeCommand(command, args) {
|
async executeCommand(command, args) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
import { execFile, spawn } from 'node:child_process';
|
|
||||||
import { access, readFile } from 'node:fs/promises';
|
import { access, readFile } from 'node:fs/promises';
|
||||||
import { Socket } from 'node:net';
|
import { Socket } from 'node:net';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { promisify } from 'node:util';
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
execFileAsync,
|
||||||
|
findProcessesByName,
|
||||||
|
getPortProcess,
|
||||||
|
killProcess,
|
||||||
|
spawnDetached,
|
||||||
|
} from '../../lib/process-utils';
|
||||||
import {
|
import {
|
||||||
DEFAULT_PORT_CONFIG,
|
DEFAULT_PORT_CONFIG,
|
||||||
type KitDevServiceDeps,
|
type KitDevServiceDeps,
|
||||||
@@ -21,10 +26,8 @@ import {
|
|||||||
KitMailboxStatusOutputSchema,
|
KitMailboxStatusOutputSchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
export function registerKitDevTools(server: McpServer, rootPath?: string) {
|
||||||
|
const service = createKitDevService(createKitDevDeps(rootPath));
|
||||||
export function registerKitDevTools(server: McpServer) {
|
|
||||||
const service = createKitDevService(createKitDevDeps());
|
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'kit_dev_start',
|
'kit_dev_start',
|
||||||
@@ -235,13 +238,7 @@ export function createKitDevDeps(rootPath = process.cwd()): KitDevServiceDeps {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
async spawnDetached(command: string, args: string[]) {
|
async spawnDetached(command: string, args: string[]) {
|
||||||
const child = spawn(command, args, {
|
const child = spawnDetached(command, args, { cwd: rootPath });
|
||||||
cwd: rootPath,
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore',
|
|
||||||
});
|
|
||||||
|
|
||||||
child.unref();
|
|
||||||
|
|
||||||
if (!child.pid) {
|
if (!child.pid) {
|
||||||
throw new Error(`Failed to spawn ${command}`);
|
throw new Error(`Failed to spawn ${command}`);
|
||||||
@@ -264,42 +261,7 @@ export function createKitDevDeps(rootPath = process.cwd()): KitDevServiceDeps {
|
|||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
async getPortProcess(port: number) {
|
async getPortProcess(port: number) {
|
||||||
try {
|
return getPortProcess(port, rootPath);
|
||||||
const result = await execFileAsync(
|
|
||||||
'lsof',
|
|
||||||
['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-Fpc'],
|
|
||||||
{
|
|
||||||
cwd: rootPath,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const pidLine = result.stdout
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.find((line) => line.startsWith('p'));
|
|
||||||
|
|
||||||
const commandLine = result.stdout
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.find((line) => line.startsWith('c'));
|
|
||||||
|
|
||||||
if (!pidLine || !commandLine) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pid = Number(pidLine.slice(1));
|
|
||||||
|
|
||||||
if (!Number.isFinite(pid)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
pid,
|
|
||||||
command: commandLine.slice(1),
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async isProcessRunning(pid: number) {
|
async isProcessRunning(pid: number) {
|
||||||
try {
|
try {
|
||||||
@@ -310,44 +272,10 @@ export function createKitDevDeps(rootPath = process.cwd()): KitDevServiceDeps {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async findProcessesByName(pattern: string) {
|
async findProcessesByName(pattern: string) {
|
||||||
try {
|
return findProcessesByName(pattern, rootPath);
|
||||||
const result = await execFileAsync('pgrep', ['-fl', pattern], {
|
|
||||||
cwd: rootPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.stdout
|
|
||||||
.split('\n')
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((line) => {
|
|
||||||
const spaceIdx = line.indexOf(' ');
|
|
||||||
|
|
||||||
if (spaceIdx <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pid = Number(line.slice(0, spaceIdx));
|
|
||||||
const command = line.slice(spaceIdx + 1).trim();
|
|
||||||
|
|
||||||
if (!Number.isFinite(pid)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { pid, command };
|
|
||||||
})
|
|
||||||
.filter((p): p is { pid: number; command: string } => p !== null);
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async killProcess(pid: number, signal = 'SIGTERM') {
|
async killProcess(pid: number, signal = 'SIGTERM') {
|
||||||
try {
|
return killProcess(pid, signal);
|
||||||
// Kill the entire process group (negative PID) since services
|
|
||||||
// are spawned detached and become process group leaders.
|
|
||||||
process.kill(-pid, signal);
|
|
||||||
} catch {
|
|
||||||
// Fall back to killing the individual process if group kill fails.
|
|
||||||
process.kill(pid, signal);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async sleep(ms: number) {
|
async sleep(ms: number) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ type TextContent = {
|
|||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function registerKitEmailTemplatesTools(server: McpServer) {
|
export function registerKitEmailTemplatesTools(
|
||||||
const service = createKitEmailsService(createKitEmailsDeps());
|
server: McpServer,
|
||||||
|
rootPath?: string,
|
||||||
|
) {
|
||||||
|
const service = createKitEmailsService(createKitEmailsDeps(rootPath));
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'kit_email_templates_list',
|
'kit_email_templates_list',
|
||||||
|
|||||||
54
packages/mcp-server/src/tools/env/__tests__/find-workspace-root.test.ts
vendored
Normal file
54
packages/mcp-server/src/tools/env/__tests__/find-workspace-root.test.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { findWorkspaceRoot } from '../scanner';
|
||||||
|
|
||||||
|
describe('findWorkspaceRoot', () => {
|
||||||
|
let tmp: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmp = join(tmpdir(), `fwr-test-${Date.now()}`);
|
||||||
|
mkdirSync(tmp, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(tmp, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns directory containing pnpm-workspace.yaml', () => {
|
||||||
|
writeFileSync(join(tmp, 'pnpm-workspace.yaml'), '');
|
||||||
|
|
||||||
|
expect(findWorkspaceRoot(tmp)).toBe(tmp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('walks up to find workspace root from nested dir', () => {
|
||||||
|
const nested = join(tmp, 'packages', 'foo', 'src');
|
||||||
|
mkdirSync(nested, { recursive: true });
|
||||||
|
writeFileSync(join(tmp, 'pnpm-workspace.yaml'), '');
|
||||||
|
|
||||||
|
expect(findWorkspaceRoot(nested)).toBe(tmp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns startPath when no workspace file found within depth', () => {
|
||||||
|
const deep = join(tmp, 'a', 'b', 'c', 'd', 'e', 'f', 'g');
|
||||||
|
mkdirSync(deep, { recursive: true });
|
||||||
|
writeFileSync(join(tmp, 'pnpm-workspace.yaml'), '');
|
||||||
|
|
||||||
|
// 7 levels deep, limit is 6 — should NOT find it
|
||||||
|
expect(findWorkspaceRoot(deep)).toBe(deep);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns startPath when no workspace file exists at all', () => {
|
||||||
|
expect(findWorkspaceRoot(tmp)).toBe(tmp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds root at exact depth boundary (5 levels up)', () => {
|
||||||
|
const nested = join(tmp, 'a', 'b', 'c', 'd', 'e');
|
||||||
|
mkdirSync(nested, { recursive: true });
|
||||||
|
writeFileSync(join(tmp, 'pnpm-workspace.yaml'), '');
|
||||||
|
|
||||||
|
expect(findWorkspaceRoot(nested)).toBe(tmp);
|
||||||
|
});
|
||||||
|
});
|
||||||
4
packages/mcp-server/src/tools/env/index.ts
vendored
4
packages/mcp-server/src/tools/env/index.ts
vendored
@@ -23,8 +23,8 @@ type TextContent = {
|
|||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function registerKitEnvTools(server: McpServer) {
|
export function registerKitEnvTools(server: McpServer, rootPath?: string) {
|
||||||
const service = createKitEnvService(createKitEnvDeps());
|
const service = createKitEnvService(createKitEnvDeps(rootPath));
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'kit_env_schema',
|
'kit_env_schema',
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
import { execFile } from 'node:child_process';
|
|
||||||
import { Socket } from 'node:net';
|
import { Socket } from 'node:net';
|
||||||
import { promisify } from 'node:util';
|
|
||||||
|
|
||||||
|
import { execFileAsync } from '../../lib/process-utils';
|
||||||
import {
|
import {
|
||||||
type KitMailboxDeps,
|
type KitMailboxDeps,
|
||||||
createKitMailboxService,
|
createKitMailboxService,
|
||||||
@@ -16,15 +15,13 @@ import {
|
|||||||
KitEmailsSetReadStatusOutputSchema,
|
KitEmailsSetReadStatusOutputSchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
|
||||||
|
|
||||||
type TextContent = {
|
type TextContent = {
|
||||||
type: 'text';
|
type: 'text';
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function registerKitEmailsTools(server: McpServer) {
|
export function registerKitEmailsTools(server: McpServer, rootPath?: string) {
|
||||||
const service = createKitMailboxService(createKitMailboxDeps());
|
const service = createKitMailboxService(createKitMailboxDeps(rootPath));
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'kit_emails_list',
|
'kit_emails_list',
|
||||||
|
|||||||
@@ -1,33 +1,60 @@
|
|||||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
import { execSync } from 'node:child_process';
|
|
||||||
import { readFile, readdir } from 'node:fs/promises';
|
import { readFile, readdir } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { z } from 'zod/v3';
|
import { z } from 'zod/v3';
|
||||||
|
|
||||||
|
import { crossExecFileSync } from '../lib/process-utils';
|
||||||
|
|
||||||
export class MigrationsTool {
|
export class MigrationsTool {
|
||||||
|
private static _rootPath = process.cwd();
|
||||||
|
|
||||||
|
static setRootPath(path: string) {
|
||||||
|
this._rootPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static get MIGRATIONS_DIR() {
|
||||||
|
return join(this._rootPath, 'apps', 'web', 'supabase', 'migrations');
|
||||||
|
}
|
||||||
|
|
||||||
static GetMigrations() {
|
static GetMigrations() {
|
||||||
return readdir(
|
return readdir(this.MIGRATIONS_DIR);
|
||||||
join(process.cwd(), 'apps', 'web', 'supabase', 'migrations'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getMigrationContent(path: string) {
|
static getMigrationContent(path: string) {
|
||||||
return readFile(
|
return readFile(join(this.MIGRATIONS_DIR, path), 'utf8');
|
||||||
join(process.cwd(), 'apps', 'web', 'supabase', 'migrations', path),
|
|
||||||
'utf8',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static CreateMigration(name: string) {
|
static CreateMigration(name: string) {
|
||||||
return execSync(`pnpm --filter web supabase migrations new ${name}`);
|
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
||||||
|
throw new Error(
|
||||||
|
'Migration name must contain only letters, numbers, hyphens, or underscores',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return crossExecFileSync(
|
||||||
|
'pnpm',
|
||||||
|
['--filter', 'web', 'supabase', 'migrations', 'new', name],
|
||||||
|
{ cwd: this._rootPath },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Diff() {
|
static Diff() {
|
||||||
return execSync(`pnpm --filter web supabase db diff`);
|
return crossExecFileSync(
|
||||||
|
'pnpm',
|
||||||
|
['--filter', 'web', 'supabase', 'db', 'diff'],
|
||||||
|
{ cwd: this._rootPath },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerGetMigrationsTools(server: McpServer) {
|
export function registerGetMigrationsTools(
|
||||||
|
server: McpServer,
|
||||||
|
rootPath?: string,
|
||||||
|
) {
|
||||||
|
if (rootPath) {
|
||||||
|
MigrationsTool.setRootPath(rootPath);
|
||||||
|
}
|
||||||
|
|
||||||
createGetMigrationsTool(server);
|
createGetMigrationsTool(server);
|
||||||
createGetMigrationContentTool(server);
|
createGetMigrationContentTool(server);
|
||||||
createCreateMigrationTool(server);
|
createCreateMigrationTool(server);
|
||||||
@@ -89,7 +116,7 @@ function createGetMigrationContentTool(server: McpServer) {
|
|||||||
'get_migration_content',
|
'get_migration_content',
|
||||||
{
|
{
|
||||||
description:
|
description:
|
||||||
'📜 Get migration file content (HISTORICAL) - For current state use get_schema_content instead',
|
'Get migration file content (HISTORICAL) - For current state use get_schema_content instead',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
state: z.object({
|
state: z.object({
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
@@ -103,7 +130,7 @@ function createGetMigrationContentTool(server: McpServer) {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: `📜 MIGRATION FILE: ${state.path} (HISTORICAL)\n\nNote: This shows historical changes. For current database state, use get_schema_content instead.\n\n${content}`,
|
text: `MIGRATION FILE: ${state.path} (HISTORICAL)\n\nNote: This shows historical changes. For current database state, use get_schema_content instead.\n\n${content}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -116,7 +143,7 @@ function createGetMigrationsTool(server: McpServer) {
|
|||||||
'get_migrations',
|
'get_migrations',
|
||||||
{
|
{
|
||||||
description:
|
description:
|
||||||
'📜 Get migration files (HISTORICAL CHANGES) - Use schema files for current state instead',
|
'Get migration files (HISTORICAL CHANGES) - Use schema files for current state instead',
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
const migrations = await MigrationsTool.GetMigrations();
|
const migrations = await MigrationsTool.GetMigrations();
|
||||||
@@ -125,7 +152,7 @@ function createGetMigrationsTool(server: McpServer) {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: `📜 MIGRATION FILES (HISTORICAL CHANGES)\n\nNote: For current database state, use get_schema_files instead. Migrations show historical changes.\n\n${migrations.join('\n')}`,
|
text: `MIGRATION FILES (HISTORICAL CHANGES)\n\nNote: For current database state, use get_schema_files instead. Migrations show historical changes.\n\n${migrations.join('\n')}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -967,7 +967,11 @@ export class PRDManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MCP Server Tool Registration
|
// MCP Server Tool Registration
|
||||||
export function registerPRDTools(server: McpServer) {
|
export function registerPRDTools(server: McpServer, rootPath?: string) {
|
||||||
|
if (rootPath) {
|
||||||
|
PRDManager.setRootPath(rootPath);
|
||||||
|
}
|
||||||
|
|
||||||
createListPRDsTool(server);
|
createListPRDsTool(server);
|
||||||
createGetPRDTool(server);
|
createGetPRDTool(server);
|
||||||
createCreatePRDTool(server);
|
createCreatePRDTool(server);
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
import { execFile } from 'node:child_process';
|
|
||||||
import { access, readFile } from 'node:fs/promises';
|
import { access, readFile } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { promisify } from 'node:util';
|
|
||||||
|
|
||||||
|
import { execFileAsync } from '../../lib/process-utils';
|
||||||
import {
|
import {
|
||||||
type KitPrerequisitesDeps,
|
type KitPrerequisitesDeps,
|
||||||
createKitPrerequisitesService,
|
createKitPrerequisitesService,
|
||||||
@@ -13,9 +12,10 @@ import {
|
|||||||
KitPrerequisitesOutputSchema,
|
KitPrerequisitesOutputSchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
export function registerKitPrerequisitesTool(
|
||||||
|
server: McpServer,
|
||||||
export function registerKitPrerequisitesTool(server: McpServer) {
|
rootPath?: string,
|
||||||
|
) {
|
||||||
return server.registerTool(
|
return server.registerTool(
|
||||||
'kit_prerequisites',
|
'kit_prerequisites',
|
||||||
{
|
{
|
||||||
@@ -28,7 +28,7 @@ export function registerKitPrerequisitesTool(server: McpServer) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const service = createKitPrerequisitesService(
|
const service = createKitPrerequisitesService(
|
||||||
createKitPrerequisitesDeps(),
|
createKitPrerequisitesDeps(rootPath),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await service.check(parsedInput);
|
const result = await service.check(parsedInput);
|
||||||
@@ -57,9 +57,9 @@ export function registerKitPrerequisitesTool(server: McpServer) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createKitPrerequisitesDeps(): KitPrerequisitesDeps {
|
function createKitPrerequisitesDeps(
|
||||||
const rootPath = process.cwd();
|
rootPath = process.cwd(),
|
||||||
|
): KitPrerequisitesDeps {
|
||||||
return {
|
return {
|
||||||
async getVariantFamily() {
|
async getVariantFamily() {
|
||||||
const variant = await resolveVariant(rootPath);
|
const variant = await resolveVariant(rootPath);
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
import { execFile } from 'node:child_process';
|
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { promisify } from 'node:util';
|
|
||||||
|
|
||||||
|
import { execFileAsync } from '../../lib/process-utils';
|
||||||
import {
|
import {
|
||||||
type RunChecksDeps,
|
type RunChecksDeps,
|
||||||
createRunChecksService,
|
createRunChecksService,
|
||||||
} from './run-checks.service';
|
} from './run-checks.service';
|
||||||
import { RunChecksInputSchema, RunChecksOutputSchema } from './schema';
|
import { RunChecksInputSchema, RunChecksOutputSchema } from './schema';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
export function registerRunChecksTool(server: McpServer, rootPath?: string) {
|
||||||
|
const service = createRunChecksService(createRunChecksDeps(rootPath));
|
||||||
export function registerRunChecksTool(server: McpServer) {
|
|
||||||
const service = createRunChecksService(createRunChecksDeps());
|
|
||||||
|
|
||||||
return server.registerTool(
|
return server.registerTool(
|
||||||
'run_checks',
|
'run_checks',
|
||||||
|
|||||||
@@ -21,8 +21,14 @@ interface ScriptInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ScriptsTool {
|
export class ScriptsTool {
|
||||||
|
private static _rootPath = process.cwd();
|
||||||
|
|
||||||
|
static setRootPath(path: string) {
|
||||||
|
this._rootPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
static async getScripts(): Promise<ScriptInfo[]> {
|
static async getScripts(): Promise<ScriptInfo[]> {
|
||||||
const packageJsonPath = join(process.cwd(), 'package.json');
|
const packageJsonPath = join(this._rootPath, 'package.json');
|
||||||
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
||||||
|
|
||||||
const scripts: ScriptInfo[] = [];
|
const scripts: ScriptInfo[] = [];
|
||||||
@@ -41,7 +47,7 @@ export class ScriptsTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async getScriptDetails(scriptName: string): Promise<ScriptInfo> {
|
static async getScriptDetails(scriptName: string): Promise<ScriptInfo> {
|
||||||
const packageJsonPath = join(process.cwd(), 'package.json');
|
const packageJsonPath = join(this._rootPath, 'package.json');
|
||||||
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
||||||
|
|
||||||
const command = packageJson.scripts[scriptName];
|
const command = packageJson.scripts[scriptName];
|
||||||
@@ -234,7 +240,11 @@ export class ScriptsTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerScriptsTools(server: McpServer) {
|
export function registerScriptsTools(server: McpServer, rootPath?: string) {
|
||||||
|
if (rootPath) {
|
||||||
|
ScriptsTool.setRootPath(rootPath);
|
||||||
|
}
|
||||||
|
|
||||||
createGetScriptsTool(server);
|
createGetScriptsTool(server);
|
||||||
createGetScriptDetailsTool(server);
|
createGetScriptDetailsTool(server);
|
||||||
createGetHealthcheckScriptsTool(server);
|
createGetHealthcheckScriptsTool(server);
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
import { execFile } from 'node:child_process';
|
|
||||||
import { access, readFile, stat } from 'node:fs/promises';
|
import { access, readFile, stat } from 'node:fs/promises';
|
||||||
import { Socket } from 'node:net';
|
import { Socket } from 'node:net';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { promisify } from 'node:util';
|
|
||||||
|
|
||||||
|
import { execFileAsync } from '../../lib/process-utils';
|
||||||
import {
|
import {
|
||||||
type KitStatusDeps,
|
type KitStatusDeps,
|
||||||
createKitStatusService,
|
createKitStatusService,
|
||||||
} from './kit-status.service';
|
} from './kit-status.service';
|
||||||
import { KitStatusInputSchema, KitStatusOutputSchema } from './schema';
|
import { KitStatusInputSchema, KitStatusOutputSchema } from './schema';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
export function registerKitStatusTool(server: McpServer, rootPath?: string) {
|
||||||
|
|
||||||
export function registerKitStatusTool(server: McpServer) {
|
|
||||||
return server.registerTool(
|
return server.registerTool(
|
||||||
'kit_status',
|
'kit_status',
|
||||||
{
|
{
|
||||||
@@ -25,7 +22,7 @@ export function registerKitStatusTool(server: McpServer) {
|
|||||||
const parsedInput = KitStatusInputSchema.parse(input);
|
const parsedInput = KitStatusInputSchema.parse(input);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const service = createKitStatusService(createKitStatusDeps());
|
const service = createKitStatusService(createKitStatusDeps(rootPath));
|
||||||
const status = await service.getStatus(parsedInput);
|
const status = await service.getStatus(parsedInput);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -54,9 +51,7 @@ export function registerKitStatusTool(server: McpServer) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createKitStatusDeps(): KitStatusDeps {
|
function createKitStatusDeps(rootPath = process.cwd()): KitStatusDeps {
|
||||||
const rootPath = process.cwd();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rootPath,
|
rootPath,
|
||||||
async readJsonFile(path: string): Promise<unknown> {
|
async readJsonFile(path: string): Promise<unknown> {
|
||||||
|
|||||||
@@ -27,8 +27,13 @@ type TextContent = {
|
|||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function registerKitTranslationsTools(server: McpServer) {
|
export function registerKitTranslationsTools(
|
||||||
const service = createKitTranslationsService(createKitTranslationsDeps());
|
server: McpServer,
|
||||||
|
rootPath?: string,
|
||||||
|
) {
|
||||||
|
const service = createKitTranslationsService(
|
||||||
|
createKitTranslationsDeps(rootPath),
|
||||||
|
);
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'kit_translations_list',
|
'kit_translations_list',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
ExclusiveEventHintOrCaptureContext,
|
||||||
Event as SentryEvent,
|
Event as SentryEvent,
|
||||||
User as SentryUser,
|
User as SentryUser,
|
||||||
captureEvent,
|
captureEvent,
|
||||||
@@ -29,8 +30,11 @@ export class SentryMonitoringService implements MonitoringService {
|
|||||||
return this.readyPromise;
|
return this.readyPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
captureException(error: Error | null) {
|
captureException(
|
||||||
return captureException(error);
|
error: Error | null,
|
||||||
|
context?: ExclusiveEventHintOrCaptureContext,
|
||||||
|
) {
|
||||||
|
return captureException(error, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
captureEvent<Extra extends SentryEvent>(event: string, extra?: Extra) {
|
captureEvent<Extra extends SentryEvent>(event: string, extra?: Extra) {
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"lucide-react": "catalog:",
|
"lucide-react": "catalog:",
|
||||||
"radix-ui": "1.4.3",
|
"radix-ui": "1.4.3",
|
||||||
"react-dropzone": "^14.4.0",
|
"react-dropzone": "^15.0.0",
|
||||||
"react-top-loading-bar": "3.0.2",
|
"react-top-loading-bar": "3.0.2",
|
||||||
"recharts": "2.15.3",
|
"recharts": "2.15.3",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "workspace:*",
|
"@kit/eslint-config": "workspace:*",
|
||||||
@@ -33,7 +33,6 @@
|
|||||||
"@types/react-dom": "catalog:",
|
"@types/react-dom": "catalog:",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"eslint": "^9.39.2",
|
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
@@ -42,7 +41,6 @@
|
|||||||
"react-i18next": "catalog:",
|
"react-i18next": "catalog:",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
|
|||||||
2703
pnpm-lock.yaml
generated
2703
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -4,38 +4,39 @@ packages:
|
|||||||
- tooling/*
|
- tooling/*
|
||||||
|
|
||||||
catalog:
|
catalog:
|
||||||
|
'@eslint/js': 10.0.1
|
||||||
'@marsidev/react-turnstile': 1.4.2
|
'@marsidev/react-turnstile': 1.4.2
|
||||||
'@next/bundle-analyzer': 16.1.6
|
'@next/bundle-analyzer': 16.1.6
|
||||||
'@next/eslint-plugin-next': 16.1.6
|
'@next/eslint-plugin-next': 16.1.6
|
||||||
'@react-email/components': 1.0.7
|
'@react-email/components': 1.0.8
|
||||||
'@sentry/nextjs': 10.38.0
|
'@sentry/nextjs': 10.40.0
|
||||||
'@stripe/react-stripe-js': 5.6.0
|
'@stripe/react-stripe-js': 5.6.0
|
||||||
'@stripe/stripe-js': 8.7.0
|
'@stripe/stripe-js': 8.8.0
|
||||||
'@supabase/supabase-js': 2.95.3
|
'@supabase/supabase-js': 2.97.0
|
||||||
'@tailwindcss/postcss': 4.1.18
|
'@tailwindcss/postcss': 4.2.1
|
||||||
'@tanstack/react-query': 5.90.21
|
'@tanstack/react-query': 5.90.21
|
||||||
'@types/eslint': 9.6.1
|
'@types/eslint': 9.6.1
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.3.1
|
||||||
'@types/nodemailer': 7.0.9
|
'@types/nodemailer': 7.0.11
|
||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
'@types/react-dom': 19.2.3
|
'@types/react-dom': 19.2.3
|
||||||
eslint: 9.39.2
|
eslint: 10.0.1
|
||||||
eslint-config-next: 16.1.6
|
eslint-config-next: 16.1.6
|
||||||
eslint-config-turbo: 2.8.5
|
eslint-config-turbo: 2.8.11
|
||||||
i18next: 25.8.5
|
i18next: 25.8.13
|
||||||
i18next-browser-languagedetector: 8.2.0
|
i18next-browser-languagedetector: 8.2.1
|
||||||
i18next-resources-to-backend: 1.2.1
|
i18next-resources-to-backend: 1.2.1
|
||||||
lucide-react: 0.563.0
|
lucide-react: 0.575.0
|
||||||
next: 16.1.6
|
next: 16.1.6
|
||||||
nodemailer: 8.0.1
|
nodemailer: 8.0.1
|
||||||
pino: 10.3.1
|
pino: 10.3.1
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4
|
react-dom: 19.2.4
|
||||||
react-hook-form: 7.71.1
|
react-hook-form: 7.71.2
|
||||||
react-i18next: 16.5.4
|
react-i18next: 16.5.4
|
||||||
stripe: 20.3.1
|
stripe: 20.4.0
|
||||||
supabase: 2.76.7
|
supabase: 2.76.15
|
||||||
tailwindcss: 4.1.18
|
tailwindcss: 4.2.1
|
||||||
tsup: 8.5.1
|
tsup: 8.5.1
|
||||||
tw-animate-css: 1.4.0
|
tw-animate-css: 1.4.0
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default defineConfig(
|
|||||||
'@typescript-eslint/non-nullable-type-assertion-style': 'off',
|
'@typescript-eslint/non-nullable-type-assertion-style': 'off',
|
||||||
'@typescript-eslint/only-throw-error': 'off',
|
'@typescript-eslint/only-throw-error': 'off',
|
||||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||||
|
'preserve-caught-error': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'error',
|
'error',
|
||||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"format": "prettier --check \"**/*.{js,json}\""
|
"format": "prettier --check \"**/*.{js,json}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@eslint/js": "catalog:",
|
||||||
"@next/eslint-plugin-next": "catalog:",
|
"@next/eslint-plugin-next": "catalog:",
|
||||||
"@types/eslint": "catalog:",
|
"@types/eslint": "catalog:",
|
||||||
"eslint-config-next": "catalog:",
|
"eslint-config-next": "catalog:",
|
||||||
|
|||||||
@@ -1,42 +1,37 @@
|
|||||||
import { execSync } from 'child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import path from 'path';
|
import path from 'node:path';
|
||||||
|
|
||||||
const endpoint = 'https://makerkit.dev/api/license/check';
|
const endpoint = 'https://makerkit.dev/api/license/check';
|
||||||
|
|
||||||
|
function runGitCommand(...args) {
|
||||||
|
try {
|
||||||
|
return execFileSync('git', args, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
}).trim();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkLicense() {
|
async function checkLicense() {
|
||||||
let gitUser, gitEmail;
|
let gitUser, gitEmail;
|
||||||
|
|
||||||
try {
|
gitUser = runGitCommand('config', 'user.username');
|
||||||
gitUser = execSync('git config user.username').toString().trim();
|
|
||||||
|
|
||||||
if (!gitUser) {
|
if (!gitUser) {
|
||||||
gitUser = execSync('git config user.name').toString().trim();
|
gitUser = runGitCommand('config', 'user.name');
|
||||||
}
|
|
||||||
|
|
||||||
gitEmail = execSync('git config user.email').toString().trim();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Error checking git user: ${error.message}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gitUser && !gitEmail) {
|
gitEmail = runGitCommand('config', 'user.email');
|
||||||
|
|
||||||
|
if (!gitUser || !gitEmail) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Please set the git user name with the command 'git config user.username <username>'. The username needs to match the GitHub username in your Makerkit organization.",
|
"Please set the git user name with the command 'git config user.username <username>'. The username needs to match the GitHub username in your Makerkit organization.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
gitEmail = execSync('git config user.email').toString().trim();
|
|
||||||
} catch (error) {
|
|
||||||
console.info('Error getting git config:', error.message);
|
|
||||||
|
|
||||||
if (!gitUser) {
|
|
||||||
throw new Error(
|
|
||||||
"Please set the git user name with the command 'git config user.username <username>'. The username needs to match the GitHub username in your Makerkit organization.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
searchParams.append('username', gitUser);
|
searchParams.append('username', gitUser);
|
||||||
@@ -77,9 +72,7 @@ function checkVisibility() {
|
|||||||
let remoteUrl;
|
let remoteUrl;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
remoteUrl = execSync('git config --get remote.origin.url')
|
remoteUrl = runGitCommand('config', '--get', 'remote.origin.url');
|
||||||
.toString()
|
|
||||||
.trim();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@@ -158,6 +151,8 @@ async function main() {
|
|||||||
console.error(`Check failed with error: ${error.message}`);
|
console.error(`Check failed with error: ${error.message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Check failed with error: ${error.message}`);
|
console.error(`Check failed with error: ${error.message}`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,93 +1,166 @@
|
|||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
import { checkPendingMigrations } from './migrations.mjs';
|
const isWindows = process.platform === 'win32';
|
||||||
|
|
||||||
function runGitCommand(command) {
|
function runGitCommand(...args) {
|
||||||
try {
|
try {
|
||||||
return execSync(command, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
return execFileSync('git', args, {
|
||||||
} catch (error) {
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
timeout: 15000,
|
||||||
|
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
||||||
|
}).trim();
|
||||||
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkMakerkitVersion() {
|
function log(color, message) {
|
||||||
// Fetch the latest changes from upstream without merging
|
const colors = { red: 31, yellow: 33, green: 32, cyan: 36 };
|
||||||
const fetchResult = runGitCommand('git fetch upstream');
|
const code = colors[color] ?? 0;
|
||||||
|
|
||||||
if (fetchResult === null) {
|
if (isWindows) {
|
||||||
console.info(
|
console.log(message);
|
||||||
'\x1b[33m%s\x1b[0m',
|
} else {
|
||||||
|
console.log(`\x1b[${code}m%s\x1b[0m`, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocalVersion() {
|
||||||
|
try {
|
||||||
|
// Use git repo root to match upstream/main:package.json path
|
||||||
|
const repoRoot = runGitCommand('rev-parse', '--show-toplevel');
|
||||||
|
const root = repoRoot || process.cwd();
|
||||||
|
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
||||||
|
|
||||||
|
return pkg.version;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpstreamVersion() {
|
||||||
|
const raw = runGitCommand('show', 'upstream/main:package.json');
|
||||||
|
|
||||||
|
if (!raw) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw).version;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVersion(version) {
|
||||||
|
if (typeof version !== 'string') return null;
|
||||||
|
|
||||||
|
// Strip pre-release suffix (e.g. "1.3.0-canary.1" → "1.3.0")
|
||||||
|
const clean = version.split('-')[0];
|
||||||
|
const parts = clean.split('.').map(Number);
|
||||||
|
|
||||||
|
if (parts.length < 3 || parts.some(Number.isNaN)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { major: parts[0], minor: parts[1], patch: parts[2] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareTuples(a, b) {
|
||||||
|
if (a.major !== b.major) return a.major > b.major ? 1 : -1;
|
||||||
|
if (a.minor !== b.minor) return a.minor > b.minor ? 1 : -1;
|
||||||
|
if (a.patch !== b.patch) return a.patch > b.patch ? 1 : -1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareVersions(local, upstream) {
|
||||||
|
const l = parseVersion(local);
|
||||||
|
const u = parseVersion(upstream);
|
||||||
|
|
||||||
|
if (!l || !u) return 'success';
|
||||||
|
|
||||||
|
const cmp = compareTuples(l, u);
|
||||||
|
|
||||||
|
if (cmp > 0) return 'canary';
|
||||||
|
if (cmp === 0) return 'success';
|
||||||
|
|
||||||
|
// Upstream is ahead — determine severity
|
||||||
|
if (l.major < u.major || l.minor < u.minor) return 'critical';
|
||||||
|
if (u.patch - l.patch > 3) return 'critical';
|
||||||
|
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkMakerkitVersion() {
|
||||||
|
// Check if upstream remote exists before attempting fetch
|
||||||
|
const upstreamUrl = runGitCommand('remote', 'get-url', 'upstream');
|
||||||
|
|
||||||
|
if (!upstreamUrl) {
|
||||||
|
log(
|
||||||
|
'yellow',
|
||||||
"⚠️ You have not setup 'upstream'. Please set up the upstream remote so you can update your Makerkit version.",
|
"⚠️ You have not setup 'upstream'. Please set up the upstream remote so you can update your Makerkit version.",
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the number of commits the local branch is behind upstream
|
// Fetch only main branch from upstream (may fail with SSH auth issues)
|
||||||
const behindCount = runGitCommand('git rev-list --count HEAD..upstream/main');
|
const fetchResult = runGitCommand('fetch', '--quiet', 'upstream', 'main');
|
||||||
|
|
||||||
if (behindCount === null) {
|
if (fetchResult === null) {
|
||||||
console.warn(
|
log(
|
||||||
"Failed to get commit count. Ensure you're on a branch that tracks upstream/main.",
|
'yellow',
|
||||||
|
'⚠️ Could not fetch from upstream. Checking cached upstream/main data...',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isWindows && upstreamUrl.includes('git@')) {
|
||||||
|
log(
|
||||||
|
'yellow',
|
||||||
|
'💡 Tip: On Windows, SSH remotes may not authenticate automatically. Consider switching to HTTPS:\n' +
|
||||||
|
` git remote set-url upstream ${upstreamUrl.replace(/^git@([^:]+):/, 'https://$1/').replace(/\.git$/, '')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const localVersion = getLocalVersion();
|
||||||
|
const upstreamVersion = getUpstreamVersion();
|
||||||
|
|
||||||
|
if (!localVersion || !upstreamVersion) {
|
||||||
|
console.warn('Failed to read version from package.json.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const count = parseInt(behindCount, 10);
|
const severity = compareVersions(localVersion, upstreamVersion);
|
||||||
const { severity } = getSeveriyLevel(count);
|
|
||||||
|
|
||||||
if (severity === 'critical') {
|
if (severity === 'canary') {
|
||||||
// error emoji: ❌
|
log(
|
||||||
console.log(
|
'cyan',
|
||||||
'\x1b[31m%s\x1b[0m',
|
`🚀 Running canary version (${localVersion}), ahead of stable (${upstreamVersion}).`,
|
||||||
'❌ Your Makerkit version is outdated. Please update to the latest version.',
|
);
|
||||||
|
} else if (severity === 'critical') {
|
||||||
|
log(
|
||||||
|
'red',
|
||||||
|
`❌ Your Makerkit version (${localVersion}) is outdated. Latest is ${upstreamVersion}.`,
|
||||||
);
|
);
|
||||||
} else if (severity === 'warning') {
|
} else if (severity === 'warning') {
|
||||||
console.log(
|
log(
|
||||||
'\x1b[33m%s\x1b[0m',
|
'yellow',
|
||||||
'⚠️ Your Makerkit version is outdated! Best to update to the latest version.',
|
`⚠️ Your Makerkit version (${localVersion}) is behind. Latest is ${upstreamVersion}.`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('\x1b[32m%s\x1b[0m', '✅ Your Makerkit version is up to date!');
|
log('green', `✅ Your Makerkit version (${localVersion}) is up to date!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count > 0) {
|
if (severity === 'warning' || severity === 'critical') {
|
||||||
logInstructions(count);
|
log('yellow', 'Please update for bug fixes and optimizations.');
|
||||||
|
log('cyan', 'To update, run: git pull upstream main');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function logInstructions(count) {
|
try {
|
||||||
console.log(
|
checkMakerkitVersion();
|
||||||
'\x1b[33m%s\x1b[0m',
|
} catch {
|
||||||
`You are ${count} commit(s) behind the latest version.`,
|
// Version check is informational — never block the dev server
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'\x1b[33m%s\x1b[0m',
|
|
||||||
'Please consider updating to the latest version for bug fixes and optimizations that your version does not have.',
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('\x1b[36m%s\x1b[0m', 'To update, run: git pull upstream main');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSeveriyLevel(count) {
|
|
||||||
if (count > 5) {
|
|
||||||
return {
|
|
||||||
severity: 'critical',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count > 0) {
|
|
||||||
return {
|
|
||||||
severity: 'warning',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
severity: 'success',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
checkMakerkitVersion();
|
|
||||||
checkPendingMigrations();
|
|
||||||
|
|||||||
45
turbo.json
45
turbo.json
@@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://turborepo.org/schema.json",
|
"$schema": "https://turborepo.org/schema.json",
|
||||||
"globalDependencies": [
|
"globalDependencies": ["**/.env"],
|
||||||
"**/.env"
|
|
||||||
],
|
|
||||||
"ui": "stream",
|
"ui": "stream",
|
||||||
"globalEnv": [
|
"globalEnv": [
|
||||||
"STRIPE_SECRET_KEY",
|
"STRIPE_SECRET_KEY",
|
||||||
@@ -47,50 +45,31 @@
|
|||||||
],
|
],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"topo": {
|
"topo": {
|
||||||
"dependsOn": [
|
"dependsOn": ["^topo"]
|
||||||
"^topo"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": [
|
"dependsOn": ["^build"],
|
||||||
"^build"
|
"outputs": [".next/**", "!.next/cache/**", "next-env.d.ts"]
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
".next/**",
|
|
||||||
"!.next/cache/**",
|
|
||||||
"next-env.d.ts"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
"persistent": true,
|
"persistent": true,
|
||||||
"cache": false
|
"cache": false,
|
||||||
|
"passThroughEnv": ["SSH_AUTH_SOCK", "GIT_SSH"]
|
||||||
},
|
},
|
||||||
"format": {
|
"format": {
|
||||||
"outputs": [
|
"outputs": ["node_modules/.cache/.prettiercache"],
|
||||||
"node_modules/.cache/.prettiercache"
|
|
||||||
],
|
|
||||||
"outputLogs": "new-only"
|
"outputLogs": "new-only"
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"dependsOn": [
|
"dependsOn": ["^topo"],
|
||||||
"^topo"
|
"outputs": ["node_modules/.cache/.eslintcache"]
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
"node_modules/.cache/.eslintcache"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"typecheck": {
|
"typecheck": {
|
||||||
"dependsOn": [
|
"dependsOn": ["^topo"],
|
||||||
"^topo"
|
"outputs": ["node_modules/.cache/tsbuildinfo.json"]
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
"node_modules/.cache/tsbuildinfo.json"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"dependsOn": [
|
"dependsOn": ["^topo"]
|
||||||
"^topo"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"clean": {
|
"clean": {
|
||||||
"cache": false
|
"cache": false
|
||||||
|
|||||||
Reference in New Issue
Block a user