import type { ListScope, TaskOperation, TaskPriority, TaskStatus } from "./tasks";
export type ParsedTasksCommand =
| { type: "interactive" }
| { type: "help"; text: string }
| { type: "operation"; operation: TaskOperation }
| { type: "error"; message: string };
export const TASKS_HELP = `Usage: /tasks [subcommand] [options]
Run /tasks without a subcommand to open the interactive task navigator.
Subcommands:
list [--archived|--all] List active tasks by default
create [options]
Create a task
show Show one task
update [options] Update title, status, priority, notes, or dependencies
status Set status: todo, in_progress, blocked, done, archived
complete Set status to done
reopen Set status to todo
block Set status to blocked
archive Set status to archived
delete Rejected; physical deletion is out of scope
Options:
--title New title for update
--status todo, in_progress, blocked, done, archived
--priority low, medium, high, urgent
--notes Task notes; empty string is allowed
--depends Task dependencies; use an empty value to clear
--help, -h Show this help
Examples:
/tasks create --priority high "Implement Telos"
/tasks list --all
/tasks update TSK-abc123ef --notes "Blocked on review"
/tasks complete TSK-abc123ef`;
export function parseTasksCommand(rawArgs: string): ParsedTasksCommand {
const tokens = tokenizeArgs(rawArgs.trim());
if (tokens.length === 0) return { type: "interactive" };
const [subcommand, ...rest] = tokens;
if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") return { type: "help", text: TASKS_HELP };
try {
switch (subcommand) {
case "list":
return { type: "operation", operation: parseList(rest) };
case "create":
case "add":
return { type: "operation", operation: parseCreate(rest) };
case "show":
case "view":
return { type: "operation", operation: { action: "show", id: onlyPositional(rest, "show ")[0] } };
case "update":
case "edit":
return { type: "operation", operation: parseUpdate(rest) };
case "status":
return { type: "operation", operation: parseStatus(rest) };
case "complete":
case "done":
case "reopen":
case "block":
case "archive":
case "delete":
return { type: "operation", operation: parseIdOnly(subcommand, rest) };
default:
return { type: "error", message: `Unknown /tasks subcommand: ${subcommand}` };
}
} catch (error) {
return { type: "error", message: error instanceof Error ? error.message : String(error) };
}
}
export function tokenizeArgs(input: string): string[] {
const tokens: string[] = [];
let current = "";
let quote: "'" | '"' | undefined;
let escaping = false;
for (const char of input) {
if (escaping) {
current += char;
escaping = false;
continue;
}
if (char === "\\") {
escaping = true;
continue;
}
if (quote) {
if (char === quote) quote = undefined;
else current += char;
continue;
}
if (char === "'" || char === '"') {
quote = char;
continue;
}
if (/\s/.test(char)) {
if (current) {
tokens.push(current);
current = "";
}
continue;
}
current += char;
}
if (escaping) current += "\\";
if (quote) throw new Error("Unterminated quoted argument");
if (current) tokens.push(current);
return tokens;
}
function parseList(tokens: string[]): TaskOperation {
let scope: ListScope = "active";
for (const token of tokens) {
if (token === "--archived") scope = "archived";
else if (token === "--all") scope = "all";
else if (token === "--active") scope = "active";
else if (token === "--help" || token === "-h") throw new Error(TASKS_HELP);
else throw new Error(`Unknown list option: ${token}`);
}
return { action: "list", scope };
}
function parseCreate(tokens: string[]): TaskOperation {
const options = parseOptions(tokens, new Set(["--status", "--priority", "--notes", "--depends", "--dependencies"]));
if (options.positionals.length === 0) throw new Error("create requires a title");
const operation: TaskOperation = { action: "create", title: options.positionals.join(" ") };
const status = options.values.get("--status");
const priority = options.values.get("--priority");
const notes = options.values.get("--notes");
const dependencies = options.values.get("--depends") ?? options.values.get("--dependencies");
if (status !== undefined) operation.status = status as TaskStatus;
if (priority !== undefined) operation.priority = priority as TaskPriority;
if (notes !== undefined) operation.notes = notes;
if (dependencies !== undefined) operation.dependencies = parseDependencies(dependencies);
return operation;
}
function parseUpdate(tokens: string[]): TaskOperation {
if (tokens.length === 0) throw new Error("update requires an id");
const [id, ...rest] = tokens;
const options = parseOptions(rest, new Set(["--title", "--status", "--priority", "--notes", "--depends", "--dependencies"]));
if (options.positionals.length > 0) throw new Error(`Unexpected update argument: ${options.positionals[0]}`);
const operation: TaskOperation = { action: "update", id };
const title = options.values.get("--title");
const status = options.values.get("--status");
const priority = options.values.get("--priority");
const notes = options.values.get("--notes");
const dependencies = options.values.get("--depends") ?? options.values.get("--dependencies");
if (title !== undefined) operation.title = title;
if (status !== undefined) operation.status = status as TaskStatus;
if (priority !== undefined) operation.priority = priority as TaskPriority;
if (notes !== undefined) operation.notes = notes;
if (dependencies !== undefined) operation.dependencies = parseDependencies(dependencies);
return operation;
}
function parseStatus(tokens: string[]): TaskOperation {
const positionals = onlyPositional(tokens, "status ");
if (positionals.length !== 2) throw new Error("status requires an id and status");
return { action: "status", id: positionals[0], status: positionals[1] as TaskStatus };
}
function parseIdOnly(action: string, tokens: string[]): TaskOperation {
const positionals = onlyPositional(tokens, `${action} `);
if (positionals.length !== 1) throw new Error(`${action} requires exactly one id`);
const normalized = action === "done" ? "complete" : action;
return { action: normalized as "complete" | "reopen" | "block" | "archive" | "delete", id: positionals[0] };
}
function onlyPositional(tokens: string[], usage: string): string[] {
for (const token of tokens) {
if (token.startsWith("-")) throw new Error(`${usage} does not accept option ${token}`);
}
return tokens;
}
function parseDependencies(value: string): string[] {
return value
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
}
function parseOptions(tokens: string[], allowed: Set): { values: Map; positionals: string[] } {
const values = new Map();
const positionals: string[] = [];
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token === "--") {
positionals.push(...tokens.slice(i + 1));
break;
}
if (!token.startsWith("--")) {
positionals.push(token);
continue;
}
const eq = token.indexOf("=");
const name = eq === -1 ? token : token.slice(0, eq);
if (!allowed.has(name)) throw new Error(`Unknown option: ${name}`);
const value = eq === -1 ? tokens[++i] : token.slice(eq + 1);
if (value === undefined) throw new Error(`${name} requires a value`);
values.set(name, value);
}
return { values, positionals };
}