style: standardize formatting in opencode tooling files

Reformat JSON configs and TypeScript scripts to use consistent
tab indentation, semicolons, and double quotes.
This commit is contained in:
2026-04-07 13:10:35 -04:00
parent 4e09059a3d
commit cbae9fa1c9
5 changed files with 936 additions and 879 deletions

View File

@@ -1,6 +1,6 @@
{ {
"source": "/tmp/skill-selector-curated-1953505229", "source": "/tmp/skill-selector-curated-1953505229",
"sourceType": "local", "sourceType": "local",
"localPath": "/tmp/skill-selector-curated-1953505229/zod-validation-expert", "localPath": "/tmp/skill-selector-curated-1953505229/zod-validation-expert",
"installedAt": "2026-04-07T15:11:20.921Z" "installedAt": "2026-04-07T15:11:20.921Z"
} }

View File

@@ -1,363 +1,341 @@
{ {
"$schema": "https://opencode.ai/schemas/agent-metadata.json", "$schema": "https://opencode.ai/schemas/agent-metadata.json",
"schema_version": "1.0.0", "schema_version": "1.0.0",
"description": "Centralized metadata for OpenAgents Control agents. This file stores metadata that is not part of the OpenCode agent schema but is needed for registry management, installation, and documentation.", "description": "Centralized metadata for OpenAgents Control agents. This file stores metadata that is not part of the OpenCode agent schema but is needed for registry management, installation, and documentation.",
"agents": { "agents": {
"openagent": { "openagent": {
"id": "openagent", "id": "openagent",
"name": "OpenAgent", "name": "OpenAgent",
"category": "core", "category": "core",
"type": "agent", "type": "agent",
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["universal", "coordination", "primary"], "tags": ["universal", "coordination", "primary"],
"dependencies": [ "dependencies": [
"subagent:task-manager", "subagent:task-manager",
"subagent:batch-executor", "subagent:batch-executor",
"subagent:documentation", "subagent:documentation",
"subagent:contextscout", "subagent:contextscout",
"subagent:externalscout", "subagent:externalscout",
"context:standards-code", "context:standards-code",
"context:standards-docs", "context:standards-docs",
"context:standards-tests", "context:standards-tests",
"context:review-ref", "context:review-ref",
"context:delegation-ref", "context:delegation-ref",
"context:external-libraries-workflow" "context:external-libraries-workflow"
] ]
}, },
"opencoder": { "opencoder": {
"id": "opencoder", "id": "opencoder",
"name": "OpenCoder", "name": "OpenCoder",
"category": "core", "category": "core",
"type": "agent", "type": "agent",
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["development", "coding", "implementation"], "tags": ["development", "coding", "implementation"],
"dependencies": [ "dependencies": [
"subagent:documentation", "subagent:documentation",
"subagent:task-manager", "subagent:task-manager",
"subagent:batch-executor", "subagent:batch-executor",
"subagent:coder-agent", "subagent:coder-agent",
"subagent:tester", "subagent:tester",
"subagent:reviewer", "subagent:reviewer",
"subagent:build-agent", "subagent:build-agent",
"subagent:contextscout", "subagent:contextscout",
"subagent:externalscout", "subagent:externalscout",
"context:standards-code", "context:standards-code",
"context:task-delegation-basics", "context:task-delegation-basics",
"context:component-planning", "context:component-planning",
"context:external-libraries-workflow" "context:external-libraries-workflow"
] ]
}, },
"repo-manager": { "repo-manager": {
"id": "repo-manager", "id": "repo-manager",
"name": "Repo Manager", "name": "Repo Manager",
"category": "meta", "category": "meta",
"type": "agent", "type": "agent",
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["repository", "management", "orchestration"], "tags": ["repository", "management", "orchestration"],
"dependencies": [ "dependencies": [
"subagent:task-manager", "subagent:task-manager",
"subagent:contextscout", "subagent:contextscout",
"subagent:documentation", "subagent:documentation",
"subagent:coder-agent", "subagent:coder-agent",
"subagent:tester", "subagent:tester",
"subagent:reviewer", "subagent:reviewer",
"subagent:build-agent" "subagent:build-agent"
] ]
}, },
"system-builder": { "system-builder": {
"id": "system-builder", "id": "system-builder",
"name": "System Builder", "name": "System Builder",
"category": "meta", "category": "meta",
"type": "agent", "type": "agent",
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["system-generation", "architecture", "scaffolding"], "tags": ["system-generation", "architecture", "scaffolding"],
"dependencies": [ "dependencies": [
"subagent:agent-generator", "subagent:agent-generator",
"subagent:command-creator", "subagent:command-creator",
"subagent:domain-analyzer", "subagent:domain-analyzer",
"subagent:context-organizer", "subagent:context-organizer",
"subagent:workflow-designer" "subagent:workflow-designer"
] ]
}, },
"copywriter": { "copywriter": {
"id": "copywriter", "id": "copywriter",
"name": "Copywriter", "name": "Copywriter",
"category": "content", "category": "content",
"type": "agent", "type": "agent",
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["content", "marketing", "writing"], "tags": ["content", "marketing", "writing"],
"dependencies": [ "dependencies": ["context:standards-docs"]
"context:standards-docs" },
] "technical-writer": {
}, "id": "technical-writer",
"technical-writer": { "name": "Technical Writer",
"id": "technical-writer", "category": "content",
"name": "Technical Writer", "type": "agent",
"category": "content", "version": "1.0.0",
"type": "agent", "author": "opencode",
"version": "1.0.0", "tags": ["documentation", "technical", "writing"],
"author": "opencode", "dependencies": ["context:standards-docs"]
"tags": ["documentation", "technical", "writing"], },
"dependencies": [ "data-analyst": {
"context:standards-docs" "id": "data-analyst",
] "name": "Data Analyst",
}, "category": "data",
"data-analyst": { "type": "agent",
"id": "data-analyst", "version": "1.0.0",
"name": "Data Analyst", "author": "opencode",
"category": "data", "tags": ["data", "analysis", "visualization"],
"type": "agent", "dependencies": []
"version": "1.0.0", },
"author": "opencode", "eval-runner": {
"tags": ["data", "analysis", "visualization"], "id": "eval-runner",
"dependencies": [] "name": "Eval Runner",
}, "category": "testing",
"eval-runner": { "type": "agent",
"id": "eval-runner", "version": "1.0.0",
"name": "Eval Runner", "author": "opencode",
"category": "testing", "tags": ["testing", "evaluation", "quality"],
"type": "agent", "dependencies": ["context:standards-tests"]
"version": "1.0.0", },
"author": "opencode", "task-manager": {
"tags": ["testing", "evaluation", "quality"], "id": "task-manager",
"dependencies": [ "name": "TaskManager",
"context:standards-tests" "category": "subagents/core",
] "type": "subagent",
}, "version": "2.0.0",
"task-manager": { "author": "opencode",
"id": "task-manager", "tags": ["task-breakdown", "planning", "coordination"],
"name": "TaskManager", "dependencies": ["context:task-delegation-basics"]
"category": "subagents/core", },
"type": "subagent", "batch-executor": {
"version": "2.0.0", "id": "batch-executor",
"author": "opencode", "name": "BatchExecutor",
"tags": ["task-breakdown", "planning", "coordination"], "category": "subagents/core",
"dependencies": [ "type": "subagent",
"context:task-delegation-basics" "version": "1.0.0",
] "author": "opencode",
}, "tags": ["parallel-execution", "batch-management", "coordination"],
"batch-executor": { "dependencies": ["subagent:coder-agent", "subagent:task-manager"]
"id": "batch-executor", },
"name": "BatchExecutor", "documentation": {
"category": "subagents/core", "id": "documentation",
"type": "subagent", "name": "DocWriter",
"version": "1.0.0", "category": "subagents/core",
"author": "opencode", "type": "subagent",
"tags": ["parallel-execution", "batch-management", "coordination"], "version": "1.0.0",
"dependencies": [ "author": "opencode",
"subagent:coder-agent", "tags": ["documentation", "writing"],
"subagent:task-manager" "dependencies": ["context:standards-docs"]
] },
}, "contextscout": {
"documentation": { "id": "contextscout",
"id": "documentation", "name": "ContextScout",
"name": "DocWriter", "category": "subagents/core",
"category": "subagents/core", "type": "subagent",
"type": "subagent", "version": "1.0.0",
"version": "1.0.0", "author": "opencode",
"author": "opencode", "tags": ["context", "discovery", "search"],
"tags": ["documentation", "writing"], "dependencies": []
"dependencies": [ },
"context:standards-docs" "externalscout": {
] "id": "externalscout",
}, "name": "ExternalScout",
"contextscout": { "category": "subagents/core",
"id": "contextscout", "type": "subagent",
"name": "ContextScout", "version": "1.0.0",
"category": "subagents/core", "author": "opencode",
"type": "subagent", "tags": ["external", "documentation", "search"],
"version": "1.0.0", "dependencies": []
"author": "opencode", },
"tags": ["context", "discovery", "search"], "context-manager": {
"dependencies": [] "id": "context-manager",
}, "name": "ContextManager",
"externalscout": { "category": "subagents/core",
"id": "externalscout", "type": "subagent",
"name": "ExternalScout", "version": "1.0.0",
"category": "subagents/core", "author": "opencode",
"type": "subagent", "tags": ["context", "management", "organization"],
"version": "1.0.0", "dependencies": []
"author": "opencode", },
"tags": ["external", "documentation", "search"], "context-retriever": {
"dependencies": [] "id": "context-retriever",
}, "name": "Context Retriever",
"context-manager": { "category": "subagents/core",
"id": "context-manager", "type": "subagent",
"name": "ContextManager", "version": "1.0.0",
"category": "subagents/core", "author": "opencode",
"type": "subagent", "tags": ["context", "retrieval", "search"],
"version": "1.0.0", "dependencies": []
"author": "opencode", },
"tags": ["context", "management", "organization"], "coder-agent": {
"dependencies": [] "id": "coder-agent",
}, "name": "CoderAgent",
"context-retriever": { "category": "subagents/code",
"id": "context-retriever", "type": "subagent",
"name": "Context Retriever", "version": "1.0.0",
"category": "subagents/core", "author": "opencode",
"type": "subagent", "tags": ["coding", "implementation"],
"version": "1.0.0", "dependencies": ["context:standards-code"]
"author": "opencode", },
"tags": ["context", "retrieval", "search"], "tester": {
"dependencies": [] "id": "tester",
}, "name": "TestEngineer",
"coder-agent": { "category": "subagents/code",
"id": "coder-agent", "type": "subagent",
"name": "CoderAgent", "version": "1.0.0",
"category": "subagents/code", "author": "opencode",
"type": "subagent", "tags": ["testing", "tdd", "quality"],
"version": "1.0.0", "dependencies": ["context:standards-tests"]
"author": "opencode", },
"tags": ["coding", "implementation"], "reviewer": {
"dependencies": [ "id": "reviewer",
"context:standards-code" "name": "CodeReviewer",
] "category": "subagents/code",
}, "type": "subagent",
"tester": { "version": "1.0.0",
"id": "tester", "author": "opencode",
"name": "TestEngineer", "tags": ["review", "security", "quality"],
"category": "subagents/code", "dependencies": ["context:standards-code", "context:review-ref"]
"type": "subagent", },
"version": "1.0.0", "build-agent": {
"author": "opencode", "id": "build-agent",
"tags": ["testing", "tdd", "quality"], "name": "BuildAgent",
"dependencies": [ "category": "subagents/code",
"context:standards-tests" "type": "subagent",
] "version": "1.0.0",
}, "author": "opencode",
"reviewer": { "tags": ["build", "validation", "type-checking"],
"id": "reviewer", "dependencies": []
"name": "CodeReviewer", },
"category": "subagents/code", "frontend-specialist": {
"type": "subagent", "id": "frontend-specialist",
"version": "1.0.0", "name": "OpenFrontendSpecialist",
"author": "opencode", "category": "subagents/development",
"tags": ["review", "security", "quality"], "type": "subagent",
"dependencies": [ "version": "1.0.0",
"context:standards-code", "author": "opencode",
"context:review-ref" "tags": ["frontend", "ui", "design"],
] "dependencies": ["context:standards-code"]
}, },
"build-agent": { "devops-specialist": {
"id": "build-agent", "id": "devops-specialist",
"name": "BuildAgent", "name": "OpenDevopsSpecialist",
"category": "subagents/code", "category": "subagents/development",
"type": "subagent", "type": "subagent",
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["build", "validation", "type-checking"], "tags": ["devops", "ci-cd", "infrastructure"],
"dependencies": [] "dependencies": []
}, },
"frontend-specialist": { "agent-generator": {
"id": "frontend-specialist", "id": "agent-generator",
"name": "OpenFrontendSpecialist", "name": "AgentGenerator",
"category": "subagents/development", "category": "subagents/system-builder",
"type": "subagent", "type": "subagent",
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["frontend", "ui", "design"], "tags": ["generation", "agents", "scaffolding"],
"dependencies": [ "dependencies": []
"context:standards-code" },
] "command-creator": {
}, "id": "command-creator",
"devops-specialist": { "name": "CommandCreator",
"id": "devops-specialist", "category": "subagents/system-builder",
"name": "OpenDevopsSpecialist", "type": "subagent",
"category": "subagents/development", "version": "1.0.0",
"type": "subagent", "author": "opencode",
"version": "1.0.0", "tags": ["commands", "generation", "scaffolding"],
"author": "opencode", "dependencies": []
"tags": ["devops", "ci-cd", "infrastructure"], },
"dependencies": [] "domain-analyzer": {
}, "id": "domain-analyzer",
"agent-generator": { "name": "DomainAnalyzer",
"id": "agent-generator", "category": "subagents/system-builder",
"name": "AgentGenerator", "type": "subagent",
"category": "subagents/system-builder", "version": "1.0.0",
"type": "subagent", "author": "opencode",
"version": "1.0.0", "tags": ["analysis", "domain", "architecture"],
"author": "opencode", "dependencies": []
"tags": ["generation", "agents", "scaffolding"], },
"dependencies": [] "context-organizer": {
}, "id": "context-organizer",
"command-creator": { "name": "ContextOrganizer",
"id": "command-creator", "category": "subagents/system-builder",
"name": "CommandCreator", "type": "subagent",
"category": "subagents/system-builder", "version": "1.0.0",
"type": "subagent", "author": "opencode",
"version": "1.0.0", "tags": ["context", "organization", "structure"],
"author": "opencode", "dependencies": []
"tags": ["commands", "generation", "scaffolding"], },
"dependencies": [] "workflow-designer": {
}, "id": "workflow-designer",
"domain-analyzer": { "name": "WorkflowDesigner",
"id": "domain-analyzer", "category": "subagents/system-builder",
"name": "DomainAnalyzer", "type": "subagent",
"category": "subagents/system-builder", "version": "1.0.0",
"type": "subagent", "author": "opencode",
"version": "1.0.0", "tags": ["workflow", "design", "architecture"],
"author": "opencode", "dependencies": []
"tags": ["analysis", "domain", "architecture"], },
"dependencies": [] "image-specialist": {
}, "id": "image-specialist",
"context-organizer": { "name": "Image Specialist",
"id": "context-organizer", "category": "subagents/utils",
"name": "ContextOrganizer", "type": "subagent",
"category": "subagents/system-builder", "version": "1.0.0",
"type": "subagent", "author": "opencode",
"version": "1.0.0", "tags": ["images", "editing", "generation"],
"author": "opencode", "dependencies": []
"tags": ["context", "organization", "structure"], },
"dependencies": [] "simple-responder": {
}, "id": "simple-responder",
"workflow-designer": { "name": "Simple Responder",
"id": "workflow-designer", "category": "subagents/test",
"name": "WorkflowDesigner", "type": "subagent",
"category": "subagents/system-builder", "version": "1.0.0",
"type": "subagent", "author": "opencode",
"version": "1.0.0", "tags": ["testing", "evaluation"],
"author": "opencode", "dependencies": []
"tags": ["workflow", "design", "architecture"], }
"dependencies": [] },
}, "defaults": {
"image-specialist": { "agent": {
"id": "image-specialist", "version": "1.0.0",
"name": "Image Specialist", "author": "opencode",
"category": "subagents/utils", "type": "agent",
"type": "subagent", "tags": []
"version": "1.0.0", },
"author": "opencode", "subagent": {
"tags": ["images", "editing", "generation"], "version": "1.0.0",
"dependencies": [] "author": "opencode",
}, "type": "subagent",
"simple-responder": { "tags": []
"id": "simple-responder", }
"name": "Simple Responder", }
"category": "subagents/test",
"type": "subagent",
"version": "1.0.0",
"author": "opencode",
"tags": ["testing", "evaluation"],
"dependencies": []
}
},
"defaults": {
"agent": {
"version": "1.0.0",
"author": "opencode",
"type": "agent",
"tags": []
},
"subagent": {
"version": "1.0.0",
"author": "opencode",
"type": "subagent",
"tags": []
}
}
} }

View File

@@ -1,7 +1,7 @@
{ {
"description": "Additional context file paths - agents load this via @ reference for dynamic pathing", "description": "Additional context file paths - agents load this via @ reference for dynamic pathing",
"paths": { "paths": {
"local": ".opencode/context", "local": ".opencode/context",
"global": "~/.config/opencode/context" "global": "~/.config/opencode/context"
} }
} }

View File

@@ -19,517 +19,576 @@
* .tmp/tasks/completed/{feature-slug}/ * .tmp/tasks/completed/{feature-slug}/
*/ */
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
// Find project root (look for .git or package.json) // Find project root (look for .git or package.json)
function findProjectRoot(): string { function findProjectRoot(): string {
let dir = process.cwd(); let dir = process.cwd();
while (dir !== path.dirname(dir)) { while (dir !== path.dirname(dir)) {
if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'))) { if (
return dir; fs.existsSync(path.join(dir, ".git")) ||
} fs.existsSync(path.join(dir, "package.json"))
dir = path.dirname(dir); ) {
} return dir;
return process.cwd(); }
dir = path.dirname(dir);
}
return process.cwd();
} }
const PROJECT_ROOT = findProjectRoot(); const PROJECT_ROOT = findProjectRoot();
const TASKS_DIR = path.join(PROJECT_ROOT, '.tmp', 'tasks'); const TASKS_DIR = path.join(PROJECT_ROOT, ".tmp", "tasks");
const COMPLETED_DIR = path.join(TASKS_DIR, 'completed'); const COMPLETED_DIR = path.join(TASKS_DIR, "completed");
interface Task { interface Task {
id: string; id: string;
name: string; name: string;
status: 'active' | 'completed' | 'blocked' | 'archived'; status: "active" | "completed" | "blocked" | "archived";
objective: string; objective: string;
context_files: string[]; context_files: string[];
reference_files?: string[]; reference_files?: string[];
exit_criteria: string[]; exit_criteria: string[];
subtask_count: number; subtask_count: number;
completed_count: number; completed_count: number;
created_at: string; created_at: string;
completed_at: string | null; completed_at: string | null;
} }
interface Subtask { interface Subtask {
id: string; id: string;
seq: string; seq: string;
title: string; title: string;
status: 'pending' | 'in_progress' | 'completed' | 'blocked'; status: "pending" | "in_progress" | "completed" | "blocked";
depends_on: string[]; depends_on: string[];
parallel: boolean; parallel: boolean;
context_files: string[]; context_files: string[];
reference_files?: string[]; reference_files?: string[];
acceptance_criteria: string[]; acceptance_criteria: string[];
deliverables: string[]; deliverables: string[];
agent_id: string | null; agent_id: string | null;
suggested_agent?: string; suggested_agent?: string;
started_at: string | null; started_at: string | null;
completed_at: string | null; completed_at: string | null;
completion_summary: string | null; completion_summary: string | null;
} }
// Helpers // Helpers
function getFeatureDirs(): string[] { function getFeatureDirs(): string[] {
if (!fs.existsSync(TASKS_DIR)) return []; if (!fs.existsSync(TASKS_DIR)) return [];
return fs.readdirSync(TASKS_DIR).filter((f: string) => { return fs.readdirSync(TASKS_DIR).filter((f: string) => {
const fullPath = path.join(TASKS_DIR, f); const fullPath = path.join(TASKS_DIR, f);
return fs.statSync(fullPath).isDirectory() && f !== 'completed'; return fs.statSync(fullPath).isDirectory() && f !== "completed";
}); });
} }
function loadTask(feature: string): Task | null { function loadTask(feature: string): Task | null {
const taskPath = path.join(TASKS_DIR, feature, 'task.json'); const taskPath = path.join(TASKS_DIR, feature, "task.json");
if (!fs.existsSync(taskPath)) return null; if (!fs.existsSync(taskPath)) return null;
return JSON.parse(fs.readFileSync(taskPath, 'utf-8')); return JSON.parse(fs.readFileSync(taskPath, "utf-8"));
} }
function loadSubtasks(feature: string): Subtask[] { function loadSubtasks(feature: string): Subtask[] {
const featureDir = path.join(TASKS_DIR, feature); const featureDir = path.join(TASKS_DIR, feature);
if (!fs.existsSync(featureDir)) return []; if (!fs.existsSync(featureDir)) return [];
const files = fs.readdirSync(featureDir) const files = fs
.filter((f: string) => f.match(/^subtask_\d{2}\.json$/)) .readdirSync(featureDir)
.sort(); .filter((f: string) => f.match(/^subtask_\d{2}\.json$/))
.sort();
return files.map((f: string) => JSON.parse(fs.readFileSync(path.join(featureDir, f), 'utf-8'))); return files.map((f: string) =>
JSON.parse(fs.readFileSync(path.join(featureDir, f), "utf-8")),
);
} }
function saveSubtask(feature: string, subtask: Subtask): void { function saveSubtask(feature: string, subtask: Subtask): void {
const subtaskPath = path.join(TASKS_DIR, feature, `subtask_${subtask.seq}.json`); const subtaskPath = path.join(
fs.writeFileSync(subtaskPath, JSON.stringify(subtask, null, 2)); TASKS_DIR,
feature,
`subtask_${subtask.seq}.json`,
);
fs.writeFileSync(subtaskPath, JSON.stringify(subtask, null, 2));
} }
function saveTask(feature: string, task: Task): void { function saveTask(feature: string, task: Task): void {
const taskPath = path.join(TASKS_DIR, feature, 'task.json'); const taskPath = path.join(TASKS_DIR, feature, "task.json");
fs.writeFileSync(taskPath, JSON.stringify(task, null, 2)); fs.writeFileSync(taskPath, JSON.stringify(task, null, 2));
} }
// Commands // Commands
function cmdStatus(feature?: string): void { function cmdStatus(feature?: string): void {
const features = feature ? [feature] : getFeatureDirs(); const features = feature ? [feature] : getFeatureDirs();
if (features.length === 0) { if (features.length === 0) {
console.log('No active features found.'); console.log("No active features found.");
return; return;
} }
for (const f of features) { for (const f of features) {
const task = loadTask(f); const task = loadTask(f);
const subtasks = loadSubtasks(f); const subtasks = loadSubtasks(f);
if (!task) { if (!task) {
console.log(`\n[${f}] - No task.json found`); console.log(`\n[${f}] - No task.json found`);
continue; continue;
} }
const counts = { const counts = {
pending: subtasks.filter(s => s.status === 'pending').length, pending: subtasks.filter((s) => s.status === "pending").length,
in_progress: subtasks.filter(s => s.status === 'in_progress').length, in_progress: subtasks.filter((s) => s.status === "in_progress").length,
completed: subtasks.filter(s => s.status === 'completed').length, completed: subtasks.filter((s) => s.status === "completed").length,
blocked: subtasks.filter(s => s.status === 'blocked').length, blocked: subtasks.filter((s) => s.status === "blocked").length,
}; };
const progress = subtasks.length > 0 const progress =
? Math.round((counts.completed / subtasks.length) * 100) subtasks.length > 0
: 0; ? Math.round((counts.completed / subtasks.length) * 100)
: 0;
console.log(`\n[${f}] ${task.name}`); console.log(`\n[${f}] ${task.name}`);
console.log(` Status: ${task.status} | Progress: ${progress}% (${counts.completed}/${subtasks.length})`); console.log(
console.log(` Pending: ${counts.pending} | In Progress: ${counts.in_progress} | Completed: ${counts.completed} | Blocked: ${counts.blocked}`); ` Status: ${task.status} | Progress: ${progress}% (${counts.completed}/${subtasks.length})`,
} );
console.log(
` Pending: ${counts.pending} | In Progress: ${counts.in_progress} | Completed: ${counts.completed} | Blocked: ${counts.blocked}`,
);
}
} }
function cmdNext(feature?: string): void { function cmdNext(feature?: string): void {
const features = feature ? [feature] : getFeatureDirs(); const features = feature ? [feature] : getFeatureDirs();
console.log('\n=== Ready Tasks (deps satisfied) ===\n'); console.log("\n=== Ready Tasks (deps satisfied) ===\n");
for (const f of features) { for (const f of features) {
const subtasks = loadSubtasks(f); const subtasks = loadSubtasks(f);
const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq)); const completedSeqs = new Set(
subtasks.filter((s) => s.status === "completed").map((s) => s.seq),
);
const ready = subtasks.filter(s => { const ready = subtasks.filter((s) => {
if (s.status !== 'pending') return false; if (s.status !== "pending") return false;
return s.depends_on.every(dep => completedSeqs.has(dep)); return s.depends_on.every((dep) => completedSeqs.has(dep));
}); });
if (ready.length > 0) { if (ready.length > 0) {
console.log(`[${f}]`); console.log(`[${f}]`);
for (const s of ready) { for (const s of ready) {
const parallel = s.parallel ? '[parallel]' : '[sequential]'; const parallel = s.parallel ? "[parallel]" : "[sequential]";
console.log(` ${s.seq} - ${s.title} ${parallel}`); console.log(` ${s.seq} - ${s.title} ${parallel}`);
} }
console.log(); console.log();
} }
} }
} }
function cmdParallel(feature?: string): void { function cmdParallel(feature?: string): void {
const features = feature ? [feature] : getFeatureDirs(); const features = feature ? [feature] : getFeatureDirs();
console.log('\n=== Parallelizable Tasks Ready Now ===\n'); console.log("\n=== Parallelizable Tasks Ready Now ===\n");
for (const f of features) { for (const f of features) {
const subtasks = loadSubtasks(f); const subtasks = loadSubtasks(f);
const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq)); const completedSeqs = new Set(
subtasks.filter((s) => s.status === "completed").map((s) => s.seq),
);
const parallel = subtasks.filter(s => { const parallel = subtasks.filter((s) => {
if (s.status !== 'pending') return false; if (s.status !== "pending") return false;
if (!s.parallel) return false; if (!s.parallel) return false;
return s.depends_on.every(dep => completedSeqs.has(dep)); return s.depends_on.every((dep) => completedSeqs.has(dep));
}); });
if (parallel.length > 0) { if (parallel.length > 0) {
console.log(`[${f}] - ${parallel.length} parallel tasks:`); console.log(`[${f}] - ${parallel.length} parallel tasks:`);
for (const s of parallel) { for (const s of parallel) {
console.log(` ${s.seq} - ${s.title}`); console.log(` ${s.seq} - ${s.title}`);
} }
console.log(); console.log();
} }
} }
} }
function cmdDeps(feature: string, seq: string): void { function cmdDeps(feature: string, seq: string): void {
const subtasks = loadSubtasks(feature); const subtasks = loadSubtasks(feature);
const target = subtasks.find(s => s.seq === seq); const target = subtasks.find((s) => s.seq === seq);
if (!target) { if (!target) {
console.log(`Task ${seq} not found in ${feature}`); console.log(`Task ${seq} not found in ${feature}`);
return; return;
} }
console.log(`\n=== Dependency Tree: ${feature}/${seq} ===\n`); console.log(`\n=== Dependency Tree: ${feature}/${seq} ===\n`);
console.log(`${seq} - ${target.title} [${target.status}]`); console.log(`${seq} - ${target.title} [${target.status}]`);
if (target.depends_on.length === 0) { if (target.depends_on.length === 0) {
console.log(' └── (no dependencies)'); console.log(" └── (no dependencies)");
return; return;
} }
const printDeps = (seqs: string[], indent: string = ' '): void => { const printDeps = (seqs: string[], indent: string = " "): void => {
for (let i = 0; i < seqs.length; i++) { for (let i = 0; i < seqs.length; i++) {
const depSeq = seqs[i]; const depSeq = seqs[i];
const dep = subtasks.find(s => s.seq === depSeq); const dep = subtasks.find((s) => s.seq === depSeq);
const isLast = i === seqs.length - 1; const isLast = i === seqs.length - 1;
const branch = isLast ? '└──' : '├──'; const branch = isLast ? "└──" : "├──";
if (dep) { if (dep) {
const statusIcon = dep.status === 'completed' ? '✓' : dep.status === 'in_progress' ? '~' : '○'; const statusIcon =
console.log(`${indent}${branch} ${statusIcon} ${depSeq} - ${dep.title} [${dep.status}]`); dep.status === "completed"
if (dep.depends_on.length > 0) { ? "✓"
const newIndent = indent + (isLast ? ' ' : '│ '); : dep.status === "in_progress"
printDeps(dep.depends_on, newIndent); ? "~"
} : "○";
} else { console.log(
console.log(`${indent}${branch} ? ${depSeq} - NOT FOUND`); `${indent}${branch} ${statusIcon} ${depSeq} - ${dep.title} [${dep.status}]`,
} );
} if (dep.depends_on.length > 0) {
}; const newIndent = indent + (isLast ? " " : "│ ");
printDeps(dep.depends_on, newIndent);
}
} else {
console.log(`${indent}${branch} ? ${depSeq} - NOT FOUND`);
}
}
};
printDeps(target.depends_on); printDeps(target.depends_on);
} }
function cmdBlocked(feature?: string): void { function cmdBlocked(feature?: string): void {
const features = feature ? [feature] : getFeatureDirs(); const features = feature ? [feature] : getFeatureDirs();
console.log('\n=== Blocked Tasks ===\n'); console.log("\n=== Blocked Tasks ===\n");
for (const f of features) { for (const f of features) {
const subtasks = loadSubtasks(f); const subtasks = loadSubtasks(f);
const completedSeqs = new Set(subtasks.filter(s => s.status === 'completed').map(s => s.seq)); const completedSeqs = new Set(
subtasks.filter((s) => s.status === "completed").map((s) => s.seq),
);
const blocked = subtasks.filter(s => { const blocked = subtasks.filter((s) => {
if (s.status === 'blocked') return true; if (s.status === "blocked") return true;
if (s.status !== 'pending') return false; if (s.status !== "pending") return false;
return !s.depends_on.every(dep => completedSeqs.has(dep)); return !s.depends_on.every((dep) => completedSeqs.has(dep));
}); });
if (blocked.length > 0) { if (blocked.length > 0) {
console.log(`[${f}]`); console.log(`[${f}]`);
for (const s of blocked) { for (const s of blocked) {
const waitingFor = s.depends_on.filter(dep => !completedSeqs.has(dep)); const waitingFor = s.depends_on.filter(
const reason = s.status === 'blocked' (dep) => !completedSeqs.has(dep),
? 'explicitly blocked' );
: `waiting: ${waitingFor.join(', ')}`; const reason =
console.log(` ${s.seq} - ${s.title} (${reason})`); s.status === "blocked"
} ? "explicitly blocked"
console.log(); : `waiting: ${waitingFor.join(", ")}`;
} console.log(` ${s.seq} - ${s.title} (${reason})`);
} }
console.log();
}
}
} }
function cmdComplete(feature: string, seq: string, summary: string): void { function cmdComplete(feature: string, seq: string, summary: string): void {
if (summary.length > 200) { if (summary.length > 200) {
console.log('Error: Summary must be max 200 characters'); console.log("Error: Summary must be max 200 characters");
process.exit(1); process.exit(1);
} }
const subtasks = loadSubtasks(feature); const subtasks = loadSubtasks(feature);
const subtask = subtasks.find(s => s.seq === seq); const subtask = subtasks.find((s) => s.seq === seq);
if (!subtask) { if (!subtask) {
console.log(`Task ${seq} not found in ${feature}`); console.log(`Task ${seq} not found in ${feature}`);
process.exit(1); process.exit(1);
} }
subtask.status = 'completed'; subtask.status = "completed";
subtask.completed_at = new Date().toISOString(); subtask.completed_at = new Date().toISOString();
subtask.completion_summary = summary; subtask.completion_summary = summary;
saveSubtask(feature, subtask); saveSubtask(feature, subtask);
// Update task.json counts // Update task.json counts
const task = loadTask(feature); const task = loadTask(feature);
if (task) { if (task) {
const newSubtasks = loadSubtasks(feature); const newSubtasks = loadSubtasks(feature);
task.completed_count = newSubtasks.filter(s => s.status === 'completed').length; task.completed_count = newSubtasks.filter(
saveTask(feature, task); (s) => s.status === "completed",
} ).length;
saveTask(feature, task);
}
console.log(`\n✓ Marked ${feature}/${seq} as completed`); console.log(`\n✓ Marked ${feature}/${seq} as completed`);
console.log(` Summary: ${summary}`); console.log(` Summary: ${summary}`);
if (task) { if (task) {
console.log(` Progress: ${task.completed_count}/${task.subtask_count}`); console.log(` Progress: ${task.completed_count}/${task.subtask_count}`);
} }
} }
function cmdValidate(feature?: string): void { function cmdValidate(feature?: string): void {
const features = feature ? [feature] : getFeatureDirs(); const features = feature ? [feature] : getFeatureDirs();
let hasErrors = false; let hasErrors = false;
const validTaskStatuses = new Set(['active', 'completed', 'blocked', 'archived']); const validTaskStatuses = new Set([
const validSubtaskStatuses = new Set(['pending', 'in_progress', 'completed', 'blocked']); "active",
"completed",
"blocked",
"archived",
]);
const validSubtaskStatuses = new Set([
"pending",
"in_progress",
"completed",
"blocked",
]);
const requiredTaskFields = [ const requiredTaskFields = [
'id', "id",
'name', "name",
'status', "status",
'objective', "objective",
'context_files', "context_files",
'exit_criteria', "exit_criteria",
'subtask_count', "subtask_count",
'completed_count', "completed_count",
'created_at', "created_at",
'completed_at', "completed_at",
]; ];
const requiredSubtaskFields = [ const requiredSubtaskFields = [
'id', "id",
'seq', "seq",
'title', "title",
'status', "status",
'depends_on', "depends_on",
'parallel', "parallel",
'context_files', "context_files",
'acceptance_criteria', "acceptance_criteria",
'deliverables', "deliverables",
'agent_id', "agent_id",
'started_at', "started_at",
'completed_at', "completed_at",
'completion_summary', "completion_summary",
]; ];
const hasField = (obj: any, field: string): boolean => Object.prototype.hasOwnProperty.call(obj, field); const hasField = (obj: any, field: string): boolean =>
const isStringArray = (value: any): boolean => Array.isArray(value) && value.every(v => typeof v === 'string'); Object.prototype.hasOwnProperty.call(obj, field);
const isStringArray = (value: any): boolean =>
Array.isArray(value) && value.every((v) => typeof v === "string");
console.log('\n=== Validation Results ===\n'); console.log("\n=== Validation Results ===\n");
for (const f of features) { for (const f of features) {
const errors: string[] = []; const errors: string[] = [];
// Check task.json exists // Check task.json exists
const task = loadTask(f); const task = loadTask(f);
if (!task) { if (!task) {
errors.push('Missing task.json'); errors.push("Missing task.json");
} }
// Load and validate subtasks // Load and validate subtasks
const subtasks = loadSubtasks(f); const subtasks = loadSubtasks(f);
const seqCounts = new Map<string, number>(); const seqCounts = new Map<string, number>();
for (const s of subtasks) { for (const s of subtasks) {
const seq = typeof s.seq === 'string' ? s.seq : ''; const seq = typeof s.seq === "string" ? s.seq : "";
seqCounts.set(seq, (seqCounts.get(seq) || 0) + 1); seqCounts.set(seq, (seqCounts.get(seq) || 0) + 1);
} }
const seqs = new Set(subtasks.map(s => s.seq)); const seqs = new Set(subtasks.map((s) => s.seq));
if (task) { if (task) {
// Required fields in task.json // Required fields in task.json
for (const field of requiredTaskFields) { for (const field of requiredTaskFields) {
if (!hasField(task, field)) { if (!hasField(task, field)) {
errors.push(`task.json: missing required field '${field}'`); errors.push(`task.json: missing required field '${field}'`);
} }
} }
// Task ID should match feature slug // Task ID should match feature slug
if (task.id !== f) { if (task.id !== f) {
errors.push(`task.json id ('${task.id}') should match feature slug ('${f}')`); errors.push(
} `task.json id ('${task.id}') should match feature slug ('${f}')`,
);
}
// Task status should be valid // Task status should be valid
if (!validTaskStatuses.has(task.status)) { if (!validTaskStatuses.has(task.status)) {
errors.push(`task.json: invalid status '${task.status}'`); errors.push(`task.json: invalid status '${task.status}'`);
} }
// Basic type checks for key task fields // Basic type checks for key task fields
if (!isStringArray(task.context_files)) { if (!isStringArray(task.context_files)) {
errors.push('task.json: context_files must be string[]'); errors.push("task.json: context_files must be string[]");
} }
if (hasField(task, 'reference_files') && task.reference_files !== undefined && !isStringArray(task.reference_files)) { if (
errors.push('task.json: reference_files must be string[] when present'); hasField(task, "reference_files") &&
} task.reference_files !== undefined &&
if (!isStringArray(task.exit_criteria)) { !isStringArray(task.reference_files)
errors.push('task.json: exit_criteria must be string[]'); ) {
} errors.push("task.json: reference_files must be string[] when present");
if (typeof task.subtask_count !== 'number') { }
errors.push('task.json: subtask_count must be number'); if (!isStringArray(task.exit_criteria)) {
} errors.push("task.json: exit_criteria must be string[]");
if (typeof task.completed_count !== 'number') { }
errors.push('task.json: completed_count must be number'); if (typeof task.subtask_count !== "number") {
} errors.push("task.json: subtask_count must be number");
} }
if (typeof task.completed_count !== "number") {
errors.push("task.json: completed_count must be number");
}
}
for (const s of subtasks) { for (const s of subtasks) {
// Required fields in subtask files // Required fields in subtask files
for (const field of requiredSubtaskFields) { for (const field of requiredSubtaskFields) {
if (!hasField(s, field)) { if (!hasField(s, field)) {
errors.push(`${s.seq || '??'}: missing required field '${field}'`); errors.push(`${s.seq || "??"}: missing required field '${field}'`);
} }
} }
// Sequence format and uniqueness // Sequence format and uniqueness
if (!/^\d{2}$/.test(s.seq)) { if (!/^\d{2}$/.test(s.seq)) {
errors.push(`${s.seq}: sequence must be 2 digits (e.g., 01, 02)`); errors.push(`${s.seq}: sequence must be 2 digits (e.g., 01, 02)`);
} }
if ((seqCounts.get(s.seq) || 0) > 1) { if ((seqCounts.get(s.seq) || 0) > 1) {
errors.push(`${s.seq}: duplicate sequence number`); errors.push(`${s.seq}: duplicate sequence number`);
} }
// Check ID format // Check ID format
if (!s.id.startsWith(f)) { if (!s.id.startsWith(f)) {
errors.push(`${s.seq}: ID should start with feature name`); errors.push(`${s.seq}: ID should start with feature name`);
} }
// Status should be valid // Status should be valid
if (!validSubtaskStatuses.has(s.status)) { if (!validSubtaskStatuses.has(s.status)) {
errors.push(`${s.seq}: invalid status '${s.status}'`); errors.push(`${s.seq}: invalid status '${s.status}'`);
} }
// Type checks // Type checks
if (!isStringArray(s.depends_on)) { if (!isStringArray(s.depends_on)) {
errors.push(`${s.seq}: depends_on must be string[]`); errors.push(`${s.seq}: depends_on must be string[]`);
} }
if (typeof s.parallel !== 'boolean') { if (typeof s.parallel !== "boolean") {
errors.push(`${s.seq}: parallel must be boolean`); errors.push(`${s.seq}: parallel must be boolean`);
} }
if (!isStringArray(s.context_files)) { if (!isStringArray(s.context_files)) {
errors.push(`${s.seq}: context_files must be string[]`); errors.push(`${s.seq}: context_files must be string[]`);
} }
if (hasField(s, 'reference_files') && s.reference_files !== undefined && !isStringArray(s.reference_files)) { if (
errors.push(`${s.seq}: reference_files must be string[] when present`); hasField(s, "reference_files") &&
} s.reference_files !== undefined &&
if (!isStringArray(s.acceptance_criteria)) { !isStringArray(s.reference_files)
errors.push(`${s.seq}: acceptance_criteria must be string[]`); ) {
} else if (s.acceptance_criteria.length === 0) { errors.push(`${s.seq}: reference_files must be string[] when present`);
errors.push(`${s.seq}: No acceptance criteria defined`); }
} if (!isStringArray(s.acceptance_criteria)) {
if (!isStringArray(s.deliverables)) { errors.push(`${s.seq}: acceptance_criteria must be string[]`);
errors.push(`${s.seq}: deliverables must be string[]`); } else if (s.acceptance_criteria.length === 0) {
} else if (s.deliverables.length === 0) { errors.push(`${s.seq}: No acceptance criteria defined`);
errors.push(`${s.seq}: No deliverables defined`); }
} if (!isStringArray(s.deliverables)) {
errors.push(`${s.seq}: deliverables must be string[]`);
} else if (s.deliverables.length === 0) {
errors.push(`${s.seq}: No deliverables defined`);
}
// Self dependency is invalid // Self dependency is invalid
if (Array.isArray(s.depends_on) && s.depends_on.includes(s.seq)) { if (Array.isArray(s.depends_on) && s.depends_on.includes(s.seq)) {
errors.push(`${s.seq}: task cannot depend on itself`); errors.push(`${s.seq}: task cannot depend on itself`);
} }
// Check for missing dependencies // Check for missing dependencies
for (const dep of (Array.isArray(s.depends_on) ? s.depends_on : [])) { for (const dep of Array.isArray(s.depends_on) ? s.depends_on : []) {
if (!seqs.has(dep)) { if (!seqs.has(dep)) {
errors.push(`${s.seq}: depends on non-existent task ${dep}`); errors.push(`${s.seq}: depends on non-existent task ${dep}`);
} }
} }
// Check for circular dependencies // Check for circular dependencies
const visited = new Set<string>(); const visited = new Set<string>();
const checkCircular = (seq: string, path: string[]): boolean => { const checkCircular = (seq: string, path: string[]): boolean => {
if (path.includes(seq)) { if (path.includes(seq)) {
errors.push(`${s.seq}: circular dependency detected: ${[...path, seq].join(' -> ')}`); errors.push(
return true; `${s.seq}: circular dependency detected: ${[...path, seq].join(" -> ")}`,
} );
if (visited.has(seq)) return false; return true;
visited.add(seq); }
if (visited.has(seq)) return false;
visited.add(seq);
const task = subtasks.find(t => t.seq === seq); const task = subtasks.find((t) => t.seq === seq);
if (task) { if (task) {
for (const dep of task.depends_on) { for (const dep of task.depends_on) {
if (checkCircular(dep, [...path, seq])) return true; if (checkCircular(dep, [...path, seq])) return true;
} }
} }
return false; return false;
}; };
checkCircular(s.seq, []); checkCircular(s.seq, []);
} }
// Check counts match // Check counts match
if (task && task.subtask_count !== subtasks.length) { if (task && task.subtask_count !== subtasks.length) {
errors.push(`task.json subtask_count (${task.subtask_count}) doesn't match actual count (${subtasks.length})`); errors.push(
} `task.json subtask_count (${task.subtask_count}) doesn't match actual count (${subtasks.length})`,
);
}
// Print results // Print results
console.log(`[${f}]`); console.log(`[${f}]`);
if (errors.length === 0) { if (errors.length === 0) {
console.log(' ✓ All checks passed'); console.log(" ✓ All checks passed");
} else { } else {
for (const e of errors) { for (const e of errors) {
console.log(` ✗ ERROR: ${e}`); console.log(` ✗ ERROR: ${e}`);
hasErrors = true; hasErrors = true;
} }
} }
console.log(); console.log();
} }
process.exit(hasErrors ? 1 : 0); process.exit(hasErrors ? 1 : 0);
} }
// Main // Main
const [,, command, ...args] = process.argv; const [, , command, ...args] = process.argv;
switch (command) { switch (command) {
case 'status': case "status":
cmdStatus(args[0]); cmdStatus(args[0]);
break; break;
case 'next': case "next":
cmdNext(args[0]); cmdNext(args[0]);
break; break;
case 'parallel': case "parallel":
cmdParallel(args[0]); cmdParallel(args[0]);
break; break;
case 'deps': case "deps":
if (args.length < 2) { if (args.length < 2) {
console.log('Usage: deps <feature> <seq>'); console.log("Usage: deps <feature> <seq>");
process.exit(1); process.exit(1);
} }
cmdDeps(args[0], args[1]); cmdDeps(args[0], args[1]);
break; break;
case 'blocked': case "blocked":
cmdBlocked(args[0]); cmdBlocked(args[0]);
break; break;
case 'complete': case "complete":
if (args.length < 3) { if (args.length < 3) {
console.log('Usage: complete <feature> <seq> "summary"'); console.log('Usage: complete <feature> <seq> "summary"');
process.exit(1); process.exit(1);
} }
cmdComplete(args[0], args[1], args.slice(2).join(' ')); cmdComplete(args[0], args[1], args.slice(2).join(" "));
break; break;
case 'validate': case "validate":
cmdValidate(args[0]); cmdValidate(args[0]);
break; break;
default: default:
console.log(` console.log(`
Task Management CLI Task Management CLI
Usage: bunx --bun ts-node task-cli.ts <command> [feature] [args...] Usage: bunx --bun ts-node task-cli.ts <command> [feature] [args...]

View File

@@ -1,168 +1,188 @@
import { readFile } from "fs/promises" import { readFile } from "fs/promises";
import { resolve } from "path" import { resolve } from "path";
/** /**
* Configuration for environment variable loading * Configuration for environment variable loading
*/ */
export interface EnvLoaderConfig { export interface EnvLoaderConfig {
/** Custom paths to search for .env files (relative to current working directory) */ /** Custom paths to search for .env files (relative to current working directory) */
searchPaths?: string[] searchPaths?: string[];
/** Whether to log when environment variables are loaded */ /** Whether to log when environment variables are loaded */
verbose?: boolean verbose?: boolean;
/** Whether to override existing environment variables */ /** Whether to override existing environment variables */
override?: boolean override?: boolean;
} }
/** /**
* Default search paths for .env files * Default search paths for .env files
*/ */
const DEFAULT_ENV_PATHS = [ const DEFAULT_ENV_PATHS = [
'./.env', "./.env",
'../.env', "../.env",
'../../.env', "../../.env",
'../plugin/.env', "../plugin/.env",
'../../../.env' "../../../.env",
] ];
/** /**
* Load environment variables from .env files * Load environment variables from .env files
* Searches multiple common locations for .env files and loads them into process.env * Searches multiple common locations for .env files and loads them into process.env
* *
* @param config Configuration options * @param config Configuration options
* @returns Object containing loaded environment variables * @returns Object containing loaded environment variables
*/ */
export async function loadEnvVariables(config: EnvLoaderConfig = {}): Promise<Record<string, string>> { export async function loadEnvVariables(
const { config: EnvLoaderConfig = {},
searchPaths = DEFAULT_ENV_PATHS, ): Promise<Record<string, string>> {
verbose = false, const {
override = false searchPaths = DEFAULT_ENV_PATHS,
} = config verbose = false,
override = false,
const loadedVars: Record<string, string> = {} } = config;
for (const envPath of searchPaths) { const loadedVars: Record<string, string> = {};
try {
const fullPath = resolve(envPath) for (const envPath of searchPaths) {
const content = await readFile(fullPath, 'utf8') try {
const fullPath = resolve(envPath);
if (verbose) { const content = await readFile(fullPath, "utf8");
console.log(`Checking .env file: ${envPath}`)
} if (verbose) {
console.log(`Checking .env file: ${envPath}`);
// Parse .env file content }
const lines = content.split('\n')
for (const line of lines) { // Parse .env file content
const trimmed = line.trim() const lines = content.split("\n");
if (trimmed && !trimmed.startsWith('#') && trimmed.includes('=')) { for (const line of lines) {
const [key, ...valueParts] = trimmed.split('=') const trimmed = line.trim();
const value = valueParts.join('=').trim() if (trimmed && !trimmed.startsWith("#") && trimmed.includes("=")) {
const [key, ...valueParts] = trimmed.split("=");
// Remove quotes if present const value = valueParts.join("=").trim();
const cleanValue = value.replace(/^["']|["']$/g, '')
// Remove quotes if present
if (key && cleanValue && (override || !process.env[key])) { const cleanValue = value.replace(/^["']|["']$/g, "");
process.env[key] = cleanValue
loadedVars[key] = cleanValue if (key && cleanValue && (override || !process.env[key])) {
process.env[key] = cleanValue;
if (verbose) { loadedVars[key] = cleanValue;
console.log(`Loaded ${key} from ${envPath}`)
} if (verbose) {
} console.log(`Loaded ${key} from ${envPath}`);
} }
} }
} catch (error) { }
// File doesn't exist or can't be read, continue to next }
if (verbose) { } catch (error) {
console.log(`Could not read ${envPath}: ${error.message}`) // File doesn't exist or can't be read, continue to next
} if (verbose) {
} console.log(`Could not read ${envPath}: ${error.message}`);
} }
}
return loadedVars }
return loadedVars;
} }
/** /**
* Get a specific environment variable with automatic .env file loading * Get a specific environment variable with automatic .env file loading
* *
* @param varName Name of the environment variable * @param varName Name of the environment variable
* @param config Configuration options * @param config Configuration options
* @returns The environment variable value or null if not found * @returns The environment variable value or null if not found
*/ */
export async function getEnvVariable(varName: string, config: EnvLoaderConfig = {}): Promise<string | null> { export async function getEnvVariable(
// First check if it's already in the environment varName: string,
let value = process.env[varName] config: EnvLoaderConfig = {},
): Promise<string | null> {
if (!value) { // First check if it's already in the environment
// Try to load from .env files let value = process.env[varName];
const loadedVars = await loadEnvVariables(config)
value = loadedVars[varName] || process.env[varName] if (!value) {
} // Try to load from .env files
const loadedVars = await loadEnvVariables(config);
return value || null value = loadedVars[varName] || process.env[varName];
}
return value || null;
} }
/** /**
* Get a required environment variable with automatic .env file loading * Get a required environment variable with automatic .env file loading
* Throws an error if the variable is not found * Throws an error if the variable is not found
* *
* @param varName Name of the environment variable * @param varName Name of the environment variable
* @param config Configuration options * @param config Configuration options
* @returns The environment variable value * @returns The environment variable value
* @throws Error if the variable is not found * @throws Error if the variable is not found
*/ */
export async function getRequiredEnvVariable(varName: string, config: EnvLoaderConfig = {}): Promise<string> { export async function getRequiredEnvVariable(
const value = await getEnvVariable(varName, config) varName: string,
config: EnvLoaderConfig = {},
if (!value) { ): Promise<string> {
const searchPaths = config.searchPaths || DEFAULT_ENV_PATHS const value = await getEnvVariable(varName, config);
throw new Error(`${varName} not found. Please set it in your environment or .env file.
if (!value) {
const searchPaths = config.searchPaths || DEFAULT_ENV_PATHS;
throw new Error(`${varName} not found. Please set it in your environment or .env file.
To fix this: To fix this:
1. Add to .env file: ${varName}=your_value_here 1. Add to .env file: ${varName}=your_value_here
2. Or export it: export ${varName}=your_value_here 2. Or export it: export ${varName}=your_value_here
Current working directory: ${process.cwd()} Current working directory: ${process.cwd()}
Searched paths: ${searchPaths.join(', ')} Searched paths: ${searchPaths.join(", ")}
Environment variables available: ${Object.keys(process.env).filter(k => k.includes(varName.split('_')[0])).join(', ') || 'none matching'}`) Environment variables available: ${
} Object.keys(process.env)
.filter((k) => k.includes(varName.split("_")[0]))
return value .join(", ") || "none matching"
}`);
}
return value;
} }
/** /**
* Load multiple required environment variables at once * Load multiple required environment variables at once
* *
* @param varNames Array of environment variable names * @param varNames Array of environment variable names
* @param config Configuration options * @param config Configuration options
* @returns Object with variable names as keys and values as values * @returns Object with variable names as keys and values as values
* @throws Error if any variable is not found * @throws Error if any variable is not found
*/ */
export async function getRequiredEnvVariables(varNames: string[], config: EnvLoaderConfig = {}): Promise<Record<string, string>> { export async function getRequiredEnvVariables(
const result: Record<string, string> = {} varNames: string[],
config: EnvLoaderConfig = {},
// Load all .env files first ): Promise<Record<string, string>> {
await loadEnvVariables(config) const result: Record<string, string> = {};
// Check each required variable // Load all .env files first
for (const varName of varNames) { await loadEnvVariables(config);
const value = process.env[varName]
if (!value) { // Check each required variable
throw new Error(`Required environment variable ${varName} not found. Please set it in your environment or .env file.`) for (const varName of varNames) {
} const value = process.env[varName];
result[varName] = value if (!value) {
} throw new Error(
`Required environment variable ${varName} not found. Please set it in your environment or .env file.`,
return result );
}
result[varName] = value;
}
return result;
} }
/** /**
* Utility function specifically for API keys * Utility function specifically for API keys
* *
* @param apiKeyName Name of the API key environment variable * @param apiKeyName Name of the API key environment variable
* @param config Configuration options * @param config Configuration options
* @returns The API key value * @returns The API key value
* @throws Error if the API key is not found * @throws Error if the API key is not found
*/ */
export async function getApiKey(apiKeyName: string, config: EnvLoaderConfig = {}): Promise<string> { export async function getApiKey(
return getRequiredEnvVariable(apiKeyName, config) apiKeyName: string,
} config: EnvLoaderConfig = {},
): Promise<string> {
return getRequiredEnvVariable(apiKeyName, config);
}