From 66ddc3762fba32e5cd3bd48645f80d9c948e94d9 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sun, 12 Jul 2026 16:46:16 +0500 Subject: [PATCH] feat: initial release --- .gitignore | 2 + .prettierignore | 1 + .prettierrc | 16 +++ .vscode/launch.json | 13 +++ .vscode/tasks.json | 15 +++ .vscodeignore | 7 ++ docs/PLAN.md | 78 ++++++++++++++ media/icon.svg | 9 ++ package-lock.json | 208 +++++++++++++++++++++++++++++++++++++ package.json | 112 ++++++++++++++++++++ src/extension.ts | 55 ++++++++++ src/model/task.ts | 14 +++ src/storage/configStore.ts | 24 +++++ src/storage/taskStore.ts | 114 ++++++++++++++++++++ src/utils/markdown.ts | 33 ++++++ src/utils/uuid.ts | 5 + tsconfig.json | 19 ++++ 17 files changed, 725 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 .vscodeignore create mode 100644 docs/PLAN.md create mode 100644 media/icon.svg create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/extension.ts create mode 100644 src/model/task.ts create mode 100644 src/storage/configStore.ts create mode 100644 src/storage/taskStore.ts create mode 100644 src/utils/markdown.ts create mode 100644 src/utils/uuid.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72aae85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +out/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..483a9c4 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +package-lock.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a4dfee2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,16 @@ +{ + "useTabs": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 80, + "proseWrap": "always", + "endOfLine": "lf", + "overrides": [ + { + "files": "*.md", + "options": { + "useTabs": false + } + } + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5e64e00 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "npm: compile" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..3793125 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,15 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "compile", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$tsc", + "label": "npm: compile" + } + ] +} diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..1b2e58f --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,7 @@ +.vscode/** +.vscode-test/** +src/** +node_modules/** +.gitignore +tsconfig.json +PLAN.md diff --git a/docs/PLAN.md b/docs/PLAN.md new file mode 100644 index 0000000..1ba8a5b --- /dev/null +++ b/docs/PLAN.md @@ -0,0 +1,78 @@ +# project-tasks — VS Code Extension + +## Хранение + +Формат: Markdown + YAML frontmatter (один `.task.md` файл на задачу) + +```markdown +--- +title: "Пофиксить баг логина" +status: todo # todo | in-progress | done | cancelled +priority: high # low | medium | high | critical +tags: [bug, frontend] +assignee: "" +created: 2026-07-12T10:00:00Z +updated: 2026-07-12T10:00:00Z +--- + +Описание задачи в markdown... +``` + +**Структура папок:** + +- `.vscode/tasks/` — проектные задачи +- `~/project-tasks/` — глобальные задачи (настраивается) + +Оба пути настраиваются через VS Code settings. + +## UI в основном окне (Split-редактор) + +Через **Custom Editor** + **WebView**: + +1. **Activity Bar** — своя вкладка с Task Dashboard +2. **Task Dashboard** — WebView в editor area со split layout: + - Левая панель — список задач с фильтрацией/группировкой + - Правая панель — содержимое выбранной задачи (редактируемый markdown) +3. **TreeView в Explorer** — быстрый просмотр + +## Архитектура кода + +```txt +src/ +├── extension.ts # Активация, регистрация +├── storage/ +│ ├── taskStore.ts # CRUD для .task.md (gray-matter парсинг) +│ └── configStore.ts # Настройки +├── model/ +│ └── task.ts # Интерфейс Task + типы +├── views/ +│ ├── taskTreeProvider.ts # TreeView в sidebar +│ ├── taskListProvider.ts # Data provider для WebView +│ └── taskDashboardPanel.ts # WebView split layout +├── commands/ +│ ├── createTask.ts +│ ├── openTask.ts +│ ├── deleteTask.ts +│ └── changeStatus.ts +└── utils/ + ├── uuid.ts + └── markdown.ts +``` + +## План реализации + +| Фаза | Что делаем | +| ------------------------ | -------------------------------------------------------------- | +| **1. Scaffold** | `yo code`, TypeScript, настройка package.json | +| **2. Storage** | TaskStore — чтение `.task.md`, парсинг через gray-matter, CRUD | +| **3. TreeView** | Боковая панель со списком задач по статусу | +| **4. Команды** | createTask, deleteTask, changeStatus, openTask | +| **5. Task Dashboard** | WebView split layout в editor area | +| **6. Глобальные задачи** | Второй storage (глобальная папка) | +| **7. Kanban** | Drag-n-drop доска | +| **8. Публикация** | vsce publish | + +## Зависимости + +- `gray-matter` — парсинг YAML frontmatter +- `uuid` — генерация ID diff --git a/media/icon.svg b/media/icon.svg new file mode 100644 index 0000000..63ddc9f --- /dev/null +++ b/media/icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d122f17 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,208 @@ +{ + "name": "xboct-flow", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "xboct-flow", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "gray-matter": "^4.0.3", + "prettier": "^3.9.5", + "uuid": "^11.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/uuid": "^10.0.0", + "@types/vscode": "^1.96.0", + "typescript": "^5.7.0" + }, + "engines": { + "vscode": "^1.96.0" + } + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-yaml": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d420e8b --- /dev/null +++ b/package.json @@ -0,0 +1,112 @@ +{ + "name": "xboct-flow", + "displayName": "XBOCT flow", + "description": "Local-first task management for solo developers and small teams", + "version": "0.0.1", + "publisher": "", + "license": "MIT", + "engines": { + "vscode": "^1.96.0" + }, + "categories": [ + "Other" + ], + "keywords": [ + "tasks", + "todo", + "issues", + "kanban", + "project-management" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "projectTasks.createTask", + "title": "Project Tasks: Create Task" + }, + { + "command": "projectTasks.openDashboard", + "title": "Project Tasks: Open Dashboard" + } + ], + "configuration": { + "title": "Project Tasks", + "properties": { + "projectTasks.projectPath": { + "type": "string", + "default": ".vscode/tasks", + "description": "Path to project-level tasks folder (relative to workspace root)" + }, + "projectTasks.globalPath": { + "type": "string", + "default": "", + "description": "Path to global tasks folder (absolute path, shared across projects)" + }, + "projectTasks.fileExtension": { + "type": "string", + "default": ".task.md", + "enum": [ + ".task.md", + ".md", + ".todo.md" + ], + "description": "File extension for task files" + } + } + }, + "viewsContainers": { + "activitybar": [ + { + "id": "projectTasks", + "title": "Project Tasks", + "icon": "media/icon.svg" + } + ] + }, + "views": { + "projectTasks": [ + { + "type": "tree", + "id": "projectTasks.tasks", + "name": "Tasks", + "icon": "media/icon.svg" + } + ] + }, + "menus": { + "view/title": [ + { + "command": "projectTasks.createTask", + "when": "view == projectTasks.tasks", + "group": "navigation" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "lint": "tsc --noEmit", + "format": "prettier --write ." + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/uuid": "^10.0.0", + "@types/vscode": "^1.96.0", + "typescript": "^5.7.0" + }, + "dependencies": { + "gray-matter": "^4.0.3", + "prettier": "^3.9.5", + "uuid": "^11.0.0" + }, + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..e866325 --- /dev/null +++ b/src/extension.ts @@ -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() {} diff --git a/src/model/task.ts b/src/model/task.ts new file mode 100644 index 0000000..2a329e2 --- /dev/null +++ b/src/model/task.ts @@ -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; +} diff --git a/src/storage/configStore.ts b/src/storage/configStore.ts new file mode 100644 index 0000000..0c726e4 --- /dev/null +++ b/src/storage/configStore.ts @@ -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("projectPath") ?? ".vscode/tasks"; + return path.join(workspaceRoot, relativePath); + } + + getGlobalTaskPath(): string | undefined { + const config = vscode.workspace.getConfiguration("projectTasks"); + const globalPath = config.get("globalPath"); + return globalPath || undefined; + } + + getFileExtension(): string { + const config = vscode.workspace.getConfiguration("projectTasks"); + return config.get("fileExtension") ?? ".task.md"; + } +} diff --git a/src/storage/taskStore.ts b/src/storage/taskStore.ts new file mode 100644 index 0000000..6044865 --- /dev/null +++ b/src/storage/taskStore.ts @@ -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("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("projectPath") ?? ".vscode/tasks"; + return path.join(workspaceRoot, relativePath); + } + + getGlobalTaskPath(): string | undefined { + const config = vscode.workspace.getConfiguration("projectTasks"); + const globalPath = config.get("globalPath"); + return globalPath || undefined; + } + + async listTasks(folderPath: string): Promise { + 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 { + const tasks = await this.listTasks(folderPath); + return tasks.find((t) => t.id === taskId); + } + + async createTask( + folderPath: string, + task: Omit, + ): Promise { + 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 { + 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 { + 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}`); + } +} diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts new file mode 100644 index 0000000..1dc6895 --- /dev/null +++ b/src/utils/markdown.ts @@ -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 = { + 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; +} diff --git a/src/utils/uuid.ts b/src/utils/uuid.ts new file mode 100644 index 0000000..bea3428 --- /dev/null +++ b/src/utils/uuid.ts @@ -0,0 +1,5 @@ +import { v4 as uuidv4 } from "uuid"; + +export function generateId(): string { + return uuidv4(); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..552c1db --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2022", + "outDir": "out", + "rootDir": "src", + "lib": ["ES2022"], + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "out", ".vscode-test"] +}