#!/usr/bin/env bun
import * as fs from "fs";
import * as path from "path";
import minimist from "minimist";
// Types
interface TaskItem {
text: string;
done?: boolean;
}
interface ChecklistSection {
name: string;
items: (string | TaskItem)[];
}
interface TodoJsonData {
tasks?: (string | TaskItem)[];
checklists?: ChecklistSection[];
acceptance?: string[];
notes?: string;
links?: string[];
}
interface TodoData {
id: string;
slug: string;
title: string;
description: string;
status: "pending" | "in_progress" | "completed" | "blocked";
priority: "low" | "medium" | "high" | "critical";
assignee?: string;
due?: string;
tags: string[];
parent?: string;
tasks: { text: string; done: boolean }[];
notes: string;
created: string;
updated: string;
jsonData?: TodoJsonData;
}
// Parse command line arguments
const args = minimist(process.argv.slice(2), {
string: ["slug", "format", "priority", "status", "assignee", "due", "tags", "parent", "content", "data"],
boolean: ["no-index", "help"],
default: {
format: "md",
priority: "medium",
status: "pending",
"no-index": false,
},
alias: {
s: "slug",
f: "format",
p: "priority",
a: "assignee",
d: "due",
t: "tags",
c: "content",
h: "help",
},
});
// Show help
if (args.help) {
console.log(`
Implementation Todo - Create todo files for development tracking
Usage:
skills run implementation-todo -- "
" [options]
Options:
-s, --slug Slug name for the todo file (required)
-f, --format Output format: md, json (default: md)
-p, --priority Priority: low, medium, high, critical (default: medium)
--status Status: pending, in_progress, completed, blocked (default: pending)
-a, --assignee Person assigned to the todo
-d, --due Due date (YYYY-MM-DD)
-t, --tags Comma-separated tags
-c, --content Additional content/description to add
--data JSON data with tasks, checklists, etc.
--parent Parent todo ID for subtasks
--no-index Skip updating TODOS.md index
-h, --help Show this help
JSON Data Format (--data):
{
"tasks": ["Task 1", "Task 2", { "text": "Task 3", "done": true }],
"checklists": [
{ "name": "Before starting", "items": ["Review requirements", "Setup environment"] },
{ "name": "Completion criteria", "items": ["Tests pass", "Code reviewed"] }
],
"acceptance": ["Feature works as expected", "No regressions"],
"notes": "Additional notes here",
"links": ["https://docs.example.com", "https://ticket.example.com/123"]
}
Examples:
skills run implementation-todo -- "Add authentication" --slug auth-feature
skills run implementation-todo -- "Fix bug" --slug bug-fix --format json
skills run implementation-todo -- "Security fixes" --slug security --priority critical --assignee alice
skills run implementation-todo -- "Feature" --slug feature --data '{"tasks":["Implement login","Add tests"],"acceptance":["Works with OAuth"]}'
`);
process.exit(0);
}
// Get title
const title = args._[0] as string;
if (!title) {
console.error("Error: Title is required");
console.error('Usage: skills run implementation-todo -- "" --slug ');
process.exit(1);
}
// Get slug
const rawSlug = args.slug as string;
if (!rawSlug) {
console.error("Error: --slug is required");
console.error('Usage: skills run implementation-todo -- "" --slug ');
process.exit(1);
}
// Normalize slug (replace spaces and dashes with underscores)
const slug = rawSlug.toLowerCase().replace(/[\s-]+/g, "_").replace(/[^a-z0-9_]/g, "");
// Sanitize content for safe inclusion (handle quotes and special chars)
function sanitizeContent(text: string): string {
if (!text) return "";
// Normalize line endings and trim
return text
.replace(/\\n/g, "\n") // Handle escaped newlines from CLI
.replace(/\\t/g, "\t") // Handle escaped tabs from CLI
.trim();
}
// Get content if provided
const userContent = args.content ? sanitizeContent(args.content as string) : "";
// Find .implementation directory
function findImplementationDir(): string | null {
// Use SKILLS_CWD if available (user's working directory from remote execution)
let currentDir = process.env.SKILLS_CWD || process.cwd();
while (currentDir !== path.dirname(currentDir)) {
const implDir = path.join(currentDir, ".implementation");
if (fs.existsSync(implDir)) {
return implDir;
}
currentDir = path.dirname(currentDir);
}
return null;
}
const implDir = findImplementationDir();
if (!implDir) {
console.error("Error: .implementation directory not found");
console.error("Run 'skills run implementation-init' first to create the folder structure");
process.exit(1);
}
// Determine format and output directory
const format = args.format as "md" | "json";
const outputDir = path.join(implDir, "data", "todos", format);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Get next sequence number
function getNextSequence(): number {
const jsonDir = path.join(implDir, "data", "todos", "json");
const mdDir = path.join(implDir, "data", "todos", "md");
let maxSeq = 0;
for (const dir of [jsonDir, mdDir]) {
if (!fs.existsSync(dir)) continue;
const files = fs.readdirSync(dir);
for (const file of files) {
const match = file.match(/^todo_(\d{5})_/);
if (match) {
const seq = parseInt(match[1], 10);
if (seq > maxSeq) maxSeq = seq;
}
}
}
return maxSeq + 1;
}
const sequence = getNextSequence();
const todoId = `todo_${String(sequence).padStart(5, "0")}`;
const timestamp = new Date().toISOString().split("T")[0];
// Parse JSON data if provided
let jsonData: TodoJsonData | undefined;
if (args.data) {
try {
jsonData = JSON.parse(args.data as string);
} catch (e) {
console.error("Error: Invalid JSON data provided");
console.error("Make sure to escape quotes properly in the shell");
process.exit(1);
}
}
// Parse tags
const tags = args.tags ? (args.tags as string).split(",").map((t: string) => t.trim()) : [];
// Build todo data
const todoData: TodoData = {
id: todoId,
slug,
title,
description: userContent || title,
status: args.status as TodoData["status"],
priority: args.priority as TodoData["priority"],
assignee: args.assignee,
due: args.due,
tags,
parent: args.parent,
tasks: [],
notes: jsonData?.notes || "",
created: timestamp,
updated: timestamp,
jsonData,
};
// Helper function to render task item
function renderTaskItem(task: string | TaskItem): string {
if (typeof task === "string") {
return `- [ ] ${task}\n`;
} else {
const checkbox = task.done ? "[x]" : "[ ]";
return `- ${checkbox} ${task.text}\n`;
}
}
// Generate markdown content
function generateMarkdown(data: TodoData): string {
const json = data.jsonData;
let content = `# Todo: ${data.title}\n\n`;
// Metadata
content += `- **ID**: ${data.id}\n`;
content += `- **Slug**: ${data.slug}\n`;
content += `- **Status**: ${data.status}\n`;
content += `- **Priority**: ${data.priority}\n`;
if (data.assignee) {
content += `- **Assignee**: ${data.assignee}\n`;
}
if (data.due) {
content += `- **Due**: ${data.due}\n`;
}
if (data.tags.length > 0) {
content += `- **Tags**: ${data.tags.join(", ")}\n`;
}
if (data.parent) {
content += `- **Parent**: ${data.parent}\n`;
}
content += `- **Created**: ${data.created}\n`;
content += `- **Updated**: ${data.updated}\n`;
// Description
content += `\n## Description\n\n`;
if (userContent) {
content += `${userContent}\n`;
} else {
content += `\n`;
}
// Tasks section
content += `\n## Tasks\n\n`;
if (json?.tasks && json.tasks.length > 0) {
for (const task of json.tasks) {
content += renderTaskItem(task);
}
} else {
content += `\n`;
content += `- [ ] \n`;
}
// Checklists (if provided)
if (json?.checklists && json.checklists.length > 0) {
for (const checklist of json.checklists) {
content += `\n### ${checklist.name}\n\n`;
if (checklist.items.length > 0) {
for (const item of checklist.items) {
content += renderTaskItem(item);
}
} else {
content += `- [ ] \n`;
}
}
}
// Acceptance Criteria (if provided)
if (json?.acceptance && json.acceptance.length > 0) {
content += `\n## Acceptance Criteria\n\n`;
for (const criteria of json.acceptance) {
content += `- [ ] ${criteria}\n`;
}
}
// Links (if provided)
if (json?.links && json.links.length > 0) {
content += `\n## Links\n\n`;
for (const link of json.links) {
content += `- ${link}\n`;
}
}
// Notes section
content += `\n## Notes\n\n`;
if (json?.notes) {
content += `${json.notes}\n`;
} else {
content += `\n`;
}
// Progress Log
content += `\n## Progress\n\n`;
content += `| Date | Update |\n`;
content += `|------|--------|\n`;
content += `| ${data.created} | Created |\n`;
return content;
}
// Generate JSON content
function generateJson(data: TodoData): string {
return JSON.stringify(data, null, 2);
}
// Update index file
function updateIndex(data: TodoData, filename: string): void {
const indexPath = path.join(implDir, "data", "indexes", "TODOS.md");
if (!fs.existsSync(indexPath)) {
console.error("Warning: TODOS.md index not found, skipping index update");
return;
}
let content = fs.readFileSync(indexPath, "utf-8");
// Find the "No todos yet" placeholder and replace it, or add new row
const newRow = `| ${data.id} | ${filename} | ${data.title} | ${data.status} | ${data.priority} |`;
if (content.includes("| - | - | No todos yet | - | - |")) {
content = content.replace("| - | - | No todos yet | - | - |", newRow);
} else {
// Find the end of the Active Todos table and add new row
const tableMatch = content.match(/(## Active Todos[\s\S]*?\|[-|]+\|[-|]+\|[-|]+\|[-|]+\|[-|]+\|)/);
if (tableMatch) {
const insertPoint = tableMatch.index! + tableMatch[0].length;
content = content.slice(0, insertPoint) + "\n" + newRow + content.slice(insertPoint);
}
}
// Update timestamp
content = content.replace(/\*Updated.*\*/, `*Updated: ${timestamp}*`);
fs.writeFileSync(indexPath, content);
}
// Main execution
async function main(): Promise {
console.log(`\nImplementation Todo`);
console.log(`===================\n`);
// Generate filename
const extension = format === "json" ? "json" : "md";
const filename = `${todoId}_${slug}.${extension}`;
const outputPath = path.join(outputDir, filename);
// Generate content
const content = format === "json" ? generateJson(todoData) : generateMarkdown(todoData);
// Write file
fs.writeFileSync(outputPath, content);
console.log(`Created: ${outputPath}`);
console.log(`\nTodo Details:`);
console.log(` ID: ${todoData.id}`);
console.log(` Title: ${todoData.title}`);
console.log(` Slug: ${todoData.slug}`);
console.log(` Status: ${todoData.status}`);
console.log(` Priority: ${todoData.priority}`);
if (todoData.assignee) {
console.log(` Assignee: ${todoData.assignee}`);
}
if (todoData.due) {
console.log(` Due: ${todoData.due}`);
}
if (todoData.tags.length > 0) {
console.log(` Tags: ${todoData.tags.join(", ")}`);
}
// Update index
if (!args["no-index"]) {
console.log(`\nUpdating TODOS.md index...`);
updateIndex(todoData, filename);
console.log(`Index updated.`);
}
console.log(`\n${"=".repeat(40)}`);
console.log(`\nTodo created successfully!`);
console.log(`File: .implementation/data/todos/${format}/${filename}`);
}
main().catch((error) => {
console.error(`Error: ${error.message}`);
process.exit(1);
});