@factorialco/shadowdog
Version:
<img src="https://raw.githubusercontent.com/factorialco/shadowdog/refs/heads/main/logo.png" alt="drawing" width="100"/>
905 lines (904 loc) β’ 43.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const fs_1 = require("fs");
const path = __importStar(require("path"));
const http_1 = require("http");
const utils_1 = require("../utils");
const chalk_1 = __importDefault(require("chalk"));
// Global state
let config = null;
let lockFilePath = '';
let server = null;
let httpServer = null;
let eventEmitter = null;
// Pending changes tracking (similar to git plugin)
let pendingChangedFiles = [];
let isPaused = false;
// Helper to read lock file data
const readLockFileData = () => {
if (!lockFilePath || !(0, fs_1.existsSync)(lockFilePath)) {
return null;
}
try {
const content = (0, fs_1.readFileSync)(lockFilePath, 'utf8');
return JSON.parse(content);
}
catch {
return null;
}
};
// Helper to get all artifacts from config and lock file
const getAllArtifacts = () => {
var _a;
if (!config) {
return [];
}
const artifacts = [];
const lockData = readLockFileData();
for (const watcher of config.watchers) {
for (const commandConfig of watcher.commands) {
for (const artifact of commandConfig.artifacts) {
const lockArtifact = lockData === null || lockData === void 0 ? void 0 : lockData.artifacts.find((a) => a.output === artifact.output);
artifacts.push({
output: artifact.output,
command: commandConfig.command,
files: (lockArtifact === null || lockArtifact === void 0 ? void 0 : lockArtifact.fileManifest.watchedFiles) || watcher.files,
environment: watcher.environment,
lastUpdated: lockArtifact
? new Date((0, fs_1.existsSync)(path.join(process.cwd(), artifact.output))
? ((_a = statSyncSafe(path.join(process.cwd(), artifact.output))) === null || _a === void 0 ? void 0 : _a.mtime.toISOString()) ||
''
: '').toISOString()
: undefined,
cacheIdentifier: lockArtifact === null || lockArtifact === void 0 ? void 0 : lockArtifact.cacheIdentifier,
outputSha: lockArtifact === null || lockArtifact === void 0 ? void 0 : lockArtifact.outputSha,
});
}
}
}
return artifacts;
};
// Safe stat sync helper
const statSyncSafe = (filePath) => {
try {
return (0, fs_1.statSync)(filePath);
}
catch {
return null;
}
};
// Helper to handle file changes when paused
const handleFileChange = (filePath) => {
if (isPaused) {
// Track the file change for later processing
if (!pendingChangedFiles.includes(filePath)) {
pendingChangedFiles.push(filePath);
}
return true; // Indicates the change was handled (ignored)
}
return false; // Indicates the change should be processed normally
};
// Helper to replay pending changes when resuming
const replayPendingChanges = () => {
if (pendingChangedFiles.length === 0) {
return;
}
(0, utils_1.logMessage)(`π Replaying ${chalk_1.default.cyan(pendingChangedFiles.length)} file changes that occurred while paused...`);
const now = new Date();
pendingChangedFiles.forEach((filePath) => {
try {
// Touch the file to trigger file watchers
(0, fs_1.utimesSync)(filePath, now, now);
(0, utils_1.logMessage)(` β Replayed: ${chalk_1.default.blue(filePath)}`);
}
catch (error) {
(0, utils_1.logMessage)(` β Failed to replay: ${chalk_1.default.red(filePath)} - ${error.message}`);
}
});
(0, utils_1.logMessage)(`β
Successfully replayed ${chalk_1.default.cyan(pendingChangedFiles.length)} file changes.`);
// Clear the pending changes
pendingChangedFiles = [];
};
// Helper to find command config for a specific artifact
const findCommandForArtifact = (artifactOutput) => {
if (!config) {
return null;
}
for (const watcher of config.watchers) {
for (const commandConfig of watcher.commands) {
for (const artifact of commandConfig.artifacts) {
if (artifact.output === artifactOutput) {
return {
command: commandConfig.command,
workingDirectory: commandConfig.workingDirectory,
files: watcher.files,
environment: watcher.environment,
};
}
}
}
}
return null;
};
// MCP Tools definitions
const TOOLS = [
{
name: 'pause-shadowdog',
description: 'Pauses shadowdog when running in watch mode. Use this before making changes to prevent automatic artifact generation. This properly integrates with the daemon using events.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'resume-shadowdog',
description: 'Resumes shadowdog after being paused. Use this after finishing changes to re-enable automatic artifact generation. This properly integrates with the daemon using events.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get-artifacts',
description: 'Retrieves information about all artifacts being generated by shadowdog, including their status, last update time, and associated files.',
inputSchema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'Optional filter to search for specific artifacts by output path (case-insensitive substring match)',
},
},
required: [],
},
},
{
name: 'compute-artifact',
description: "Computes a specific artifact by triggering the daemon's artifact generation system. This properly integrates with shadowdog's artifact management system, respects configuration settings, uses the same task runner and middleware as the daemon, and provides consistent logging. This allows generating individual artifacts without running the entire build.",
inputSchema: {
type: 'object',
properties: {
artifactOutput: {
type: 'string',
description: 'The output path of the artifact to compute (e.g., "build/app.js", "dist/styles.css", "docs/api.md")',
},
},
required: ['artifactOutput'],
},
},
{
name: 'get-shadowdog-status',
description: 'Gets the current status of shadowdog, including daemon availability, configuration summary, and artifact information.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'clear-shadowdog-cache',
description: "Clears shadowdog's local cache, lock files, and socket files. This removes all cached artifacts and forces a fresh build on the next run.",
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'compute-all-artifacts',
description: "Computes all artifacts by triggering the daemon's artifact generation system for every configured artifact. This properly integrates with shadowdog's artifact management system, respects configuration settings, uses the same task runner and middleware as the daemon, and provides consistent logging. This allows generating all artifacts at once without running individual artifact commands.",
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
];
// Initialize MCP server
const initializeMCPServer = () => {
if (server) {
return; // Already initialized
}
server = new index_js_1.Server({
name: 'shadowdog-mcp',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// Register tool handlers
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
return {
tools: TOOLS,
};
});
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'pause-shadowdog': {
if (!eventEmitter) {
return {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} Shadowdog daemon not available.`,
},
],
isError: true,
};
}
isPaused = true;
eventEmitter.emit('pause');
return {
content: [
{
type: 'text',
text: `βΈοΈ ${chalk_1.default.yellow('Successfully paused shadowdog.')} File changes will be tracked and replayed on resume.`,
},
],
};
}
case 'resume-shadowdog': {
if (!eventEmitter) {
return {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} Shadowdog daemon not available.`,
},
],
isError: true,
};
}
isPaused = false;
eventEmitter.emit('resume');
// Replay any pending changes that occurred while paused
replayPendingChanges();
return {
content: [
{
type: 'text',
text: `βΆοΈ ${chalk_1.default.green('Successfully resumed shadowdog.')} Automatic artifact generation is now enabled.`,
},
],
};
}
case 'get-artifacts': {
const artifacts = getAllArtifacts();
const filter = args === null || args === void 0 ? void 0 : args.filter;
const filteredArtifacts = filter
? artifacts.filter((a) => a.output.toLowerCase().includes(filter.toLowerCase()))
: artifacts;
const artifactInfo = filteredArtifacts.map((artifact) => {
const status = (0, fs_1.existsSync)(path.join(process.cwd(), artifact.output))
? 'β exists'
: 'β missing';
return {
output: artifact.output,
status,
command: artifact.command,
lastUpdated: artifact.lastUpdated || 'unknown',
watchedFiles: artifact.files.length,
cacheIdentifier: artifact.cacheIdentifier,
outputSha: artifact.outputSha,
};
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
total: filteredArtifacts.length,
artifacts: artifactInfo,
}, null, 2),
},
],
};
}
case 'compute-artifact': {
const artifactOutput = args === null || args === void 0 ? void 0 : args.artifactOutput;
if (!artifactOutput) {
return {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} artifactOutput parameter is required`,
},
],
isError: true,
};
}
if (!eventEmitter) {
return {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} Shadowdog daemon not available.`,
},
],
isError: true,
};
}
// Check if artifact exists in config
const commandInfo = findCommandForArtifact(artifactOutput);
if (!commandInfo) {
return {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} No command found for artifact '${chalk_1.default.blue(artifactOutput)}'`,
},
],
isError: true,
};
}
// Emit event to daemon to compute the artifact
eventEmitter.emit('computeArtifact', { artifactOutput });
return {
content: [
{
type: 'text',
text: `π¨ ${chalk_1.default.blue('Artifact computation request sent for')} '${chalk_1.default.cyan(artifactOutput)}'. Check the daemon logs for progress.`,
},
],
};
}
case 'get-shadowdog-status': {
const artifacts = getAllArtifacts();
const existingArtifacts = artifacts.filter((a) => (0, fs_1.existsSync)(path.join(process.cwd(), a.output)));
return {
content: [
{
type: 'text',
text: JSON.stringify({
daemonAvailable: eventEmitter !== null,
configLoaded: config !== null,
totalWatchers: (config === null || config === void 0 ? void 0 : config.watchers.length) || 0,
totalArtifacts: artifacts.length,
existingArtifacts: existingArtifacts.length,
lockFilePath: lockFilePath,
lockFileExists: (0, fs_1.existsSync)(lockFilePath),
}, null, 2),
},
],
};
}
case 'clear-shadowdog-cache': {
try {
// Clear the main shadowdog temp directory
const shadowdogTempDir = '/tmp/shadowdog';
if ((0, fs_1.existsSync)(shadowdogTempDir)) {
(0, fs_1.rmSync)(shadowdogTempDir, { recursive: true, force: true });
}
// Also clear the local lock file if it exists
if (lockFilePath && (0, fs_1.existsSync)(lockFilePath)) {
(0, fs_1.rmSync)(lockFilePath, { force: true });
}
return {
content: [
{
type: 'text',
text: `β
${chalk_1.default.green('Successfully cleared shadowdog cache.')} All cached artifacts, lock files, and socket files have been removed.`,
},
],
};
}
catch (error) {
return {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error clearing cache:')} ${error.message}`,
},
],
isError: true,
};
}
}
case 'compute-all-artifacts': {
if (!eventEmitter) {
return {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} Shadowdog daemon not available.`,
},
],
isError: true,
};
}
if (!config) {
return {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} Shadowdog configuration not loaded.`,
},
],
isError: true,
};
}
// Get all artifacts from config
const allArtifacts = getAllArtifacts();
if (allArtifacts.length === 0) {
return {
content: [
{
type: 'text',
text: `βΉοΈ ${chalk_1.default.blue('No artifacts found in configuration.')} Nothing to compute.`,
},
],
};
}
// Emit event to daemon to compute all artifacts
eventEmitter.emit('computeAllArtifacts', {
artifacts: allArtifacts.map((artifact) => ({ output: artifact.output })),
});
return {
content: [
{
type: 'text',
text: `π¨ ${chalk_1.default.blue('All artifacts computation request sent.')} Computing ${chalk_1.default.cyan(allArtifacts.length)} artifacts. Check the daemon logs for progress.`,
},
],
};
}
default:
return {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Unknown tool:')} ${chalk_1.default.blue(name)}`,
},
],
isError: true,
};
}
}
catch (error) {
return {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error executing tool')} '${chalk_1.default.blue(name)}': ${error.message}`,
},
],
isError: true,
};
}
});
// Start the HTTP server
const port = process.env.SHADOWDOG_MCP_PORT ? parseInt(process.env.SHADOWDOG_MCP_PORT) : 8473;
const host = process.env.SHADOWDOG_MCP_HOST || 'localhost';
httpServer = (0, http_1.createServer)(async (req, res) => {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
if (req.method !== 'POST' || req.url !== '/mcp') {
res.writeHead(404);
res.end('Not Found');
return;
}
try {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', async () => {
try {
const request = JSON.parse(body);
// Handle the MCP request
let response;
if (request.method === 'initialize') {
response = {
jsonrpc: '2.0',
id: request.id,
result: {
protocolVersion: '2024-11-05',
capabilities: {
tools: {},
},
serverInfo: {
name: 'shadowdog-mcp',
version: '1.0.0',
},
},
};
}
else if (request.method === 'tools/list') {
response = {
jsonrpc: '2.0',
id: request.id,
result: { tools: TOOLS },
};
}
else if (request.method === 'tools/call') {
const toolName = request.params.name;
const toolArgs = request.params.arguments || {};
// Call the appropriate tool
let result;
switch (toolName) {
case 'pause-shadowdog':
if (!eventEmitter) {
result = {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} Shadowdog daemon not available.`,
},
],
isError: true,
};
}
else {
isPaused = true;
eventEmitter.emit('pause');
result = {
content: [
{
type: 'text',
text: `βΈοΈ ${chalk_1.default.yellow('Successfully paused shadowdog.')} File changes will be tracked and replayed on resume.`,
},
],
};
}
break;
case 'resume-shadowdog':
if (!eventEmitter) {
result = {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} Shadowdog daemon not available.`,
},
],
isError: true,
};
}
else {
isPaused = false;
eventEmitter.emit('resume');
// Replay any pending changes that occurred while paused
replayPendingChanges();
result = {
content: [
{
type: 'text',
text: `βΆοΈ ${chalk_1.default.green('Successfully resumed shadowdog.')} Automatic artifact generation is now enabled.`,
},
],
};
}
break;
case 'get-artifacts': {
const artifacts = getAllArtifacts();
const filter = toolArgs.filter;
const filteredArtifacts = filter
? artifacts.filter((a) => a.output.toLowerCase().includes(filter.toLowerCase()))
: artifacts;
const artifactInfo = filteredArtifacts.map((artifact) => {
const status = (0, fs_1.existsSync)(path.join(process.cwd(), artifact.output))
? 'β exists'
: 'β missing';
return {
output: artifact.output,
status,
command: artifact.command,
lastUpdated: artifact.lastUpdated || 'unknown',
watchedFiles: artifact.files.length,
cacheIdentifier: artifact.cacheIdentifier,
outputSha: artifact.outputSha,
};
});
result = {
content: [
{
type: 'text',
text: JSON.stringify({ total: filteredArtifacts.length, artifacts: artifactInfo }, null, 2),
},
],
};
break;
}
case 'compute-artifact': {
const artifactOutput = toolArgs.artifactOutput;
if (!artifactOutput) {
result = {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} artifactOutput parameter is required`,
},
],
isError: true,
};
}
else if (!eventEmitter) {
result = {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} Shadowdog daemon not available.`,
},
],
isError: true,
};
}
else {
const commandInfo = findCommandForArtifact(artifactOutput);
if (!commandInfo) {
result = {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} No command found for artifact '${chalk_1.default.blue(artifactOutput)}'`,
},
],
isError: true,
};
}
else {
eventEmitter.emit('computeArtifact', { artifactOutput });
result = {
content: [
{
type: 'text',
text: `π¨ ${chalk_1.default.blue('Artifact computation request sent for')} '${chalk_1.default.cyan(artifactOutput)}'. Check the daemon logs for progress.`,
},
],
};
}
}
break;
}
case 'get-shadowdog-status': {
const allArtifacts = getAllArtifacts();
const existingArtifacts = allArtifacts.filter((a) => (0, fs_1.existsSync)(path.join(process.cwd(), a.output)));
result = {
content: [
{
type: 'text',
text: JSON.stringify({
daemonAvailable: eventEmitter !== null,
configLoaded: config !== null,
totalWatchers: (config === null || config === void 0 ? void 0 : config.watchers.length) || 0,
totalArtifacts: allArtifacts.length,
existingArtifacts: existingArtifacts.length,
lockFilePath: lockFilePath,
lockFileExists: (0, fs_1.existsSync)(lockFilePath),
}, null, 2),
},
],
};
break;
}
case 'clear-shadowdog-cache': {
try {
// Clear the main shadowdog temp directory
const shadowdogTempDir = '/tmp/shadowdog';
if ((0, fs_1.existsSync)(shadowdogTempDir)) {
(0, fs_1.rmSync)(shadowdogTempDir, { recursive: true, force: true });
}
// Also clear the local lock file if it exists
if (lockFilePath && (0, fs_1.existsSync)(lockFilePath)) {
(0, fs_1.rmSync)(lockFilePath, { force: true });
}
result = {
content: [
{
type: 'text',
text: `β
${chalk_1.default.green('Successfully cleared shadowdog cache.')} All cached artifacts, lock files, and socket files have been removed.`,
},
],
};
}
catch (error) {
result = {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error clearing cache:')} ${error.message}`,
},
],
isError: true,
};
}
break;
}
case 'compute-all-artifacts': {
if (!eventEmitter) {
result = {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} Shadowdog daemon not available.`,
},
],
isError: true,
};
}
else if (!config) {
result = {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Error:')} Shadowdog configuration not loaded.`,
},
],
isError: true,
};
}
else {
// Get all artifacts from config
const allArtifacts = getAllArtifacts();
if (allArtifacts.length === 0) {
result = {
content: [
{
type: 'text',
text: `βΉοΈ ${chalk_1.default.blue('No artifacts found in configuration.')} Nothing to compute.`,
},
],
};
}
else {
// Emit event to daemon to compute all artifacts
eventEmitter.emit('computeAllArtifacts', {
artifacts: allArtifacts.map((artifact) => ({ output: artifact.output })),
});
result = {
content: [
{
type: 'text',
text: `π¨ ${chalk_1.default.blue('All artifacts computation request sent.')} Computing ${chalk_1.default.cyan(allArtifacts.length)} artifacts. Check the daemon logs for progress.`,
},
],
};
}
}
break;
}
default:
result = {
content: [
{
type: 'text',
text: `β ${chalk_1.default.red('Unknown tool:')} ${chalk_1.default.blue(toolName)}`,
},
],
isError: true,
};
}
response = {
jsonrpc: '2.0',
id: request.id,
result,
};
}
else {
response = {
jsonrpc: '2.0',
id: request.id,
error: { code: -32601, message: 'Method not found' },
};
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
}
catch {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
jsonrpc: '2.0',
id: null,
error: { code: -32700, message: 'Parse error' },
}));
}
});
}
catch {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
jsonrpc: '2.0',
id: null,
error: { code: -32603, message: 'Internal error' },
}));
}
});
httpServer.listen(port, host, () => {
const serverUrl = `http://${host}:${port}/mcp`;
(0, utils_1.logMessage)(`π MCP Server initialized and ready at ${chalk_1.default.green(serverUrl)}`);
// Only show connection details when running in MCP mode
if (process.argv.includes('--mcp')) {
(0, utils_1.logMessage)(`π To connect from Cursor, add to your MCP config:`);
(0, utils_1.logMessage)(` ${chalk_1.default.yellow(`"shadowdog-mcp": { "url": "${serverUrl}" }`)}`);
(0, utils_1.logMessage)(` Available tools: pause-shadowdog, resume-shadowdog, get-artifacts, compute-artifact, compute-all-artifacts, get-shadowdog-status, clear-shadowdog-cache`);
(0, utils_1.logMessage)(`π Setup guide: ${chalk_1.default.underline('https://cursor.com/docs/context/mcp/install-links')}`);
}
});
httpServer.on('error', (error) => {
(0, utils_1.logMessage)(`β ${chalk_1.default.red('HTTP Server error:')} ${error.message}`);
});
};
// Event listener plugin implementation
const listener = (eventEmitterParam) => {
// Store event emitter reference
eventEmitter = eventEmitterParam;
// Initialize lock file path
lockFilePath = path.resolve(process.cwd(), 'shadowdog-lock.json');
// Store config reference when it's loaded
eventEmitter.on('configLoaded', ({ config: loadedConfig }) => {
config = loadedConfig;
});
// Initialize MCP server when shadowdog initializes
eventEmitter.on('initialized', () => {
initializeMCPServer();
});
// Listen for file changes to track them when paused
eventEmitter.on('changed', ({ path: filePath }) => {
handleFileChange(filePath);
});
// Clean up on exit
eventEmitter.on('exit', () => {
if (httpServer) {
httpServer.close(() => {
(0, utils_1.logMessage)(`π MCP HTTP Server closed`);
});
}
if (server) {
server.close().catch((error) => {
(0, utils_1.logMessage)(`β Failed to close MCP server: ${chalk_1.default.red(error.message)}`);
});
}
});
};
exports.default = {
listener,
};