@vibeflow-tools/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +141 -0
  2. package/dist/chunk-43MUGU6Z.js +67 -0
  3. package/dist/chunk-4Y62J4KR.js +438 -0
  4. package/dist/chunk-6ABTC7IA.js +482 -0
  5. package/dist/chunk-7K2VHAWF.js +569 -0
  6. package/dist/chunk-F57OXWVU.js +33 -0
  7. package/dist/chunk-I2OSZY66.js +803 -0
  8. package/dist/chunk-JKC6MUZ4.js +558 -0
  9. package/dist/chunk-KJ5LYG6A.js +2 -0
  10. package/dist/chunk-KKBXVPTJ.js +2 -0
  11. package/dist/chunk-LK3IGDR6.js +438 -0
  12. package/dist/chunk-MKOKIWTN.js +569 -0
  13. package/dist/chunk-MW4OS3IK.js +438 -0
  14. package/dist/chunk-NRPOXSY6.js +568 -0
  15. package/dist/chunk-NUM3G22J.js +569 -0
  16. package/dist/chunk-O564QSMS.js +569 -0
  17. package/dist/chunk-OAGEPYIT.js +535 -0
  18. package/dist/chunk-PHBHAIHA.js +33 -0
  19. package/dist/chunk-PK737AKV.js +560 -0
  20. package/dist/chunk-TWAAPROG.js +822 -0
  21. package/dist/chunk-XF2CNPE4.js +803 -0
  22. package/dist/cli/chunk-43MUGU6Z.js +67 -0
  23. package/dist/cli/chunk-4Y62J4KR.js +438 -0
  24. package/dist/cli/chunk-6ABTC7IA.js +482 -0
  25. package/dist/cli/chunk-7K2VHAWF.js +569 -0
  26. package/dist/cli/chunk-F57OXWVU.js +33 -0
  27. package/dist/cli/chunk-I2OSZY66.js +803 -0
  28. package/dist/cli/chunk-JKC6MUZ4.js +558 -0
  29. package/dist/cli/chunk-KJ5LYG6A.js +2 -0
  30. package/dist/cli/chunk-KKBXVPTJ.js +2 -0
  31. package/dist/cli/chunk-LK3IGDR6.js +438 -0
  32. package/dist/cli/chunk-MKOKIWTN.js +569 -0
  33. package/dist/cli/chunk-MW4OS3IK.js +438 -0
  34. package/dist/cli/chunk-NRPOXSY6.js +568 -0
  35. package/dist/cli/chunk-NUM3G22J.js +569 -0
  36. package/dist/cli/chunk-O564QSMS.js +569 -0
  37. package/dist/cli/chunk-OAGEPYIT.js +535 -0
  38. package/dist/cli/chunk-PHBHAIHA.js +33 -0
  39. package/dist/cli/chunk-PK737AKV.js +560 -0
  40. package/dist/cli/chunk-TWAAPROG.js +822 -0
  41. package/dist/cli/chunk-XF2CNPE4.js +803 -0
  42. package/dist/cli/files-27OYPA7W.js +20 -0
  43. package/dist/cli/files-2TK74THO.js +22 -0
  44. package/dist/cli/files-7275M2PW.js +20 -0
  45. package/dist/cli/files-D3YPV7QT.js +20 -0
  46. package/dist/cli/files-FER4UZ4X.js +22 -0
  47. package/dist/cli/files-H4FRDKJV.js +22 -0
  48. package/dist/cli/files-IJZVMROA.js +22 -0
  49. package/dist/cli/files-KH3UEFN7.js +20 -0
  50. package/dist/cli/files-LTDT5ZFT.js +22 -0
  51. package/dist/cli/files-OYO6A6MZ.js +22 -0
  52. package/dist/cli/files-R6QHQBH4.js +22 -0
  53. package/dist/cli/files-UCALOYWZ.js +22 -0
  54. package/dist/cli/files-UWZP7P6B.js +2 -0
  55. package/dist/cli/files-VIDLQM7Y.js +20 -0
  56. package/dist/cli/files-X2RDLF3W.js +22 -0
  57. package/dist/cli/files-XVDNOAZB.js +22 -0
  58. package/dist/cli/index.d.ts +2 -0
  59. package/dist/cli/index.js +29252 -0
  60. package/dist/cli/workspace-KGOQ67YV.js +2 -0
  61. package/dist/cli/workspace-NB6BACZA.js +12 -0
  62. package/dist/cli/workspace-X2NGGGTQ.js +12 -0
  63. package/dist/cli/workspace-X4QXECQQ.js +12 -0
  64. package/dist/client/kanban-browser.js +96 -0
  65. package/dist/client/kanban.css +2 -0
  66. package/dist/client/overlay-browser.js +1635 -0
  67. package/dist/files-27OYPA7W.js +20 -0
  68. package/dist/files-2TK74THO.js +22 -0
  69. package/dist/files-7275M2PW.js +20 -0
  70. package/dist/files-D3YPV7QT.js +20 -0
  71. package/dist/files-FER4UZ4X.js +22 -0
  72. package/dist/files-H4FRDKJV.js +22 -0
  73. package/dist/files-IJZVMROA.js +22 -0
  74. package/dist/files-KH3UEFN7.js +20 -0
  75. package/dist/files-LTDT5ZFT.js +22 -0
  76. package/dist/files-OYO6A6MZ.js +22 -0
  77. package/dist/files-R6QHQBH4.js +22 -0
  78. package/dist/files-UCALOYWZ.js +22 -0
  79. package/dist/files-UWZP7P6B.js +2 -0
  80. package/dist/files-VIDLQM7Y.js +20 -0
  81. package/dist/files-X2RDLF3W.js +22 -0
  82. package/dist/files-XVDNOAZB.js +22 -0
  83. package/dist/index.d.ts +2 -0
  84. package/dist/index.js +29252 -0
  85. package/dist/overlay-host.css +14 -0
  86. package/dist/overlay.css +1530 -0
  87. package/dist/workspace-KGOQ67YV.js +2 -0
  88. package/dist/workspace-NB6BACZA.js +12 -0
  89. package/dist/workspace-X2NGGGTQ.js +12 -0
  90. package/dist/workspace-X4QXECQQ.js +12 -0
  91. package/package.json +92 -0
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # vibeflow
2
+
3
+ > **Feedback that becomes code.**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/vibeflow?color=blue)](https://www.npmjs.com/package/vibeflow)
6
+ [![License](https://img.shields.io/badge/license-Apache--2.0-green)](https://www.apache.org/licenses/LICENSE-2.0)
7
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
8
+
9
+ Annotate anything in the browser, hand structured context to your AI coding agent, ship. No Slack threads. No tickets nobody reads. No switching context.
10
+
11
+ ```bash
12
+ npm install -g vibeflow
13
+ npx vibeflow kanban
14
+ ```
15
+
16
+ ## Website
17
+
18
+ - Main website: https://www.vibeflow.tools/
19
+ - Tutorials: https://www.vibeflow.tools/tutorials.html
20
+
21
+ ---
22
+
23
+ ## The problem
24
+
25
+ You review your AI agent's output in the browser. You spot issues. Then the hard part: re-explaining every little thing in plain text, hoping the agent understands where to look and what's wrong.
26
+
27
+ - **"Just sent you a Slack message about it"** — context-switching costs 23 minutes to recover. The fix takes 2 minutes.
28
+ - **"Here's a screenshot, the thing on the left"** — screenshots without structure become detective work. Your agent needs selectors, not JPEGs.
29
+ - **"I described it to the AI but it got confused"** — prose descriptions lose precision. Agents work best with structured, reproducible context.
30
+
31
+ **Vibeflow eliminates the guessing loop.**
32
+
33
+ ---
34
+
35
+ ## How it works
36
+
37
+ Three steps. Zero ceremony.
38
+
39
+ ### 1. Serve
40
+
41
+ One command starts a local server that injects an annotation overlay into any HTML file or add overlay script to the live app.
42
+
43
+ ```bash
44
+ vibeflow serve ./my-project
45
+ ```
46
+
47
+ ### 2. Annotate
48
+
49
+ Open the browser. Click any element, write a quick note, set priority. Every annotation becomes a task stored as JSON in `.vibeflow/` — versioned in git, visible in the Kanban board.
50
+
51
+ ```bash
52
+ vibeflow kanban # open the Kanban board
53
+ ```
54
+
55
+ ### 3. Hand to your agent
56
+
57
+ Paste the task output into GitHub Copilot, Claude, Cursor, or any AI tool. The agent receives the CSS selector, source file, line number, and your note — no manual explanation needed.
58
+
59
+ ```bash
60
+ vibeflow tasks --status todo
61
+ ```
62
+
63
+ > **The magic prompt for your agent:**
64
+ > *"Get new tasks from `vibeflow tasks --status todo` and implement them."*
65
+
66
+ ---
67
+
68
+ ## Commands
69
+
70
+ ### Serving
71
+
72
+ | Command | Description |
73
+ |---------|-------------|
74
+ | `vibeflow serve [target]` | Serve an HTML folder with the annotation overlay injected |
75
+ | `vibeflow kanban [dir]` | Start the server and open the Kanban board in your browser |
76
+
77
+ ### Tasks
78
+
79
+ | Command | Description |
80
+ |---------|-------------|
81
+ | `vibeflow tasks` | List all tasks |
82
+ | `vibeflow tasks --status todo` | Filter to open tasks |
83
+ | `vibeflow tasks --type Bug` | Filter by type (Task, Bug, Feature, Enhancement, Research) |
84
+ | `vibeflow tasks --get <id>` | Full task detail with comments, file attachments, and linked commits |
85
+ | `vibeflow tasks --add --title "..." --description "..."` | Create a new task |
86
+ | `vibeflow tasks --edit <id> --set-status review --comment "..."` | Update a task status and add an implementation report |
87
+ | `vibeflow tasks --json` | Machine-readable JSON output |
88
+
89
+ **Task statuses:** backlog → todo → in-progress → review → done
90
+
91
+ ---
92
+
93
+ ## Features
94
+
95
+ | Feature | Description |
96
+ |---------|-------------|
97
+ | **Local-first** | Runs entirely on your machine. Tasks in `.vibeflow/` — committed to git, readable by any tool |
98
+ | **Agent-ready** | Works with GitHub Copilot, Claude, Cursor, Windsurf — any tool that accepts a prompt |
99
+ | **Click-to-annotate** | Captures exact CSS selector, URL, and source location automatically |
100
+ | **Kanban board** | Full task manager with backlog, in-progress, review, done — view in browser or drive from CLI |
101
+ | **Screenshots** | Paste a screenshot onto any task, or let the tool capture it automatically |
102
+ | **Keyboard shortcut** | `Alt+A` to toggle annotation mode |
103
+ | **Error recording** | Captures recent console errors into bug reports |
104
+ | **Offline** | Works 100% locally, no account needed |
105
+
106
+ ---
107
+
108
+ ## Quick Start
109
+
110
+ ```bash
111
+ # Open the Kanban board
112
+ npx vibeflow kanban
113
+
114
+ # Tell your agent to implement open tasks
115
+ npx vibeflow tasks --status todo
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Agent workflow
121
+
122
+ When running `vibeflow tasks`, the CLI prints agent instructions automatically. Key rules:
123
+
124
+ 1. **Claim first** — set status to `in-progress` before reading or planning
125
+ 2. **Never set `done`** — use `review`; only humans mark tasks done
126
+ 3. **Never edit `.vibeflow/` files directly** — use CLI commands only
127
+ 4. **Research tasks** — attach a `.md` report before marking review; never generate code
128
+ 5. **Bug tasks** — include error logs and stack traces in commit comments
129
+
130
+ ---
131
+
132
+ ## Requirements
133
+
134
+ - **Node.js 22+** — install from [nodejs.org](https://nodejs.org)
135
+ - **React in dev mode (recommended)** — for component names, source files, and line numbers. Run in `NODE_ENV=development` (the default for `next dev`).
136
+
137
+ ---
138
+
139
+ ## License
140
+
141
+ [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
9
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
10
+ }) : x)(function(x) {
11
+ if (typeof require !== "undefined") return require.apply(this, arguments);
12
+ throw Error('Dynamic require of "' + x + '" is not supported');
13
+ });
14
+ var __commonJS = (cb, mod) => function __require2() {
15
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
16
+ };
17
+ var __copyProps = (to, from, except, desc) => {
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (let key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key) && key !== except)
21
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
26
+ // If the importer is in node compatibility mode or this is not an ESM
27
+ // file that has been converted to a CommonJS file using a Babel-
28
+ // compatible transform (i.e. "__esModule" has not been set), then set
29
+ // "default" to the CommonJS "module.exports" for node compatibility.
30
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
31
+ mod
32
+ ));
33
+
34
+ // src/auth/workspace.ts
35
+ import fs from "fs/promises";
36
+ import os from "os";
37
+ import path from "path";
38
+ var SPOTA_DIR = path.join(os.homedir(), ".spota");
39
+ var WORKSPACE_PATH = path.join(SPOTA_DIR, "workspace");
40
+ async function readWorkspace() {
41
+ try {
42
+ const content = await fs.readFile(WORKSPACE_PATH, "utf8");
43
+ return JSON.parse(content);
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ async function writeWorkspace(workspace) {
49
+ await fs.mkdir(SPOTA_DIR, { recursive: true });
50
+ await fs.writeFile(WORKSPACE_PATH, JSON.stringify(workspace, null, 2), { mode: 384 });
51
+ }
52
+ async function deleteWorkspace() {
53
+ try {
54
+ await fs.unlink(WORKSPACE_PATH);
55
+ } catch {
56
+ }
57
+ }
58
+
59
+ export {
60
+ __require,
61
+ __commonJS,
62
+ __toESM,
63
+ readWorkspace,
64
+ writeWorkspace,
65
+ deleteWorkspace
66
+ };
67
+ //# sourceMappingURL=chunk-43MUGU6Z.js.map
@@ -0,0 +1,438 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/files.ts
4
+ import {
5
+ readdirSync as readdirSync2,
6
+ writeFileSync as writeFileSync2,
7
+ readFileSync as readFileSync2,
8
+ unlinkSync as unlinkSync2,
9
+ statSync,
10
+ mkdirSync as mkdirSync2,
11
+ existsSync as existsSync2
12
+ } from "fs";
13
+ import { join as join2, basename } from "path";
14
+
15
+ // src/core/types.ts
16
+ var PROTO_DIR = ".vibeflow";
17
+ var TASKS_DIR = "tasks";
18
+ var FILES_DIR = "tasks/files";
19
+ var SCREENSHOTS_DIR = "tasks/screenshots";
20
+ var CONFIG_FILE = "config.json";
21
+
22
+ // src/core/tasks.ts
23
+ import {
24
+ existsSync,
25
+ readFileSync,
26
+ writeFileSync,
27
+ mkdirSync,
28
+ readdirSync,
29
+ unlinkSync,
30
+ renameSync
31
+ } from "fs";
32
+ import { join, extname } from "path";
33
+ import { randomBytes } from "crypto";
34
+ var VALID_STATUSES = ["backlog", "todo", "in-progress", "review", "done"];
35
+ function generateTaskId() {
36
+ return randomBytes(15).toString("hex");
37
+ }
38
+ function getTasksDir(projectDir) {
39
+ return join(projectDir, PROTO_DIR, TASKS_DIR);
40
+ }
41
+ function migrateLegacyTaskAssetFolders(projectDir) {
42
+ const protoRoot = join(projectDir, PROTO_DIR);
43
+ const legacyFilesDir = join(protoRoot, "files");
44
+ const nextFilesDir = join(protoRoot, FILES_DIR);
45
+ const legacyScreenshotsDir = join(protoRoot, "screenshots");
46
+ const nextScreenshotsDir = join(protoRoot, SCREENSHOTS_DIR);
47
+ if (existsSync(legacyFilesDir)) {
48
+ mkdirSync(nextFilesDir, { recursive: true });
49
+ for (const entry of readdirSync(legacyFilesDir)) {
50
+ const from = join(legacyFilesDir, entry);
51
+ const to = join(nextFilesDir, entry);
52
+ if (existsSync(to)) continue;
53
+ try {
54
+ renameSync(from, to);
55
+ } catch {
56
+ }
57
+ }
58
+ }
59
+ if (existsSync(legacyScreenshotsDir)) {
60
+ mkdirSync(nextScreenshotsDir, { recursive: true });
61
+ for (const entry of readdirSync(legacyScreenshotsDir)) {
62
+ const from = join(legacyScreenshotsDir, entry);
63
+ const to = join(nextScreenshotsDir, entry);
64
+ if (existsSync(to)) continue;
65
+ try {
66
+ renameSync(from, to);
67
+ } catch {
68
+ }
69
+ }
70
+ }
71
+ }
72
+ function ensureTaskDirs(projectDir) {
73
+ migrateLegacyTaskAssetFolders(projectDir);
74
+ mkdirSync(getTasksDir(projectDir), { recursive: true });
75
+ mkdirSync(join(projectDir, PROTO_DIR, SCREENSHOTS_DIR), { recursive: true });
76
+ }
77
+ function getDateSubdir(isoDate) {
78
+ return isoDate.slice(0, 10);
79
+ }
80
+ function getTaskFilePath(projectDir, taskId, created) {
81
+ const tasksDir = getTasksDir(projectDir);
82
+ if (created) {
83
+ return join(tasksDir, getDateSubdir(created), `${taskId}.json`);
84
+ }
85
+ return join(tasksDir, `${taskId}.json`);
86
+ }
87
+ function findTaskFilePath(projectDir, taskId) {
88
+ const tasksDir = getTasksDir(projectDir);
89
+ if (!existsSync(tasksDir)) return null;
90
+ for (const entry of readdirSync(tasksDir, { withFileTypes: true })) {
91
+ if (entry.isDirectory()) {
92
+ const candidate = join(tasksDir, entry.name, `${taskId}.json`);
93
+ if (existsSync(candidate)) return candidate;
94
+ } else if (entry.name === `${taskId}.json`) {
95
+ return join(tasksDir, entry.name);
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ function normalizeTask(raw) {
101
+ const normalizedType = (() => {
102
+ if (typeof raw.type !== "string") return void 0;
103
+ const t = raw.type.trim();
104
+ if (!t || t === "[object Object]") return void 0;
105
+ return t;
106
+ })();
107
+ return {
108
+ id: String(raw.id ?? ""),
109
+ title: String(raw.title ?? "Untitled"),
110
+ description: String(raw.description ?? ""),
111
+ status: VALID_STATUSES.includes(raw.status) ? raw.status : "todo",
112
+ url: raw.url ? String(raw.url) : void 0,
113
+ selector: (() => {
114
+ const sel = String(raw.selector ?? "/");
115
+ if (raw.file && !raw.cssSelector && sel.startsWith(String(raw.file))) {
116
+ return raw.url ? String(raw.url) : "/";
117
+ }
118
+ return sel;
119
+ })(),
120
+ cssSelector: raw.cssSelector && String(raw.cssSelector) !== String(raw.selector ?? "/") ? String(raw.cssSelector) : void 0,
121
+ file: raw.file ? String(raw.file) : void 0,
122
+ line: raw.line != null ? Number(raw.line) : void 0,
123
+ col: raw.col != null ? Number(raw.col) : void 0,
124
+ component: raw.component ? String(raw.component) : void 0,
125
+ type: normalizedType,
126
+ priority: raw.priority ? String(raw.priority) : void 0,
127
+ ...raw.reportBack === true && { reportBack: true },
128
+ agent: raw.agent ? String(raw.agent) : void 0,
129
+ model: raw.model ? String(raw.model) : void 0,
130
+ author: raw.author ? String(raw.author) : void 0,
131
+ commit: raw.commit ? String(raw.commit) : void 0,
132
+ commits: Array.isArray(raw.commits) ? raw.commits.map((c) => ({
133
+ sha: String(c.sha ?? ""),
134
+ message: String(c.message ?? ""),
135
+ timestamp: String(c.timestamp ?? (/* @__PURE__ */ new Date()).toISOString())
136
+ })) : void 0,
137
+ created: String(raw.created ?? (/* @__PURE__ */ new Date()).toISOString()),
138
+ updated: raw.updated ? String(raw.updated) : void 0,
139
+ comments: Array.isArray(raw.comments) ? raw.comments.map((c) => ({
140
+ ...c,
141
+ // Normalize legacy 'content' field → 'text' (some older agent comments used 'content')
142
+ text: c.text ?? c.content ?? ""
143
+ })) : [],
144
+ files: Array.isArray(raw.files) ? raw.files.map((f) => {
145
+ if (typeof f === "string") {
146
+ return { name: f, addedAt: (/* @__PURE__ */ new Date()).toISOString() };
147
+ }
148
+ return {
149
+ name: String(f.name ?? ""),
150
+ addedAt: String(f.addedAt ?? (/* @__PURE__ */ new Date()).toISOString()),
151
+ linkedPath: f.linkedPath ? String(f.linkedPath) : void 0,
152
+ mimeType: f.mimeType ? String(f.mimeType) : void 0
153
+ };
154
+ }).filter((f) => f.name) : [],
155
+ screenshot: raw.screenshot ? String(raw.screenshot) : void 0,
156
+ annotatedElementText: raw.annotatedElementText ? String(raw.annotatedElementText) : void 0
157
+ };
158
+ }
159
+ function writeTaskJson(projectDir, task) {
160
+ const dateDir = join(getTasksDir(projectDir), getDateSubdir(task.created));
161
+ mkdirSync(dateDir, { recursive: true });
162
+ const filePath = join(dateDir, `${task.id}.json`);
163
+ writeFileSync(filePath, JSON.stringify(task, null, 2), "utf-8");
164
+ }
165
+ function createTask(projectDir, input) {
166
+ const normalizedPriority = (() => {
167
+ const raw = (input.priority ?? "").trim().toLowerCase();
168
+ if (raw === "critical") return "Critical";
169
+ if (raw === "high") return "High";
170
+ if (raw === "low") return "Low";
171
+ return "Medium";
172
+ })();
173
+ const task = {
174
+ ...input,
175
+ priority: normalizedPriority,
176
+ id: generateTaskId(),
177
+ created: (/* @__PURE__ */ new Date()).toISOString(),
178
+ comments: [],
179
+ files: []
180
+ };
181
+ writeTaskJson(projectDir, task);
182
+ return task;
183
+ }
184
+ function readTaskFile(filePath) {
185
+ try {
186
+ const content = readFileSync(filePath, "utf-8");
187
+ if (filePath.endsWith(".json")) {
188
+ const raw = JSON.parse(content);
189
+ if (!raw || typeof raw !== "object" || !("id" in raw)) return null;
190
+ return normalizeTask(raw);
191
+ }
192
+ return null;
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
197
+ function collectTaskFiles(projectDir) {
198
+ const tasksDir = getTasksDir(projectDir);
199
+ if (!existsSync(tasksDir)) return [];
200
+ const results = [];
201
+ for (const entry of readdirSync(tasksDir, { withFileTypes: true })) {
202
+ if (entry.isDirectory()) {
203
+ const dateDir = join(tasksDir, entry.name);
204
+ for (const file of readdirSync(dateDir)) {
205
+ if (extname(file) === ".json") {
206
+ const filePath = join(dateDir, file);
207
+ const task = readTaskFile(filePath);
208
+ if (task) results.push({ task, filePath });
209
+ }
210
+ }
211
+ } else if (extname(entry.name) === ".json") {
212
+ const filePath = join(tasksDir, entry.name);
213
+ const task = readTaskFile(filePath);
214
+ if (task) results.push({ task, filePath });
215
+ }
216
+ }
217
+ return results.sort((a, b) => {
218
+ const aDate = a.task.updated ?? a.task.created;
219
+ const bDate = b.task.updated ?? b.task.created;
220
+ return new Date(bDate).getTime() - new Date(aDate).getTime();
221
+ });
222
+ }
223
+ function listTasks(projectDir) {
224
+ return collectTaskFiles(projectDir).map(({ task }) => task);
225
+ }
226
+ function listTasksWithPaths(projectDir) {
227
+ return collectTaskFiles(projectDir).map(({ task, filePath }) => ({ ...task, filePath }));
228
+ }
229
+ function updateTask(projectDir, taskId, updates) {
230
+ const existingPath = findTaskFilePath(projectDir, taskId);
231
+ const task = existingPath ? readTaskFile(existingPath) : null;
232
+ if (!task) return null;
233
+ const updated = { ...task, ...updates, updated: (/* @__PURE__ */ new Date()).toISOString() };
234
+ writeTaskJson(projectDir, updated);
235
+ if (existingPath && existingPath !== getTaskFilePath(projectDir, taskId, updated.created)) {
236
+ try {
237
+ unlinkSync(existingPath);
238
+ } catch {
239
+ }
240
+ }
241
+ return updated;
242
+ }
243
+ function deleteTask(projectDir, taskId) {
244
+ const filePath = findTaskFilePath(projectDir, taskId);
245
+ if (!filePath) return false;
246
+ unlinkSync(filePath);
247
+ return true;
248
+ }
249
+ function formatTaskForAgent(task, comments, files) {
250
+ return {
251
+ id: task.id,
252
+ status: task.status,
253
+ title: task.title,
254
+ description: task.description,
255
+ ...task.url && { url: task.url },
256
+ selector: task.selector,
257
+ ...task.file && { file: task.file },
258
+ ...task.line != null && { line: task.line },
259
+ ...task.col != null && { col: task.col },
260
+ ...task.component && { component: task.component },
261
+ ...task.type && { type: task.type },
262
+ ...task.priority && { priority: task.priority },
263
+ ...comments && comments.length > 0 && { structuredComments: comments },
264
+ ...files && files.length > 0 && { linkedFiles: files },
265
+ ...task.reportBack && { reportBack: true },
266
+ created: task.created
267
+ };
268
+ }
269
+
270
+ // src/core/files.ts
271
+ var LINKED_MANIFEST = ".linked.json";
272
+ function getFilesDir(projectDir, taskId) {
273
+ return join2(projectDir, PROTO_DIR, FILES_DIR, taskId);
274
+ }
275
+ function ensureFilesDir(projectDir, taskId) {
276
+ mkdirSync2(getFilesDir(projectDir, taskId), { recursive: true });
277
+ }
278
+ function readLinked(projectDir, taskId) {
279
+ const manifestPath = join2(getFilesDir(projectDir, taskId), LINKED_MANIFEST);
280
+ if (!existsSync2(manifestPath)) return [];
281
+ try {
282
+ return JSON.parse(readFileSync2(manifestPath, "utf-8"));
283
+ } catch {
284
+ return [];
285
+ }
286
+ }
287
+ function getTaskFileRefs(projectDir, taskId) {
288
+ const filePath = findTaskFilePath(projectDir, taskId);
289
+ const task = filePath ? readTaskFile(filePath) : null;
290
+ if (!task?.files || task.files.length === 0) return [];
291
+ return task.files;
292
+ }
293
+ function setTaskFileRefs(projectDir, taskId, refs) {
294
+ updateTask(projectDir, taskId, { files: refs });
295
+ }
296
+ function migrateLegacyLinkedRefs(projectDir, taskId) {
297
+ const refs = getTaskFileRefs(projectDir, taskId);
298
+ const manifestPath = join2(getFilesDir(projectDir, taskId), LINKED_MANIFEST);
299
+ if (!existsSync2(manifestPath)) return refs;
300
+ const legacy = readLinked(projectDir, taskId);
301
+ if (legacy.length === 0) {
302
+ try {
303
+ unlinkSync2(manifestPath);
304
+ } catch {
305
+ }
306
+ return refs;
307
+ }
308
+ const next = refs.slice();
309
+ let added = false;
310
+ for (const entry of legacy) {
311
+ if (!next.find((f) => f.linkedPath === entry.path || f.name === entry.name && f.linkedPath)) {
312
+ next.push({
313
+ name: entry.name,
314
+ linkedPath: entry.path,
315
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
316
+ });
317
+ added = true;
318
+ }
319
+ }
320
+ if (added) {
321
+ setTaskFileRefs(projectDir, taskId, next);
322
+ }
323
+ try {
324
+ unlinkSync2(manifestPath);
325
+ } catch {
326
+ }
327
+ return next;
328
+ }
329
+ function listFiles(projectDir, taskId) {
330
+ const dir = getFilesDir(projectDir, taskId);
331
+ const refs = migrateLegacyLinkedRefs(projectDir, taskId);
332
+ const byName = /* @__PURE__ */ new Map();
333
+ for (const ref of refs) {
334
+ if (ref.linkedPath && existsSync2(ref.linkedPath)) {
335
+ const stat = statSync(ref.linkedPath);
336
+ byName.set(ref.name, {
337
+ name: ref.name,
338
+ size: stat.size,
339
+ url: `/api/tasks/${taskId}/files/${encodeURIComponent(ref.name)}`,
340
+ linkedPath: ref.linkedPath,
341
+ createdAt: stat.mtime.toISOString()
342
+ });
343
+ continue;
344
+ }
345
+ const uploadedPath = join2(dir, ref.name);
346
+ if (existsSync2(uploadedPath)) {
347
+ const stat = statSync(uploadedPath);
348
+ byName.set(ref.name, {
349
+ name: ref.name,
350
+ size: stat.size,
351
+ url: `/api/tasks/${taskId}/files/${encodeURIComponent(ref.name)}`,
352
+ createdAt: stat.mtime.toISOString()
353
+ });
354
+ }
355
+ }
356
+ if (existsSync2(dir)) {
357
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
358
+ if (!entry.isFile() || entry.name === LINKED_MANIFEST || byName.has(entry.name)) continue;
359
+ const fullPath = join2(dir, entry.name);
360
+ const stat = statSync(fullPath);
361
+ byName.set(entry.name, {
362
+ name: entry.name,
363
+ size: stat.size,
364
+ url: `/api/tasks/${taskId}/files/${encodeURIComponent(entry.name)}`,
365
+ createdAt: stat.mtime.toISOString()
366
+ });
367
+ }
368
+ }
369
+ return Array.from(byName.values());
370
+ }
371
+ function saveFile(projectDir, taskId, filename, data) {
372
+ const safe = basename(filename);
373
+ ensureFilesDir(projectDir, taskId);
374
+ writeFileSync2(join2(getFilesDir(projectDir, taskId), safe), data);
375
+ const refs = migrateLegacyLinkedRefs(projectDir, taskId);
376
+ if (!refs.find((f) => f.name === safe && !f.linkedPath)) {
377
+ refs.push({ name: safe, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
378
+ setTaskFileRefs(projectDir, taskId, refs);
379
+ }
380
+ return {
381
+ name: safe,
382
+ size: data.length,
383
+ url: `/api/tasks/${taskId}/files/${encodeURIComponent(safe)}`
384
+ };
385
+ }
386
+ function deleteFile(projectDir, taskId, filename) {
387
+ const safe = basename(filename);
388
+ const refs = migrateLegacyLinkedRefs(projectDir, taskId);
389
+ const idx = refs.findIndex((f) => f.name === safe);
390
+ if (idx !== -1) {
391
+ const [removed] = refs.splice(idx, 1);
392
+ setTaskFileRefs(projectDir, taskId, refs);
393
+ if (removed && !removed.linkedPath) {
394
+ const uploadedPath = join2(getFilesDir(projectDir, taskId), safe);
395
+ if (existsSync2(uploadedPath)) unlinkSync2(uploadedPath);
396
+ }
397
+ return true;
398
+ }
399
+ const filePath = join2(getFilesDir(projectDir, taskId), safe);
400
+ if (!existsSync2(filePath)) return false;
401
+ unlinkSync2(filePath);
402
+ return true;
403
+ }
404
+ function getFilePath(projectDir, taskId, filename) {
405
+ const safe = basename(filename);
406
+ const linkedRef = migrateLegacyLinkedRefs(projectDir, taskId).find((f) => f.name === safe && f.linkedPath);
407
+ if (linkedRef?.linkedPath && existsSync2(linkedRef.linkedPath)) return linkedRef.linkedPath;
408
+ const filePath = join2(getFilesDir(projectDir, taskId), safe);
409
+ return existsSync2(filePath) ? filePath : null;
410
+ }
411
+ function getFileCount(projectDir, taskId) {
412
+ return listFiles(projectDir, taskId).length;
413
+ }
414
+
415
+ export {
416
+ PROTO_DIR,
417
+ SCREENSHOTS_DIR,
418
+ CONFIG_FILE,
419
+ generateTaskId,
420
+ ensureTaskDirs,
421
+ getTaskFilePath,
422
+ findTaskFilePath,
423
+ createTask,
424
+ readTaskFile,
425
+ listTasks,
426
+ listTasksWithPaths,
427
+ updateTask,
428
+ deleteTask,
429
+ formatTaskForAgent,
430
+ getFilesDir,
431
+ ensureFilesDir,
432
+ listFiles,
433
+ saveFile,
434
+ deleteFile,
435
+ getFilePath,
436
+ getFileCount
437
+ };
438
+ //# sourceMappingURL=chunk-4Y62J4KR.js.map