init project
This commit is contained in:
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import { registerLighthouseAuditTool } from './lighthouse.js';
|
||||
export function registerAuditTools(server) {
|
||||
registerLighthouseAuditTool(server);
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/audit/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AAE9D,MAAM,UAAU,kBAAkB,CAAC,MAAiB;IAClD,2BAA2B,CAAC,MAAM,CAAC,CAAC;AACtC,CAAC"}
|
||||
Vendored
+140
@@ -0,0 +1,140 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
function scorePercent(score) {
|
||||
if (score == null)
|
||||
return null;
|
||||
return Number((score * 100).toFixed(1));
|
||||
}
|
||||
function extractScores(report) {
|
||||
const scores = {};
|
||||
for (const [id, category] of Object.entries(report.categories ?? {})) {
|
||||
scores[id] = scorePercent(category.score);
|
||||
}
|
||||
return scores;
|
||||
}
|
||||
function extractCategoryDetails(report) {
|
||||
return Object.entries(report.categories ?? {}).map(([id, category]) => ({
|
||||
id,
|
||||
title: category.title ?? id,
|
||||
description: category.description ?? null,
|
||||
score: scorePercent(category.score),
|
||||
auditCount: category.auditRefs?.length ?? 0,
|
||||
}));
|
||||
}
|
||||
function categoryForAudit(report, auditId) {
|
||||
for (const [categoryId, category] of Object.entries(report.categories ?? {})) {
|
||||
if (category.auditRefs?.some((ref) => ref.id === auditId))
|
||||
return categoryId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function weightForAudit(report, auditId) {
|
||||
for (const category of Object.values(report.categories ?? {})) {
|
||||
const ref = category.auditRefs?.find((r) => r.id === auditId);
|
||||
if (ref?.weight != null)
|
||||
return ref.weight;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function topOpportunities(report, limit = 12) {
|
||||
const audits = report.audits ?? {};
|
||||
const candidates = Object.entries(audits)
|
||||
.filter(([, audit]) => typeof audit.score === 'number' && (audit.score ?? 1) < 0.9)
|
||||
.filter(([, audit]) => audit.scoreDisplayMode !== 'informative' && audit.scoreDisplayMode !== 'manual')
|
||||
.map(([id, audit]) => {
|
||||
const details = audit.details;
|
||||
return {
|
||||
id,
|
||||
title: audit.title ?? id,
|
||||
category: categoryForAudit(report, id),
|
||||
score: audit.score ?? null,
|
||||
displayValue: audit.displayValue ?? null,
|
||||
weight: weightForAudit(report, id),
|
||||
savingsMs: details?.overallSavingsMs ?? null,
|
||||
savingsBytes: details?.overallSavingsBytes ?? null,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0) ||
|
||||
(b.savingsMs ?? 0) - (a.savingsMs ?? 0) ||
|
||||
(a.score ?? 1) - (b.score ?? 1));
|
||||
return candidates.slice(0, limit);
|
||||
}
|
||||
function failingByCategory(report, perCategory = 5) {
|
||||
const byCategory = {};
|
||||
for (const opportunity of topOpportunities(report, 50)) {
|
||||
const cat = opportunity.category ?? 'other';
|
||||
if (!byCategory[cat])
|
||||
byCategory[cat] = [];
|
||||
if (byCategory[cat].length < perCategory)
|
||||
byCategory[cat].push(opportunity);
|
||||
}
|
||||
return byCategory;
|
||||
}
|
||||
export function parseLighthouseOutput(raw) {
|
||||
// Lighthouse may print non-JSON noise before/after the JSON blob when run via npx.
|
||||
const start = raw.indexOf('{');
|
||||
const end = raw.lastIndexOf('}');
|
||||
if (start === -1 || end === -1 || end <= start) {
|
||||
throw new Error('Could not parse Lighthouse JSON output.');
|
||||
}
|
||||
const report = JSON.parse(raw.slice(start, end + 1));
|
||||
return {
|
||||
scores: extractScores(report),
|
||||
categories: extractCategoryDetails(report),
|
||||
opportunities: topOpportunities(report),
|
||||
failingByCategory: failingByCategory(report),
|
||||
};
|
||||
}
|
||||
export function registerLighthouseAuditTool(server) {
|
||||
server.registerTool('lighthouse_audit', {
|
||||
title: 'Lighthouse Audit',
|
||||
description: 'Run a headless Lighthouse audit against a URL (prefer localhost / the running dev server). Returns per-category ' +
|
||||
'scores and titles, top opportunities across categories (with savings when available), and failing audits grouped by category.',
|
||||
inputSchema: {
|
||||
url: z.string().url().describe('URL to audit, e.g. http://127.0.0.1:5173/'),
|
||||
categories: z
|
||||
.array(z.enum(['performance', 'accessibility', 'best-practices', 'seo', 'pwa']))
|
||||
.optional()
|
||||
.describe('Optional subset of Lighthouse categories. Defaults to performance, accessibility, best-practices, seo (add pwa explicitly if needed).'),
|
||||
},
|
||||
}, async ({ url, categories }) => {
|
||||
try {
|
||||
const selected = categories && categories.length > 0
|
||||
? categories
|
||||
: ['performance', 'accessibility', 'best-practices', 'seo'];
|
||||
// Prefer npx so lighthouse need not be globally installed; npx is already allow-listed.
|
||||
const result = await runCommand('npx', [
|
||||
'--yes',
|
||||
'lighthouse',
|
||||
url,
|
||||
'--output=json',
|
||||
'--quiet',
|
||||
'--chrome-flags=--headless --no-sandbox',
|
||||
...selected.flatMap((category) => ['--only-categories', category]),
|
||||
], {
|
||||
cwd: config.workspaceRoot,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
if (result.failed && !result.output.includes('{')) {
|
||||
return errorResult(`Lighthouse failed (exit ${result.exitCode}). Ensure Chrome/Chromium is available.\n${result.output}`);
|
||||
}
|
||||
const parsed = parseLighthouseOutput(result.output);
|
||||
return jsonResult({
|
||||
url,
|
||||
requestedCategories: selected,
|
||||
scores: parsed.scores,
|
||||
categories: parsed.categories,
|
||||
opportunities: parsed.opportunities,
|
||||
failingByCategory: parsed.failingByCategory,
|
||||
timedOut: result.timedOut,
|
||||
exitCode: result.exitCode,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=lighthouse.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
+346
@@ -0,0 +1,346 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { detectProject } from '../../lib/frameworks/detect.js';
|
||||
import { kebabCase, pascalCase, camelCase } from '../../lib/strings.js';
|
||||
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
function pluralize(word) {
|
||||
const lower = word.toLowerCase();
|
||||
if (lower.endsWith('ies'))
|
||||
return word;
|
||||
if (lower.endsWith('y') && !/[aeiou]y$/i.test(word))
|
||||
return `${word.slice(0, -1)}ies`;
|
||||
if (/(s|x|z|ch|sh)$/i.test(word))
|
||||
return `${word}es`;
|
||||
if (lower.endsWith('s'))
|
||||
return word;
|
||||
return `${word}s`;
|
||||
}
|
||||
function requireDjango() {
|
||||
const detected = detectProject(config.workspaceRoot);
|
||||
if (detected.backend?.kind !== 'django') {
|
||||
throw new Error('No Django project detected. Expected manage.py or a Django dependency file.');
|
||||
}
|
||||
return detected.backend;
|
||||
}
|
||||
function assertWritable(file, overwrite) {
|
||||
if (!overwrite && fs.existsSync(file)) {
|
||||
throw new Error(`"${file}" already exists. Pass overwrite: true to replace it.`);
|
||||
}
|
||||
}
|
||||
export function registerGenerateDjangoResourceTool(server) {
|
||||
server.registerTool('generate_django_resource', {
|
||||
title: 'Generate Django Resource',
|
||||
description: 'Scaffold a Django app resource: models.py model class, views (APIView-style stubs), urls.py, and admin registration. ' +
|
||||
'Writes files under the given app directory (created if missing). Does not run migrate.',
|
||||
inputSchema: {
|
||||
name: z.string().min(1).describe('Resource/model name, e.g. "Post" or "blog_post".'),
|
||||
app: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Django app directory relative to the project root. Defaults to a kebab-case plural of the name.'),
|
||||
overwrite: z.boolean().optional().default(false),
|
||||
},
|
||||
}, async ({ name, app, overwrite }) => {
|
||||
try {
|
||||
const django = requireDjango();
|
||||
const model = pascalCase(name);
|
||||
const appName = app?.trim() || kebabCase(pluralize(model)).replace(/-/g, '_');
|
||||
const appDir = resolveWorkspacePath(django.root, appName);
|
||||
fs.mkdirSync(appDir, { recursive: true });
|
||||
const files = [];
|
||||
const modelFile = path.join(appDir, 'models.py');
|
||||
const viewsFile = path.join(appDir, 'views.py');
|
||||
const urlsFile = path.join(appDir, 'urls.py');
|
||||
const adminFile = path.join(appDir, 'admin.py');
|
||||
const appsFile = path.join(appDir, 'apps.py');
|
||||
const initFile = path.join(appDir, '__init__.py');
|
||||
for (const file of [modelFile, viewsFile, urlsFile, adminFile, appsFile]) {
|
||||
assertWritable(file, overwrite ?? false);
|
||||
}
|
||||
if (!fs.existsSync(initFile)) {
|
||||
fs.writeFileSync(initFile, '');
|
||||
files.push(initFile);
|
||||
}
|
||||
fs.writeFileSync(modelFile, `from django.db import models
|
||||
|
||||
|
||||
class ${model}(models.Model):
|
||||
title = models.CharField(max_length=255)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.title
|
||||
`);
|
||||
files.push(modelFile);
|
||||
fs.writeFileSync(viewsFile, `from django.http import JsonResponse
|
||||
from django.views import View
|
||||
|
||||
from .models import ${model}
|
||||
|
||||
|
||||
class ${model}ListView(View):
|
||||
def get(self, request):
|
||||
items = list(${model}.objects.values("id", "title", "created_at"))
|
||||
return JsonResponse({"results": items})
|
||||
`);
|
||||
files.push(viewsFile);
|
||||
const routeName = kebabCase(pluralize(model));
|
||||
fs.writeFileSync(urlsFile, `from django.urls import path
|
||||
|
||||
from .views import ${model}ListView
|
||||
|
||||
urlpatterns = [
|
||||
path("${routeName}/", ${model}ListView.as_view(), name="${routeName}-list"),
|
||||
]
|
||||
`);
|
||||
files.push(urlsFile);
|
||||
fs.writeFileSync(adminFile, `from django.contrib import admin
|
||||
|
||||
from .models import ${model}
|
||||
|
||||
admin.site.register(${model})
|
||||
`);
|
||||
files.push(adminFile);
|
||||
fs.writeFileSync(appsFile, `from django.apps import AppConfig
|
||||
|
||||
|
||||
class ${pascalCase(appName)}Config(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "${appName}"
|
||||
`);
|
||||
files.push(appsFile);
|
||||
return jsonResult({
|
||||
kind: 'django',
|
||||
model,
|
||||
app: appName,
|
||||
route: `/${routeName}/`,
|
||||
files,
|
||||
nextSteps: [
|
||||
`Add "${appName}" to INSTALLED_APPS.`,
|
||||
`Include path("${appName}/", include("${appName}.urls")) in the project urls.py.`,
|
||||
'Run python manage.py makemigrations && python manage.py migrate.',
|
||||
],
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGenerateRailsResourceTool(server) {
|
||||
server.registerTool('generate_rails_resource', {
|
||||
title: 'Generate Rails Resource',
|
||||
description: 'Scaffold Rails model + controller stubs and append a resources route hint to config/routes.rb when present. ' +
|
||||
'File-based (does not invoke `rails generate`).',
|
||||
inputSchema: {
|
||||
name: z.string().min(1).describe('Resource name, e.g. "Post" or "blog_post".'),
|
||||
api: z.boolean().optional().default(true).describe('If true, generate an API-style controller under app/controllers/api.'),
|
||||
overwrite: z.boolean().optional().default(false),
|
||||
},
|
||||
}, async ({ name, api, overwrite }) => {
|
||||
try {
|
||||
const detected = detectProject(config.workspaceRoot);
|
||||
if (detected.backend?.kind !== 'rails') {
|
||||
return errorResult('No Rails project detected. Expected a Gemfile with rails and/or bin/rails.');
|
||||
}
|
||||
const root = detected.backend.root;
|
||||
const model = pascalCase(name);
|
||||
const plural = pluralize(model);
|
||||
const controllerName = `${plural}Controller`;
|
||||
const table = kebabCase(plural).replace(/-/g, '_');
|
||||
const modelFile = resolveWorkspacePath(root, path.join('app', 'models', `${kebabCase(model).replace(/-/g, '_')}.rb`));
|
||||
const controllerRel = api
|
||||
? path.join('app', 'controllers', 'api', `${kebabCase(plural).replace(/-/g, '_')}_controller.rb`)
|
||||
: path.join('app', 'controllers', `${kebabCase(plural).replace(/-/g, '_')}_controller.rb`);
|
||||
const controllerFile = resolveWorkspacePath(root, controllerRel);
|
||||
assertWritable(modelFile, overwrite ?? false);
|
||||
assertWritable(controllerFile, overwrite ?? false);
|
||||
fs.mkdirSync(path.dirname(modelFile), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(controllerFile), { recursive: true });
|
||||
const files = [];
|
||||
fs.writeFileSync(modelFile, `class ${model} < ApplicationRecord
|
||||
# self.table_name = "${table}"
|
||||
end
|
||||
`);
|
||||
files.push(modelFile);
|
||||
const parent = api ? 'ApplicationController' : 'ApplicationController';
|
||||
fs.writeFileSync(controllerFile, api
|
||||
? `module Api
|
||||
class ${controllerName} < ${parent}
|
||||
def index
|
||||
render json: ${model}.all
|
||||
end
|
||||
|
||||
def show
|
||||
render json: ${model}.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
`
|
||||
: `class ${controllerName} < ${parent}
|
||||
def index
|
||||
@${camelCase(plural)} = ${model}.all
|
||||
end
|
||||
|
||||
def show
|
||||
@${camelCase(model)} = ${model}.find(params[:id])
|
||||
end
|
||||
end
|
||||
`);
|
||||
files.push(controllerFile);
|
||||
const routesFile = path.join(root, 'config', 'routes.rb');
|
||||
let routeWired = false;
|
||||
const routeLine = api
|
||||
? ` namespace :api do\n resources :${table}, only: %i[index show]\n end`
|
||||
: ` resources :${table}, only: %i[index show]`;
|
||||
if (fs.existsSync(routesFile)) {
|
||||
const text = fs.readFileSync(routesFile, 'utf8');
|
||||
if (!text.includes(`resources :${table}`)) {
|
||||
const wired = text.includes('Rails.application.routes.draw')
|
||||
? text.replace(/Rails\.application\.routes\.draw do\s*\n/, (match) => `${match}${routeLine}\n`)
|
||||
: `${text.trimEnd()}\n${routeLine}\n`;
|
||||
fs.writeFileSync(routesFile, wired);
|
||||
routeWired = true;
|
||||
files.push(routesFile);
|
||||
}
|
||||
}
|
||||
return jsonResult({
|
||||
kind: 'rails',
|
||||
model,
|
||||
controller: controllerName,
|
||||
api: Boolean(api),
|
||||
route: api ? `/api/${table}` : `/${table}`,
|
||||
routeWired,
|
||||
files,
|
||||
nextSteps: [
|
||||
'Add a migration for the model (bin/rails g migration Create...).',
|
||||
routeWired ? 'Review config/routes.rb for the new resources entry.' : 'Add a resources route in config/routes.rb.',
|
||||
],
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGenerateSymfonyResourceTool(server) {
|
||||
server.registerTool('generate_symfony_resource', {
|
||||
title: 'Generate Symfony Resource',
|
||||
description: 'Scaffold a Symfony Entity + Repository + Controller stub under src/. File-based (does not invoke MakerBundle).',
|
||||
inputSchema: {
|
||||
name: z.string().min(1).describe('Resource/entity name, e.g. "Post" or "blog_post".'),
|
||||
overwrite: z.boolean().optional().default(false),
|
||||
},
|
||||
}, async ({ name, overwrite }) => {
|
||||
try {
|
||||
const detected = detectProject(config.workspaceRoot);
|
||||
if (detected.backend?.kind !== 'symfony') {
|
||||
return errorResult('No Symfony project detected. Expected symfony/framework-bundle or bin/console.');
|
||||
}
|
||||
const root = detected.backend.root;
|
||||
const entity = pascalCase(name);
|
||||
const route = kebabCase(pluralize(entity));
|
||||
const entityFile = resolveWorkspacePath(root, path.join('src', 'Entity', `${entity}.php`));
|
||||
const repoFile = resolveWorkspacePath(root, path.join('src', 'Repository', `${entity}Repository.php`));
|
||||
const controllerFile = resolveWorkspacePath(root, path.join('src', 'Controller', `${entity}Controller.php`));
|
||||
for (const file of [entityFile, repoFile, controllerFile]) {
|
||||
assertWritable(file, overwrite ?? false);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(entityFile, `<?php
|
||||
|
||||
namespace App\\Entity;
|
||||
|
||||
use App\\Repository\\${entity}Repository;
|
||||
use Doctrine\\ORM\\Mapping as ORM;
|
||||
|
||||
#[ORM\\Entity(repositoryClass: ${entity}Repository::class)]
|
||||
class ${entity}
|
||||
{
|
||||
#[ORM\\Id]
|
||||
#[ORM\\GeneratedValue]
|
||||
#[ORM\\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\\Column(length: 255)]
|
||||
private ?string $title = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): static
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
`);
|
||||
fs.writeFileSync(repoFile, `<?php
|
||||
|
||||
namespace App\\Repository;
|
||||
|
||||
use App\\Entity\\${entity};
|
||||
use Doctrine\\Bundle\\DoctrineBundle\\Repository\\ServiceEntityRepository;
|
||||
use Doctrine\\Persistence\\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<${entity}>
|
||||
*/
|
||||
class ${entity}Repository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ${entity}::class);
|
||||
}
|
||||
}
|
||||
`);
|
||||
fs.writeFileSync(controllerFile, `<?php
|
||||
|
||||
namespace App\\Controller;
|
||||
|
||||
use App\\Repository\\${entity}Repository;
|
||||
use Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;
|
||||
use Symfony\\Component\\HttpFoundation\\JsonResponse;
|
||||
use Symfony\\Component\\Routing\\Attribute\\Route;
|
||||
|
||||
class ${entity}Controller extends AbstractController
|
||||
{
|
||||
#[Route('/${route}', name: 'app_${route}_index', methods: ['GET'])]
|
||||
public function index(${entity}Repository $repository): JsonResponse
|
||||
{
|
||||
$items = array_map(
|
||||
static fn (${entity} $item) => ['id' => $item->getId(), 'title' => $item->getTitle()],
|
||||
$repository->findAll(),
|
||||
);
|
||||
|
||||
return $this->json(['results' => $items]);
|
||||
}
|
||||
}
|
||||
`);
|
||||
return jsonResult({
|
||||
kind: 'symfony',
|
||||
entity,
|
||||
route: `/${route}`,
|
||||
files: [entityFile, repoFile, controllerFile],
|
||||
nextSteps: [
|
||||
'Run doctrine migrations if Doctrine is configured (php bin/console make:migration / doctrine:migrations:migrate).',
|
||||
'Ensure Doctrine ORM and attributes are enabled for the Entity mapping.',
|
||||
],
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=generateResources.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import { registerGenerateDjangoResourceTool, registerGenerateRailsResourceTool, registerGenerateSymfonyResourceTool, } from './generateResources.js';
|
||||
export function registerBackendResourceTools(server) {
|
||||
registerGenerateDjangoResourceTool(server);
|
||||
registerGenerateRailsResourceTool(server);
|
||||
registerGenerateSymfonyResourceTool(server);
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/backends/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,kCAAkC,EAClC,iCAAiC,EACjC,mCAAmC,GACpC,MAAM,wBAAwB,CAAC;AAEhC,MAAM,UAAU,4BAA4B,CAAC,MAAiB;IAC5D,kCAAkC,CAAC,MAAM,CAAC,CAAC;IAC3C,iCAAiC,CAAC,MAAM,CAAC,CAAC;IAC1C,mCAAmC,CAAC,MAAM,CAAC,CAAC;AAC9C,CAAC"}
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
import { z } from 'zod';
|
||||
import { writeBaseline, listBaselines } from '../../lib/baselines.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { browserManager } from './browserManager.js';
|
||||
export function registerBrowserScreenshotBaselineTool(server) {
|
||||
server.registerTool('browser_screenshot_baseline', {
|
||||
title: 'Save Screenshot Baseline',
|
||||
description: 'Capture the current page (or a CSS selector) and save it as a PNG baseline under `.web-dev-mcp/baselines/{name}.png` for later visual diffs.',
|
||||
inputSchema: {
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Baseline name (no path separators), e.g. "home" or "login-form".'),
|
||||
selector: z.string().optional().describe('Optional CSS selector; if omitted, captures the page.'),
|
||||
fullPage: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Capture the full scrollable page when selector is omitted.'),
|
||||
},
|
||||
}, async ({ name, selector, fullPage }) => {
|
||||
try {
|
||||
const session = await browserManager.getSession();
|
||||
let buffer;
|
||||
if (selector) {
|
||||
const element = await session.page.locator(selector).first();
|
||||
await element.waitFor({ state: 'visible', timeout: 5000 });
|
||||
buffer = await element.screenshot({ type: 'png' });
|
||||
}
|
||||
else {
|
||||
buffer = await session.page.screenshot({ fullPage, type: 'png' });
|
||||
}
|
||||
const saved = writeBaseline(name, buffer);
|
||||
return jsonResult({
|
||||
name,
|
||||
selector: selector ?? null,
|
||||
fullPage: selector ? false : fullPage,
|
||||
sizeBytes: buffer.length,
|
||||
relativePath: saved.relativePath,
|
||||
baselines: listBaselines(),
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=baseline.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../../src/tools/browser/baseline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,UAAU,qCAAqC,CAAC,MAAiB;IACrE,MAAM,CAAC,YAAY,CACjB,6BAA6B,EAC7B;QACE,KAAK,EAAE,0BAA0B;QACjC,WAAW,EACT,8IAA8I;QAChJ,WAAW,EAAE;YACX,IAAI,EAAE,CAAC;iBACJ,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CAAC,kEAAkE,CAAC;YAC/E,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,uDAAuD,CAAC;YACjG,QAAQ,EAAE,CAAC;iBACR,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,OAAO,CAAC,KAAK,CAAC;iBACd,QAAQ,CAAC,4DAA4D,CAAC;SAC1E;KACF,EACD,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE;QACrC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,UAAU,EAAE,CAAC;YAClD,IAAI,MAAc,CAAC;YACnB,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAC;gBAC7D,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC3D,MAAM,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACrD,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACpE,CAAC;YAED,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC1C,OAAO,UAAU,CAAC;gBAChB,IAAI;gBACJ,QAAQ,EAAE,QAAQ,IAAI,IAAI;gBAC1B,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ;gBACrC,SAAS,EAAE,MAAM,CAAC,MAAM;gBACxB,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,SAAS,EAAE,aAAa,EAAE;aAC3B,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { logger } from '../../lib/logger.js';
|
||||
class BrowserManager {
|
||||
session = null;
|
||||
async getSession(headless = true) {
|
||||
if (this.session)
|
||||
return this.session;
|
||||
try {
|
||||
const browser = await chromium.launch({ headless });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
||||
const page = await context.newPage();
|
||||
const consoleLogs = [];
|
||||
page.on('console', (msg) => {
|
||||
const location = msg.location();
|
||||
consoleLogs.push({
|
||||
type: msg.type(),
|
||||
text: msg.text(),
|
||||
location: `${location.url}:${location.lineNumber}:${location.columnNumber}`,
|
||||
time: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
page.on('pageerror', (error) => {
|
||||
consoleLogs.push({
|
||||
type: 'error',
|
||||
text: `Page error: ${error.message}`,
|
||||
time: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
page.on('requestfailed', (request) => {
|
||||
consoleLogs.push({
|
||||
type: 'error',
|
||||
text: `Network request failed: ${request.method()} ${request.url()}`,
|
||||
time: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
this.session = { browser, context, page, consoleLogs };
|
||||
return this.session;
|
||||
}
|
||||
catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.includes("Executable doesn't exist") || message.includes('browserType.launch')) {
|
||||
throw new Error('Playwright Chromium browser is not installed. ' +
|
||||
'Install it with: npx playwright install chromium');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async close() {
|
||||
if (!this.session)
|
||||
return;
|
||||
try {
|
||||
await this.session.browser.close();
|
||||
}
|
||||
catch (error) {
|
||||
logger.error('error closing browser', { error: error instanceof Error ? error.message : error });
|
||||
}
|
||||
finally {
|
||||
this.session = null;
|
||||
}
|
||||
}
|
||||
resetLogs() {
|
||||
if (this.session) {
|
||||
this.session.consoleLogs.length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
export const browserManager = new BrowserManager();
|
||||
//# sourceMappingURL=browserManager.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"browserManager.js","sourceRoot":"","sources":["../../../src/tools/browser/browserManager.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
import { z } from 'zod';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { browserManager } from './browserManager.js';
|
||||
export function registerBrowserConsoleLogsTool(server) {
|
||||
server.registerTool('browser_get_console_logs', {
|
||||
title: 'Browser Console Logs',
|
||||
description: 'Return console logs, JavaScript page errors, and failed network requests captured since the last browser_navigate call (or since the browser session started).',
|
||||
inputSchema: {
|
||||
level: z
|
||||
.enum(['all', 'error', 'warn', 'log', 'info', 'debug'])
|
||||
.optional()
|
||||
.default('all')
|
||||
.describe('Filter logs by type.'),
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.default(100)
|
||||
.describe('Maximum number of logs to return.'),
|
||||
},
|
||||
}, async ({ level, limit }) => {
|
||||
try {
|
||||
const session = await browserManager.getSession();
|
||||
let logs = session.consoleLogs.slice(-limit);
|
||||
if (level !== 'all') {
|
||||
logs = logs.filter((log) => log.type === level);
|
||||
}
|
||||
return jsonResult({
|
||||
totalCaptured: session.consoleLogs.length,
|
||||
returned: logs.length,
|
||||
logs,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=consoleLogs.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"consoleLogs.js","sourceRoot":"","sources":["../../../src/tools/browser/consoleLogs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,cAAc,EAAmB,MAAM,qBAAqB,CAAC;AAEtE,MAAM,UAAU,8BAA8B,CAAC,MAAiB;IAC9D,MAAM,CAAC,YAAY,CACjB,0BAA0B,EAC1B;QACE,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EACT,gKAAgK;QAClK,WAAW,EAAE;YACX,KAAK,EAAE,CAAC;iBACL,IAAI,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;iBACtD,QAAQ,EAAE;iBACV,OAAO,CAAC,KAAK,CAAC;iBACd,QAAQ,CAAC,sBAAsB,CAAC;YACnC,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,QAAQ,EAAE;iBACV,QAAQ,EAAE;iBACV,OAAO,CAAC,GAAG,CAAC;iBACZ,QAAQ,CAAC,mCAAmC,CAAC;SACjD;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE;QACzB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,UAAU,EAAE,CAAC;YAClD,IAAI,IAAI,GAAiB,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;YAC3D,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;gBACpB,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC;YAClD,CAAC;YACD,OAAO,UAAU,CAAC;gBAChB,aAAa,EAAE,OAAO,CAAC,WAAW,CAAC,MAAM;gBACzC,QAAQ,EAAE,IAAI,CAAC,MAAM;gBACrB,IAAI;aACL,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
import { registerBrowserNavigateTool } from './navigate.js';
|
||||
import { registerBrowserScreenshotTool } from './screenshot.js';
|
||||
import { registerBrowserConsoleLogsTool } from './consoleLogs.js';
|
||||
import { registerBrowserInspectDomTool } from './inspectDom.js';
|
||||
import { registerBrowserInteractTools } from './interact.js';
|
||||
import { registerBrowserScreenshotBaselineTool } from './baseline.js';
|
||||
import { registerBrowserVisualDiffTool } from './visualDiff.js';
|
||||
export function registerBrowserTools(server) {
|
||||
registerBrowserNavigateTool(server);
|
||||
registerBrowserScreenshotTool(server);
|
||||
registerBrowserScreenshotBaselineTool(server);
|
||||
registerBrowserVisualDiffTool(server);
|
||||
registerBrowserConsoleLogsTool(server);
|
||||
registerBrowserInspectDomTool(server);
|
||||
registerBrowserInteractTools(server);
|
||||
}
|
||||
export { browserManager } from './browserManager.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/browser/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,8BAA8B,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,qCAAqC,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAC;AAEhE,MAAM,UAAU,oBAAoB,CAAC,MAAiB;IACpD,2BAA2B,CAAC,MAAM,CAAC,CAAC;IACpC,6BAA6B,CAAC,MAAM,CAAC,CAAC;IACtC,qCAAqC,CAAC,MAAM,CAAC,CAAC;IAC9C,6BAA6B,CAAC,MAAM,CAAC,CAAC;IACtC,8BAA8B,CAAC,MAAM,CAAC,CAAC;IACvC,6BAA6B,CAAC,MAAM,CAAC,CAAC;IACtC,4BAA4B,CAAC,MAAM,CAAC,CAAC;AACvC,CAAC;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
/// <reference lib="dom" />
|
||||
import { z } from 'zod';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { browserManager } from './browserManager.js';
|
||||
export function registerBrowserInspectDomTool(server) {
|
||||
server.registerTool('browser_inspect_dom', {
|
||||
title: 'Browser Inspect DOM',
|
||||
description: 'Query the DOM of the current page by CSS selector and return the outerHTML, inner text, and selected computed styles of the first matching element.',
|
||||
inputSchema: {
|
||||
selector: z.string().min(1).describe('CSS selector to query.'),
|
||||
includeStyles: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.default(['color', 'backgroundColor', 'fontSize', 'display'])
|
||||
.describe('Computed CSS properties to return.'),
|
||||
},
|
||||
}, async ({ selector, includeStyles }) => {
|
||||
try {
|
||||
const session = await browserManager.getSession();
|
||||
const element = session.page.locator(selector).first();
|
||||
await element.waitFor({ state: 'attached', timeout: 5000 });
|
||||
const outerHTML = await element.evaluate((el) => el.outerHTML).catch(() => null);
|
||||
const text = await element.innerText().catch(() => null);
|
||||
const styles = await element.evaluate((el, props) => {
|
||||
const computed = window.getComputedStyle(el);
|
||||
return Object.fromEntries(props.map((p) => [p, computed.getPropertyValue(p)]));
|
||||
}, includeStyles);
|
||||
const count = await session.page.locator(selector).count();
|
||||
return jsonResult({
|
||||
selector,
|
||||
matchCount: count,
|
||||
outerHTML,
|
||||
text,
|
||||
styles,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=inspectDom.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"inspectDom.js","sourceRoot":"","sources":["../../../src/tools/browser/inspectDom.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,UAAU,6BAA6B,CAAC,MAAiB;IAC7D,MAAM,CAAC,YAAY,CACjB,qBAAqB,EACrB;QACE,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EACT,qJAAqJ;QACvJ,WAAW,EAAE;YACX,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,wBAAwB,CAAC;YAC9D,aAAa,EAAE,CAAC;iBACb,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;iBACjB,QAAQ,EAAE;iBACV,OAAO,CAAC,CAAC,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;iBAC5D,QAAQ,CAAC,oCAAoC,CAAC;SAClD;KACF,EACD,KAAK,EAAE,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE;QACpC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,UAAU,EAAE,CAAC;YAClD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAC;YACvD,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YAE5D,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;YACjF,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;YACzD,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,QAAQ,CACnC,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE;gBACZ,MAAM,QAAQ,GAAG,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC;gBAC7C,OAAO,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACjF,CAAC,EACD,aAAa,CACd,CAAC;YACF,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAC;YAE3D,OAAO,UAAU,CAAC;gBAChB,QAAQ;gBACR,UAAU,EAAE,KAAK;gBACjB,SAAS;gBACT,IAAI;gBACJ,MAAM;aACP,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
import { z } from 'zod';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { browserManager } from './browserManager.js';
|
||||
export function registerBrowserInteractTools(server) {
|
||||
server.registerTool('browser_click', {
|
||||
title: 'Browser Click',
|
||||
description: 'Click the first element matching a CSS selector on the current page.',
|
||||
inputSchema: {
|
||||
selector: z.string().min(1).describe('CSS selector of the element to click.'),
|
||||
},
|
||||
}, async ({ selector }) => {
|
||||
try {
|
||||
const session = await browserManager.getSession();
|
||||
await session.page.locator(selector).first().click();
|
||||
return jsonResult({ clicked: true, selector });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
server.registerTool('browser_fill', {
|
||||
title: 'Browser Fill',
|
||||
description: 'Fill an input/textarea element with the provided text.',
|
||||
inputSchema: {
|
||||
selector: z.string().min(1).describe('CSS selector of the input element.'),
|
||||
value: z.string().describe('Text to type into the element.'),
|
||||
clearFirst: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe('Clear the existing value before filling.'),
|
||||
},
|
||||
}, async ({ selector, value, clearFirst }) => {
|
||||
try {
|
||||
const session = await browserManager.getSession();
|
||||
const locator = session.page.locator(selector).first();
|
||||
if (clearFirst)
|
||||
await locator.fill(value);
|
||||
else
|
||||
await locator.pressSequentially(value);
|
||||
return jsonResult({ filled: true, selector, value });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
server.registerTool('browser_eval', {
|
||||
title: 'Browser Eval',
|
||||
description: 'Evaluate a JavaScript function in the context of the current page and return the result. The function is stringified and executed in the browser, so it can access the DOM (document, window, etc.).',
|
||||
inputSchema: {
|
||||
script: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('JavaScript function body to evaluate. Must be a function expression returning a JSON-serializable value.'),
|
||||
},
|
||||
}, async ({ script }) => {
|
||||
try {
|
||||
const session = await browserManager.getSession();
|
||||
const wrapped = `(async () => { ${script} })()`;
|
||||
const result = await session.page.evaluate(wrapped);
|
||||
return jsonResult({ result });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=interact.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"interact.js","sourceRoot":"","sources":["../../../src/tools/browser/interact.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
import { z } from 'zod';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { browserManager } from './browserManager.js';
|
||||
export function registerBrowserNavigateTool(server) {
|
||||
server.registerTool('browser_navigate', {
|
||||
title: 'Browser Navigate',
|
||||
description: 'Open a URL in a headless/headed Playwright browser session. Reuses the same session across calls, so later tools (screenshot, inspect_dom, click, etc.) operate on the same page.',
|
||||
inputSchema: {
|
||||
url: z.string().url().describe('URL to navigate to, e.g. the local dev server URL from start_dev_server.'),
|
||||
headless: z.boolean().optional().default(true).describe('Run browser headlessly.'),
|
||||
waitUntil: z
|
||||
.enum(['load', 'domcontentloaded', 'networkidle'])
|
||||
.optional()
|
||||
.default('load')
|
||||
.describe('When to consider navigation complete.'),
|
||||
resetLogs: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe('Clear previously captured console logs before navigating.'),
|
||||
},
|
||||
}, async ({ url, headless, waitUntil, resetLogs: shouldResetLogs }) => {
|
||||
try {
|
||||
const session = await browserManager.getSession(headless);
|
||||
if (shouldResetLogs)
|
||||
browserManager.resetLogs();
|
||||
const response = await session.page.goto(url, { waitUntil });
|
||||
const title = await session.page.title().catch(() => '');
|
||||
return jsonResult({
|
||||
url,
|
||||
title,
|
||||
status: response?.status() ?? null,
|
||||
finalUrl: response?.url() ?? url,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=navigate.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"navigate.js","sourceRoot":"","sources":["../../../src/tools/browser/navigate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,UAAU,2BAA2B,CAAC,MAAiB;IAC3D,MAAM,CAAC,YAAY,CACjB,kBAAkB,EAClB;QACE,KAAK,EAAE,kBAAkB;QACzB,WAAW,EACT,mLAAmL;QACrL,WAAW,EAAE;YACX,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,0EAA0E,CAAC;YAC1G,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,yBAAyB,CAAC;YAClF,SAAS,EAAE,CAAC;iBACT,IAAI,CAAC,CAAC,MAAM,EAAE,kBAAkB,EAAE,aAAa,CAAC,CAAC;iBACjD,QAAQ,EAAE;iBACV,OAAO,CAAC,MAAM,CAAC;iBACf,QAAQ,CAAC,uCAAuC,CAAC;YACpD,SAAS,EAAE,CAAC;iBACT,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,OAAO,CAAC,IAAI,CAAC;iBACb,QAAQ,CAAC,2DAA2D,CAAC;SACzE;KACF,EACD,KAAK,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,eAAe,EAAE,EAAE,EAAE;QACjE,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;YAC1D,IAAI,eAAe;gBAAE,cAAc,CAAC,SAAS,EAAE,CAAC;YAChD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;YAC7D,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YACzD,OAAO,UAAU,CAAC;gBAChB,GAAG;gBACH,KAAK;gBACL,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,IAAI;gBAClC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,GAAG;aACjC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { browserManager } from './browserManager.js';
|
||||
export function registerBrowserScreenshotTool(server) {
|
||||
server.registerTool('browser_screenshot', {
|
||||
title: 'Browser Screenshot',
|
||||
description: 'Capture a screenshot of the current page or a specific element. Returns the image as a base64 data URI (PNG).',
|
||||
inputSchema: {
|
||||
selector: z.string().optional().describe('CSS selector of the element to screenshot. If omitted, captures the full page.'),
|
||||
fullPage: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Capture the full scrollable page (only used when selector is omitted).'),
|
||||
},
|
||||
}, async ({ selector, fullPage }) => {
|
||||
try {
|
||||
const session = await browserManager.getSession();
|
||||
let buffer;
|
||||
if (selector) {
|
||||
const element = await session.page.locator(selector).first();
|
||||
await element.waitFor({ state: 'visible', timeout: 5000 });
|
||||
buffer = await element.screenshot({ type: 'png' });
|
||||
}
|
||||
else {
|
||||
buffer = await session.page.screenshot({ fullPage, type: 'png' });
|
||||
}
|
||||
const base64 = buffer.toString('base64');
|
||||
const dataUri = `data:image/png;base64,${base64}`;
|
||||
return jsonResult({
|
||||
selector: selector ?? null,
|
||||
fullPage: selector ? false : fullPage,
|
||||
sizeBytes: buffer.length,
|
||||
dataUri,
|
||||
truncatedDataUri: dataUri.length > config.maxToolOutputChars ? `${dataUri.slice(0, config.maxToolOutputChars)}...` : dataUri,
|
||||
note: 'The full dataUri is returned above. If it is truncated, use the returned base64 value directly from your client.',
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=screenshot.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../../src/tools/browser/screenshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,UAAU,6BAA6B,CAAC,MAAiB;IAC7D,MAAM,CAAC,YAAY,CACjB,oBAAoB,EACpB;QACE,KAAK,EAAE,oBAAoB;QAC3B,WAAW,EACT,+GAA+G;QACjH,WAAW,EAAE;YACX,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gFAAgF,CAAC;YAC1H,QAAQ,EAAE,CAAC;iBACR,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,OAAO,CAAC,KAAK,CAAC;iBACd,QAAQ,CAAC,wEAAwE,CAAC;SACtF;KACF,EACD,KAAK,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC/B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,UAAU,EAAE,CAAC;YAClD,IAAI,MAAc,CAAC;YACnB,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAC;gBAC7D,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC3D,MAAM,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACrD,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACpE,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,yBAAyB,MAAM,EAAE,CAAC;YAClD,OAAO,UAAU,CAAC;gBAChB,QAAQ,EAAE,QAAQ,IAAI,IAAI;gBAC1B,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ;gBACrC,SAAS,EAAE,MAAM,CAAC,MAAM;gBACxB,OAAO;gBACP,gBAAgB,EAAE,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO;gBAC5H,IAAI,EAAE,kHAAkH;aACzH,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+89
@@ -0,0 +1,89 @@
|
||||
import { PNG } from 'pngjs';
|
||||
import pixelmatch from 'pixelmatch';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { readBaseline, resolveBaselinePath } from '../../lib/baselines.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { browserManager } from './browserManager.js';
|
||||
function toDataUri(png) {
|
||||
return `data:image/png;base64,${png.toString('base64')}`;
|
||||
}
|
||||
export function comparePngBuffers(baselinePng, currentPng, threshold = 0.1) {
|
||||
const baseline = PNG.sync.read(baselinePng);
|
||||
const current = PNG.sync.read(currentPng);
|
||||
if (baseline.width !== current.width || baseline.height !== current.height) {
|
||||
throw new Error(`Screenshot dimensions differ from baseline (${baseline.width}x${baseline.height} vs ${current.width}x${current.height}). ` +
|
||||
'Recapture the baseline at the same viewport/selector size.');
|
||||
}
|
||||
const { width, height } = baseline;
|
||||
const diff = new PNG({ width, height });
|
||||
const mismatchedPixels = pixelmatch(baseline.data, current.data, diff.data, width, height, {
|
||||
threshold,
|
||||
});
|
||||
const total = width * height;
|
||||
const mismatchPercent = total === 0 ? 0 : (mismatchedPixels / total) * 100;
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
mismatchedPixels,
|
||||
mismatchPercent,
|
||||
diffPng: PNG.sync.write(diff),
|
||||
};
|
||||
}
|
||||
export function registerBrowserVisualDiffTool(server) {
|
||||
server.registerTool('browser_visual_diff', {
|
||||
title: 'Visual Screenshot Diff',
|
||||
description: 'Capture the current page/element and compare it to a previously saved baseline with pixelmatch. Returns mismatch stats and a diff PNG data URI.',
|
||||
inputSchema: {
|
||||
name: z.string().min(1).describe('Baseline name previously saved with browser_screenshot_baseline.'),
|
||||
selector: z.string().optional().describe('Optional CSS selector to screenshot for comparison.'),
|
||||
fullPage: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Capture the full scrollable page when selector is omitted.'),
|
||||
threshold: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(1)
|
||||
.optional()
|
||||
.default(0.1)
|
||||
.describe('pixelmatch threshold (0–1). Lower is stricter.'),
|
||||
},
|
||||
}, async ({ name, selector, fullPage, threshold }) => {
|
||||
try {
|
||||
const baselinePng = readBaseline(name);
|
||||
const session = await browserManager.getSession();
|
||||
let currentPng;
|
||||
if (selector) {
|
||||
const element = await session.page.locator(selector).first();
|
||||
await element.waitFor({ state: 'visible', timeout: 5000 });
|
||||
currentPng = await element.screenshot({ type: 'png' });
|
||||
}
|
||||
else {
|
||||
currentPng = await session.page.screenshot({ fullPage, type: 'png' });
|
||||
}
|
||||
const comparison = comparePngBuffers(baselinePng, currentPng, threshold ?? 0.1);
|
||||
const diffDataUri = toDataUri(comparison.diffPng);
|
||||
return jsonResult({
|
||||
name,
|
||||
baselinePath: resolveBaselinePath(name),
|
||||
selector: selector ?? null,
|
||||
width: comparison.width,
|
||||
height: comparison.height,
|
||||
mismatchedPixels: comparison.mismatchedPixels,
|
||||
mismatchPercent: Number(comparison.mismatchPercent.toFixed(4)),
|
||||
passed: comparison.mismatchedPixels === 0,
|
||||
threshold: threshold ?? 0.1,
|
||||
diffDataUri,
|
||||
truncatedDiffDataUri: diffDataUri.length > config.maxToolOutputChars
|
||||
? `${diffDataUri.slice(0, config.maxToolOutputChars)}...`
|
||||
: diffDataUri,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=visualDiff.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"visualDiff.js","sourceRoot":"","sources":["../../../src/tools/browser/visualDiff.ts"],"names":[],"mappings":"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"}
|
||||
@@ -0,0 +1,144 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { kebabCase, pascalCase } from '../../lib/strings.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireCodeIgniter } from './requireCodeIgniter.js';
|
||||
function pluralize(word) {
|
||||
const lower = word.toLowerCase();
|
||||
if (lower.endsWith('ies'))
|
||||
return word;
|
||||
if (lower.endsWith('y') && !/[aeiou]y$/i.test(word))
|
||||
return `${word.slice(0, -1)}ies`;
|
||||
if (/(s|x|z|ch|sh)$/i.test(word))
|
||||
return `${word}es`;
|
||||
if (lower.endsWith('s'))
|
||||
return word;
|
||||
return `${word}s`;
|
||||
}
|
||||
export function registerGenerateCodeIgniterResourceTool(server) {
|
||||
server.registerTool('generate_codeigniter_resource', {
|
||||
title: 'Generate CodeIgniter Resource',
|
||||
description: 'One-shot scaffold of a full CodeIgniter 4 resource: model + migration + seeder + ResourceController, ' +
|
||||
'then wire the route in app/Config/Routes.php. Supports an optional API version path prefix.',
|
||||
inputSchema: {
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Resource name, e.g. "Post" or "blog_post". Converted to PascalCase for the model/controller.'),
|
||||
migrate: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('If true, run `php spark migrate` after scaffolding. Requires a configured database.'),
|
||||
apiVersion: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional API version prefix for the route, e.g. "v1" → resource at /v1/{name}.'),
|
||||
resourceController: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe('If true (default), generate a ResourceController via spark --resource.'),
|
||||
},
|
||||
}, async ({ name, migrate, apiVersion, resourceController }) => {
|
||||
try {
|
||||
const ci = requireCodeIgniter();
|
||||
const model = pascalCase(name);
|
||||
const controller = `${model}Controller`;
|
||||
const routeName = kebabCase(pluralize(model));
|
||||
const versionPrefix = apiVersion?.replace(/^\/+|\/+$/g, '') || '';
|
||||
const resourcePath = versionPrefix ? `${versionPrefix}/${routeName}` : routeName;
|
||||
const steps = [];
|
||||
const modelResult = await runCommand('php', ['spark', 'make:model', model, '--no-interaction'], {
|
||||
cwd: ci.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
steps.push({ step: 'make:model', exitCode: modelResult.exitCode, failed: modelResult.failed, output: modelResult.output });
|
||||
const migrationResult = await runCommand('php', ['spark', 'make:migration', `create_${kebabCase(pluralize(model))}_table`, '--no-interaction'], { cwd: ci.root, timeoutMs: config.scaffoldCommandTimeoutMs });
|
||||
steps.push({
|
||||
step: 'make:migration',
|
||||
exitCode: migrationResult.exitCode,
|
||||
failed: migrationResult.failed,
|
||||
output: migrationResult.output,
|
||||
});
|
||||
const seederResult = await runCommand('php', ['spark', 'make:seeder', `${model}Seeder`, '--no-interaction'], {
|
||||
cwd: ci.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
steps.push({
|
||||
step: 'make:seeder',
|
||||
exitCode: seederResult.exitCode,
|
||||
failed: seederResult.failed,
|
||||
output: seederResult.output,
|
||||
});
|
||||
const controllerArgs = ['spark', 'make:controller', controller, '--no-interaction'];
|
||||
if (resourceController !== false)
|
||||
controllerArgs.splice(3, 0, '--resource');
|
||||
const controllerResult = await runCommand('php', controllerArgs, {
|
||||
cwd: ci.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
steps.push({
|
||||
step: 'make:controller',
|
||||
exitCode: controllerResult.exitCode,
|
||||
failed: controllerResult.failed,
|
||||
output: controllerResult.output,
|
||||
});
|
||||
const routesFile = path.join(ci.root, 'app', 'Config', 'Routes.php');
|
||||
const routeLine = `$routes->resource('${resourcePath}', ['controller' => '\\App\\Controllers\\${controller}']);`;
|
||||
const routeWired = wireRoute(routesFile, routeLine);
|
||||
steps.push({ step: 'wire-route', routesFile, routeLine, wired: routeWired });
|
||||
let migrateResult = null;
|
||||
if (migrate) {
|
||||
migrateResult = await runCommand('php', ['spark', 'migrate', '--no-interaction'], {
|
||||
cwd: ci.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
steps.push({
|
||||
step: 'migrate',
|
||||
exitCode: migrateResult.exitCode,
|
||||
failed: migrateResult.failed,
|
||||
output: migrateResult.output,
|
||||
});
|
||||
}
|
||||
return jsonResult({
|
||||
model,
|
||||
controller,
|
||||
apiVersion: versionPrefix || null,
|
||||
route: `resource /${resourcePath}`,
|
||||
resourceController: resourceController !== false,
|
||||
routesFile,
|
||||
routeWired,
|
||||
steps,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
function wireRoute(routesFile, routeLine) {
|
||||
fs.mkdirSync(path.dirname(routesFile), { recursive: true });
|
||||
if (!fs.existsSync(routesFile)) {
|
||||
fs.writeFileSync(routesFile, `<?php\n\nuse CodeIgniter\\Router\\RouteCollection;\n\n/**\n * @var RouteCollection $routes\n */\n\n${routeLine}\n`);
|
||||
return true;
|
||||
}
|
||||
const existing = fs.readFileSync(routesFile, 'utf8');
|
||||
if (existing.includes(routeLine.trim()))
|
||||
return false;
|
||||
let next = existing;
|
||||
if (!/use\s+CodeIgniter\\Router\\RouteCollection\s*;/.test(next)) {
|
||||
if (next.startsWith('<?php')) {
|
||||
next = next.replace('<?php', "<?php\n\nuse CodeIgniter\\Router\\RouteCollection;");
|
||||
}
|
||||
else {
|
||||
next = `<?php\n\nuse CodeIgniter\\Router\\RouteCollection;\n\n${next}`;
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(routesFile, `${next.trimEnd()}\n${routeLine}\n`);
|
||||
return true;
|
||||
}
|
||||
//# sourceMappingURL=generateCodeIgniterResource.js.map
|
||||
File diff suppressed because one or more lines are too long
+61
@@ -0,0 +1,61 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { hasComposerDependency, readComposerJson } from '../../lib/frameworks/composer.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireCodeIgniter } from './requireCodeIgniter.js';
|
||||
export function registerGenerateCodeIgniterShieldAuthTool(server) {
|
||||
server.registerTool('generate_codeigniter_shield_auth', {
|
||||
title: 'Scaffold CodeIgniter Shield Auth',
|
||||
description: 'Install/setup CodeIgniter Shield auth helpers. Requires codeigniter4/shield. ' +
|
||||
'Runs `php spark shield:setup` (and optionally composer require). Requires confirm: true.',
|
||||
inputSchema: {
|
||||
installPackage: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('If true and Shield is missing, run composer require codeigniter4/shield first.'),
|
||||
confirm: z.boolean().optional().describe('Required as true — Shield setup modifies project files.'),
|
||||
},
|
||||
}, async ({ installPackage, confirm }) => {
|
||||
try {
|
||||
if (confirm !== true) {
|
||||
return errorResult('generate_codeigniter_shield_auth requires confirm: true.');
|
||||
}
|
||||
const ci = requireCodeIgniter();
|
||||
const composer = readComposerJson(ci.root);
|
||||
const steps = [];
|
||||
let hasShield = hasComposerDependency(composer, 'codeigniter4/shield');
|
||||
if (!hasShield && installPackage) {
|
||||
const requireResult = await runCommand('composer', ['require', 'codeigniter4/shield', '--no-interaction'], { cwd: ci.root, timeoutMs: config.scaffoldCommandTimeoutMs });
|
||||
steps.push({
|
||||
step: 'composer-require',
|
||||
exitCode: requireResult.exitCode,
|
||||
failed: requireResult.failed,
|
||||
output: requireResult.output,
|
||||
});
|
||||
if (requireResult.failed) {
|
||||
return errorResult(`Failed to install codeigniter4/shield:\n${requireResult.output}`);
|
||||
}
|
||||
hasShield = true;
|
||||
}
|
||||
if (!hasShield) {
|
||||
return errorResult('codeigniter4/shield is not installed. Pass installPackage: true (with confirm: true) ' +
|
||||
'or run composer_require with package "codeigniter4/shield" first.');
|
||||
}
|
||||
const setup = await runCommand('php', ['spark', 'shield:setup', '--no-interaction'], {
|
||||
cwd: ci.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
steps.push({ step: 'shield:setup', exitCode: setup.exitCode, failed: setup.failed, output: setup.output });
|
||||
return jsonResult({
|
||||
package: 'codeigniter4/shield',
|
||||
steps,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=generateShieldAuth.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"generateShieldAuth.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/generateShieldAuth.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { registerSparkTool } from './spark.js';
|
||||
import { registerGenerateCodeIgniterResourceTool } from './generateCodeIgniterResource.js';
|
||||
import { registerCodeIgniterTestTool } from './runCodeIgniterTests.js';
|
||||
import { registerGenerateCodeIgniterShieldAuthTool } from './generateShieldAuth.js';
|
||||
import { registerSparkDiscoverTool } from './sparkDiscover.js';
|
||||
export function registerCodeIgniterTools(server) {
|
||||
registerSparkTool(server);
|
||||
registerGenerateCodeIgniterResourceTool(server);
|
||||
registerCodeIgniterTestTool(server);
|
||||
registerGenerateCodeIgniterShieldAuthTool(server);
|
||||
registerSparkDiscoverTool(server);
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,uCAAuC,EAAE,MAAM,kCAAkC,CAAC;AAC3F,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,EAAE,yCAAyC,EAAE,MAAM,yBAAyB,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,MAAM,UAAU,wBAAwB,CAAC,MAAiB;IACxD,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,uCAAuC,CAAC,MAAM,CAAC,CAAC;IAChD,2BAA2B,CAAC,MAAM,CAAC,CAAC;IACpC,yCAAyC,CAAC,MAAM,CAAC,CAAC;IAClD,yBAAyB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC"}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { config } from '../../config.js';
|
||||
import { detectProject } from '../../lib/frameworks/detect.js';
|
||||
import { CodeIgniterAdapter } from '../../lib/frameworks/codeigniter.js';
|
||||
export function requireCodeIgniter() {
|
||||
const detected = detectProject(config.workspaceRoot);
|
||||
if (detected.backend instanceof CodeIgniterAdapter) {
|
||||
return detected.backend;
|
||||
}
|
||||
throw new Error('No CodeIgniter 4 project detected in WORKSPACE_ROOT. ' +
|
||||
'Expected composer.json with codeigniter4/framework (or a spark file) at the workspace root.');
|
||||
}
|
||||
//# sourceMappingURL=requireCodeIgniter.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"requireCodeIgniter.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/requireCodeIgniter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAEzE,MAAM,UAAU,kBAAkB;IAChC,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACrD,IAAI,QAAQ,CAAC,OAAO,YAAY,kBAAkB,EAAE,CAAC;QACnD,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC1B,CAAC;IACD,MAAM,IAAI,KAAK,CACb,uDAAuD;QACrD,6FAA6F,CAChG,CAAC;AACJ,CAAC"}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { vendorBinExists } from '../../lib/frameworks/composer.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireCodeIgniter } from './requireCodeIgniter.js';
|
||||
function vendorBin(root, bin) {
|
||||
const suffix = process.platform === 'win32' ? '.bat' : '';
|
||||
return path.join(root, 'vendor', 'bin', `${bin}${suffix}`);
|
||||
}
|
||||
function parsePhpUnitSummary(output) {
|
||||
const ok = output.match(/OK\s*\((\d+)\s+tests?,\s*(\d+)\s+assertions?\)/i);
|
||||
if (ok) {
|
||||
return { passed: true, tests: Number(ok[1]), assertions: Number(ok[2]), failures: 0, errors: 0 };
|
||||
}
|
||||
const summary = output.match(/Tests:\s*(\d+),\s*Assertions:\s*(\d+)(?:,\s*Errors:\s*(\d+))?(?:,\s*Failures:\s*(\d+))?(?:,\s*Skipped:\s*(\d+))?/i);
|
||||
if (summary) {
|
||||
return {
|
||||
passed: !/FAILURES!|ERRORS!/i.test(output),
|
||||
tests: Number(summary[1]),
|
||||
assertions: Number(summary[2]),
|
||||
errors: Number(summary[3] ?? 0),
|
||||
failures: Number(summary[4] ?? 0),
|
||||
skipped: Number(summary[5] ?? 0),
|
||||
};
|
||||
}
|
||||
return { passed: null, note: 'Could not parse a pass/fail summary from the runner output.' };
|
||||
}
|
||||
export function registerCodeIgniterTestTool(server) {
|
||||
server.registerTool('run_codeigniter_tests', {
|
||||
title: 'Run CodeIgniter Tests',
|
||||
description: "Run the CodeIgniter 4 project's PHPUnit-based test suite (CIUnitTestCase) via vendor/bin/phpunit, " +
|
||||
'or `php spark test` as a fallback. Returns parsed pass/fail counts when possible.',
|
||||
inputSchema: {
|
||||
extraArgs: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Extra CLI args, e.g. ["--filter", "UserTest"].'),
|
||||
},
|
||||
}, async ({ extraArgs }) => {
|
||||
try {
|
||||
const ci = requireCodeIgniter();
|
||||
const args = extraArgs ?? [];
|
||||
let command;
|
||||
let cmdArgs;
|
||||
if (vendorBinExists(ci.root, 'phpunit')) {
|
||||
command = vendorBin(ci.root, 'phpunit');
|
||||
cmdArgs = args;
|
||||
}
|
||||
else if (vendorBinExists(ci.root, 'spark')) {
|
||||
// CI4 does not ship a built-in `spark test` command out of the box,
|
||||
// but projects sometimes add one. If phpunit is missing, try it.
|
||||
command = 'php';
|
||||
cmdArgs = ['spark', 'test', ...args];
|
||||
}
|
||||
else {
|
||||
return errorResult(`No vendor/bin/phpunit found at ${ci.root}. Run composer install (or run_install) first.`);
|
||||
}
|
||||
const result = await runCommand(command, cmdArgs, {
|
||||
cwd: ci.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({ runner: 'phpunit', summary: parsePhpUnitSummary(result.output), ...result });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=runCodeIgniterTests.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"runCodeIgniterTests.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/runCodeIgniterTests.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { isAllowedSparkCommand, SPARK_ALLOWED, SPARK_DESTRUCTIVE } from './sparkAllowList.js';
|
||||
import { requireCodeIgniter } from './requireCodeIgniter.js';
|
||||
export function registerSparkTool(server) {
|
||||
server.registerTool('spark', {
|
||||
title: 'Run Spark Command',
|
||||
description: 'Run a whitelisted `php spark` subcommand in the detected CodeIgniter 4 project and return stdout/stderr. ' +
|
||||
`Allowed commands: ${SPARK_ALLOWED.join(', ')}. ` +
|
||||
'Destructive command (migrate:refresh) requires confirm: true.',
|
||||
inputSchema: {
|
||||
command: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Spark subcommand, e.g. "make:controller" or "migrate".'),
|
||||
args: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.default([])
|
||||
.describe('Arguments forwarded after the subcommand, e.g. ["PostController", "--resource"].'),
|
||||
confirm: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Required as true for destructive command (migrate:refresh).'),
|
||||
},
|
||||
}, async ({ command, args, confirm }) => {
|
||||
try {
|
||||
if (!isAllowedSparkCommand(command)) {
|
||||
return errorResult(`Spark command "${command}" is not on the allow-list. Allowed: ${SPARK_ALLOWED.join(', ')}.`);
|
||||
}
|
||||
if (SPARK_DESTRUCTIVE.has(command) && confirm !== true) {
|
||||
return errorResult(`Spark command "${command}" is destructive and requires confirm: true.`);
|
||||
}
|
||||
const ci = requireCodeIgniter();
|
||||
const phpArgs = ['spark', command, ...(args ?? [])];
|
||||
const result = await runCommand('php', phpArgs, {
|
||||
cwd: ci.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({
|
||||
sparkCommand: command,
|
||||
sparkArgs: args ?? [],
|
||||
exitCode: result.exitCode,
|
||||
timedOut: result.timedOut,
|
||||
failed: result.failed,
|
||||
output: result.output,
|
||||
cwd: result.cwd,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=spark.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"spark.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/spark.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,UAAU,iBAAiB,CAAC,MAAiB;IACjD,MAAM,CAAC,YAAY,CACjB,OAAO,EACP;QACE,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EACT,2GAA2G;YAC3G,qBAAqB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YACjD,+DAA+D;QACjE,WAAW,EAAE;YACX,OAAO,EAAE,CAAC;iBACP,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CAAC,wDAAwD,CAAC;YACrE,IAAI,EAAE,CAAC;iBACJ,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;iBACjB,QAAQ,EAAE;iBACV,OAAO,CAAC,EAAE,CAAC;iBACX,QAAQ,CAAC,kFAAkF,CAAC;YAC/F,OAAO,EAAE,CAAC;iBACP,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,QAAQ,CAAC,6DAA6D,CAAC;SAC3E;KACF,EACD,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;QACnC,IAAI,CAAC;YACH,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,CAAC;gBACpC,OAAO,WAAW,CAChB,kBAAkB,OAAO,wCAAwC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC7F,CAAC;YACJ,CAAC;YACD,IAAI,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACvD,OAAO,WAAW,CAChB,kBAAkB,OAAO,8CAA8C,CACxE,CAAC;YACJ,CAAC;YAED,MAAM,EAAE,GAAG,kBAAkB,EAAE,CAAC;YAChC,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;YAEpD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE;gBAC9C,GAAG,EAAE,EAAE,CAAC,IAAI;gBACZ,SAAS,EAAE,MAAM,CAAC,wBAAwB;aAC3C,CAAC,CAAC;YACH,OAAO,UAAU,CAAC;gBAChB,YAAY,EAAE,OAAO;gBACrB,SAAS,EAAE,IAAI,IAAI,EAAE;gBACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,GAAG,EAAE,MAAM,CAAC,GAAG;aAChB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Spark subcommands the `spark` tool is willing to run.
|
||||
* Anything not on this list is rejected so the agent cannot pass arbitrary
|
||||
* spark commands (e.g. `spark make:migration` with a bad path, destructive
|
||||
* migrations without confirm, etc.).
|
||||
*/
|
||||
export const SPARK_ALLOWED = [
|
||||
'make:controller',
|
||||
'make:model',
|
||||
'make:entity',
|
||||
'make:filter',
|
||||
'make:command',
|
||||
'make:validation',
|
||||
'make:migration',
|
||||
'make:seeder',
|
||||
'make:test',
|
||||
'make:config',
|
||||
'migrate',
|
||||
'migrate:rollback',
|
||||
'migrate:refresh',
|
||||
'migrate:status',
|
||||
'db:seed',
|
||||
'routes',
|
||||
'cache:clear',
|
||||
'optimize',
|
||||
'list',
|
||||
'about',
|
||||
];
|
||||
export const SPARK_DESTRUCTIVE = new Set(['migrate:refresh']);
|
||||
export function isAllowedSparkCommand(command) {
|
||||
return SPARK_ALLOWED.includes(command);
|
||||
}
|
||||
//# sourceMappingURL=sparkAllowList.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sparkAllowList.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/sparkAllowList.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,iBAAiB;IACjB,YAAY;IACZ,aAAa;IACb,aAAa;IACb,cAAc;IACd,iBAAiB;IACjB,gBAAgB;IAChB,aAAa;IACb,WAAW;IACX,aAAa;IACb,SAAS;IACT,kBAAkB;IAClB,iBAAiB;IACjB,gBAAgB;IAChB,SAAS;IACT,QAAQ;IACR,aAAa;IACb,UAAU;IACV,MAAM;IACN,OAAO;CACC,CAAC;AAIX,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAS,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAEtE,MAAM,UAAU,qBAAqB,CAAC,OAAe;IACnD,OAAQ,aAAmC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AAChE,CAAC"}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { isAllowedSparkCommand, SPARK_ALLOWED } from './sparkAllowList.js';
|
||||
import { requireCodeIgniter } from './requireCodeIgniter.js';
|
||||
export function registerSparkDiscoverTool(server) {
|
||||
server.registerTool('spark_discover', {
|
||||
title: 'Discover Spark Commands',
|
||||
description: 'Run `php spark list` and return only commands that are on the server allow-list (intersected with SPARK_ALLOWED).',
|
||||
inputSchema: {},
|
||||
}, async () => {
|
||||
try {
|
||||
const ci = requireCodeIgniter();
|
||||
const result = await runCommand('php', ['spark', 'list', '--no-interaction'], {
|
||||
cwd: ci.root,
|
||||
timeoutMs: config.defaultCommandTimeoutMs,
|
||||
});
|
||||
const discovered = new Set();
|
||||
for (const line of result.output.split(/\r?\n/)) {
|
||||
const match = line.match(/^\s*([a-z][\w:-]*)\s+/i);
|
||||
const command = match?.[1];
|
||||
if (command && isAllowedSparkCommand(command)) {
|
||||
discovered.add(command);
|
||||
}
|
||||
}
|
||||
// Also include allow-listed commands even if spark list formatting hid them.
|
||||
for (const allowed of SPARK_ALLOWED) {
|
||||
if (result.output.includes(allowed))
|
||||
discovered.add(allowed);
|
||||
}
|
||||
return jsonResult({
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
allowed: [...SPARK_ALLOWED],
|
||||
discovered: [...discovered].sort(),
|
||||
note: 'Only allow-listed spark commands are returned. Use the spark tool to run one.',
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=sparkDiscover.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sparkDiscover.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/sparkDiscover.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,UAAU,yBAAyB,CAAC,MAAiB;IACzD,MAAM,CAAC,YAAY,CACjB,gBAAgB,EAChB;QACE,KAAK,EAAE,yBAAyB;QAChC,WAAW,EACT,mHAAmH;QACrH,WAAW,EAAE,EAAE;KAChB,EACD,KAAK,IAAI,EAAE;QACT,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,kBAAkB,EAAE,CAAC;YAChC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,kBAAkB,CAAC,EAAE;gBAC5E,GAAG,EAAE,EAAE,CAAC,IAAI;gBACZ,SAAS,EAAE,MAAM,CAAC,uBAAuB;aAC1C,CAAC,CAAC;YAEH,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;YACrC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;gBAChD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;gBACnD,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;gBAC3B,IAAI,OAAO,IAAI,qBAAqB,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC9C,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC;YAED,6EAA6E;YAC7E,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;gBACpC,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;oBAAE,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAC/D,CAAC;YAED,OAAO,UAAU,CAAC;gBAChB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,OAAO,EAAE,CAAC,GAAG,aAAa,CAAC;gBAC3B,UAAU,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,EAAE;gBAClC,IAAI,EAAE,+EAA+E;aACtF,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager } from './dockerManager.js';
|
||||
export function registerDockerBuildImageTool(server) {
|
||||
server.registerTool('docker_build_image', {
|
||||
title: 'Docker Build Image',
|
||||
description: 'Build a Docker image from a Dockerfile in the workspace. Streams the build output back and returns the resulting image tag.',
|
||||
inputSchema: {
|
||||
tag: z.string().min(1).describe('Image tag, e.g. "my-app:latest".'),
|
||||
dockerfile: z
|
||||
.string()
|
||||
.optional()
|
||||
.default('Dockerfile')
|
||||
.describe('Path to the Dockerfile, relative to the workspace root.'),
|
||||
buildContext: z
|
||||
.string()
|
||||
.optional()
|
||||
.default('.')
|
||||
.describe('Build context directory, relative to the workspace root.'),
|
||||
},
|
||||
}, async ({ tag, dockerfile, buildContext }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
const dockerfilePath = resolveWorkspacePath(config.workspaceRoot, dockerfile);
|
||||
const contextPath = resolveWorkspacePath(config.workspaceRoot, buildContext);
|
||||
if (!fs.existsSync(dockerfilePath)) {
|
||||
return errorResult(`Dockerfile not found: ${dockerfilePath}`);
|
||||
}
|
||||
const stream = await dockerManager.getDocker().buildImage({
|
||||
context: contextPath,
|
||||
src: [path.basename(dockerfilePath), ...listContextFiles(contextPath)],
|
||||
}, { t: tag, dockerfile: path.basename(dockerfilePath) });
|
||||
const output = [];
|
||||
await new Promise((resolve, reject) => {
|
||||
dockerManager
|
||||
.getDocker()
|
||||
.modem.followProgress(stream, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const last = res?.[res.length - 1];
|
||||
if (last && 'error' in last && last.error) {
|
||||
reject(new Error(String(last.error)));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
}, (event) => {
|
||||
const line = typeof event === 'string' ? event : JSON.stringify(event);
|
||||
output.push(line);
|
||||
if (output.length > config.maxBufferedLogLines) {
|
||||
output.splice(0, output.length - config.maxBufferedLogLines);
|
||||
}
|
||||
});
|
||||
});
|
||||
return jsonResult({
|
||||
tag,
|
||||
dockerfile: dockerfilePath,
|
||||
context: contextPath,
|
||||
output: output.join('\n').slice(-config.maxToolOutputChars),
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
function listContextFiles(root) {
|
||||
const files = [];
|
||||
const walk = (dir) => {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
const rel = path.relative(root, full);
|
||||
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist')
|
||||
continue;
|
||||
if (entry.isDirectory()) {
|
||||
walk(full);
|
||||
}
|
||||
else {
|
||||
files.push(rel);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(root);
|
||||
return files;
|
||||
}
|
||||
//# sourceMappingURL=buildImage.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"buildImage.js","sourceRoot":"","sources":["../../../src/tools/docker/buildImage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,UAAU,4BAA4B,CAAC,MAAiB;IAC5D,MAAM,CAAC,YAAY,CACjB,oBAAoB,EACpB;QACE,KAAK,EAAE,oBAAoB;QAC3B,WAAW,EACT,6HAA6H;QAC/H,WAAW,EAAE;YACX,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,kCAAkC,CAAC;YACnE,UAAU,EAAE,CAAC;iBACV,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,OAAO,CAAC,YAAY,CAAC;iBACrB,QAAQ,CAAC,yDAAyD,CAAC;YACtE,YAAY,EAAE,CAAC;iBACZ,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,OAAO,CAAC,GAAG,CAAC;iBACZ,QAAQ,CAAC,0DAA0D,CAAC;SACxE;KACF,EACD,KAAK,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,YAAY,EAAE,EAAE,EAAE;QAC1C,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,eAAe,EAAE,CAAC;YACtC,MAAM,cAAc,GAAG,oBAAoB,CAAC,MAAM,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;YAC9E,MAAM,WAAW,GAAG,oBAAoB,CAAC,MAAM,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;YAC7E,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;gBACnC,OAAO,WAAW,CAAC,yBAAyB,cAAc,EAAE,CAAC,CAAC;YAChE,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,SAAS,EAAE,CAAC,UAAU,CACvD;gBACE,OAAO,EAAE,WAAW;gBACpB,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;aACvE,EACD,EAAE,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CACtD,CAAC;YAEF,MAAM,MAAM,GAAa,EAAE,CAAC;YAC5B,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAC1C,aAAa;qBACV,SAAS,EAAE;qBACX,KAAK,CAAC,cAAc,CACnB,MAAM,EACN,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;oBACX,IAAI,GAAG,EAAE,CAAC;wBACR,MAAM,CAAC,GAAG,CAAC,CAAC;wBACZ,OAAO;oBACT,CAAC;oBACD,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;oBACnC,IAAI,IAAI,IAAI,OAAO,IAAI,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;wBAC1C,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;wBACtC,OAAO;oBACT,CAAC;oBACD,OAAO,EAAE,CAAC;gBACZ,CAAC,EACD,CAAC,KAAK,EAAE,EAAE;oBACR,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;oBACvE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBAClB,IAAI,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,mBAAmB,EAAE,CAAC;wBAC/C,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;oBAC/D,CAAC;gBACH,CAAC,CACF,CAAC;YACN,CAAC,CAAC,CAAC;YAEH,OAAO,UAAU,CAAC;gBAChB,GAAG;gBACH,UAAU,EAAE,cAAc;gBAC1B,OAAO,EAAE,WAAW;gBACpB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,kBAAkB,CAAC;aAC5D,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,EAAE;QAC3B,MAAM,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACxC,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACtC,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;gBAAE,SAAS;YAC9F,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,IAAI,CAAC,IAAI,CAAC,CAAC;YACb,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAClB,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IACF,IAAI,CAAC,IAAI,CAAC,CAAC;IACX,OAAO,KAAK,CAAC;AACf,CAAC"}
|
||||
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
/**
|
||||
* Docker Compose is invoked via the local `docker compose` or `docker-compose`
|
||||
* CLI. The server does not parse compose files itself; it delegates to the
|
||||
* official tool and streams the output back, just like workflow tools.
|
||||
*/
|
||||
export function registerDockerComposeTools(server) {
|
||||
server.registerTool('docker_compose_up', {
|
||||
title: 'Docker Compose Up',
|
||||
description: 'Bring up a docker-compose.yml stack in the workspace root. Detached by default so the server does not block.',
|
||||
inputSchema: {
|
||||
file: z
|
||||
.string()
|
||||
.optional()
|
||||
.default('docker-compose.yml')
|
||||
.describe('Compose file path, relative to the workspace root.'),
|
||||
services: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Specific services to start. Omit to start all.'),
|
||||
build: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Build images before starting.'),
|
||||
},
|
||||
}, async ({ file, services, build }) => {
|
||||
try {
|
||||
const args = ['compose', '-f', file, 'up', '-d'];
|
||||
if (build)
|
||||
args.push('--build');
|
||||
if (services && services.length > 0)
|
||||
args.push(...services);
|
||||
const result = await runCommand('docker', args, {
|
||||
cwd: config.workspaceRoot,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({ file, ...result });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
server.registerTool('docker_compose_down', {
|
||||
title: 'Docker Compose Down',
|
||||
description: 'Shut down a docker-compose.yml stack in the workspace root.',
|
||||
inputSchema: {
|
||||
file: z
|
||||
.string()
|
||||
.optional()
|
||||
.default('docker-compose.yml')
|
||||
.describe('Compose file path, relative to the workspace root.'),
|
||||
volumes: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Also remove named volumes declared in the volumes section.'),
|
||||
},
|
||||
}, async ({ file, volumes }) => {
|
||||
try {
|
||||
const args = ['compose', '-f', file, 'down'];
|
||||
if (volumes)
|
||||
args.push('-v');
|
||||
const result = await runCommand('docker', args, {
|
||||
cwd: config.workspaceRoot,
|
||||
timeoutMs: config.defaultCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({ file, ...result });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=compose.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../../../src/tools/docker/compose.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager } from './dockerManager.js';
|
||||
export function registerDockerContainerLogsTool(server) {
|
||||
server.registerTool('docker_get_container_logs', {
|
||||
title: 'Docker Get Container Logs',
|
||||
description: 'Fetch logs from a running or stopped Docker container started by this server.',
|
||||
inputSchema: {
|
||||
containerId: z.string().min(1).describe('Container ID (short or long) returned by docker_run_container.'),
|
||||
tail: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.default(200)
|
||||
.describe('Only return the last N lines.'),
|
||||
follow: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Stream logs (not implemented; kept for API compatibility).'),
|
||||
},
|
||||
}, async ({ containerId, tail, follow }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
if (follow) {
|
||||
return jsonResult({
|
||||
containerId,
|
||||
note: 'Live log streaming is not supported via this tool; use docker_get_container_logs with follow:false to poll.',
|
||||
logs: '',
|
||||
});
|
||||
}
|
||||
const docker = dockerManager.getDocker();
|
||||
const buffer = await docker.getContainer(containerId).logs({
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
tail,
|
||||
});
|
||||
const raw = Buffer.isBuffer(buffer) ? buffer.toString('utf8') : String(buffer);
|
||||
return jsonResult({
|
||||
containerId,
|
||||
logs: raw.slice(-config.maxToolOutputChars),
|
||||
bytes: raw.length,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=containerLogs.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"containerLogs.js","sourceRoot":"","sources":["../../../src/tools/docker/containerLogs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,UAAU,+BAA+B,CAAC,MAAiB;IAC/D,MAAM,CAAC,YAAY,CACjB,2BAA2B,EAC3B;QACE,KAAK,EAAE,2BAA2B;QAClC,WAAW,EACT,+EAA+E;QACjF,WAAW,EAAE;YACX,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,gEAAgE,CAAC;YACzG,IAAI,EAAE,CAAC;iBACJ,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,QAAQ,EAAE;iBACV,QAAQ,EAAE;iBACV,OAAO,CAAC,GAAG,CAAC;iBACZ,QAAQ,CAAC,+BAA+B,CAAC;YAC5C,MAAM,EAAE,CAAC;iBACN,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,OAAO,CAAC,KAAK,CAAC;iBACd,QAAQ,CAAC,4DAA4D,CAAC;SAC1E;KACF,EACD,KAAK,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE;QACtC,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,eAAe,EAAE,CAAC;YACtC,IAAI,MAAM,EAAE,CAAC;gBACX,OAAO,UAAU,CAAC;oBAChB,WAAW;oBACX,IAAI,EAAE,6GAA6G;oBACnH,IAAI,EAAE,EAAE;iBACT,CAAC,CAAC;YACL,CAAC;YAED,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,EAAE,CAAC;YACzC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC;gBACzD,MAAM,EAAE,IAAI;gBACZ,MAAM,EAAE,IAAI;gBACZ,IAAI;aACL,CAAC,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC/E,OAAO,UAAU,CAAC;gBAChB,WAAW;gBACX,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,kBAAkB,CAAC;gBAC3C,KAAK,EAAE,GAAG,CAAC,MAAM;aAClB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
import Docker from 'dockerode';
|
||||
import { config } from '../../config.js';
|
||||
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
||||
import { logger } from '../../lib/logger.js';
|
||||
class DockerManager {
|
||||
docker;
|
||||
containers = new Map();
|
||||
daemonReachable = null;
|
||||
constructor() {
|
||||
this.docker = new Docker();
|
||||
}
|
||||
async ensureReachable() {
|
||||
if (this.daemonReachable === true)
|
||||
return;
|
||||
try {
|
||||
await this.docker.ping();
|
||||
this.daemonReachable = true;
|
||||
}
|
||||
catch (error) {
|
||||
this.daemonReachable = false;
|
||||
throw new Error('Docker daemon is not reachable. Make sure Docker is running and the current user can access it. ' +
|
||||
`Original error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
getDocker() {
|
||||
return this.docker;
|
||||
}
|
||||
track(containerId, image, name, ports) {
|
||||
if (this.containers.size >= config.maxConcurrentContainers) {
|
||||
throw new Error(`Refusing to start another container: already at the max of ${config.maxConcurrentContainers} concurrent containers.`);
|
||||
}
|
||||
const shortId = containerId.substring(0, 12);
|
||||
const managed = {
|
||||
id: shortId,
|
||||
image,
|
||||
name,
|
||||
startedAt: new Date().toISOString(),
|
||||
ports,
|
||||
};
|
||||
this.containers.set(shortId, managed);
|
||||
return managed;
|
||||
}
|
||||
getTracked(id) {
|
||||
return this.containers.get(id) ?? this.containers.get(id.substring(0, 12));
|
||||
}
|
||||
listTracked() {
|
||||
return Array.from(this.containers.values());
|
||||
}
|
||||
untrack(id) {
|
||||
this.containers.delete(id.substring(0, 12));
|
||||
}
|
||||
async cleanupAll() {
|
||||
for (const [id, managed] of this.containers.entries()) {
|
||||
try {
|
||||
const container = this.docker.getContainer(id);
|
||||
await container.stop({ t: 5 }).catch(() => null);
|
||||
await container.remove({ force: true }).catch(() => null);
|
||||
}
|
||||
catch (error) {
|
||||
logger.error(`failed to cleanup container ${managed.name} (${id})`, {
|
||||
error: error instanceof Error ? error.message : error,
|
||||
});
|
||||
}
|
||||
finally {
|
||||
this.containers.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export const dockerManager = new DockerManager();
|
||||
/**
|
||||
* Validates that a volume mount specification keeps the source path inside the
|
||||
* workspace root. Rejects Docker socket mounts, host-relative escapes, etc.
|
||||
*/
|
||||
export function validateVolumeMount(spec) {
|
||||
const parts = spec.split(':');
|
||||
if (parts.length < 2) {
|
||||
throw new Error(`Invalid volume mount "${spec}": expected format "source:target[:mode]".`);
|
||||
}
|
||||
const source = parts[0] ?? '';
|
||||
const target = parts[1] ?? '';
|
||||
const mode = parts[2] ?? 'rw';
|
||||
if (!source || !target) {
|
||||
throw new Error(`Invalid volume mount "${spec}": source and target are required.`);
|
||||
}
|
||||
const absoluteSource = resolveWorkspacePath(config.workspaceRoot, source);
|
||||
if (absoluteSource.toLowerCase().startsWith('\\\\.\\pipe\\docker_engine') || source.includes('/var/run/docker.sock')) {
|
||||
throw new Error(`Refusing to mount the Docker socket: "${source}".`);
|
||||
}
|
||||
return { source: absoluteSource, target, mode };
|
||||
}
|
||||
export function isAllowListedImage(image) {
|
||||
const base = (image.split(':')[0] ?? '').split('/').pop()?.toLowerCase() ?? '';
|
||||
return config.dockerAllowedBaseImages.some((allowed) => base === allowed || base.startsWith(allowed));
|
||||
}
|
||||
export function isLocallyBuiltImage(image) {
|
||||
// Heuristic: an image built by this server via docker_build_image won't have
|
||||
// a registry prefix. We treat "my-app" or "web-dev-mcp" as local; anything
|
||||
// with a registry host (contains '.' or '/') is not.
|
||||
return !/[/.]/.test(image);
|
||||
}
|
||||
//# sourceMappingURL=dockerManager.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"dockerManager.js","sourceRoot":"","sources":["../../../src/tools/docker/dockerManager.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager } from './dockerManager.js';
|
||||
export function registerDockerExecTool(server) {
|
||||
server.registerTool('docker_exec', {
|
||||
title: 'Docker Exec',
|
||||
description: 'Run a one-off command inside a running Docker container started by this server and return stdout/stderr.',
|
||||
inputSchema: {
|
||||
containerId: z.string().min(1).describe('Container ID (short or long) returned by docker_run_container.'),
|
||||
command: z.array(z.string()).min(1).describe('Command and arguments to execute inside the container.'),
|
||||
workingDir: z.string().optional().describe('Working directory inside the container.'),
|
||||
env: z.record(z.string()).optional().describe('Additional environment variables for the exec.'),
|
||||
},
|
||||
}, async ({ containerId, command, workingDir, env }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
const docker = dockerManager.getDocker();
|
||||
const exec = await docker.getContainer(containerId).exec({
|
||||
Cmd: command,
|
||||
WorkingDir: workingDir,
|
||||
Env: env ? Object.entries(env).map(([k, v]) => `${k}=${v}`) : undefined,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
});
|
||||
const stream = await exec.start({ Detach: false, Tty: false });
|
||||
const chunks = [];
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.on('data', (chunk) => chunks.push(chunk));
|
||||
stream.on('end', resolve);
|
||||
stream.on('error', reject);
|
||||
});
|
||||
const raw = Buffer.concat(chunks).toString('utf8');
|
||||
const result = await exec.inspect();
|
||||
return jsonResult({
|
||||
containerId,
|
||||
command,
|
||||
exitCode: result.ExitCode ?? null,
|
||||
output: raw.slice(-config.maxToolOutputChars),
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=exec.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"exec.js","sourceRoot":"","sources":["../../../src/tools/docker/exec.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+232
@@ -0,0 +1,232 @@
|
||||
import fs from 'node:fs';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { detectProject, resolvePackage } from '../../lib/frameworks/detect.js';
|
||||
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { requireLaravel } from '../laravel/requireLaravel.js';
|
||||
import { errorResult, jsonResult, packageSchema, toErrorMessage } from '../shared.js';
|
||||
function nodeDockerfile() {
|
||||
return `# Generated by web-dev-mcp
|
||||
FROM node:24-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:24-alpine
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
COPY --from=builder /app/dist ./dist
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/index.js"]
|
||||
`;
|
||||
}
|
||||
function phpDockerfile(kind) {
|
||||
const publicDir = kind === 'laravel' ? 'public' : 'public';
|
||||
return `# Generated by web-dev-mcp for ${kind}
|
||||
FROM php:8.3-cli-alpine AS vendor
|
||||
WORKDIR /app
|
||||
COPY composer.json composer.lock* ./
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \\
|
||||
&& composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist || composer install --no-interaction --prefer-dist
|
||||
|
||||
FROM php:8.3-apache
|
||||
WORKDIR /var/www/html
|
||||
RUN docker-php-ext-install pdo pdo_mysql
|
||||
COPY --from=vendor /app/vendor ./vendor
|
||||
COPY . .
|
||||
ENV APACHE_DOCUMENT_ROOT=/var/www/html/${publicDir}
|
||||
RUN sed -ri -e 's!/var/www/html!\\\${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \\
|
||||
&& a2enmod rewrite
|
||||
EXPOSE 80
|
||||
`;
|
||||
}
|
||||
function nginxNodeCompose() {
|
||||
return `# Generated by web-dev-mcp
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
depends_on:
|
||||
- redis
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: app
|
||||
POSTGRES_PASSWORD: app
|
||||
POSTGRES_DB: app
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
db_data:
|
||||
`;
|
||||
}
|
||||
function phpCompose() {
|
||||
return `# Generated by web-dev-mcp
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
APP_ENV: local
|
||||
DB_HOST: db
|
||||
DB_DATABASE: app
|
||||
DB_USERNAME: app
|
||||
DB_PASSWORD: app
|
||||
REDIS_HOST: redis
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
db:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_DATABASE: app
|
||||
MYSQL_USER: app
|
||||
MYSQL_PASSWORD: app
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
db_data:
|
||||
`;
|
||||
}
|
||||
export function registerGenerateDockerfileTool(server) {
|
||||
server.registerTool('generate_dockerfile', {
|
||||
title: 'Generate Dockerfile',
|
||||
description: 'Write a stack-aware Dockerfile at the workspace (or package) root based on detected framework (Node multi-stage or PHP/Apache for Laravel/CodeIgniter).',
|
||||
inputSchema: {
|
||||
package: packageSchema,
|
||||
overwrite: z.boolean().optional().default(false),
|
||||
},
|
||||
}, async ({ package: packageName, overwrite }) => {
|
||||
try {
|
||||
const root = packageName
|
||||
? resolvePackage(config.workspaceRoot, packageName).pkg.absolutePath
|
||||
: config.workspaceRoot;
|
||||
const detected = detectProject(root);
|
||||
const file = resolveWorkspacePath(root, 'Dockerfile');
|
||||
if (!overwrite && fs.existsSync(file)) {
|
||||
return errorResult(`"${file}" already exists. Pass overwrite: true to replace it.`);
|
||||
}
|
||||
let contents;
|
||||
let stack;
|
||||
if (detected.backend?.kind === 'laravel' ||
|
||||
detected.backend?.kind === 'codeigniter' ||
|
||||
detected.backend?.kind === 'symfony') {
|
||||
stack = detected.backend.kind;
|
||||
contents = phpDockerfile(detected.backend.kind === 'codeigniter' ? 'codeigniter' : 'laravel');
|
||||
}
|
||||
else {
|
||||
const front = detected.frontend?.kind;
|
||||
stack =
|
||||
front === 'react' || front === 'vue' || front === 'nuxt'
|
||||
? `${front}/node`
|
||||
: detected.backend?.kind === 'django' || detected.backend?.kind === 'rails'
|
||||
? detected.backend.kind
|
||||
: 'node';
|
||||
contents = nodeDockerfile();
|
||||
}
|
||||
fs.writeFileSync(file, contents);
|
||||
return jsonResult({ file, stack, package: packageName ?? null });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGenerateComposeTool(server) {
|
||||
server.registerTool('generate_compose', {
|
||||
title: 'Generate docker-compose.yml',
|
||||
description: 'Write a docker-compose.yml with app + db + redis presets inferred from the detected stack (Postgres for Node, MySQL for PHP).',
|
||||
inputSchema: {
|
||||
package: packageSchema,
|
||||
overwrite: z.boolean().optional().default(false),
|
||||
},
|
||||
}, async ({ package: packageName, overwrite }) => {
|
||||
try {
|
||||
const root = packageName
|
||||
? resolvePackage(config.workspaceRoot, packageName).pkg.absolutePath
|
||||
: config.workspaceRoot;
|
||||
const detected = detectProject(root);
|
||||
const file = resolveWorkspacePath(root, 'docker-compose.yml');
|
||||
if (!overwrite && fs.existsSync(file)) {
|
||||
return errorResult(`"${file}" already exists. Pass overwrite: true to replace it.`);
|
||||
}
|
||||
const isPhp = detected.backend?.kind === 'laravel' ||
|
||||
detected.backend?.kind === 'codeigniter' ||
|
||||
detected.backend?.kind === 'symfony';
|
||||
const contents = isPhp ? phpCompose() : nginxNodeCompose();
|
||||
fs.writeFileSync(file, contents);
|
||||
return jsonResult({
|
||||
file,
|
||||
stack: isPhp
|
||||
? detected.backend?.kind
|
||||
: (detected.frontend?.kind ?? detected.backend?.kind ?? 'node'),
|
||||
services: ['app', 'db', 'redis'],
|
||||
package: packageName ?? null,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGenerateSailTool(server) {
|
||||
server.registerTool('generate_sail', {
|
||||
title: 'Install Laravel Sail',
|
||||
description: 'Run `php artisan sail:install` in a detected Laravel project. Requires confirm: true because it modifies docker-compose and related files.',
|
||||
inputSchema: {
|
||||
with: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Optional Sail services, e.g. ["mysql", "redis", "mailpit"].'),
|
||||
confirm: z.boolean().optional().describe('Required as true to run sail:install.'),
|
||||
},
|
||||
}, async ({ with: services, confirm }) => {
|
||||
try {
|
||||
if (confirm !== true) {
|
||||
return errorResult('generate_sail requires confirm: true.');
|
||||
}
|
||||
const laravel = requireLaravel();
|
||||
const args = ['artisan', 'sail:install', '--no-interaction'];
|
||||
if (services && services.length > 0) {
|
||||
args.push(`--with=${services.join(',')}`);
|
||||
}
|
||||
const result = await runCommand('php', args, {
|
||||
cwd: laravel.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({
|
||||
services: services ?? null,
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
output: result.output,
|
||||
cwd: result.cwd,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=generate.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
import { registerDockerBuildImageTool } from './buildImage.js';
|
||||
import { registerDockerRunContainerTool } from './runContainer.js';
|
||||
import { registerDockerListContainersTool } from './listContainers.js';
|
||||
import { registerDockerContainerLogsTool } from './containerLogs.js';
|
||||
import { registerDockerExecTool } from './exec.js';
|
||||
import { registerDockerStopRemoveTools } from './stopRemove.js';
|
||||
import { registerDockerComposeTools } from './compose.js';
|
||||
import { registerGenerateComposeTool, registerGenerateDockerfileTool, registerGenerateSailTool, } from './generate.js';
|
||||
import { registerDockerPushImageTool } from './pushImage.js';
|
||||
export function registerDockerTools(server) {
|
||||
registerDockerBuildImageTool(server);
|
||||
registerDockerRunContainerTool(server);
|
||||
registerDockerListContainersTool(server);
|
||||
registerDockerContainerLogsTool(server);
|
||||
registerDockerExecTool(server);
|
||||
registerDockerStopRemoveTools(server);
|
||||
registerDockerComposeTools(server);
|
||||
registerGenerateDockerfileTool(server);
|
||||
registerGenerateComposeTool(server);
|
||||
registerGenerateSailTool(server);
|
||||
registerDockerPushImageTool(server);
|
||||
}
|
||||
export { dockerManager } from './dockerManager.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/docker/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,+BAA+B,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,EAC9B,wBAAwB,GACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAE7D,MAAM,UAAU,mBAAmB,CAAC,MAAiB;IACnD,4BAA4B,CAAC,MAAM,CAAC,CAAC;IACrC,8BAA8B,CAAC,MAAM,CAAC,CAAC;IACvC,gCAAgC,CAAC,MAAM,CAAC,CAAC;IACzC,+BAA+B,CAAC,MAAM,CAAC,CAAC;IACxC,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,6BAA6B,CAAC,MAAM,CAAC,CAAC;IACtC,0BAA0B,CAAC,MAAM,CAAC,CAAC;IACnC,8BAA8B,CAAC,MAAM,CAAC,CAAC;IACvC,2BAA2B,CAAC,MAAM,CAAC,CAAC;IACpC,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACjC,2BAA2B,CAAC,MAAM,CAAC,CAAC;AACtC,CAAC;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
import { z } from 'zod';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager } from './dockerManager.js';
|
||||
export function registerDockerListContainersTool(server) {
|
||||
server.registerTool('docker_list_containers', {
|
||||
title: 'Docker List Containers',
|
||||
description: 'List Docker containers started by this server with their current status and port mappings.',
|
||||
inputSchema: {
|
||||
includeDockerStatus: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('Also query the Docker daemon for live status of each tracked container.'),
|
||||
},
|
||||
}, async ({ includeDockerStatus }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
const tracked = dockerManager.listTracked();
|
||||
if (!includeDockerStatus) {
|
||||
return jsonResult({ containers: tracked });
|
||||
}
|
||||
const docker = dockerManager.getDocker();
|
||||
const enriched = await Promise.all(tracked.map(async (c) => {
|
||||
try {
|
||||
const info = await docker.getContainer(c.id).inspect();
|
||||
return {
|
||||
...c,
|
||||
state: info.State?.Status ?? 'unknown',
|
||||
running: info.State?.Running ?? false,
|
||||
exitCode: info.State?.ExitCode ?? null,
|
||||
};
|
||||
}
|
||||
catch {
|
||||
return { ...c, state: 'unknown', running: false, exitCode: null };
|
||||
}
|
||||
}));
|
||||
return jsonResult({ containers: enriched });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=listContainers.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"listContainers.js","sourceRoot":"","sources":["../../../src/tools/docker/listContainers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,UAAU,gCAAgC,CAAC,MAAiB;IAChE,MAAM,CAAC,YAAY,CACjB,wBAAwB,EACxB;QACE,KAAK,EAAE,wBAAwB;QAC/B,WAAW,EAAE,4FAA4F;QACzG,WAAW,EAAE;YACX,mBAAmB,EAAE,CAAC;iBACnB,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,OAAO,CAAC,KAAK,CAAC;iBACd,QAAQ,CAAC,yEAAyE,CAAC;SACvF;KACF,EACD,KAAK,EAAE,EAAE,mBAAmB,EAAE,EAAE,EAAE;QAChC,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,eAAe,EAAE,CAAC;YACtC,MAAM,OAAO,GAAG,aAAa,CAAC,WAAW,EAAE,CAAC;YAC5C,IAAI,CAAC,mBAAmB,EAAE,CAAC;gBACzB,OAAO,UAAU,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;YAC7C,CAAC;YAED,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,EAAE,CAAC;YACzC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAChC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;gBACtB,IAAI,CAAC;oBACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;oBACvD,OAAO;wBACL,GAAG,CAAC;wBACJ,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,IAAI,SAAS;wBACtC,OAAO,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,IAAI,KAAK;wBACrC,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,QAAQ,IAAI,IAAI;qBACvC,CAAC;gBACJ,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;gBACpE,CAAC;YACH,CAAC,CAAC,CACH,CAAC;YACF,OAAO,UAAU,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager } from './dockerManager.js';
|
||||
export function registerDockerPushImageTool(server) {
|
||||
server.registerTool('docker_push_image', {
|
||||
title: 'Docker Push Image',
|
||||
description: 'Push a locally built or allow-listed Docker image to a registry (`docker push`). Requires confirm: true. ' +
|
||||
'Credentials must already be available via the Docker credential store / environment — never pass secrets as tool args.',
|
||||
inputSchema: {
|
||||
image: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Image reference to push, e.g. "ghcr.io/org/app:latest" or "my-app:1.0".'),
|
||||
confirm: z.boolean().optional().describe('Required as true to push an image.'),
|
||||
},
|
||||
}, async ({ image, confirm }) => {
|
||||
try {
|
||||
if (confirm !== true) {
|
||||
return errorResult('docker_push_image requires confirm: true.');
|
||||
}
|
||||
if (image.includes('--force') || /\s/.test(image)) {
|
||||
return errorResult('Invalid image reference.');
|
||||
}
|
||||
await dockerManager.ensureReachable();
|
||||
const result = await runCommand('docker', ['push', image], {
|
||||
cwd: config.workspaceRoot,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({
|
||||
image,
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
timedOut: result.timedOut,
|
||||
output: result.output,
|
||||
note: 'Auth must come from `docker login` / credential helpers / env — not from this tool.',
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=pushImage.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"pushImage.js","sourceRoot":"","sources":["../../../src/tools/docker/pushImage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,UAAU,2BAA2B,CAAC,MAAiB;IAC3D,MAAM,CAAC,YAAY,CACjB,mBAAmB,EACnB;QACE,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EACT,2GAA2G;YAC3G,wHAAwH;QAC1H,WAAW,EAAE;YACX,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CAAC,yEAAyE,CAAC;YACtF,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oCAAoC,CAAC;SAC/E;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3B,IAAI,CAAC;YACH,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,OAAO,WAAW,CAAC,2CAA2C,CAAC,CAAC;YAClE,CAAC;YACD,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBAClD,OAAO,WAAW,CAAC,0BAA0B,CAAC,CAAC;YACjD,CAAC;YAED,MAAM,aAAa,CAAC,eAAe,EAAE,CAAC;YAEtC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE;gBACzD,GAAG,EAAE,MAAM,CAAC,aAAa;gBACzB,SAAS,EAAE,MAAM,CAAC,wBAAwB;aAC3C,CAAC,CAAC;YAEH,OAAO,UAAU,CAAC;gBAChB,KAAK;gBACL,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,IAAI,EAAE,qFAAqF;aAC5F,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager, isAllowListedImage, isLocallyBuiltImage, validateVolumeMount } from './dockerManager.js';
|
||||
export function registerDockerRunContainerTool(server) {
|
||||
server.registerTool('docker_run_container', {
|
||||
title: 'Docker Run Container',
|
||||
description: 'Run a Docker container from an image with port mappings, env vars, and workspace-confined volume mounts. ' +
|
||||
'Locally built images are allowed; non-local images must be on the allow-list unless confirm: true is passed.',
|
||||
inputSchema: {
|
||||
image: z.string().min(1).describe('Docker image to run, e.g. "my-app:latest" or "nginx".'),
|
||||
name: z.string().optional().describe('Optional container name.'),
|
||||
ports: z
|
||||
.record(z.number().int().positive())
|
||||
.optional()
|
||||
.describe('Port mapping { hostPort: containerPort }.'),
|
||||
env: z.record(z.string()).optional().describe('Environment variables.'),
|
||||
volumes: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Volume mounts in "source:target[:mode]" format. Source paths are confined to WORKSPACE_ROOT.'),
|
||||
command: z.array(z.string()).optional().describe('Override the container command.'),
|
||||
memory: z.string().optional().describe('Memory limit, e.g. "512m". Defaults to 512m.'),
|
||||
cpus: z.string().optional().describe('CPU limit, e.g. "1.0". Defaults to 1.0.'),
|
||||
confirm: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Required as true to run an image not on the allow-list.'),
|
||||
},
|
||||
}, async ({ image, name, ports, env, volumes, command, memory, cpus, confirm }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
if (!isLocallyBuiltImage(image) && !isAllowListedImage(image) && confirm !== true) {
|
||||
return errorResult(`Image "${image}" is not locally built and not on the allow-list (${config.dockerAllowedBaseImages.join(', ')}). ` +
|
||||
'Pass confirm: true to run it anyway.');
|
||||
}
|
||||
const portBindings = {};
|
||||
const exposedPorts = {};
|
||||
const portMap = {};
|
||||
if (ports) {
|
||||
for (const [hostPort, containerPort] of Object.entries(ports)) {
|
||||
const key = `${containerPort}/tcp`;
|
||||
portBindings[key] = [{ HostPort: hostPort }];
|
||||
exposedPorts[key] = {};
|
||||
portMap[Number(hostPort)] = containerPort;
|
||||
}
|
||||
}
|
||||
const binds = [];
|
||||
if (volumes) {
|
||||
for (const spec of volumes) {
|
||||
const mount = validateVolumeMount(spec);
|
||||
binds.push(`${mount.source}:${mount.target}:${mount.mode}`);
|
||||
}
|
||||
}
|
||||
const createOptions = {
|
||||
Image: image,
|
||||
name,
|
||||
Cmd: command,
|
||||
Env: env ? Object.entries(env).map(([k, v]) => `${k}=${v}`) : undefined,
|
||||
ExposedPorts: Object.keys(exposedPorts).length > 0 ? exposedPorts : undefined,
|
||||
HostConfig: {
|
||||
PortBindings: Object.keys(portBindings).length > 0 ? portBindings : undefined,
|
||||
Binds: binds.length > 0 ? binds : undefined,
|
||||
Memory: parseMemory(memory ?? config.defaultContainerMemory),
|
||||
NanoCpus: parseCpus(cpus ?? config.defaultContainerCpus),
|
||||
// Security guardrails:
|
||||
Privileged: false,
|
||||
NetworkMode: 'bridge',
|
||||
PidMode: undefined,
|
||||
IpcMode: undefined,
|
||||
},
|
||||
};
|
||||
const docker = dockerManager.getDocker();
|
||||
const container = await docker.createContainer(createOptions);
|
||||
await container.start();
|
||||
const info = await container.inspect();
|
||||
const managed = dockerManager.track(container.id, image, info.Name?.replace(/^\//, '') ?? container.id, portMap);
|
||||
return jsonResult({ ...managed, shortId: container.id.substring(0, 12) });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
function parseMemory(value) {
|
||||
const match = value.match(/^([0-9.]+)([kmgt]?b?)$/i);
|
||||
if (!match || !match[1])
|
||||
return 512 * 1024 * 1024;
|
||||
const num = Number.parseFloat(match[1]);
|
||||
const unit = match[2]?.toLowerCase() ?? '';
|
||||
switch (unit) {
|
||||
case 'k':
|
||||
case 'kb':
|
||||
return num * 1024;
|
||||
case 'm':
|
||||
case 'mb':
|
||||
return num * 1024 * 1024;
|
||||
case 'g':
|
||||
case 'gb':
|
||||
return num * 1024 * 1024 * 1024;
|
||||
case 't':
|
||||
case 'tb':
|
||||
return num * 1024 * 1024 * 1024 * 1024;
|
||||
default:
|
||||
return num;
|
||||
}
|
||||
}
|
||||
function parseCpus(value) {
|
||||
const num = Number.parseFloat(value);
|
||||
return Number.isNaN(num) ? 1_000_000_000 : Math.round(num * 1_000_000_000);
|
||||
}
|
||||
//# sourceMappingURL=runContainer.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"runContainer.js","sourceRoot":"","sources":["../../../src/tools/docker/runContainer.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
import { z } from 'zod';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { dockerManager } from './dockerManager.js';
|
||||
export function registerDockerStopRemoveTools(server) {
|
||||
server.registerTool('docker_stop_container', {
|
||||
title: 'Docker Stop Container',
|
||||
description: 'Stop a Docker container started by this server.',
|
||||
inputSchema: {
|
||||
containerId: z.string().min(1).describe('Container ID returned by docker_run_container.'),
|
||||
timeout: z.number().int().positive().optional().default(10).describe('Seconds to wait before killing.'),
|
||||
},
|
||||
}, async ({ containerId, timeout }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
await dockerManager.getDocker().getContainer(containerId).stop({ t: timeout });
|
||||
return jsonResult({ stopped: true, containerId });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
server.registerTool('docker_remove_container', {
|
||||
title: 'Docker Remove Container',
|
||||
description: 'Remove a Docker container started by this server.',
|
||||
inputSchema: {
|
||||
containerId: z.string().min(1).describe('Container ID returned by docker_run_container.'),
|
||||
force: z.boolean().optional().default(false).describe('Force removal even if running.'),
|
||||
},
|
||||
}, async ({ containerId, force }) => {
|
||||
try {
|
||||
await dockerManager.ensureReachable();
|
||||
await dockerManager.getDocker().getContainer(containerId).remove({ force });
|
||||
dockerManager.untrack(containerId);
|
||||
return jsonResult({ removed: true, containerId });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=stopRemove.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"stopRemove.js","sourceRoot":"","sources":["../../../src/tools/docker/stopRemove.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,UAAU,6BAA6B,CAAC,MAAiB;IAC7D,MAAM,CAAC,YAAY,CACjB,uBAAuB,EACvB;QACE,KAAK,EAAE,uBAAuB;QAC9B,WAAW,EAAE,iDAAiD;QAC9D,WAAW,EAAE;YACX,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,gDAAgD,CAAC;YACzF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,iCAAiC,CAAC;SACxG;KACF,EACD,KAAK,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,EAAE;QACjC,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,eAAe,EAAE,CAAC;YACtC,MAAM,aAAa,CAAC,SAAS,EAAE,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;YAC/E,OAAO,UAAU,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,yBAAyB,EACzB;QACE,KAAK,EAAE,yBAAyB;QAChC,WAAW,EAAE,mDAAmD;QAChE,WAAW,EAAE;YACX,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,gDAAgD,CAAC;YACzF,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,gCAAgC,CAAC;SACxF;KACF,EACD,KAAK,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE;QAC/B,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,eAAe,EAAE,CAAC;YACtC,MAAM,aAAa,CAAC,SAAS,EAAE,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;YAC5E,aAAa,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YACnC,OAAO,UAAU,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { detectProject } from '../../lib/frameworks/detect.js';
|
||||
import { readPackageJson } from '../../lib/frameworks/packageManager.js';
|
||||
import { readComposerJson } from '../../lib/frameworks/composer.js';
|
||||
import { errorResult, jsonResult, targetSchema, toErrorMessage } from '../shared.js';
|
||||
export function registerGetPackageInfoTool(server) {
|
||||
server.registerTool('get_package_info', {
|
||||
title: 'Get Package Info',
|
||||
description: 'Return installed package metadata from package.json (React/Node projects) or composer.json (PHP projects): version, scripts, and key dependencies.',
|
||||
inputSchema: {
|
||||
target: targetSchema,
|
||||
ecosystem: z
|
||||
.enum(['npm', 'composer', 'auto'])
|
||||
.optional()
|
||||
.default('auto')
|
||||
.describe('Which manifest to read. Auto detects from the resolved project.'),
|
||||
},
|
||||
}, async ({ target, ecosystem }) => {
|
||||
try {
|
||||
const detected = detectProject(config.workspaceRoot);
|
||||
const adapter = target === 'frontend'
|
||||
? detected.frontend
|
||||
: target === 'backend'
|
||||
? detected.backend
|
||||
: (detected.frontend ?? detected.backend ?? detected.primary);
|
||||
if (!adapter)
|
||||
return errorResult('No project detected in WORKSPACE_ROOT.');
|
||||
const resolvedEcosystem = ecosystem === 'auto' ? (adapter.packageManager === 'composer' ? 'composer' : 'npm') : ecosystem;
|
||||
const root = adapter.root;
|
||||
if (resolvedEcosystem === 'composer') {
|
||||
const composerJson = readComposerJson(root);
|
||||
if (!composerJson)
|
||||
return errorResult(`No composer.json found at ${root}`);
|
||||
const lockPath = path.join(root, 'composer.lock');
|
||||
let installed = {};
|
||||
if (fs.existsSync(lockPath)) {
|
||||
try {
|
||||
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
||||
installed = Object.fromEntries((lock.packages ?? []).slice(0, 50).map((p) => [p.name, p.version]));
|
||||
}
|
||||
catch {
|
||||
// ignore lock parse errors
|
||||
}
|
||||
}
|
||||
return jsonResult({
|
||||
ecosystem: 'composer',
|
||||
root,
|
||||
name: composerJson.name ?? null,
|
||||
require: composerJson.require ?? {},
|
||||
requireDev: composerJson['require-dev'] ?? {},
|
||||
scripts: composerJson.scripts ?? {},
|
||||
installedSample: installed,
|
||||
});
|
||||
}
|
||||
const packageJson = readPackageJson(root);
|
||||
if (!packageJson)
|
||||
return errorResult(`No package.json found at ${root}`);
|
||||
return jsonResult({
|
||||
ecosystem: 'npm',
|
||||
root,
|
||||
name: packageJson.name ?? null,
|
||||
scripts: packageJson.scripts ?? {},
|
||||
dependencies: packageJson.dependencies ?? {},
|
||||
devDependencies: packageJson.devDependencies ?? {},
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=getPackageInfo.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"getPackageInfo.js","sourceRoot":"","sources":["../../../src/tools/docs/getPackageInfo.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { registerSearchPackageDocsTool } from './searchPackageDocs.js';
|
||||
import { registerSearchMdnTool } from './searchMdn.js';
|
||||
import { registerSearchLaravelDocsTool } from './searchLaravelDocs.js';
|
||||
import { registerSearchCodeIgniterDocsTool } from './searchCodeIgniterDocs.js';
|
||||
import { registerGetPackageInfoTool } from './getPackageInfo.js';
|
||||
export function registerDocsTools(server) {
|
||||
registerSearchPackageDocsTool(server);
|
||||
registerSearchMdnTool(server);
|
||||
registerSearchLaravelDocsTool(server);
|
||||
registerSearchCodeIgniterDocsTool(server);
|
||||
registerGetPackageInfoTool(server);
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/docs/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,EAAE,iCAAiC,EAAE,MAAM,4BAA4B,CAAC;AAC/E,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,MAAM,UAAU,iBAAiB,CAAC,MAAiB;IACjD,6BAA6B,CAAC,MAAM,CAAC,CAAC;IACtC,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,6BAA6B,CAAC,MAAM,CAAC,CAAC;IACtC,iCAAiC,CAAC,MAAM,CAAC,CAAC;IAC1C,0BAA0B,CAAC,MAAM,CAAC,CAAC;AACrC,CAAC"}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { detectProject } from '../../lib/frameworks/detect.js';
|
||||
import { readComposerJson } from '../../lib/frameworks/composer.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
function extractMajorMinor(versionConstraint) {
|
||||
if (!versionConstraint)
|
||||
return '4';
|
||||
const match = versionConstraint.match(/(\d+)\.(\d+)/);
|
||||
if (!match)
|
||||
return '4';
|
||||
const [, major] = match;
|
||||
return `${major}`;
|
||||
}
|
||||
export function registerSearchCodeIgniterDocsTool(server) {
|
||||
server.registerTool('search_codeigniter_docs', {
|
||||
title: 'Search CodeIgniter Docs',
|
||||
description: 'Return a deep link to the CodeIgniter 4 user guide for a topic, version-matched to the codeigniter4/framework constraint in composer.json.',
|
||||
inputSchema: {
|
||||
topic: z.string().min(1).describe('Documentation topic, e.g. "models" or "database".'),
|
||||
version: z.string().optional().describe('Override the major version, e.g. "4". Auto-detected by default.'),
|
||||
},
|
||||
}, async ({ topic, version }) => {
|
||||
try {
|
||||
const detected = detectProject(config.workspaceRoot);
|
||||
const ciRoot = detected.backend?.kind === 'codeigniter' ? detected.backend.root : config.workspaceRoot;
|
||||
const composerJson = readComposerJson(ciRoot);
|
||||
const constraint = composerJson?.require?.['codeigniter4/framework'];
|
||||
const docsVersion = version ?? extractMajorMinor(constraint);
|
||||
const slug = topic.toLowerCase().replace(/\s+/g, '_');
|
||||
const url = `https://codeigniter4.github.io/userguide/${docsVersion}/${slug}.html`;
|
||||
return jsonResult({
|
||||
topic,
|
||||
version: docsVersion,
|
||||
composerConstraint: constraint ?? null,
|
||||
url,
|
||||
note: 'This is a best-effort documentation link. If the topic page does not exist, the user guide index may help.',
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=searchCodeIgniterDocs.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"searchCodeIgniterDocs.js","sourceRoot":"","sources":["../../../src/tools/docs/searchCodeIgniterDocs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEvE,SAAS,iBAAiB,CAAC,iBAAqC;IAC9D,IAAI,CAAC,iBAAiB;QAAE,OAAO,GAAG,CAAC;IACnC,MAAM,KAAK,GAAG,iBAAiB,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IACtD,IAAI,CAAC,KAAK;QAAE,OAAO,GAAG,CAAC;IACvB,MAAM,CAAC,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC;IACxB,OAAO,GAAG,KAAK,EAAE,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,iCAAiC,CAAC,MAAiB;IACjE,MAAM,CAAC,YAAY,CACjB,yBAAyB,EACzB;QACE,KAAK,EAAE,yBAAyB;QAChC,WAAW,EACT,4IAA4I;QAC9I,WAAW,EAAE;YACX,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,mDAAmD,CAAC;YACtF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iEAAiE,CAAC;SAC3G;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3B,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;YACrD,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,EAAE,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC;YACvG,MAAM,YAAY,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;YAC9C,MAAM,UAAU,GAAG,YAAY,EAAE,OAAO,EAAE,CAAC,wBAAwB,CAAC,CAAC;YACrE,MAAM,WAAW,GAAG,OAAO,IAAI,iBAAiB,CAAC,UAAU,CAAC,CAAC;YAC7D,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YACtD,MAAM,GAAG,GAAG,4CAA4C,WAAW,IAAI,IAAI,OAAO,CAAC;YACnF,OAAO,UAAU,CAAC;gBAChB,KAAK;gBACL,OAAO,EAAE,WAAW;gBACpB,kBAAkB,EAAE,UAAU,IAAI,IAAI;gBACtC,GAAG;gBACH,IAAI,EAAE,4GAA4G;aACnH,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { detectProject } from '../../lib/frameworks/detect.js';
|
||||
import { readComposerJson } from '../../lib/frameworks/composer.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
function extractMajorMinor(versionConstraint) {
|
||||
if (!versionConstraint)
|
||||
return '11.x';
|
||||
const match = versionConstraint.match(/(\d+)\.(\d+)/);
|
||||
if (!match)
|
||||
return '11.x';
|
||||
const [, major, minor] = match;
|
||||
return `${major}.${minor}`;
|
||||
}
|
||||
export function registerSearchLaravelDocsTool(server) {
|
||||
server.registerTool('search_laravel_docs', {
|
||||
title: 'Search Laravel Docs',
|
||||
description: 'Return a deep link to the Laravel documentation for a topic, version-matched to the laravel/framework constraint in composer.json.',
|
||||
inputSchema: {
|
||||
topic: z.string().min(1).describe('Documentation topic, e.g. "eloquent" or "validation".'),
|
||||
version: z.string().optional().describe('Override the version segment, e.g. "11.x". Auto-detected by default.'),
|
||||
},
|
||||
}, async ({ topic, version }) => {
|
||||
try {
|
||||
const detected = detectProject(config.workspaceRoot);
|
||||
const laravelRoot = detected.backend?.kind === 'laravel' ? detected.backend.root : config.workspaceRoot;
|
||||
const composerJson = readComposerJson(laravelRoot);
|
||||
const constraint = composerJson?.require?.['laravel/framework'];
|
||||
const docsVersion = version ?? extractMajorMinor(constraint) + '.x';
|
||||
const slug = topic.toLowerCase().replace(/\s+/g, '-');
|
||||
const url = `https://laravel.com/docs/${docsVersion}/${slug}`;
|
||||
return jsonResult({
|
||||
topic,
|
||||
version: docsVersion,
|
||||
composerConstraint: constraint ?? null,
|
||||
url,
|
||||
note: 'This is a best-effort documentation link. If the topic page does not exist, MDN/search engines may help refine the query.',
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=searchLaravelDocs.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"searchLaravelDocs.js","sourceRoot":"","sources":["../../../src/tools/docs/searchLaravelDocs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEvE,SAAS,iBAAiB,CAAC,iBAAqC;IAC9D,IAAI,CAAC,iBAAiB;QAAE,OAAO,MAAM,CAAC;IACtC,MAAM,KAAK,GAAG,iBAAiB,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IACtD,IAAI,CAAC,KAAK;QAAE,OAAO,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC;IAC/B,OAAO,GAAG,KAAK,IAAI,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,6BAA6B,CAAC,MAAiB;IAC7D,MAAM,CAAC,YAAY,CACjB,qBAAqB,EACrB;QACE,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EACT,oIAAoI;QACtI,WAAW,EAAE;YACX,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,uDAAuD,CAAC;YAC1F,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sEAAsE,CAAC;SAChH;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;QAC3B,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;YACrD,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC;YACxG,MAAM,YAAY,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;YACnD,MAAM,UAAU,GAAG,YAAY,EAAE,OAAO,EAAE,CAAC,mBAAmB,CAAC,CAAC;YAChE,MAAM,WAAW,GAAG,OAAO,IAAI,iBAAiB,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;YACpE,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YACtD,MAAM,GAAG,GAAG,4BAA4B,WAAW,IAAI,IAAI,EAAE,CAAC;YAC9D,OAAO,UAAU,CAAC;gBAChB,KAAK;gBACL,OAAO,EAAE,WAAW;gBACpB,kBAAkB,EAAE,UAAU,IAAI,IAAI;gBACtC,GAAG;gBACH,IAAI,EAAE,2HAA2H;aAClI,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
export function registerSearchMdnTool(server) {
|
||||
server.registerTool('search_mdn', {
|
||||
title: 'Search MDN',
|
||||
description: 'Search MDN for a web platform API, CSS property, or HTML element and return the top results.',
|
||||
inputSchema: {
|
||||
query: z.string().min(1).describe('Search query, e.g. "Array.prototype.map" or "fetch API".'),
|
||||
limit: z.number().int().positive().optional().default(5).describe('Maximum number of results to return.'),
|
||||
},
|
||||
}, async ({ query, limit }) => {
|
||||
try {
|
||||
const url = `https://developer.mozilla.org/api/v1/search?q=${encodeURIComponent(query)}&locale=en-US`;
|
||||
const response = await fetch(url, { headers: { Accept: 'application/json' } });
|
||||
if (!response.ok) {
|
||||
throw new Error(`MDN search returned HTTP ${response.status}`);
|
||||
}
|
||||
const data = (await response.json());
|
||||
const results = data.documents?.slice(0, limit).map((doc) => ({
|
||||
title: doc.title,
|
||||
url: `https://developer.mozilla.org${doc.mdn_url}`,
|
||||
summary: doc.summary,
|
||||
})) ?? [];
|
||||
return jsonResult({ query, results });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=searchMdn.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"searchMdn.js","sourceRoot":"","sources":["../../../src/tools/docs/searchMdn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAWvE,MAAM,UAAU,qBAAqB,CAAC,MAAiB;IACrD,MAAM,CAAC,YAAY,CACjB,YAAY,EACZ;QACE,KAAK,EAAE,YAAY;QACnB,WAAW,EAAE,8FAA8F;QAC3G,WAAW,EAAE;YACX,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,0DAA0D,CAAC;YAC7F,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,sCAAsC,CAAC;SAC1G;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE;QACzB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,iDAAiD,kBAAkB,CAAC,KAAK,CAAC,eAAe,CAAC;YACtG,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,CAAC,CAAC;YAC/E,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YACjE,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAoB,CAAC;YACxD,MAAM,OAAO,GACX,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAC5C,KAAK,EAAE,GAAG,CAAC,KAAK;gBAChB,GAAG,EAAE,gCAAgC,GAAG,CAAC,OAAO,EAAE;gBAClD,OAAO,EAAE,GAAG,CAAC,OAAO;aACrB,CAAC,CAAC,IAAI,EAAE,CAAC;YACZ,OAAO,UAAU,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
import { z } from 'zod';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
async function fetchJson(url) {
|
||||
const response = await fetch(url, { headers: { Accept: 'application/json' } });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} from ${url}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
async function fetchText(url) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} from ${url}`);
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
async function fetchNpmDocs(name, version) {
|
||||
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
|
||||
const info = (await fetchJson(registryUrl));
|
||||
const resolvedVersion = version ?? info['dist-tags'].latest;
|
||||
const versionData = info.versions[resolvedVersion];
|
||||
let readme = null;
|
||||
try {
|
||||
readme = await fetchText(`https://unpkg.com/${name}@${resolvedVersion}/README.md`);
|
||||
}
|
||||
catch {
|
||||
try {
|
||||
readme = await fetchText(`https://unpkg.com/${name}@${resolvedVersion}/readme.md`);
|
||||
}
|
||||
catch {
|
||||
readme = info.readme ?? null;
|
||||
}
|
||||
}
|
||||
return {
|
||||
ecosystem: 'npm',
|
||||
package: name,
|
||||
version: resolvedVersion,
|
||||
description: versionData?.description ?? null,
|
||||
homepage: versionData?.homepage ?? null,
|
||||
repository: versionData?.repository?.url ?? null,
|
||||
readme,
|
||||
readmeSource: readme ? `https://unpkg.com/${name}@${resolvedVersion}/README.md` : null,
|
||||
};
|
||||
}
|
||||
async function fetchPackagistDocs(name, version) {
|
||||
const url = `https://packagist.org/packages/${encodeURIComponent(name)}.json`;
|
||||
const info = (await fetchJson(url));
|
||||
const pkg = info.package;
|
||||
const versions = Object.keys(pkg.versions ?? {}).sort();
|
||||
const resolvedVersion = version ?? versions[versions.length - 1];
|
||||
return {
|
||||
ecosystem: 'composer',
|
||||
package: pkg.name,
|
||||
version: resolvedVersion,
|
||||
description: pkg.description ?? null,
|
||||
repository: pkg.repository ?? null,
|
||||
versions: versions.slice(-20),
|
||||
note: 'README fetching for Packagist packages is not yet implemented; use the repository URL to read the README.',
|
||||
};
|
||||
}
|
||||
export function registerSearchPackageDocsTool(server) {
|
||||
server.registerTool('search_package_docs', {
|
||||
title: 'Search Package Docs',
|
||||
description: 'Fetch README/docs for an npm or Composer/Packagist package. For npm packages, resolves the version and fetches the README from unpkg. For Composer packages, returns Packagist metadata including the repository URL.',
|
||||
inputSchema: {
|
||||
name: z.string().min(1).describe('Package name, e.g. "axios" or "laravel/framework".'),
|
||||
ecosystem: z
|
||||
.enum(['npm', 'composer', 'auto'])
|
||||
.optional()
|
||||
.default('auto')
|
||||
.describe('Which package ecosystem to search. Auto detects from the name ("laravel/framework" → composer).'),
|
||||
version: z.string().optional().describe('Specific version to look up. Defaults to latest.'),
|
||||
},
|
||||
}, async ({ name, ecosystem, version }) => {
|
||||
try {
|
||||
const resolvedEcosystem = ecosystem === 'auto' ? (name.includes('/') ? 'composer' : 'npm') : ecosystem;
|
||||
if (resolvedEcosystem === 'composer') {
|
||||
return jsonResult(await fetchPackagistDocs(name, version));
|
||||
}
|
||||
return jsonResult(await fetchNpmDocs(name, version));
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=searchPackageDocs.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"searchPackageDocs.js","sourceRoot":"","sources":["../../../src/tools/docs/searchPackageDocs.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+249
@@ -0,0 +1,249 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
async function git(args) {
|
||||
return runCommand('git', args, {
|
||||
cwd: config.workspaceRoot,
|
||||
timeoutMs: config.defaultCommandTimeoutMs,
|
||||
});
|
||||
}
|
||||
export function registerGitStatusTool(server) {
|
||||
server.registerTool('git_status', {
|
||||
title: 'Git Status',
|
||||
description: 'Return the current branch, ahead/behind counts vs upstream (when available), and porcelain status lines. Read-only.',
|
||||
inputSchema: {},
|
||||
}, async () => {
|
||||
try {
|
||||
const branchResult = await git(['rev-parse', '--abbrev-ref', 'HEAD']);
|
||||
const porcelain = await git(['status', '--porcelain=v1']);
|
||||
const aheadBehind = await git(['rev-list', '--left-right', '--count', '@{upstream}...HEAD']);
|
||||
let ahead = null;
|
||||
let behind = null;
|
||||
if (!aheadBehind.failed && aheadBehind.output.trim()) {
|
||||
const parts = aheadBehind.output.trim().split(/\s+/);
|
||||
behind = Number.parseInt(parts[0] ?? '0', 10);
|
||||
ahead = Number.parseInt(parts[1] ?? '0', 10);
|
||||
}
|
||||
return jsonResult({
|
||||
branch: branchResult.output.trim() || null,
|
||||
ahead,
|
||||
behind,
|
||||
porcelain: porcelain.output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trimEnd())
|
||||
.filter(Boolean),
|
||||
raw: porcelain.output,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGitDiffTool(server) {
|
||||
server.registerTool('git_diff', {
|
||||
title: 'Git Diff',
|
||||
description: 'Show staged and/or unstaged diffs, optionally scoped to a workspace-relative path. Read-only; output is truncated.',
|
||||
inputSchema: {
|
||||
staged: z.boolean().optional().describe('If true, show only staged changes (`git diff --cached`).'),
|
||||
path: z.string().optional().describe('Optional workspace-relative file or directory to diff.'),
|
||||
},
|
||||
}, async ({ staged, path: filePath }) => {
|
||||
try {
|
||||
const args = ['diff'];
|
||||
if (staged)
|
||||
args.push('--cached');
|
||||
if (filePath) {
|
||||
const absolute = resolveWorkspacePath(config.workspaceRoot, filePath);
|
||||
args.push('--', absolute);
|
||||
}
|
||||
const result = await git(args);
|
||||
return jsonResult({
|
||||
staged: Boolean(staged),
|
||||
path: filePath ?? null,
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
output: result.output,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGitDiffSummarizeTool(server) {
|
||||
server.registerTool('git_diff_summarize', {
|
||||
title: 'Git Diff Summarize',
|
||||
description: 'Summarize staged and/or unstaged changes: per-file insertions/deletions plus totals. Read-only; uses `git diff --numstat` (no full patch).',
|
||||
inputSchema: {
|
||||
staged: z.boolean().optional().describe('If true, summarize only staged changes (`git diff --cached --numstat`).'),
|
||||
path: z.string().optional().describe('Optional workspace-relative file or directory to summarize.'),
|
||||
},
|
||||
}, async ({ staged, path: filePath }) => {
|
||||
try {
|
||||
const args = ['diff', '--numstat'];
|
||||
if (staged)
|
||||
args.push('--cached');
|
||||
if (filePath) {
|
||||
const absolute = resolveWorkspacePath(config.workspaceRoot, filePath);
|
||||
args.push('--', absolute);
|
||||
}
|
||||
const result = await git(args);
|
||||
const files = summarizeNumstat(result.output);
|
||||
const insertions = files.reduce((sum, f) => sum + f.insertions, 0);
|
||||
const deletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
||||
return jsonResult({
|
||||
staged: Boolean(staged),
|
||||
path: filePath ?? null,
|
||||
fileCount: files.length,
|
||||
insertions,
|
||||
deletions,
|
||||
files,
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
/** Parse `git diff --numstat` lines into structured file stats. Exported for tests. */
|
||||
export function summarizeNumstat(output) {
|
||||
return output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [insRaw, delRaw, ...pathParts] = line.split(/\t/);
|
||||
const filePath = pathParts.join('\t') || '';
|
||||
const binary = insRaw === '-' || delRaw === '-';
|
||||
return {
|
||||
path: filePath,
|
||||
insertions: binary ? 0 : Number.parseInt(insRaw ?? '0', 10) || 0,
|
||||
deletions: binary ? 0 : Number.parseInt(delRaw ?? '0', 10) || 0,
|
||||
binary,
|
||||
};
|
||||
})
|
||||
.filter((row) => row.path.length > 0);
|
||||
}
|
||||
export function registerGitLogTool(server) {
|
||||
server.registerTool('git_log', {
|
||||
title: 'Git Log',
|
||||
description: 'Show recent commits (hash, author, date, subject). Read-only.',
|
||||
inputSchema: {
|
||||
n: z.number().int().min(1).max(100).optional().default(10).describe('Number of commits to return.'),
|
||||
path: z.string().optional().describe('Optional workspace-relative path to limit history.'),
|
||||
},
|
||||
}, async ({ n, path: filePath }) => {
|
||||
try {
|
||||
const args = ['log', `-n`, String(n ?? 10), '--pretty=format:%H%x09%an%x09%ad%x09%s', '--date=iso'];
|
||||
if (filePath) {
|
||||
const absolute = resolveWorkspacePath(config.workspaceRoot, filePath);
|
||||
args.push('--', absolute);
|
||||
}
|
||||
const result = await git(args);
|
||||
const commits = result.output
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [hash, author, date, ...subjectParts] = line.split('\t');
|
||||
return {
|
||||
hash: hash ?? '',
|
||||
author: author ?? '',
|
||||
date: date ?? '',
|
||||
subject: subjectParts.join('\t'),
|
||||
};
|
||||
});
|
||||
return jsonResult({ n: n ?? 10, path: filePath ?? null, commits });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGitBranchTool(server) {
|
||||
server.registerTool('git_branch', {
|
||||
title: 'Git Branch',
|
||||
description: 'List local branches, or create a new branch. Creating a branch requires confirm: true. Never force-deletes or force-pushes.',
|
||||
inputSchema: {
|
||||
action: z.enum(['list', 'create']).default('list').describe('list (default) or create a branch.'),
|
||||
name: z.string().optional().describe('Branch name (required when action is create).'),
|
||||
confirm: z.boolean().optional().describe('Required as true when creating a branch.'),
|
||||
},
|
||||
}, async ({ action, name, confirm }) => {
|
||||
try {
|
||||
if (action === 'list') {
|
||||
const result = await git(['branch', '--list', '--format=%(refname:short)%09%(HEAD)']);
|
||||
const branches = result.output
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [branchName, head] = line.split('\t');
|
||||
return { name: branchName ?? '', current: head === '*' };
|
||||
});
|
||||
return jsonResult({ action: 'list', branches });
|
||||
}
|
||||
if (!name || !name.trim()) {
|
||||
return errorResult('Branch name is required when action is "create".');
|
||||
}
|
||||
if (confirm !== true) {
|
||||
return errorResult('Creating a branch requires confirm: true.');
|
||||
}
|
||||
if (!/^[A-Za-z0-9._/-]+$/.test(name) || name.includes('..')) {
|
||||
return errorResult(`Invalid branch name "${name}".`);
|
||||
}
|
||||
const result = await git(['branch', name]);
|
||||
return jsonResult({
|
||||
action: 'create',
|
||||
name,
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
output: result.output,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGitCommitTool(server) {
|
||||
server.registerTool('git_commit', {
|
||||
title: 'Git Commit',
|
||||
description: 'Stage workspace-relative paths (or all changes with paths: ["."]) and create a commit. Requires confirm: true. Does not amend, force, or push.',
|
||||
inputSchema: {
|
||||
message: z.string().min(1).describe('Commit message.'),
|
||||
paths: z
|
||||
.array(z.string())
|
||||
.min(1)
|
||||
.describe('Workspace-relative paths to stage before committing. Use ["."] for all changes.'),
|
||||
confirm: z.boolean().optional().describe('Must be true to create a commit.'),
|
||||
},
|
||||
}, async ({ message, paths, confirm }) => {
|
||||
try {
|
||||
if (confirm !== true) {
|
||||
return errorResult('git_commit requires confirm: true.');
|
||||
}
|
||||
const absolutePaths = paths.map((p) => resolveWorkspacePath(config.workspaceRoot, p));
|
||||
const addResult = await git(['add', '--', ...absolutePaths]);
|
||||
if (addResult.failed) {
|
||||
return errorResult(`git add failed:\n${addResult.output}`);
|
||||
}
|
||||
const commitResult = await git(['commit', '-m', message]);
|
||||
return jsonResult({
|
||||
message,
|
||||
paths,
|
||||
staged: absolutePaths,
|
||||
exitCode: commitResult.exitCode,
|
||||
failed: commitResult.failed,
|
||||
output: commitResult.output,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=gitTools.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
import { registerGitBranchTool, registerGitCommitTool, registerGitDiffSummarizeTool, registerGitDiffTool, registerGitLogTool, registerGitStatusTool, } from './gitTools.js';
|
||||
export function registerGitTools(server) {
|
||||
registerGitStatusTool(server);
|
||||
registerGitDiffTool(server);
|
||||
registerGitDiffSummarizeTool(server);
|
||||
registerGitLogTool(server);
|
||||
registerGitBranchTool(server);
|
||||
registerGitCommitTool(server);
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/git/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,4BAA4B,EAC5B,mBAAmB,EACnB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AAEvB,MAAM,UAAU,gBAAgB,CAAC,MAAiB;IAChD,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,4BAA4B,CAAC,MAAM,CAAC,CAAC;IACrC,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC3B,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,qBAAqB,CAAC,MAAM,CAAC,CAAC;AAChC,CAAC"}
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { processManager } from '../../lib/processManager.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { ARTISAN_ALLOWED, ARTISAN_BACKGROUND, ARTISAN_DESTRUCTIVE, isAllowedArtisanCommand, } from './artisanAllowList.js';
|
||||
import { requireLaravel } from './requireLaravel.js';
|
||||
export function registerArtisanTool(server) {
|
||||
server.registerTool('artisan', {
|
||||
title: 'Run Artisan Command',
|
||||
description: 'Run a whitelisted `php artisan` subcommand in the detected Laravel project and return stdout/stderr. ' +
|
||||
`Allowed commands: ${ARTISAN_ALLOWED.join(', ')}. ` +
|
||||
'Destructive commands (migrate:fresh, migrate:refresh, migrate:reset, db:wipe) require confirm: true. ' +
|
||||
'Long-running commands (queue:work, queue:listen) are started as tracked background processes.',
|
||||
inputSchema: {
|
||||
command: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Artisan subcommand, e.g. "make:controller" or "migrate".'),
|
||||
args: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.default([])
|
||||
.describe('Arguments forwarded after the subcommand, e.g. ["PostController", "--api"].'),
|
||||
confirm: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Required as true for destructive commands (migrate:fresh/refresh/reset, db:wipe).'),
|
||||
},
|
||||
}, async ({ command, args, confirm }) => {
|
||||
try {
|
||||
if (!isAllowedArtisanCommand(command)) {
|
||||
return errorResult(`Artisan command "${command}" is not on the allow-list. Allowed: ${ARTISAN_ALLOWED.join(', ')}.`);
|
||||
}
|
||||
if (ARTISAN_DESTRUCTIVE.has(command) && confirm !== true) {
|
||||
return errorResult(`Artisan command "${command}" is destructive and requires confirm: true.`);
|
||||
}
|
||||
const laravel = requireLaravel();
|
||||
const phpArgs = ['artisan', command, ...(args ?? [])];
|
||||
if (ARTISAN_BACKGROUND.has(command)) {
|
||||
const info = processManager.start({
|
||||
label: `artisan:${command}`,
|
||||
command: 'php',
|
||||
args: phpArgs,
|
||||
cwd: laravel.root,
|
||||
});
|
||||
return jsonResult({
|
||||
background: true,
|
||||
processId: info.id,
|
||||
artisanCommand: command,
|
||||
artisanArgs: args ?? [],
|
||||
pid: info.pid,
|
||||
status: info.status,
|
||||
startedAt: info.startedAt,
|
||||
cwd: info.cwd,
|
||||
hint: 'Use get_process_logs / stop_process to manage this background artisan process.',
|
||||
});
|
||||
}
|
||||
const result = await runCommand('php', phpArgs, {
|
||||
cwd: laravel.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({
|
||||
background: false,
|
||||
artisanCommand: command,
|
||||
artisanArgs: args ?? [],
|
||||
exitCode: result.exitCode,
|
||||
timedOut: result.timedOut,
|
||||
failed: result.failed,
|
||||
output: result.output,
|
||||
cwd: result.cwd,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=artisan.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"artisan.js","sourceRoot":"","sources":["../../../src/tools/laravel/artisan.ts"],"names":[],"mappings":"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"}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Artisan subcommands the `artisan` tool is willing to run.
|
||||
* Anything not on this list is rejected — the agent must not pass arbitrary
|
||||
* artisan commands (e.g. `tinker` interactive, `db:wipe` without confirm, etc.).
|
||||
*
|
||||
* Destructive commands additionally require `confirm: true` in the tool call.
|
||||
*/
|
||||
export const ARTISAN_ALLOWED = [
|
||||
'make:controller',
|
||||
'make:model',
|
||||
'make:migration',
|
||||
'make:seeder',
|
||||
'make:factory',
|
||||
'make:request',
|
||||
'make:resource',
|
||||
'make:middleware',
|
||||
'make:job',
|
||||
'make:event',
|
||||
'make:listener',
|
||||
'make:mail',
|
||||
'make:notification',
|
||||
'make:policy',
|
||||
'make:command',
|
||||
'make:test',
|
||||
'migrate',
|
||||
'migrate:rollback',
|
||||
'migrate:status',
|
||||
'migrate:fresh',
|
||||
'migrate:refresh',
|
||||
'migrate:reset',
|
||||
'db:seed',
|
||||
'db:wipe',
|
||||
'route:list',
|
||||
'config:clear',
|
||||
'config:cache',
|
||||
'cache:clear',
|
||||
'view:clear',
|
||||
'route:clear',
|
||||
'optimize:clear',
|
||||
'queue:work',
|
||||
'queue:listen',
|
||||
'queue:failed',
|
||||
'queue:retry',
|
||||
'queue:batches',
|
||||
'queue:clear',
|
||||
'reverb:start',
|
||||
'storage:link',
|
||||
'key:generate',
|
||||
'about',
|
||||
'list',
|
||||
];
|
||||
/** Commands that mutate/destroy data and therefore require confirm: true. */
|
||||
export const ARTISAN_DESTRUCTIVE = new Set([
|
||||
'migrate:fresh',
|
||||
'migrate:refresh',
|
||||
'migrate:reset',
|
||||
'db:wipe',
|
||||
'queue:clear',
|
||||
]);
|
||||
/** Long-running commands tracked via processManager instead of runCommand. */
|
||||
export const ARTISAN_BACKGROUND = new Set(['queue:work', 'queue:listen', 'reverb:start']);
|
||||
export function isAllowedArtisanCommand(command) {
|
||||
return ARTISAN_ALLOWED.includes(command);
|
||||
}
|
||||
//# sourceMappingURL=artisanAllowList.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"artisanAllowList.js","sourceRoot":"","sources":["../../../src/tools/laravel/artisanAllowList.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,iBAAiB;IACjB,YAAY;IACZ,gBAAgB;IAChB,aAAa;IACb,cAAc;IACd,cAAc;IACd,eAAe;IACf,iBAAiB;IACjB,UAAU;IACV,YAAY;IACZ,eAAe;IACf,WAAW;IACX,mBAAmB;IACnB,aAAa;IACb,cAAc;IACd,WAAW;IACX,SAAS;IACT,kBAAkB;IAClB,gBAAgB;IAChB,eAAe;IACf,iBAAiB;IACjB,eAAe;IACf,SAAS;IACT,SAAS;IACT,YAAY;IACZ,cAAc;IACd,cAAc;IACd,aAAa;IACb,YAAY;IACZ,aAAa;IACb,gBAAgB;IAChB,YAAY;IACZ,cAAc;IACd,cAAc;IACd,aAAa;IACb,eAAe;IACf,aAAa;IACb,cAAc;IACd,cAAc;IACd,cAAc;IACd,OAAO;IACP,MAAM;CACE,CAAC;AAIX,6EAA6E;AAC7E,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAS;IACjD,eAAe;IACf,iBAAiB;IACjB,eAAe;IACf,SAAS;IACT,aAAa;CACd,CAAC,CAAC;AAEH,8EAA8E;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAS,CAAC,YAAY,EAAE,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC;AAElG,MAAM,UAAU,uBAAuB,CAAC,OAAe;IACrD,OAAQ,eAAqC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AAClE,CAAC"}
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requirePhpBackend } from './requireLaravel.js';
|
||||
/**
|
||||
* Composer helpers shared by Laravel and CodeIgniter (both are Composer-based).
|
||||
* Mirrors `add_dependency` but is explicitly for PHP packages.
|
||||
*/
|
||||
export function registerComposerTools(server) {
|
||||
server.registerTool('composer_require', {
|
||||
title: 'Composer Require',
|
||||
description: 'Add a Composer dependency to the detected PHP backend (Laravel or CodeIgniter). Equivalent to `composer require`.',
|
||||
inputSchema: {
|
||||
packages: z.array(z.string().min(1)).min(1).describe('Packagist package names, e.g. ["laravel/sanctum"].'),
|
||||
dev: z.boolean().optional().default(false).describe('Install into require-dev (--dev).'),
|
||||
},
|
||||
}, async ({ packages, dev }) => {
|
||||
try {
|
||||
const backend = requirePhpBackend();
|
||||
const args = ['require', ...(dev ? ['--dev'] : []), ...packages, '--no-interaction'];
|
||||
const result = await runCommand('composer', args, {
|
||||
cwd: backend.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({ adapter: backend.label, packages, dev, ...result });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
server.registerTool('composer_remove', {
|
||||
title: 'Composer Remove',
|
||||
description: 'Remove a Composer dependency from the detected PHP backend (Laravel or CodeIgniter). Requires confirm: true.',
|
||||
inputSchema: {
|
||||
packages: z.array(z.string().min(1)).min(1).describe('Packagist package names to remove.'),
|
||||
confirm: z
|
||||
.literal(true)
|
||||
.describe('Must be exactly true; removing dependencies is a deliberate opt-in.'),
|
||||
},
|
||||
}, async ({ packages, confirm }) => {
|
||||
if (!confirm)
|
||||
return errorResult('confirm: true is required to run composer_remove.');
|
||||
try {
|
||||
const backend = requirePhpBackend();
|
||||
const result = await runCommand('composer', ['remove', ...packages, '--no-interaction'], {
|
||||
cwd: backend.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({ adapter: backend.label, packages, ...result });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=composer.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"composer.js","sourceRoot":"","sources":["../../../src/tools/laravel/composer.ts"],"names":[],"mappings":"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"}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { hasComposerDependency, readComposerJson } from '../../lib/frameworks/composer.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { pascalCase } from '../../lib/strings.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireLaravel } from './requireLaravel.js';
|
||||
export function registerGenerateFilamentResourceTool(server) {
|
||||
server.registerTool('generate_filament_resource', {
|
||||
title: 'Generate Filament Resource',
|
||||
description: 'Generate a Filament admin resource via `php artisan make:filament-resource`. Requires filament/filament to be installed.',
|
||||
inputSchema: {
|
||||
name: z.string().min(1).describe('Resource/model name, e.g. "Post".'),
|
||||
generate: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe('Pass --generate to Filament when true (default).'),
|
||||
},
|
||||
}, async ({ name, generate }) => {
|
||||
try {
|
||||
const laravel = requireLaravel();
|
||||
const composer = readComposerJson(laravel.root);
|
||||
if (!hasComposerDependency(composer, 'filament/filament')) {
|
||||
return errorResult('filament/filament is not installed. Run composer_require with package "filament/filament" first ' +
|
||||
'(or `composer require filament/filament`).');
|
||||
}
|
||||
const resource = pascalCase(name);
|
||||
const args = ['artisan', 'make:filament-resource', resource, '--no-interaction'];
|
||||
if (generate !== false)
|
||||
args.push('--generate');
|
||||
const result = await runCommand('php', args, {
|
||||
cwd: laravel.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({
|
||||
package: 'filament/filament',
|
||||
resource,
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
output: result.output,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGenerateNovaResourceTool(server) {
|
||||
server.registerTool('generate_nova_resource', {
|
||||
title: 'Generate Nova Resource',
|
||||
description: 'Generate a Laravel Nova resource via `php artisan nova:resource`. Requires laravel/nova to be installed.',
|
||||
inputSchema: {
|
||||
name: z.string().min(1).describe('Resource name, e.g. "Post".'),
|
||||
},
|
||||
}, async ({ name }) => {
|
||||
try {
|
||||
const laravel = requireLaravel();
|
||||
const composer = readComposerJson(laravel.root);
|
||||
if (!hasComposerDependency(composer, 'laravel/nova')) {
|
||||
return errorResult('laravel/nova is not installed. Install Nova (license required) then re-run this tool. ' +
|
||||
'See https://nova.laravel.com/docs for installation.');
|
||||
}
|
||||
const resource = pascalCase(name);
|
||||
const result = await runCommand('php', ['artisan', 'nova:resource', resource, '--no-interaction'], {
|
||||
cwd: laravel.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({
|
||||
package: 'laravel/nova',
|
||||
resource,
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
output: result.output,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=generateAdminResources.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"generateAdminResources.js","sourceRoot":"","sources":["../../../src/tools/laravel/generateAdminResources.ts"],"names":[],"mappings":"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"}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { camelCase, pascalCase } from '../../lib/strings.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireLaravel } from './requireLaravel.js';
|
||||
const RELATION_TYPES = [
|
||||
'belongsTo',
|
||||
'hasOne',
|
||||
'hasMany',
|
||||
'belongsToMany',
|
||||
'hasManyThrough',
|
||||
'morphTo',
|
||||
'morphMany',
|
||||
'morphOne',
|
||||
];
|
||||
function findModelFile(root, model) {
|
||||
const candidates = [
|
||||
path.join(root, 'app', 'Models', `${model}.php`),
|
||||
path.join(root, 'app', `${model}.php`),
|
||||
];
|
||||
return candidates.find((file) => fs.existsSync(file)) ?? null;
|
||||
}
|
||||
function relationMethodSource(methodName, relation, relatedModel) {
|
||||
const related = `\\App\\Models\\${relatedModel}::class`;
|
||||
return `
|
||||
public function ${methodName}()
|
||||
{
|
||||
return $this->${relation}(${related});
|
||||
}
|
||||
`;
|
||||
}
|
||||
function insertMethodBeforeClosingBrace(source, method) {
|
||||
const trimmed = method.trimEnd() + '\n';
|
||||
if (source.includes(`function ${trimmed.match(/function\s+(\w+)/)?.[1] ?? ''}(`)) {
|
||||
return source;
|
||||
}
|
||||
const lastBrace = source.lastIndexOf('}');
|
||||
if (lastBrace === -1) {
|
||||
throw new Error('Could not find closing class brace in model file.');
|
||||
}
|
||||
return `${source.slice(0, lastBrace)}${trimmed}${source.slice(lastBrace)}`;
|
||||
}
|
||||
export function registerGenerateEloquentRelationTool(server) {
|
||||
server.registerTool('generate_eloquent_relation', {
|
||||
title: 'Generate Eloquent Relation',
|
||||
description: 'Add an Eloquent relationship method to a model (belongsTo, hasMany, etc.), optionally also adding the inverse relation on the related model.',
|
||||
inputSchema: {
|
||||
model: z.string().min(1).describe('Model class name, e.g. "Post".'),
|
||||
relation: z.enum(RELATION_TYPES).describe('Eloquent relation type.'),
|
||||
related: z.string().min(1).describe('Related model class name, e.g. "User".'),
|
||||
method: z.string().optional().describe('Method name on the source model. Defaults from relation type.'),
|
||||
inverse: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('If true, also add a sensible inverse method on the related model.'),
|
||||
},
|
||||
}, async ({ model, relation, related, method, inverse }) => {
|
||||
try {
|
||||
const laravel = requireLaravel();
|
||||
const modelName = pascalCase(model);
|
||||
const relatedName = pascalCase(related);
|
||||
const modelFile = findModelFile(laravel.root, modelName);
|
||||
if (!modelFile) {
|
||||
return errorResult(`Model file not found for "${modelName}" under app/Models or app/.`);
|
||||
}
|
||||
const defaultMethod = relation === 'belongsTo' || relation === 'hasOne' || relation === 'morphTo' || relation === 'morphOne'
|
||||
? camelCase(relatedName)
|
||||
: `${camelCase(relatedName)}s`;
|
||||
const methodName = method?.trim() || defaultMethod;
|
||||
const before = fs.readFileSync(modelFile, 'utf8');
|
||||
const next = insertMethodBeforeClosingBrace(before, relationMethodSource(methodName, relation, relatedName));
|
||||
fs.writeFileSync(modelFile, next);
|
||||
const updated = [modelFile];
|
||||
let inverseMethod = null;
|
||||
let inverseFile = null;
|
||||
if (inverse) {
|
||||
const inverseMap = {
|
||||
belongsTo: 'hasMany',
|
||||
hasMany: 'belongsTo',
|
||||
hasOne: 'belongsTo',
|
||||
belongsToMany: 'belongsToMany',
|
||||
morphMany: 'morphTo',
|
||||
morphOne: 'morphTo',
|
||||
};
|
||||
const inverseRelation = inverseMap[relation];
|
||||
if (inverseRelation) {
|
||||
inverseFile = findModelFile(laravel.root, relatedName);
|
||||
if (!inverseFile) {
|
||||
return errorResult(`Added ${methodName}() on ${modelName}, but related model "${relatedName}" was not found for the inverse.`);
|
||||
}
|
||||
inverseMethod =
|
||||
inverseRelation === 'belongsTo' || inverseRelation === 'hasOne' || inverseRelation === 'morphTo'
|
||||
? camelCase(modelName)
|
||||
: `${camelCase(modelName)}s`;
|
||||
const relatedSource = fs.readFileSync(inverseFile, 'utf8');
|
||||
fs.writeFileSync(inverseFile, insertMethodBeforeClosingBrace(relatedSource, relationMethodSource(inverseMethod, inverseRelation, modelName)));
|
||||
updated.push(inverseFile);
|
||||
}
|
||||
}
|
||||
return jsonResult({
|
||||
model: modelName,
|
||||
related: relatedName,
|
||||
relation,
|
||||
method: methodName,
|
||||
inverseMethod,
|
||||
files: updated,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=generateEloquentRelation.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"generateEloquentRelation.js","sourceRoot":"","sources":["../../../src/tools/laravel/generateEloquentRelation.ts"],"names":[],"mappings":"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"}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { kebabCase, pascalCase } from '../../lib/strings.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireLaravel } from './requireLaravel.js';
|
||||
function pluralize(word) {
|
||||
const lower = word.toLowerCase();
|
||||
if (lower.endsWith('ies'))
|
||||
return word;
|
||||
if (lower.endsWith('y') && !/[aeiou]y$/i.test(word))
|
||||
return `${word.slice(0, -1)}ies`;
|
||||
if (/(s|x|z|ch|sh)$/i.test(word))
|
||||
return `${word}es`;
|
||||
if (lower.endsWith('s'))
|
||||
return word;
|
||||
return `${word}s`;
|
||||
}
|
||||
export function registerGenerateLaravelResourceTool(server) {
|
||||
server.registerTool('generate_laravel_resource', {
|
||||
title: 'Generate Laravel Resource',
|
||||
description: 'One-shot scaffold of a full Laravel resource: model + migration + factory + seeder + controller, ' +
|
||||
'then wire an apiResource/resource route. Wraps the relevant `php artisan make:*` calls.',
|
||||
inputSchema: {
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Resource name, e.g. "Post" or "blog_post". Converted to PascalCase for the model/controller.'),
|
||||
api: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe('If true (default), generate an API controller (--api) and register Route::apiResource in routes/api.php. If false, generate a web resource controller and register Route::resource in routes/web.php.'),
|
||||
migrate: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('If true, run `php artisan migrate` after scaffolding. Requires a configured database.'),
|
||||
},
|
||||
}, async ({ name, api, migrate }) => {
|
||||
try {
|
||||
const laravel = requireLaravel();
|
||||
const model = pascalCase(name);
|
||||
const controller = `${model}Controller`;
|
||||
const routeName = kebabCase(pluralize(model));
|
||||
const steps = [];
|
||||
// Model + migration + factory + seeder in one go.
|
||||
const modelResult = await runCommand('php', ['artisan', 'make:model', model, '-mfs', '--no-interaction'], { cwd: laravel.root, timeoutMs: config.scaffoldCommandTimeoutMs });
|
||||
steps.push({ step: 'make:model -mfs', ...modelResult });
|
||||
const controllerArgs = api
|
||||
? ['artisan', 'make:controller', controller, '--api', '--model=' + model, '--no-interaction']
|
||||
: ['artisan', 'make:controller', controller, '--resource', '--model=' + model, '--no-interaction'];
|
||||
const controllerResult = await runCommand('php', controllerArgs, {
|
||||
cwd: laravel.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
steps.push({ step: 'make:controller', ...controllerResult });
|
||||
const routesFile = path.join(laravel.root, 'routes', api ? 'api.php' : 'web.php');
|
||||
const routeLine = api
|
||||
? `Route::apiResource('${routeName}', \\App\\Http\\Controllers\\${controller}::class);`
|
||||
: `Route::resource('${routeName}', \\App\\Http\\Controllers\\${controller}::class);`;
|
||||
const routeWired = wireRoute(routesFile, routeLine, api);
|
||||
steps.push({ step: 'wire-route', routesFile, routeLine, wired: routeWired });
|
||||
let migrateResult = null;
|
||||
if (migrate) {
|
||||
migrateResult = await runCommand('php', ['artisan', 'migrate', '--no-interaction'], {
|
||||
cwd: laravel.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
steps.push({ step: 'migrate', ...migrateResult });
|
||||
}
|
||||
return jsonResult({
|
||||
model,
|
||||
controller,
|
||||
route: `${api ? 'apiResource' : 'resource'} /${routeName}`,
|
||||
routesFile,
|
||||
routeWired,
|
||||
steps,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
function wireRoute(routesFile, routeLine, api) {
|
||||
fs.mkdirSync(path.dirname(routesFile), { recursive: true });
|
||||
if (!fs.existsSync(routesFile)) {
|
||||
const header = api
|
||||
? `<?php\n\nuse Illuminate\\Support\\Facades\\Route;\n\n`
|
||||
: `<?php\n\nuse Illuminate\\Support\\Facades\\Route;\n\n`;
|
||||
fs.writeFileSync(routesFile, header + routeLine + '\n');
|
||||
return true;
|
||||
}
|
||||
const existing = fs.readFileSync(routesFile, 'utf8');
|
||||
if (existing.includes(routeLine.trim()))
|
||||
return false;
|
||||
// Ensure the Route facade is imported if the file already exists without it.
|
||||
let next = existing;
|
||||
if (!/use\s+Illuminate\\Support\\Facades\\Route\s*;/.test(next)) {
|
||||
if (next.startsWith('<?php')) {
|
||||
next = next.replace('<?php', "<?php\n\nuse Illuminate\\Support\\Facades\\Route;");
|
||||
}
|
||||
else {
|
||||
next = `<?php\n\nuse Illuminate\\Support\\Facades\\Route;\n\n${next}`;
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(routesFile, `${next.trimEnd()}\n${routeLine}\n`);
|
||||
return true;
|
||||
}
|
||||
//# sourceMappingURL=generateLaravelResource.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"generateLaravelResource.js","sourceRoot":"","sources":["../../../src/tools/laravel/generateLaravelResource.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
import { registerArtisanTool } from './artisan.js';
|
||||
import { registerGenerateLaravelResourceTool } from './generateLaravelResource.js';
|
||||
import { registerComposerTools } from './composer.js';
|
||||
import { registerPhpTestTools } from './runPhpTests.js';
|
||||
import { registerTinkerEvalTool } from './tinkerEval.js';
|
||||
import { registerGenerateEloquentRelationTool } from './generateEloquentRelation.js';
|
||||
import { registerGenerateFilamentResourceTool, registerGenerateNovaResourceTool } from './generateAdminResources.js';
|
||||
import { registerLaravelQueueStatusTool } from './queueStatus.js';
|
||||
export function registerLaravelTools(server) {
|
||||
registerArtisanTool(server);
|
||||
registerGenerateLaravelResourceTool(server);
|
||||
registerComposerTools(server);
|
||||
registerPhpTestTools(server);
|
||||
registerTinkerEvalTool(server);
|
||||
registerGenerateEloquentRelationTool(server);
|
||||
registerGenerateFilamentResourceTool(server);
|
||||
registerGenerateNovaResourceTool(server);
|
||||
registerLaravelQueueStatusTool(server);
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/laravel/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,mCAAmC,EAAE,MAAM,8BAA8B,CAAC;AACnF,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,oCAAoC,EAAE,MAAM,+BAA+B,CAAC;AACrF,OAAO,EAAE,oCAAoC,EAAE,gCAAgC,EAAE,MAAM,6BAA6B,CAAC;AACrH,OAAO,EAAE,8BAA8B,EAAE,MAAM,kBAAkB,CAAC;AAElE,MAAM,UAAU,oBAAoB,CAAC,MAAiB;IACpD,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,mCAAmC,CAAC,MAAM,CAAC,CAAC;IAC5C,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,oCAAoC,CAAC,MAAM,CAAC,CAAC;IAC7C,oCAAoC,CAAC,MAAM,CAAC,CAAC;IAC7C,gCAAgC,CAAC,MAAM,CAAC,CAAC;IACzC,8BAA8B,CAAC,MAAM,CAAC,CAAC;AACzC,CAAC"}
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireLaravel } from './requireLaravel.js';
|
||||
export function registerLaravelQueueStatusTool(server) {
|
||||
server.registerTool('laravel_queue_status', {
|
||||
title: 'Laravel Queue Status',
|
||||
description: 'Summarize Laravel queue health via `php artisan queue:failed` (and optionally `queue:batches` when available). Read-only.',
|
||||
inputSchema: {
|
||||
includeBatches: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('If true, also run `php artisan queue:batches` when supported.'),
|
||||
},
|
||||
}, async ({ includeBatches }) => {
|
||||
try {
|
||||
const laravel = requireLaravel();
|
||||
const failed = await runCommand('php', ['artisan', 'queue:failed', '--no-interaction'], {
|
||||
cwd: laravel.root,
|
||||
timeoutMs: config.defaultCommandTimeoutMs,
|
||||
});
|
||||
let batches = null;
|
||||
if (includeBatches) {
|
||||
batches = await runCommand('php', ['artisan', 'queue:batches', '--no-interaction'], {
|
||||
cwd: laravel.root,
|
||||
timeoutMs: config.defaultCommandTimeoutMs,
|
||||
});
|
||||
}
|
||||
const failedLines = failed.output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const failedCount = failedLines.filter((line) => /^\d+\s+/.test(line) || /\|/.test(line)).length;
|
||||
return jsonResult({
|
||||
failed: {
|
||||
exitCode: failed.exitCode,
|
||||
failed: failed.failed,
|
||||
approxRows: failedCount,
|
||||
output: failed.output,
|
||||
},
|
||||
batches: batches
|
||||
? {
|
||||
exitCode: batches.exitCode,
|
||||
failed: batches.failed,
|
||||
output: batches.output,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=queueStatus.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"queueStatus.js","sourceRoot":"","sources":["../../../src/tools/laravel/queueStatus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,UAAU,8BAA8B,CAAC,MAAiB;IAC9D,MAAM,CAAC,YAAY,CACjB,sBAAsB,EACtB;QACE,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EACT,2HAA2H;QAC7H,WAAW,EAAE;YACX,cAAc,EAAE,CAAC;iBACd,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,OAAO,CAAC,KAAK,CAAC;iBACd,QAAQ,CAAC,+DAA+D,CAAC;SAC7E;KACF,EACD,KAAK,EAAE,EAAE,cAAc,EAAE,EAAE,EAAE;QAC3B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,cAAc,EAAE,kBAAkB,CAAC,EAAE;gBACtF,GAAG,EAAE,OAAO,CAAC,IAAI;gBACjB,SAAS,EAAE,MAAM,CAAC,uBAAuB;aAC1C,CAAC,CAAC;YAEH,IAAI,OAAO,GAAkD,IAAI,CAAC;YAClE,IAAI,cAAc,EAAE,CAAC;gBACnB,OAAO,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,eAAe,EAAE,kBAAkB,CAAC,EAAE;oBAClF,GAAG,EAAE,OAAO,CAAC,IAAI;oBACjB,SAAS,EAAE,MAAM,CAAC,uBAAuB;iBAC1C,CAAC,CAAC;YACL,CAAC;YAED,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM;iBAC9B,KAAK,CAAC,OAAO,CAAC;iBACd,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;iBAC1B,MAAM,CAAC,OAAO,CAAC,CAAC;YACnB,MAAM,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;YAEjG,OAAO,UAAU,CAAC;gBAChB,MAAM,EAAE;oBACN,QAAQ,EAAE,MAAM,CAAC,QAAQ;oBACzB,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,UAAU,EAAE,WAAW;oBACvB,MAAM,EAAE,MAAM,CAAC,MAAM;iBACtB;gBACD,OAAO,EAAE,OAAO;oBACd,CAAC,CAAC;wBACE,QAAQ,EAAE,OAAO,CAAC,QAAQ;wBAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;wBACtB,MAAM,EAAE,OAAO,CAAC,MAAM;qBACvB;oBACH,CAAC,CAAC,IAAI;aACT,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
import { config } from '../../config.js';
|
||||
import { detectProject } from '../../lib/frameworks/detect.js';
|
||||
import { LaravelAdapter } from '../../lib/frameworks/laravel.js';
|
||||
export function requireLaravel() {
|
||||
const detected = detectProject(config.workspaceRoot);
|
||||
if (detected.backend instanceof LaravelAdapter) {
|
||||
return detected.backend;
|
||||
}
|
||||
throw new Error('No Laravel project detected in WORKSPACE_ROOT. ' +
|
||||
'Expected composer.json with laravel/framework (or an artisan file) at the workspace root.');
|
||||
}
|
||||
/** Any Composer-based PHP backend (Laravel or CodeIgniter). */
|
||||
export function requirePhpBackend() {
|
||||
const detected = detectProject(config.workspaceRoot);
|
||||
if (detected.backend && (detected.backend.kind === 'laravel' || detected.backend.kind === 'codeigniter')) {
|
||||
return detected.backend;
|
||||
}
|
||||
throw new Error('No PHP backend (Laravel or CodeIgniter) detected in WORKSPACE_ROOT. ' +
|
||||
'Expected a composer.json with laravel/framework or codeigniter4/framework.');
|
||||
}
|
||||
//# sourceMappingURL=requireLaravel.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"requireLaravel.js","sourceRoot":"","sources":["../../../src/tools/laravel/requireLaravel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAGjE,MAAM,UAAU,cAAc;IAC5B,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACrD,IAAI,QAAQ,CAAC,OAAO,YAAY,cAAc,EAAE,CAAC;QAC/C,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC1B,CAAC;IACD,MAAM,IAAI,KAAK,CACb,iDAAiD;QAC/C,2FAA2F,CAC9F,CAAC;AACJ,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,iBAAiB;IAC/B,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACrD,IAAI,QAAQ,CAAC,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,aAAa,CAAC,EAAE,CAAC;QACzG,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC1B,CAAC;IACD,MAAM,IAAI,KAAK,CACb,sEAAsE;QACpE,4EAA4E,CAC/E,CAAC;AACJ,CAAC"}
|
||||
Vendored
+100
@@ -0,0 +1,100 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { vendorBinExists } from '../../lib/frameworks/composer.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireLaravel } from './requireLaravel.js';
|
||||
function vendorBin(root, bin) {
|
||||
const suffix = process.platform === 'win32' ? '.bat' : '';
|
||||
return path.join(root, 'vendor', 'bin', `${bin}${suffix}`);
|
||||
}
|
||||
function parsePhpUnitSummary(output) {
|
||||
// PHPUnit: "OK (12 tests, 34 assertions)" or "FAILURES!\nTests: 12, Assertions: 30, Failures: 2."
|
||||
const ok = output.match(/OK\s*\((\d+)\s+tests?,\s*(\d+)\s+assertions?\)/i);
|
||||
if (ok) {
|
||||
return { passed: true, tests: Number(ok[1]), assertions: Number(ok[2]), failures: 0, errors: 0 };
|
||||
}
|
||||
const summary = output.match(/Tests:\s*(\d+),\s*Assertions:\s*(\d+)(?:,\s*Errors:\s*(\d+))?(?:,\s*Failures:\s*(\d+))?(?:,\s*Skipped:\s*(\d+))?/i);
|
||||
if (summary) {
|
||||
return {
|
||||
passed: !/FAILURES!|ERRORS!/i.test(output),
|
||||
tests: Number(summary[1]),
|
||||
assertions: Number(summary[2]),
|
||||
errors: Number(summary[3] ?? 0),
|
||||
failures: Number(summary[4] ?? 0),
|
||||
skipped: Number(summary[5] ?? 0),
|
||||
};
|
||||
}
|
||||
// Pest often prints "Tests: 12 passed (34 assertions)" or similar.
|
||||
const pest = output.match(/Tests:\s+(\d+)\s+passed/i);
|
||||
if (pest) {
|
||||
return { passed: !/failed|FAIL/i.test(output), tests: Number(pest[1]) };
|
||||
}
|
||||
return { passed: null, note: 'Could not parse a pass/fail summary from the runner output.' };
|
||||
}
|
||||
export function registerPhpTestTools(server) {
|
||||
server.registerTool('run_phpunit', {
|
||||
title: 'Run PHPUnit',
|
||||
description: "Run the Laravel project's PHPUnit suite via vendor/bin/phpunit (or `php artisan test` as a fallback). Returns parsed pass/fail counts when possible.",
|
||||
inputSchema: {
|
||||
extraArgs: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Extra CLI args, e.g. ["--filter", "UserTest"].'),
|
||||
},
|
||||
}, async ({ extraArgs }) => {
|
||||
try {
|
||||
const laravel = requireLaravel();
|
||||
const args = extraArgs ?? [];
|
||||
let command;
|
||||
let cmdArgs;
|
||||
if (vendorBinExists(laravel.root, 'phpunit')) {
|
||||
command = vendorBin(laravel.root, 'phpunit');
|
||||
cmdArgs = args;
|
||||
}
|
||||
else if (fs.existsSync(path.join(laravel.root, 'artisan'))) {
|
||||
command = 'php';
|
||||
cmdArgs = ['artisan', 'test', '--without-tty', ...args];
|
||||
}
|
||||
else {
|
||||
return errorResult(`No vendor/bin/phpunit found at ${laravel.root}. Run composer install (or run_install) first.`);
|
||||
}
|
||||
const result = await runCommand(command, cmdArgs, {
|
||||
cwd: laravel.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({ runner: 'phpunit', summary: parsePhpUnitSummary(result.output), ...result });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
server.registerTool('run_pest', {
|
||||
title: 'Run Pest',
|
||||
description: "Run the Laravel project's Pest suite via vendor/bin/pest. Falls back to an error if Pest is not installed.",
|
||||
inputSchema: {
|
||||
extraArgs: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Extra CLI args, e.g. ["--filter", "it creates a post"].'),
|
||||
},
|
||||
}, async ({ extraArgs }) => {
|
||||
try {
|
||||
const laravel = requireLaravel();
|
||||
if (!vendorBinExists(laravel.root, 'pest')) {
|
||||
return errorResult(`No vendor/bin/pest found at ${laravel.root}. Install pestphp/pest (composer_require) or use run_phpunit instead.`);
|
||||
}
|
||||
const result = await runCommand(vendorBin(laravel.root, 'pest'), extraArgs ?? [], {
|
||||
cwd: laravel.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({ runner: 'pest', summary: parsePhpUnitSummary(result.output), ...result });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=runPhpTests.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"runPhpTests.js","sourceRoot":"","sources":["../../../src/tools/laravel/runPhpTests.ts"],"names":[],"mappings":"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"}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user