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

@@ -91,9 +91,7 @@
"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": { "technical-writer": {
"id": "technical-writer", "id": "technical-writer",
@@ -103,9 +101,7 @@
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["documentation", "technical", "writing"], "tags": ["documentation", "technical", "writing"],
"dependencies": [ "dependencies": ["context:standards-docs"]
"context:standards-docs"
]
}, },
"data-analyst": { "data-analyst": {
"id": "data-analyst", "id": "data-analyst",
@@ -125,9 +121,7 @@
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["testing", "evaluation", "quality"], "tags": ["testing", "evaluation", "quality"],
"dependencies": [ "dependencies": ["context:standards-tests"]
"context:standards-tests"
]
}, },
"task-manager": { "task-manager": {
"id": "task-manager", "id": "task-manager",
@@ -137,9 +131,7 @@
"version": "2.0.0", "version": "2.0.0",
"author": "opencode", "author": "opencode",
"tags": ["task-breakdown", "planning", "coordination"], "tags": ["task-breakdown", "planning", "coordination"],
"dependencies": [ "dependencies": ["context:task-delegation-basics"]
"context:task-delegation-basics"
]
}, },
"batch-executor": { "batch-executor": {
"id": "batch-executor", "id": "batch-executor",
@@ -149,10 +141,7 @@
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["parallel-execution", "batch-management", "coordination"], "tags": ["parallel-execution", "batch-management", "coordination"],
"dependencies": [ "dependencies": ["subagent:coder-agent", "subagent:task-manager"]
"subagent:coder-agent",
"subagent:task-manager"
]
}, },
"documentation": { "documentation": {
"id": "documentation", "id": "documentation",
@@ -162,9 +151,7 @@
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["documentation", "writing"], "tags": ["documentation", "writing"],
"dependencies": [ "dependencies": ["context:standards-docs"]
"context:standards-docs"
]
}, },
"contextscout": { "contextscout": {
"id": "contextscout", "id": "contextscout",
@@ -214,9 +201,7 @@
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["coding", "implementation"], "tags": ["coding", "implementation"],
"dependencies": [ "dependencies": ["context:standards-code"]
"context:standards-code"
]
}, },
"tester": { "tester": {
"id": "tester", "id": "tester",
@@ -226,9 +211,7 @@
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["testing", "tdd", "quality"], "tags": ["testing", "tdd", "quality"],
"dependencies": [ "dependencies": ["context:standards-tests"]
"context:standards-tests"
]
}, },
"reviewer": { "reviewer": {
"id": "reviewer", "id": "reviewer",
@@ -238,10 +221,7 @@
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["review", "security", "quality"], "tags": ["review", "security", "quality"],
"dependencies": [ "dependencies": ["context:standards-code", "context:review-ref"]
"context:standards-code",
"context:review-ref"
]
}, },
"build-agent": { "build-agent": {
"id": "build-agent", "id": "build-agent",
@@ -261,9 +241,7 @@
"version": "1.0.0", "version": "1.0.0",
"author": "opencode", "author": "opencode",
"tags": ["frontend", "ui", "design"], "tags": ["frontend", "ui", "design"],
"dependencies": [ "dependencies": ["context:standards-code"]
"context:standards-code"
]
}, },
"devops-specialist": { "devops-specialist": {
"id": "devops-specialist", "id": "devops-specialist",

View File

@@ -19,14 +19,17 @@
* .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 (
fs.existsSync(path.join(dir, ".git")) ||
fs.existsSync(path.join(dir, "package.json"))
) {
return dir; return dir;
} }
dir = path.dirname(dir); dir = path.dirname(dir);
@@ -35,13 +38,13 @@ function findProjectRoot(): string {
} }
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[];
@@ -56,7 +59,7 @@ 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[];
@@ -75,34 +78,41 @@ 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
.readdirSync(featureDir)
.filter((f: string) => f.match(/^subtask_\d{2}\.json$/)) .filter((f: string) => f.match(/^subtask_\d{2}\.json$/))
.sort(); .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(
TASKS_DIR,
feature,
`subtask_${subtask.seq}.json`,
);
fs.writeFileSync(subtaskPath, JSON.stringify(subtask, null, 2)); 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));
} }
@@ -111,7 +121,7 @@ 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;
} }
@@ -125,40 +135,47 @@ function cmdStatus(feature?: string): void {
} }
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 =
subtasks.length > 0
? Math.round((counts.completed / subtasks.length) * 100) ? Math.round((counts.completed / subtasks.length) * 100)
: 0; : 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();
@@ -169,16 +186,18 @@ function cmdNext(feature?: string): void {
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) {
@@ -193,7 +212,7 @@ function cmdParallel(feature?: string): void {
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}`);
@@ -204,22 +223,29 @@ function cmdDeps(feature: string, seq: string): void {
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"
? "✓"
: dep.status === "in_progress"
? "~"
: "○";
console.log(
`${indent}${branch} ${statusIcon} ${depSeq} - ${dep.title} [${dep.status}]`,
);
if (dep.depends_on.length > 0) { if (dep.depends_on.length > 0) {
const newIndent = indent + (isLast ? ' ' : ''); const newIndent = indent + (isLast ? " " : "");
printDeps(dep.depends_on, newIndent); printDeps(dep.depends_on, newIndent);
} }
} else { } else {
@@ -234,25 +260,30 @@ function cmdDeps(feature: string, seq: string): void {
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 =
s.status === "blocked"
? "explicitly blocked"
: `waiting: ${waitingFor.join(", ")}`;
console.log(` ${s.seq} - ${s.title} (${reason})`); console.log(` ${s.seq} - ${s.title} (${reason})`);
} }
console.log(); console.log();
@@ -262,19 +293,19 @@ function cmdBlocked(feature?: string): void {
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;
@@ -284,7 +315,9 @@ function cmdComplete(feature: string, seq: string, summary: string): void {
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(
(s) => s.status === "completed",
).length;
saveTask(feature, task); saveTask(feature, task);
} }
@@ -300,42 +333,54 @@ 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[] = [];
@@ -343,17 +388,17 @@ function cmdValidate(feature?: string): void {
// 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
@@ -365,7 +410,9 @@ function cmdValidate(feature?: string): void {
// 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
@@ -375,19 +422,23 @@ function cmdValidate(feature?: string): void {
// 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 &&
!isStringArray(task.reference_files)
) {
errors.push("task.json: reference_files must be string[] when present");
} }
if (!isStringArray(task.exit_criteria)) { if (!isStringArray(task.exit_criteria)) {
errors.push('task.json: exit_criteria must be string[]'); errors.push("task.json: exit_criteria must be string[]");
} }
if (typeof task.subtask_count !== 'number') { if (typeof task.subtask_count !== "number") {
errors.push('task.json: subtask_count must be number'); errors.push("task.json: subtask_count must be number");
} }
if (typeof task.completed_count !== 'number') { if (typeof task.completed_count !== "number") {
errors.push('task.json: completed_count must be number'); errors.push("task.json: completed_count must be number");
} }
} }
@@ -395,7 +446,7 @@ function cmdValidate(feature?: string): void {
// 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}'`);
} }
} }
@@ -421,13 +472,17 @@ function cmdValidate(feature?: string): void {
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 (
hasField(s, "reference_files") &&
s.reference_files !== undefined &&
!isStringArray(s.reference_files)
) {
errors.push(`${s.seq}: reference_files must be string[] when present`); errors.push(`${s.seq}: reference_files must be string[] when present`);
} }
if (!isStringArray(s.acceptance_criteria)) { if (!isStringArray(s.acceptance_criteria)) {
@@ -447,7 +502,7 @@ function cmdValidate(feature?: string): void {
} }
// 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}`);
} }
@@ -457,13 +512,15 @@ function cmdValidate(feature?: string): void {
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(
`${s.seq}: circular dependency detected: ${[...path, seq].join(" -> ")}`,
);
return true; return true;
} }
if (visited.has(seq)) return false; if (visited.has(seq)) return false;
visited.add(seq); 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;
@@ -476,13 +533,15 @@ function cmdValidate(feature?: string): void {
// 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}`);
@@ -499,33 +558,33 @@ function cmdValidate(feature?: string): void {
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:

View File

@@ -1,28 +1,28 @@
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
@@ -31,41 +31,43 @@ const DEFAULT_ENV_PATHS = [
* @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(
config: EnvLoaderConfig = {},
): Promise<Record<string, string>> {
const { const {
searchPaths = DEFAULT_ENV_PATHS, searchPaths = DEFAULT_ENV_PATHS,
verbose = false, verbose = false,
override = false override = false,
} = config } = config;
const loadedVars: Record<string, string> = {} const loadedVars: Record<string, string> = {};
for (const envPath of searchPaths) { for (const envPath of searchPaths) {
try { try {
const fullPath = resolve(envPath) const fullPath = resolve(envPath);
const content = await readFile(fullPath, 'utf8') const content = await readFile(fullPath, "utf8");
if (verbose) { if (verbose) {
console.log(`Checking .env file: ${envPath}`) console.log(`Checking .env file: ${envPath}`);
} }
// Parse .env file content // Parse .env file content
const lines = content.split('\n') const lines = content.split("\n");
for (const line of lines) { for (const line of lines) {
const trimmed = line.trim() const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') && trimmed.includes('=')) { if (trimmed && !trimmed.startsWith("#") && trimmed.includes("=")) {
const [key, ...valueParts] = trimmed.split('=') const [key, ...valueParts] = trimmed.split("=");
const value = valueParts.join('=').trim() const value = valueParts.join("=").trim();
// Remove quotes if present // Remove quotes if present
const cleanValue = value.replace(/^["']|["']$/g, '') const cleanValue = value.replace(/^["']|["']$/g, "");
if (key && cleanValue && (override || !process.env[key])) { if (key && cleanValue && (override || !process.env[key])) {
process.env[key] = cleanValue process.env[key] = cleanValue;
loadedVars[key] = cleanValue loadedVars[key] = cleanValue;
if (verbose) { if (verbose) {
console.log(`Loaded ${key} from ${envPath}`) console.log(`Loaded ${key} from ${envPath}`);
} }
} }
} }
@@ -73,12 +75,12 @@ export async function loadEnvVariables(config: EnvLoaderConfig = {}): Promise<Re
} catch (error) { } catch (error) {
// File doesn't exist or can't be read, continue to next // File doesn't exist or can't be read, continue to next
if (verbose) { if (verbose) {
console.log(`Could not read ${envPath}: ${error.message}`) console.log(`Could not read ${envPath}: ${error.message}`);
} }
} }
} }
return loadedVars return loadedVars;
} }
/** /**
@@ -88,17 +90,20 @@ export async function loadEnvVariables(config: EnvLoaderConfig = {}): Promise<Re
* @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(
varName: string,
config: EnvLoaderConfig = {},
): Promise<string | null> {
// First check if it's already in the environment // First check if it's already in the environment
let value = process.env[varName] let value = process.env[varName];
if (!value) { if (!value) {
// Try to load from .env files // Try to load from .env files
const loadedVars = await loadEnvVariables(config) const loadedVars = await loadEnvVariables(config);
value = loadedVars[varName] || process.env[varName] value = loadedVars[varName] || process.env[varName];
} }
return value || null return value || null;
} }
/** /**
@@ -110,11 +115,14 @@ export async function getEnvVariable(varName: string, config: EnvLoaderConfig =
* @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 = {},
): Promise<string> {
const value = await getEnvVariable(varName, config);
if (!value) { if (!value) {
const searchPaths = config.searchPaths || DEFAULT_ENV_PATHS const searchPaths = config.searchPaths || DEFAULT_ENV_PATHS;
throw new Error(`${varName} not found. Please set it in your environment or .env file. throw new Error(`${varName} not found. Please set it in your environment or .env file.
To fix this: To fix this:
@@ -122,11 +130,15 @@ To fix this:
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]))
.join(", ") || "none matching"
}`);
} }
return value return value;
} }
/** /**
@@ -137,22 +149,27 @@ Environment variables available: ${Object.keys(process.env).filter(k => k.includ
* @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 = {},
): Promise<Record<string, string>> {
const result: Record<string, string> = {};
// Load all .env files first // Load all .env files first
await loadEnvVariables(config) await loadEnvVariables(config);
// Check each required variable // Check each required variable
for (const varName of varNames) { for (const varName of varNames) {
const value = process.env[varName] const value = process.env[varName];
if (!value) { if (!value) {
throw new Error(`Required environment variable ${varName} not found. Please set it in your environment or .env file.`) throw new Error(
`Required environment variable ${varName} not found. Please set it in your environment or .env file.`,
);
} }
result[varName] = value result[varName] = value;
} }
return result return result;
} }
/** /**
@@ -163,6 +180,9 @@ export async function getRequiredEnvVariables(varNames: string[], config: EnvLoa
* @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);
} }