feat: initial release
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
out/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
package-lock.json
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 80,
|
||||||
|
"proseWrap": "always",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.md",
|
||||||
|
"options": {
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+13
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+15
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "compile",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
},
|
||||||
|
"problemMatcher": "$tsc",
|
||||||
|
"label": "npm: compile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.vscode/**
|
||||||
|
.vscode-test/**
|
||||||
|
src/**
|
||||||
|
node_modules/**
|
||||||
|
.gitignore
|
||||||
|
tsconfig.json
|
||||||
|
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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="4" y="6" width="24" height="20" rx="3" stroke="#888" stroke-width="2" fill="none"/>
|
||||||
|
<line x1="10" y1="13" x2="22" y2="13" stroke="#888" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<line x1="10" y1="18" x2="18" y2="18" stroke="#888" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<line x1="10" y1="23" x2="20" y2="23" stroke="#888" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<circle cx="25" cy="9" r="4" fill="#4CAF50"/>
|
||||||
|
<line x1="23.5" y1="9" x2="24.5" y2="10" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
<line x1="24.5" y1="10" x2="26.5" y2="7.5" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 740 B |
Generated
+208
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+112
@@ -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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user