feat: initial release
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import * as vscode from "vscode";
|
||||
import { ConfigStore } from "./storage/configStore";
|
||||
import { TaskStore } from "./storage/taskStore";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const taskStore = new TaskStore();
|
||||
const configStore = new ConfigStore();
|
||||
|
||||
const statusBarItem = vscode.window.createStatusBarItem(
|
||||
vscode.StatusBarAlignment.Left,
|
||||
);
|
||||
statusBarItem.text = "$(checklist) Tasks";
|
||||
statusBarItem.command = "projectTasks.openDashboard";
|
||||
statusBarItem.tooltip = "Project Tasks — Open Dashboard";
|
||||
statusBarItem.show();
|
||||
context.subscriptions.push(statusBarItem);
|
||||
|
||||
const createTaskCommand = vscode.commands.registerCommand(
|
||||
"projectTasks.createTask",
|
||||
async () => {
|
||||
const title = await vscode.window.showInputBox({
|
||||
prompt: "Task title",
|
||||
placeHolder: "Enter task title...",
|
||||
});
|
||||
if (!title) return;
|
||||
|
||||
const projectPath = configStore.getProjectTaskPath();
|
||||
if (!projectPath) {
|
||||
vscode.window.showErrorMessage("Open a workspace folder first");
|
||||
return;
|
||||
}
|
||||
|
||||
await taskStore.createTask(projectPath, {
|
||||
title,
|
||||
description: "",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
tags: [],
|
||||
assignee: "",
|
||||
});
|
||||
vscode.window.showInformationMessage(`Task created: ${title}`);
|
||||
},
|
||||
);
|
||||
|
||||
const openDashboardCommand = vscode.commands.registerCommand(
|
||||
"projectTasks.openDashboard",
|
||||
() => {
|
||||
vscode.window.showInformationMessage("Dashboard coming soon!");
|
||||
},
|
||||
);
|
||||
|
||||
context.subscriptions.push(createTaskCommand, openDashboardCommand);
|
||||
}
|
||||
|
||||
export function deactivate() {}
|
||||
@@ -0,0 +1,14 @@
|
||||
export type TaskStatus = "todo" | "in-progress" | "done" | "cancelled";
|
||||
export type TaskPriority = "low" | "medium" | "high" | "critical";
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: TaskStatus;
|
||||
priority: TaskPriority;
|
||||
tags: string[];
|
||||
assignee: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as path from "path";
|
||||
import * as vscode from "vscode";
|
||||
|
||||
export class ConfigStore {
|
||||
getProjectTaskPath(): string | undefined {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!workspaceRoot) return undefined;
|
||||
|
||||
const config = vscode.workspace.getConfiguration("projectTasks");
|
||||
const relativePath = config.get<string>("projectPath") ?? ".vscode/tasks";
|
||||
return path.join(workspaceRoot, relativePath);
|
||||
}
|
||||
|
||||
getGlobalTaskPath(): string | undefined {
|
||||
const config = vscode.workspace.getConfiguration("projectTasks");
|
||||
const globalPath = config.get<string>("globalPath");
|
||||
return globalPath || undefined;
|
||||
}
|
||||
|
||||
getFileExtension(): string {
|
||||
const config = vscode.workspace.getConfiguration("projectTasks");
|
||||
return config.get<string>("fileExtension") ?? ".task.md";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { Task } from "../model/task";
|
||||
import { parseTaskFile, serializeTask } from "../utils/markdown";
|
||||
import { generateId } from "../utils/uuid";
|
||||
|
||||
export class TaskStore {
|
||||
private fileExtension: string;
|
||||
|
||||
constructor() {
|
||||
const config = vscode.workspace.getConfiguration("projectTasks");
|
||||
this.fileExtension = config.get<string>("fileExtension") ?? ".task.md";
|
||||
}
|
||||
|
||||
getProjectTaskPath(): string | undefined {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!workspaceRoot) return undefined;
|
||||
|
||||
const config = vscode.workspace.getConfiguration("projectTasks");
|
||||
const relativePath = config.get<string>("projectPath") ?? ".vscode/tasks";
|
||||
return path.join(workspaceRoot, relativePath);
|
||||
}
|
||||
|
||||
getGlobalTaskPath(): string | undefined {
|
||||
const config = vscode.workspace.getConfiguration("projectTasks");
|
||||
const globalPath = config.get<string>("globalPath");
|
||||
return globalPath || undefined;
|
||||
}
|
||||
|
||||
async listTasks(folderPath: string): Promise<Task[]> {
|
||||
try {
|
||||
await fs.promises.mkdir(folderPath, { recursive: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = await fs.promises.readdir(folderPath, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
const files = entries.filter(
|
||||
(e) => e.isFile() && e.name.endsWith(this.fileExtension),
|
||||
);
|
||||
|
||||
const tasks: Task[] = [];
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = await fs.promises.readFile(
|
||||
path.join(folderPath, file.name),
|
||||
"utf-8",
|
||||
);
|
||||
tasks.push(parseTaskFile(content));
|
||||
} catch (err) {
|
||||
console.error(`Failed to parse task file ${file.name}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
return tasks.sort(
|
||||
(a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime(),
|
||||
);
|
||||
}
|
||||
|
||||
async getTask(folderPath: string, taskId: string): Promise<Task | undefined> {
|
||||
const tasks = await this.listTasks(folderPath);
|
||||
return tasks.find((t) => t.id === taskId);
|
||||
}
|
||||
|
||||
async createTask(
|
||||
folderPath: string,
|
||||
task: Omit<Task, "id" | "created" | "updated">,
|
||||
): Promise<Task> {
|
||||
const now = new Date().toISOString();
|
||||
const newTask: Task = {
|
||||
...task,
|
||||
id: generateId(),
|
||||
created: now,
|
||||
updated: now,
|
||||
};
|
||||
|
||||
await fs.promises.mkdir(folderPath, { recursive: true });
|
||||
const filePath = this.taskFilePath(folderPath, newTask);
|
||||
await fs.promises.writeFile(filePath, serializeTask(newTask), "utf-8");
|
||||
|
||||
return newTask;
|
||||
}
|
||||
|
||||
async updateTask(folderPath: string, task: Task): Promise<void> {
|
||||
const updatedTask = { ...task, updated: new Date().toISOString() };
|
||||
const filePath = this.taskFilePath(folderPath, updatedTask);
|
||||
await fs.promises.writeFile(filePath, serializeTask(updatedTask), "utf-8");
|
||||
}
|
||||
|
||||
async deleteTask(folderPath: string, taskId: string): Promise<void> {
|
||||
const task = await this.getTask(folderPath, taskId);
|
||||
if (!task) return;
|
||||
|
||||
const filePath = this.taskFilePath(folderPath, task);
|
||||
try {
|
||||
await fs.promises.unlink(filePath);
|
||||
} catch (err) {
|
||||
console.error(`Failed to delete task file:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
private taskFilePath(folderPath: string, task: Task): string {
|
||||
const safeName = task.title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u0400-\u04FF]+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.slice(0, 80);
|
||||
|
||||
return path.join(folderPath, `${safeName}${this.fileExtension}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import matter from "gray-matter";
|
||||
import { Task } from "../model/task";
|
||||
|
||||
export function parseTaskFile(content: string): Task {
|
||||
const parsed = matter(content);
|
||||
return {
|
||||
id: parsed.data.id,
|
||||
title: parsed.data.title,
|
||||
description: parsed.content.trim(),
|
||||
status: parsed.data.status ?? "todo",
|
||||
priority: parsed.data.priority ?? "medium",
|
||||
tags: parsed.data.tags ?? [],
|
||||
assignee: parsed.data.assignee ?? "",
|
||||
created: parsed.data.created ?? new Date().toISOString(),
|
||||
updated: parsed.data.updated ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeTask(task: Task): string {
|
||||
const frontmatter: Record<string, unknown> = {
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
status: task.status,
|
||||
priority: task.priority,
|
||||
tags: task.tags,
|
||||
assignee: task.assignee,
|
||||
created: task.created,
|
||||
updated: task.updated,
|
||||
};
|
||||
|
||||
const content = matter.stringify(task.description, frontmatter);
|
||||
return content;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export function generateId(): string {
|
||||
return uuidv4();
|
||||
}
|
||||
Reference in New Issue
Block a user