Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions describe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { tool } from "@opencode-ai/plugin/tool";

/**
* Raw DDEV describe output from `ddev describe -j`
*/
export type DdevDescribeRaw = {
shortroot?: string;
approot?: string;
status?: string;
name?: string;
type?: string;
php_version?: string;
webserver_type?: string;
database_type?: string;
database_version?: string;
router_status?: string;
docroot?: string;
mutagen_enabled?: boolean;
hostnames?: string[];
httpurl?: string;
httpsurl?: string;
services?: {
web?: {
host_ports_mapping?: Array<{
exposed_port: string;
host_port: string;
}>;
};
db?: {
host_ports_mapping?: Array<{
exposed_port: string;
host_port: string;
}>;
};
};
[key: string]: unknown;
};

/**
* Simplified DDEV project information for LLM consumption
*/
export type DdevProjectInfo = {
name: string;
status: string;
type: string | null;
domain: string;
httpsUrl: string;
httpUrl: string;
webPort: string | null;
dbPort: string | null;
phpVersion: string | null;
webserverType: string | null;
dbType: string | null;
dbVersion: string | null;
};

/**
* Default fields to expose in the describe tool
*/
const DEFAULT_FIELDS = [
'name',
'status',
'type',
'domain',
'httpsUrl',
'httpUrl',
'webPort',
'dbPort',
] as const;

/**
* Fetches and parses DDEV project data from `ddev describe -j`
*
* @param $ - Exec function for running shell commands
* @returns Parsed DDEV describe data or null if unavailable
*/
export async function getDdevDescribeData($: any): Promise<DdevDescribeRaw | null> {
try {
const result = await $`ddev describe -j`.quiet().nothrow();

if (result.exitCode !== 0) {
return null;
}

const output = result.stdout.toString();

try {
const data = JSON.parse(output);
return (data?.raw as DdevDescribeRaw) ?? null;
} catch {
return null;
}
} catch {
return null;
}
}

/**
* Extracts meaningful project information from raw DDEV describe data
*
* @param raw - Raw DDEV describe data
* @returns Simplified project information
*/
export function extractProjectInfo(raw: DdevDescribeRaw): DdevProjectInfo {
const name = raw.name ?? 'unknown';
const status = raw.status ?? 'unknown';
const type = raw.type ?? null;
const httpsUrl = raw.httpsurl ?? '';
const httpUrl = raw.httpurl ?? '';
const domain = (raw.hostnames?.[0] ?? httpsUrl.replace(/^https?:\/\//, '')) || 'localhost';

let webPort: string | null = null;
let dbPort: string | null = null;

if (raw.services) {
const webMapping = raw.services.web?.host_ports_mapping?.[0];
if (webMapping) {
webPort = webMapping.host_port;
}

const dbMapping = raw.services.db?.host_ports_mapping?.[0];
if (dbMapping) {
dbPort = dbMapping.host_port;
}
}

return {
name,
status,
type,
domain,
httpsUrl,
httpUrl,
webPort,
dbPort,
phpVersion: raw.php_version ?? null,
webserverType: raw.webserver_type ?? null,
dbType: raw.database_type ?? null,
dbVersion: raw.database_version ?? null,
};
}

/**
* Creates a DDEV describe tool for viewing project information
*
* Provides access to core DDEV project data like domain, ports, and status.
* Uses configurable field selection to expose relevant information without overwhelming context.
*/
export const createDdevDescribeTool = ($: any) => {
return tool({
description: "Get DDEV project information including domain, ports, and status. Use this to understand the current DDEV environment configuration.",
args: {
fields: tool.schema.array(tool.schema.string()).optional().describe(`Fields to include in the response. Available options: ${[...DEFAULT_FIELDS, 'phpVersion', 'webserverType', 'dbType', 'dbVersion'].join(', ')}. Defaults to core fields: ${DEFAULT_FIELDS.join(', ')}.`),
},
async execute(args) {
const raw = await getDdevDescribeData($);

if (!raw) {
throw new Error('Failed to get DDEV project data. Make sure DDEV is running.');
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message states "Make sure DDEV is running" but DDEV might be stopped, not installed, or the project might not be configured. Consider making the error message more accurate, such as "Failed to get DDEV project data. Ensure DDEV is installed, a project is configured, and DDEV is running."

Suggested change
throw new Error('Failed to get DDEV project data. Make sure DDEV is running.');
throw new Error('Failed to get DDEV project data. Ensure DDEV is installed, a project is configured, and DDEV is running.');

Copilot uses AI. Check for mistakes.
}

const info = extractProjectInfo(raw);
const requestedFields = args.fields ?? [...DEFAULT_FIELDS];

const result: Partial<DdevProjectInfo> = {};

for (const field of requestedFields) {
if (field in info) {
const value = info[field as keyof DdevProjectInfo];
result[field as keyof DdevProjectInfo] = value as any;
}
Comment on lines +167 to +171
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field filtering logic doesn't validate if requested fields are valid. If a user passes an invalid field name that doesn't exist in DdevProjectInfo (e.g., a typo), it will silently be ignored. Consider adding validation to warn users about invalid field names or throw an error listing valid options.

Copilot uses AI. Check for mistakes.
}

return JSON.stringify(result, null, 2);
},
});
};
41 changes: 12 additions & 29 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Plugin } from "@opencode-ai/plugin";
import { createDdevLogsTool } from "./logs";
import { createDdevDescribeTool, getDdevDescribeData } from "./describe";
import type { DdevDescribeRaw } from "./describe";

/**
* DDEV Plugin for OpenCode
Expand All @@ -9,19 +11,13 @@ import { createDdevLogsTool } from "./logs";
*/
export const DDEVPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
const CONTAINER_ROOT = '/var/www/html' as const;
const HOST_ONLY_COMMANDS = ['git', 'gh', 'docker', 'ddev'] as const;
const HOST_ONLY_COMMANDS = ['git', 'gh', 'docker', 'ddev', 'curl'] as const;
const CACHE_DURATION_MS = 120000; // 2 minutes

/**
* Raw DDEV project data from `ddev describe -j`
*/
type DdevRawData = {
shortroot?: string;
approot?: string;
status?: string;
name?: string;
[key: string]: unknown;
};
type DdevRawData = DdevDescribeRaw;
Comment on lines 17 to +20
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type alias DdevRawData is redundant. Since it's just aliasing DdevDescribeRaw without adding any additional semantics, consider removing it and using DdevDescribeRaw directly throughout the code. This reduces indirection and makes the code easier to follow.

Copilot uses AI. Check for mistakes.

/**
* Cached DDEV state with timestamp for cache invalidation
Expand Down Expand Up @@ -202,21 +198,26 @@ export const DDEVPlugin: Plugin = async ({ project, client, $, directory, worktr

/**
* Notifies LLM about DDEV environment on first command execution
*
* @link https://docs.ddev.com/en/stable/users/extend/customization-extendibility/#environment-variables-for-containers-and-services
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trailing space in the @link tag could cause documentation rendering issues. Remove the trailing space after the URL.

Suggested change
* @link https://docs.ddev.com/en/stable/users/extend/customization-extendibility/#environment-variables-for-containers-and-services
* @link https://docs.ddev.com/en/stable/users/extend/customization-extendibility/#environment-variables-for-containers-and-services

Copilot uses AI. Check for mistakes.
*/
const notifyDdevInSession = async (): Promise<void> => {
if (hasNotifiedSession || !currentSessionId) {
return;
}

const containerWorkingDir = getContainerWorkingDir();
const projectRoot = getProjectRoot();
const envFilePath = projectRoot ? `${projectRoot}/.ddev/.env` : '.ddev/.env';
const projectType = ddevCache?.raw?.type ? ` (${ddevCache.raw.type})` : '';

await client.session.prompt({
path: { id: currentSessionId },
body: {
parts: [
{
type: 'text',
text: `➡️ DDEV environment is used. Execute commands inside the DDEV container like this: \`ddev exec --dir="${containerWorkingDir}" bash -c <command>\``,
text: `➡️ DDEV is used${projectType}. Execute commands like this: \`ddev exec --dir="${containerWorkingDir}" bash -c <command>\`. Use \`ddev_logs\` tool to view logs and \`ddev_describe\` tool to get project info (domain, ports, status). Environment variables are defined in \`${envFilePath}\` not in local .env files.`,
},
],
noReply: true,
Expand Down Expand Up @@ -252,35 +253,16 @@ export const DDEVPlugin: Plugin = async ({ project, client, $, directory, worktr
}

try {
const result = await $`ddev describe -j`.quiet().nothrow();
const raw = await getDdevDescribeData($);

// DDEV not available (not installed or no project)
if (result.exitCode !== 0) {
ddevCache = null;
return;
}

const output = result.stdout.toString();

let data;
try {
data = JSON.parse(output);
} catch (parseError) {
await log('error', `Failed to parse DDEV JSON output: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
ddevCache = null;
return;
}

const raw = data?.raw as DdevRawData | undefined;

if (!raw) {
ddevCache = null;
return;
}

// Only cache when running; stopped state should be re-checked
if (raw.status !== 'running') {
// Do not cache stopped state; force re-check next time
ddevCache = { timestamp: 0, raw };
return;
}
Expand Down Expand Up @@ -379,6 +361,7 @@ export const DDEVPlugin: Plugin = async ({ project, client, $, directory, worktr
...(hasProject ? {
tool: {
ddev_logs: createDdevLogsTool($),
ddev_describe: createDdevDescribeTool($),
},
} : {}),
};
Expand Down
28 changes: 14 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"@opencode-ai/plugin": "^1.0.126"
"@opencode-ai/plugin": "^1.0.188"
},
"devDependencies": {
"@opencode-ai/plugin": "^1.0.126",
"@types/node": "^24.10.1",
"@opencode-ai/plugin": "^1.0.188",
"@types/node": "^25.0.3",
"typescript": "^5.9.3"
},
"dependencies": {
"@opencode-ai/sdk": "^1.0.126"
"@opencode-ai/sdk": "^1.0.188"
}
}