@elyracode/herd 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @elyracode/herd
2
+
3
+ Laravel Herd integration for Elyra -- environment detection, service management, logs, and .env sync.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ elyra install npm:@elyracode/herd
9
+ ```
10
+
11
+ ## Tools
12
+
13
+ | Tool | Description |
14
+ |------|-------------|
15
+ | `herd_status` | Show PHP version, active services, linked sites, Node.js version |
16
+ | `herd_logs` | Read Nginx, PHP, and Laravel error logs from Herd |
17
+ | `herd_env_sync` | Verify .env matches Herd configuration (DB host/port, Redis, APP_URL, Mailpit) |
18
+ | `herd_php` | Show or switch PHP version |
19
+
20
+ ## Commands
21
+
22
+ | Command | Description |
23
+ |---------|-------------|
24
+ | `/herd` | Show Herd dashboard |
25
+ | `/herd-logs` | Show recent error logs |
26
+ | `/herd-sync` | Check .env against Herd config |
27
+
28
+ ## Usage
29
+
30
+ ```
31
+ > What services are running in Herd?
32
+ > Show me the PHP error logs
33
+ > Does my .env match Herd's configuration?
34
+ > Switch to PHP 8.4
35
+ /herd
36
+ /herd-logs
37
+ /herd-sync
38
+ ```
39
+
40
+ ## .env Sync Checks
41
+
42
+ The sync checker verifies:
43
+ - **DB_HOST**: should be `127.0.0.1` for Herd MySQL/PostgreSQL
44
+ - **DB_PORT**: should match Herd defaults (3306 for MySQL, 5432 for PostgreSQL)
45
+ - **REDIS_HOST**: should be `127.0.0.1`
46
+ - **APP_URL**: should use `.test` domain
47
+ - **MAIL_HOST/PORT**: should match Herd Mailpit (`127.0.0.1:2525`)
48
+
49
+ ## Requirements
50
+
51
+ - Laravel Herd installed with `herd` CLI in PATH
52
+ - macOS (Herd's primary platform)
@@ -0,0 +1,394 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ import type { ExtensionAPI } from "@elyracode/coding-agent";
5
+ import { Type } from "typebox";
6
+
7
+ export default function (elyra: ExtensionAPI): void {
8
+
9
+ // ── Tool: herd_status ──
10
+ elyra.registerTool({
11
+ name: "herd_status",
12
+ label: "Herd Status",
13
+ description:
14
+ "Show the current Laravel Herd environment status: PHP version, active services " +
15
+ "(MySQL, Redis, PostgreSQL, Meilisearch, MinIO, Mailpit), linked sites, and Node.js version. " +
16
+ "Use this to understand the local development environment.",
17
+ parameters: Type.Object({}),
18
+ execute: async (_toolCallId, _params) => {
19
+ try {
20
+ const lines: string[] = ["# Laravel Herd Status", ""];
21
+
22
+ // Check if Herd CLI is available
23
+ if (!isHerdInstalled()) {
24
+ return {
25
+ content: [{ type: "text", text: "Laravel Herd is not installed or `herd` CLI is not in PATH." }],
26
+ details: {},
27
+ };
28
+ }
29
+
30
+ // PHP version
31
+ const phpVersion = herd("php --version 2>/dev/null || echo 'unknown'").split("\n")[0];
32
+ lines.push(`**PHP**: ${phpVersion}`);
33
+
34
+ // Node version
35
+ try {
36
+ const nodeVersion = herd("node --version 2>/dev/null || echo 'unknown'").trim();
37
+ lines.push(`**Node.js**: ${nodeVersion}`);
38
+ } catch { /* skip */ }
39
+
40
+ lines.push("");
41
+
42
+ // Services
43
+ lines.push("## Services");
44
+ const services = ["mysql", "redis", "postgresql", "meilisearch", "minio", "mailpit", "typesense"];
45
+ for (const service of services) {
46
+ try {
47
+ const status = herd(`services ${service} status 2>/dev/null || echo 'not available'`).trim();
48
+ const isRunning = status.toLowerCase().includes("running") || status.toLowerCase().includes("active");
49
+ const icon = isRunning ? "running" : "stopped";
50
+ lines.push(`- **${service}**: ${icon}`);
51
+ } catch {
52
+ // Service not available in this Herd version
53
+ }
54
+ }
55
+ lines.push("");
56
+
57
+ // Sites
58
+ lines.push("## Sites");
59
+ try {
60
+ const sitesOutput = herd("links 2>/dev/null || echo 'none'").trim();
61
+ if (sitesOutput && sitesOutput !== "none") {
62
+ lines.push(sitesOutput);
63
+ } else {
64
+ lines.push("No linked sites found.");
65
+ }
66
+ } catch {
67
+ lines.push("Could not list sites.");
68
+ }
69
+ lines.push("");
70
+
71
+ // Current project info
72
+ const cwd = process.cwd();
73
+ const projectName = basename(cwd);
74
+ lines.push("## Current Project");
75
+ lines.push(`- **Directory**: ${cwd}`);
76
+ lines.push(`- **Domain**: ${projectName}.test (if parked/linked)`);
77
+
78
+ // Check .env
79
+ const envPath = join(cwd, ".env");
80
+ if (existsSync(envPath)) {
81
+ const env = parseEnv(readFileSync(envPath, "utf-8"));
82
+ lines.push(`- **DB_CONNECTION**: ${env.DB_CONNECTION ?? "not set"}`);
83
+ lines.push(`- **DB_HOST**: ${env.DB_HOST ?? "not set"}`);
84
+ lines.push(`- **DB_PORT**: ${env.DB_PORT ?? "not set"}`);
85
+ lines.push(`- **REDIS_HOST**: ${env.REDIS_HOST ?? "not set"}`);
86
+ }
87
+
88
+ return {
89
+ content: [{ type: "text", text: lines.join("\n") }],
90
+ details: {},
91
+ };
92
+ } catch (error) {
93
+ const msg = error instanceof Error ? error.message : String(error);
94
+ return {
95
+ content: [{ type: "text", text: `Herd status failed: ${msg}` }],
96
+ details: {},
97
+ };
98
+ }
99
+ },
100
+ });
101
+
102
+ // ── Tool: herd_logs ──
103
+ elyra.registerTool({
104
+ name: "herd_logs",
105
+ label: "Herd Logs",
106
+ description:
107
+ "Read recent error logs from Laravel Herd -- Nginx errors, PHP errors, and Laravel application logs. " +
108
+ "Use this to debug issues with the local development server.",
109
+ parameters: Type.Object({
110
+ type: Type.Optional(
111
+ Type.Union([Type.Literal("nginx"), Type.Literal("php"), Type.Literal("laravel"), Type.Literal("all")], {
112
+ description: "Log type to read (default: all)",
113
+ }),
114
+ ),
115
+ lines: Type.Optional(
116
+ Type.Number({ description: "Number of lines to read (default: 50)" }),
117
+ ),
118
+ }),
119
+ execute: async (_toolCallId, params) => {
120
+ try {
121
+ const logType = params.type ?? "all";
122
+ const lineCount = params.lines ?? 50;
123
+ const cwd = process.cwd();
124
+ const lines: string[] = ["# Herd Logs", ""];
125
+
126
+ if (logType === "nginx" || logType === "all") {
127
+ lines.push("## Nginx Error Log");
128
+ const nginxLog = readLogFile(join(getHerdLogDir(), "nginx-error.log"), lineCount);
129
+ lines.push(nginxLog || "No recent errors.");
130
+ lines.push("");
131
+ }
132
+
133
+ if (logType === "php" || logType === "all") {
134
+ lines.push("## PHP Error Log");
135
+ const phpLog = readLogFile(join(getHerdLogDir(), "php-error.log"), lineCount);
136
+ lines.push(phpLog || "No recent errors.");
137
+ lines.push("");
138
+ }
139
+
140
+ if (logType === "laravel" || logType === "all") {
141
+ lines.push("## Laravel Application Log");
142
+ const laravelLog = readLogFile(join(cwd, "storage", "logs", "laravel.log"), lineCount);
143
+ lines.push(laravelLog || "No recent entries.");
144
+ lines.push("");
145
+ }
146
+
147
+ return {
148
+ content: [{ type: "text", text: lines.join("\n") }],
149
+ details: {},
150
+ };
151
+ } catch (error) {
152
+ const msg = error instanceof Error ? error.message : String(error);
153
+ return {
154
+ content: [{ type: "text", text: `Log reading failed: ${msg}` }],
155
+ details: {},
156
+ };
157
+ }
158
+ },
159
+ });
160
+
161
+ // ── Tool: herd_env_sync ──
162
+ elyra.registerTool({
163
+ name: "herd_env_sync",
164
+ label: "Herd .env Sync Check",
165
+ description:
166
+ "Check if the project's .env file matches the Laravel Herd environment. " +
167
+ "Verifies database host/port, Redis configuration, and service availability. " +
168
+ "Reports mismatches and suggests fixes.",
169
+ parameters: Type.Object({}),
170
+ execute: async (_toolCallId, _params) => {
171
+ try {
172
+ const cwd = process.cwd();
173
+ const envPath = join(cwd, ".env");
174
+
175
+ if (!existsSync(envPath)) {
176
+ return {
177
+ content: [{ type: "text", text: "No .env file found in the current project." }],
178
+ details: {},
179
+ };
180
+ }
181
+
182
+ const env = parseEnv(readFileSync(envPath, "utf-8"));
183
+ const issues: string[] = [];
184
+ const ok: string[] = [];
185
+
186
+ // Check DB connection
187
+ const dbConnection = env.DB_CONNECTION;
188
+ if (dbConnection === "mysql") {
189
+ const dbHost = env.DB_HOST ?? "";
190
+ if (dbHost !== "127.0.0.1" && dbHost !== "localhost") {
191
+ issues.push(`DB_HOST is '${dbHost}' -- Herd MySQL runs on 127.0.0.1. Change DB_HOST=127.0.0.1`);
192
+ } else {
193
+ ok.push("DB_HOST matches Herd MySQL");
194
+ }
195
+
196
+ const dbPort = env.DB_PORT ?? "3306";
197
+ if (dbPort !== "3306") {
198
+ issues.push(`DB_PORT is '${dbPort}' -- Herd MySQL default is 3306`);
199
+ } else {
200
+ ok.push("DB_PORT matches Herd default");
201
+ }
202
+ }
203
+
204
+ if (dbConnection === "pgsql") {
205
+ const dbPort = env.DB_PORT ?? "5432";
206
+ if (dbPort !== "5432") {
207
+ issues.push(`DB_PORT is '${dbPort}' -- Herd PostgreSQL default is 5432`);
208
+ } else {
209
+ ok.push("DB_PORT matches Herd PostgreSQL default");
210
+ }
211
+ }
212
+
213
+ // Check Redis
214
+ if (env.REDIS_HOST && env.REDIS_HOST !== "127.0.0.1" && env.REDIS_HOST !== "localhost") {
215
+ issues.push(`REDIS_HOST is '${env.REDIS_HOST}' -- Herd Redis runs on 127.0.0.1`);
216
+ } else if (env.REDIS_HOST) {
217
+ ok.push("REDIS_HOST matches Herd");
218
+ }
219
+
220
+ // Check common issues
221
+ if (env.APP_URL && !env.APP_URL.includes(".test")) {
222
+ issues.push(`APP_URL is '${env.APP_URL}' -- Herd uses .test domains (e.g., http://${basename(cwd)}.test)`);
223
+ } else if (env.APP_URL) {
224
+ ok.push("APP_URL uses .test domain");
225
+ }
226
+
227
+ // Check mail
228
+ if (env.MAIL_MAILER === "smtp" && env.MAIL_HOST === "127.0.0.1" && env.MAIL_PORT === "2525") {
229
+ ok.push("Mail configured for Herd Mailpit");
230
+ } else if (env.MAIL_MAILER === "smtp") {
231
+ issues.push("MAIL_HOST/MAIL_PORT may not match Herd Mailpit (127.0.0.1:2525)");
232
+ }
233
+
234
+ const lines: string[] = ["# Herd .env Sync Check", ""];
235
+
236
+ if (issues.length === 0) {
237
+ lines.push("All .env values match Herd configuration.");
238
+ } else {
239
+ lines.push(`Found ${issues.length} issue${issues.length > 1 ? "s" : ""}:`, "");
240
+ for (const issue of issues) {
241
+ lines.push(`- ${issue}`);
242
+ }
243
+ }
244
+
245
+ if (ok.length > 0) {
246
+ lines.push("", "Matching:");
247
+ for (const item of ok) {
248
+ lines.push(`- ${item}`);
249
+ }
250
+ }
251
+
252
+ return {
253
+ content: [{ type: "text", text: lines.join("\n") }],
254
+ details: { issues: issues.length, ok: ok.length },
255
+ };
256
+ } catch (error) {
257
+ const msg = error instanceof Error ? error.message : String(error);
258
+ return {
259
+ content: [{ type: "text", text: `Env sync check failed: ${msg}` }],
260
+ details: {},
261
+ };
262
+ }
263
+ },
264
+ });
265
+
266
+ // ── Tool: herd_php ──
267
+ elyra.registerTool({
268
+ name: "herd_php",
269
+ label: "Herd PHP Version",
270
+ description:
271
+ "Show or switch the PHP version used by Laravel Herd. " +
272
+ "Lists available PHP versions and the currently active one.",
273
+ parameters: Type.Object({
274
+ version: Type.Optional(
275
+ Type.String({ description: "PHP version to switch to (e.g., '8.3', '8.4'). Omit to show current." }),
276
+ ),
277
+ }),
278
+ execute: async (_toolCallId, params) => {
279
+ try {
280
+ if (!isHerdInstalled()) {
281
+ return {
282
+ content: [{ type: "text", text: "Herd CLI not found." }],
283
+ details: {},
284
+ };
285
+ }
286
+
287
+ if (params.version) {
288
+ const result = herd(`use php ${params.version} 2>&1`);
289
+ return {
290
+ content: [{ type: "text", text: `Switched PHP to ${params.version}:\n${result}` }],
291
+ details: {},
292
+ };
293
+ }
294
+
295
+ const current = herd("php --version 2>/dev/null").split("\n")[0];
296
+ const lines = [
297
+ "# PHP Version",
298
+ "",
299
+ `**Current**: ${current}`,
300
+ "",
301
+ "To switch: use `herd_php` with a version parameter (e.g., '8.3', '8.4')",
302
+ ];
303
+
304
+ return {
305
+ content: [{ type: "text", text: lines.join("\n") }],
306
+ details: {},
307
+ };
308
+ } catch (error) {
309
+ const msg = error instanceof Error ? error.message : String(error);
310
+ return {
311
+ content: [{ type: "text", text: `PHP version check failed: ${msg}` }],
312
+ details: {},
313
+ };
314
+ }
315
+ },
316
+ });
317
+
318
+ // ── Commands ──
319
+ elyra.registerCommand("herd", {
320
+ description: "Show Laravel Herd dashboard -- services, sites, PHP version",
321
+ handler: async (_args, _ctx) => {
322
+ elyra.sendUserMessage("Show me the Laravel Herd status: PHP version, running services, linked sites, and current project info.");
323
+ },
324
+ });
325
+
326
+ elyra.registerCommand("herd-logs", {
327
+ description: "Show recent Herd error logs",
328
+ handler: async (_args, _ctx) => {
329
+ elyra.sendUserMessage("Show me the recent error logs from Herd -- Nginx, PHP, and Laravel application logs.");
330
+ },
331
+ });
332
+
333
+ elyra.registerCommand("herd-sync", {
334
+ description: "Check if .env matches Herd configuration",
335
+ handler: async (_args, _ctx) => {
336
+ elyra.sendUserMessage("Check if my .env file matches the Laravel Herd environment and report any mismatches.");
337
+ },
338
+ });
339
+ }
340
+
341
+ function isHerdInstalled(): boolean {
342
+ try {
343
+ execSync("which herd 2>/dev/null || where herd 2>/dev/null", {
344
+ timeout: 5000,
345
+ encoding: "utf-8",
346
+ stdio: ["pipe", "pipe", "pipe"],
347
+ });
348
+ return true;
349
+ } catch {
350
+ return false;
351
+ }
352
+ }
353
+
354
+ function herd(command: string): string {
355
+ return execSync(`herd ${command}`, {
356
+ timeout: 15000,
357
+ encoding: "utf-8",
358
+ stdio: ["pipe", "pipe", "pipe"],
359
+ }).trim();
360
+ }
361
+
362
+ function getHerdLogDir(): string {
363
+ // macOS Herd log location
364
+ const home = process.env.HOME ?? "";
365
+ return join(home, "Library", "Application Support", "Herd", "Log");
366
+ }
367
+
368
+ function readLogFile(path: string, lines: number): string {
369
+ if (!existsSync(path)) return "";
370
+ try {
371
+ const content = readFileSync(path, "utf-8");
372
+ const allLines = content.split("\n");
373
+ return allLines.slice(-lines).join("\n").trim();
374
+ } catch {
375
+ return "";
376
+ }
377
+ }
378
+
379
+ function parseEnv(content: string): Record<string, string> {
380
+ const result: Record<string, string> = {};
381
+ for (const line of content.split("\n")) {
382
+ const trimmed = line.trim();
383
+ if (!trimmed || trimmed.startsWith("#")) continue;
384
+ const eqIndex = trimmed.indexOf("=");
385
+ if (eqIndex === -1) continue;
386
+ const key = trimmed.slice(0, eqIndex).trim();
387
+ let value = trimmed.slice(eqIndex + 1).trim();
388
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
389
+ value = value.slice(1, -1);
390
+ }
391
+ result[key] = value;
392
+ }
393
+ return result;
394
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@elyracode/herd",
3
+ "version": "0.5.3",
4
+ "description": "Elyra extension for Laravel Herd -- environment detection, service management, logs, and .env sync",
5
+ "type": "module",
6
+ "keywords": ["elyra-package", "herd", "laravel-herd", "local-dev", "php", "mysql", "redis"],
7
+ "license": "MIT",
8
+ "author": "Knut W. Horne",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/kwhorne/elyra.git",
12
+ "directory": "packages/herd"
13
+ },
14
+ "elyra": {
15
+ "extensions": ["./extensions/index.ts"]
16
+ },
17
+ "peerDependencies": {
18
+ "@elyracode/coding-agent": "*",
19
+ "typebox": "*"
20
+ },
21
+ "scripts": {
22
+ "clean": "echo 'nothing to clean'",
23
+ "build": "echo 'nothing to build'",
24
+ "check": "echo 'nothing to check'"
25
+ }
26
+ }