annotask 0.0.8 → 0.0.9
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.
- package/README.md +39 -19
- package/dist/chunk-7S23HMBH.js +496 -0
- package/dist/chunk-7S23HMBH.js.map +1 -0
- package/dist/chunk-BLB4GVAR.js +61 -0
- package/dist/chunk-BLB4GVAR.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/server.js +1 -1
- package/dist/shell/assets/index-BMbzZn9I.js +117 -0
- package/dist/shell/assets/index-CB4SDSPK.css +1 -0
- package/dist/shell/index.html +2 -2
- package/dist/standalone.js +2 -2
- package/dist/webpack.js +2 -2
- package/package.json +2 -1
- package/skills/annotask-apply/SKILL.md +28 -9
- package/dist/shell/assets/index-B-SRjSQF.js +0 -59
- package/dist/shell/assets/index-CeF02UAx.css +0 -1
package/README.md
CHANGED
|
@@ -15,15 +15,16 @@ Visual markup tool for web apps. Annotate your UI in the browser — pins, arrow
|
|
|
15
15
|
Annotate the UI:
|
|
16
16
|
pin elements, draw
|
|
17
17
|
sections, add notes, ──> /annotask-apply
|
|
18
|
-
describe what you want
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
describe what you want Fetches pending tasks
|
|
19
|
+
Locks each task (in_progress)
|
|
20
|
+
Applies code change
|
|
21
|
+
Marks for review — one at a time
|
|
22
|
+
Review changes live <──
|
|
23
|
+
as they come in:
|
|
23
24
|
Accept ✓ or Deny ✗
|
|
24
|
-
(denied tasks
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
(denied tasks include your
|
|
26
|
+
feedback for the agent
|
|
27
|
+
to retry with corrections)
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
### How it works
|
|
@@ -32,9 +33,9 @@ Visual markup tool for web apps. Annotate your UI in the browser — pins, arrow
|
|
|
32
33
|
|
|
33
34
|
2. **Tasks are created** — Each annotation becomes a structured task with full context: source file, line number, component name, element tag, surrounding layout, and your intent in plain language.
|
|
34
35
|
|
|
35
|
-
3. **Your coding agent applies the tasks** — Invoke `/annotask-apply` in Claude Code (or the equivalent skill in your agent). It
|
|
36
|
+
3. **Your coding agent applies the tasks** — Invoke `/annotask-apply` in Claude Code (or the equivalent skill in your agent). It processes tasks one at a time: locks the task (`in_progress`), applies the code change, then marks it for review — so you can start reviewing immediately while later tasks are still being applied.
|
|
36
37
|
|
|
37
|
-
4. **You review** —
|
|
38
|
+
4. **You review** — In the Annotask shell, click any task to open the detail drawer — see the full markdown description, screenshots, element context, interaction history, and source files. Accept or deny each change. Denied tasks include your feedback so the agent can retry with corrections.
|
|
38
39
|
|
|
39
40
|
## Agent Setup
|
|
40
41
|
|
|
@@ -166,9 +167,21 @@ Start your dev server, then open:
|
|
|
166
167
|
- **Page-level scanning** — Runs axe-core WCAG analysis on the entire page from the A11y panel
|
|
167
168
|
- **Violation cards** — Shows impact level, rule, description, and affected element count
|
|
168
169
|
- **One-click fix tasks** — Create tasks from violations with full context (HTML snippets, CSS selectors, source file/line, and fix suggestions)
|
|
170
|
+
- **Locally bundled** — axe-core and html2canvas are shipped with the package (no CDN dependency, works offline and under CSP)
|
|
171
|
+
|
|
172
|
+
### Task detail drawer
|
|
173
|
+
- **Slide-out detail view** — Click any task in the sidebar to open a full detail drawer
|
|
174
|
+
- **Markdown descriptions** — Task descriptions support full GitHub-flavored Markdown (rendered with `marked`)
|
|
175
|
+
- **Inline editing** — Click the rendered description to switch to a markdown editor; save with Ctrl+Enter
|
|
176
|
+
- **Screenshot thumbnails** — Clickable thumbnails with full-screen lightbox preview
|
|
177
|
+
- **Element display** — Shows selected element(s) with tag, classes, and component name
|
|
178
|
+
- **Multi-file view** — Shows all source files involved (primary, arrow targets, multi-element)
|
|
179
|
+
- **Interaction log** — History displayed as a numbered action log with route navigation and click details
|
|
180
|
+
- **JSON view** — Toggle raw JSON view of the complete task object with copy button
|
|
181
|
+
- **Accept/Deny actions** — Review tasks directly from the detail drawer (deny form includes screenshot, history, and DOM context options)
|
|
169
182
|
|
|
170
183
|
### Screenshots
|
|
171
|
-
- **Snipping tool** — Click "Add Screenshot" on any task form, then drag a region or click for full-page capture
|
|
184
|
+
- **Snipping tool** — Click "Add Screenshot" on any task form or deny form, then drag a region or click for full-page capture
|
|
172
185
|
- **Thumbnail preview** — Screenshot appears as a preview on the task form before submitting (removable)
|
|
173
186
|
- **Task-attached** — Screenshots are stored on the server and referenced by filename in the task
|
|
174
187
|
- **Multimodal AI context** — AI agents can download and view screenshots for visual understanding of what the user sees
|
|
@@ -188,8 +201,10 @@ These optional features give the AI agent richer context beyond just "change thi
|
|
|
188
201
|
> The agent can find breakpoints by reading config files and stylesheets — and usually will. This pre-detects them so the agent has the project's breakpoint system immediately alongside the viewport dimensions on each task. When a task is created at 375px, the agent can instantly map that to the right Tailwind prefix, Bootstrap tier, or custom media query without searching the codebase first.
|
|
189
202
|
|
|
190
203
|
### Infrastructure
|
|
191
|
-
- **Change reports** — Structured JSON of all changes, ready for agents to consume
|
|
192
|
-
- **Task pipeline** —
|
|
204
|
+
- **Change reports** — Structured JSON of all changes, ready for agents to consume (supports `?mfe=` filtering)
|
|
205
|
+
- **Task pipeline** — `pending → in_progress → review → accepted/denied` lifecycle with live status updates
|
|
206
|
+
- **Security** — CORS restricted to localhost origins, PATCH field whitelisting, postMessage sender validation
|
|
207
|
+
- **Async I/O** — In-memory task cache with atomic file writes (no race conditions under concurrent access)
|
|
193
208
|
- **CLI** — `annotask tasks`, `annotask report`, `annotask watch` for terminal access
|
|
194
209
|
- **API** — HTTP and WebSocket endpoints for programmatic access
|
|
195
210
|
|
|
@@ -207,15 +222,17 @@ Options: `--port=N`, `--host=H`, `--server=URL` (override server.json), `--mfe=N
|
|
|
207
222
|
|
|
208
223
|
## API
|
|
209
224
|
|
|
210
|
-
- `GET /__annotask/api/report` — Current change report
|
|
225
|
+
- `GET /__annotask/api/report` — Current change report (supports `?mfe=NAME` filter)
|
|
211
226
|
- `GET /__annotask/api/tasks` — Task list (supports `?mfe=NAME` filter)
|
|
212
227
|
- `POST /__annotask/api/tasks` — Create a task
|
|
213
|
-
- `PATCH /__annotask/api/tasks/:id` — Update task status
|
|
214
|
-
- `POST /__annotask/api/screenshots` — Upload a screenshot (base64 PNG)
|
|
228
|
+
- `PATCH /__annotask/api/tasks/:id` — Update task (whitelisted fields: status, description, feedback, screenshot, viewport, etc.)
|
|
229
|
+
- `POST /__annotask/api/screenshots` — Upload a screenshot (base64 PNG, max 4MB)
|
|
215
230
|
- `GET /__annotask/screenshots/:filename` — Serve a screenshot
|
|
216
231
|
- `GET /__annotask/api/status` — Health check
|
|
217
232
|
- `ws://localhost:5173/__annotask/ws` — Live WebSocket stream
|
|
218
233
|
|
|
234
|
+
CORS is restricted to localhost origins. Mutating requests from non-local origins are rejected.
|
|
235
|
+
|
|
219
236
|
## Supported Frameworks
|
|
220
237
|
|
|
221
238
|
| Framework | Vite | Webpack |
|
|
@@ -230,16 +247,17 @@ Options: `--port=N`, `--host=H`, `--server=URL` (override server.json), `--mfe=N
|
|
|
230
247
|
## Limitations
|
|
231
248
|
|
|
232
249
|
- **Dev mode only** — Annotask only runs in dev servers (Vite or Webpack), never in production builds
|
|
233
|
-
- **Local only** — API and WebSocket endpoints are
|
|
250
|
+
- **Local only** — API and WebSocket endpoints are localhost-restricted (CORS enforced, same model as Vite HMR)
|
|
234
251
|
- **Source mapping** — Works best with component files (`.vue`, `.tsx`, `.svelte`, `.astro`, `.html`); dynamic components and render functions may not map correctly
|
|
235
252
|
|
|
236
253
|
## Development
|
|
237
254
|
|
|
238
255
|
```bash
|
|
239
256
|
pnpm install
|
|
240
|
-
pnpm build # Build shell + plugin + CLI
|
|
257
|
+
pnpm build # Build shell + plugin + CLI + vendor deps
|
|
241
258
|
pnpm dev:vue-vite # Start Vue test app with Annotask
|
|
242
|
-
pnpm test # Run tests
|
|
259
|
+
pnpm test # Run unit tests
|
|
260
|
+
pnpm typecheck # Type-check (tsc + vue-tsc)
|
|
243
261
|
pnpm test:e2e # Run E2E tests (all frameworks)
|
|
244
262
|
```
|
|
245
263
|
|
|
@@ -249,6 +267,8 @@ pnpm test:e2e # Run E2E tests (all frameworks)
|
|
|
249
267
|
- `src/server/` — HTTP API, WebSocket server, shell serving, project state
|
|
250
268
|
- `src/webpack/` — Webpack plugin and transform loader
|
|
251
269
|
- `src/shell/` — Design tool UI (Vue 3 app, pre-built into dist/shell/)
|
|
270
|
+
- `src/shell/composables/` — Vue composables (style editor, tasks, screenshots, keyboard shortcuts, a11y scanner, etc.)
|
|
271
|
+
- `src/shell/components/` — UI components (inspector tabs, overlays, task detail drawer, report viewer, etc.)
|
|
252
272
|
- `src/shared/` — Shared types (postMessage bridge protocol)
|
|
253
273
|
- `src/schema.ts` — TypeScript types for change reports
|
|
254
274
|
- `src/cli/` — CLI tool for terminal interaction
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// src/server/api.ts
|
|
2
|
+
import fsp from "fs/promises";
|
|
3
|
+
import nodePath from "path";
|
|
4
|
+
var MAX_BODY_SIZE = 4194304;
|
|
5
|
+
var VALID_TASK_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "applied", "review", "accepted", "denied"]);
|
|
6
|
+
var PATCHABLE_TASK_FIELDS = /* @__PURE__ */ new Set([
|
|
7
|
+
"status",
|
|
8
|
+
"description",
|
|
9
|
+
"notes",
|
|
10
|
+
"screenshot",
|
|
11
|
+
"feedback",
|
|
12
|
+
"intent",
|
|
13
|
+
"action",
|
|
14
|
+
"context",
|
|
15
|
+
"viewport",
|
|
16
|
+
"interaction_history",
|
|
17
|
+
"element_context",
|
|
18
|
+
"mfe"
|
|
19
|
+
]);
|
|
20
|
+
function readBody(req) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
let body = "";
|
|
23
|
+
let size = 0;
|
|
24
|
+
req.on("data", (chunk) => {
|
|
25
|
+
size += chunk.length;
|
|
26
|
+
if (size > MAX_BODY_SIZE) {
|
|
27
|
+
req.destroy();
|
|
28
|
+
reject(new Error("Request body too large"));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
body += chunk.toString();
|
|
32
|
+
});
|
|
33
|
+
req.on("end", () => resolve(body));
|
|
34
|
+
req.on("error", reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function parseJSON(raw) {
|
|
38
|
+
try {
|
|
39
|
+
return { ok: true, data: JSON.parse(raw) };
|
|
40
|
+
} catch {
|
|
41
|
+
return { ok: false };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function sendError(res, status, message) {
|
|
45
|
+
res.statusCode = status;
|
|
46
|
+
res.end(JSON.stringify({ error: message }));
|
|
47
|
+
}
|
|
48
|
+
function isLocalOrigin(origin) {
|
|
49
|
+
if (!origin) return true;
|
|
50
|
+
try {
|
|
51
|
+
const url = new URL(origin);
|
|
52
|
+
const host = url.hostname;
|
|
53
|
+
return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function getCorsOrigin(req) {
|
|
59
|
+
const origin = req.headers.origin;
|
|
60
|
+
if (!origin) return null;
|
|
61
|
+
return isLocalOrigin(origin) ? origin : null;
|
|
62
|
+
}
|
|
63
|
+
function createAPIMiddleware(options) {
|
|
64
|
+
return async (req, res, next) => {
|
|
65
|
+
if (req.url?.startsWith("/__annotask/screenshots/") && req.method === "GET") {
|
|
66
|
+
const filename = req.url.replace("/__annotask/screenshots/", "").replace(/\?.*$/, "");
|
|
67
|
+
if (!/^[a-zA-Z0-9_-]+\.png$/.test(filename)) {
|
|
68
|
+
res.statusCode = 400;
|
|
69
|
+
res.end("Invalid filename");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const filePath = nodePath.join(options.projectRoot, ".annotask", "screenshots", filename);
|
|
73
|
+
try {
|
|
74
|
+
const data = await fsp.readFile(filePath);
|
|
75
|
+
res.setHeader("Content-Type", "image/png");
|
|
76
|
+
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
77
|
+
const corsOrigin2 = getCorsOrigin(req);
|
|
78
|
+
if (corsOrigin2) res.setHeader("Access-Control-Allow-Origin", corsOrigin2);
|
|
79
|
+
res.end(data);
|
|
80
|
+
} catch {
|
|
81
|
+
res.statusCode = 404;
|
|
82
|
+
res.end("Not found");
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!req.url?.startsWith("/__annotask/api/")) return next();
|
|
87
|
+
const path3 = req.url.replace("/__annotask/api/", "");
|
|
88
|
+
const corsOrigin = getCorsOrigin(req);
|
|
89
|
+
res.setHeader("Content-Type", "application/json");
|
|
90
|
+
if (corsOrigin) {
|
|
91
|
+
res.setHeader("Access-Control-Allow-Origin", corsOrigin);
|
|
92
|
+
res.setHeader("Vary", "Origin");
|
|
93
|
+
}
|
|
94
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
|
|
95
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
96
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
97
|
+
if (req.method === "OPTIONS") {
|
|
98
|
+
res.statusCode = 200;
|
|
99
|
+
res.end();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if ((req.method === "POST" || req.method === "PATCH") && !isLocalOrigin(req.headers.origin)) {
|
|
103
|
+
return sendError(res, 403, "Forbidden: non-local origin");
|
|
104
|
+
}
|
|
105
|
+
if (path3 === "report" && req.method === "GET") {
|
|
106
|
+
const report = options.getReport() ?? { version: "1.0", changes: [] };
|
|
107
|
+
const urlObj = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
108
|
+
const mfeFilter = urlObj.searchParams.get("mfe");
|
|
109
|
+
if (mfeFilter && report.changes) {
|
|
110
|
+
const filtered = { ...report, changes: report.changes.filter((c) => c.mfe === mfeFilter) };
|
|
111
|
+
res.end(JSON.stringify(filtered, null, 2));
|
|
112
|
+
} else {
|
|
113
|
+
res.end(JSON.stringify(report, null, 2));
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (path3 === "config" && req.method === "GET") {
|
|
118
|
+
res.end(JSON.stringify(options.getConfig(), null, 2));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (path3 === "design-spec" && req.method === "GET") {
|
|
122
|
+
res.end(JSON.stringify(options.getDesignSpec(), null, 2));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (path3.startsWith("tasks") && !path3.startsWith("tasks/") && req.method === "GET") {
|
|
126
|
+
const urlObj = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
127
|
+
const mfeFilter = urlObj.searchParams.get("mfe");
|
|
128
|
+
const taskData = options.getTasks();
|
|
129
|
+
if (mfeFilter) {
|
|
130
|
+
const filtered = { ...taskData, tasks: taskData.tasks.filter((t) => t.mfe === mfeFilter) };
|
|
131
|
+
res.end(JSON.stringify(filtered, null, 2));
|
|
132
|
+
} else {
|
|
133
|
+
res.end(JSON.stringify(taskData, null, 2));
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (path3 === "tasks" && req.method === "POST") {
|
|
138
|
+
let raw;
|
|
139
|
+
try {
|
|
140
|
+
raw = await readBody(req);
|
|
141
|
+
} catch {
|
|
142
|
+
return sendError(res, 413, "Request body too large");
|
|
143
|
+
}
|
|
144
|
+
const parsed = parseJSON(raw);
|
|
145
|
+
if (!parsed.ok) return sendError(res, 400, "Invalid JSON body");
|
|
146
|
+
const body = parsed.data;
|
|
147
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return sendError(res, 400, "Request body must be a JSON object");
|
|
148
|
+
if (typeof body.type !== "string" || !body.type) return sendError(res, 400, "Missing required field: type (string)");
|
|
149
|
+
if (typeof body.description !== "string") return sendError(res, 400, "Missing required field: description (string)");
|
|
150
|
+
res.end(JSON.stringify(options.addTask(body), null, 2));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (path3 === "screenshots" && req.method === "POST") {
|
|
154
|
+
let raw;
|
|
155
|
+
try {
|
|
156
|
+
raw = await readBody(req);
|
|
157
|
+
} catch {
|
|
158
|
+
return sendError(res, 413, "Request body too large");
|
|
159
|
+
}
|
|
160
|
+
const parsed = parseJSON(raw);
|
|
161
|
+
if (!parsed.ok) return sendError(res, 400, "Invalid JSON body");
|
|
162
|
+
const body = parsed.data;
|
|
163
|
+
if (!body.data || typeof body.data !== "string") return sendError(res, 400, "Missing data field");
|
|
164
|
+
const match = body.data.match(/^data:image\/png;base64,(.+)$/);
|
|
165
|
+
if (!match) return sendError(res, 400, "Invalid PNG data URL");
|
|
166
|
+
const buffer = Buffer.from(match[1], "base64");
|
|
167
|
+
if (buffer.length > 4 * 1024 * 1024) return sendError(res, 413, "Screenshot too large (max 4MB)");
|
|
168
|
+
const filename = `screenshot-${Date.now()}-${Math.random().toString(36).slice(2, 7)}.png`;
|
|
169
|
+
const dir = nodePath.join(options.projectRoot, ".annotask", "screenshots");
|
|
170
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
171
|
+
await fsp.writeFile(nodePath.join(dir, filename), buffer);
|
|
172
|
+
res.end(JSON.stringify({ filename }));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (path3.startsWith("tasks/") && req.method === "PATCH") {
|
|
176
|
+
const id = path3.replace("tasks/", "");
|
|
177
|
+
let raw;
|
|
178
|
+
try {
|
|
179
|
+
raw = await readBody(req);
|
|
180
|
+
} catch {
|
|
181
|
+
return sendError(res, 413, "Request body too large");
|
|
182
|
+
}
|
|
183
|
+
const parsed = parseJSON(raw);
|
|
184
|
+
if (!parsed.ok) return sendError(res, 400, "Invalid JSON body");
|
|
185
|
+
const body = parsed.data;
|
|
186
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return sendError(res, 400, "Request body must be a JSON object");
|
|
187
|
+
if (body.status !== void 0 && !VALID_TASK_STATUSES.has(body.status)) {
|
|
188
|
+
return sendError(res, 400, `Invalid status. Must be one of: ${[...VALID_TASK_STATUSES].join(", ")}`);
|
|
189
|
+
}
|
|
190
|
+
const sanitized = {};
|
|
191
|
+
for (const key of Object.keys(body)) {
|
|
192
|
+
if (PATCHABLE_TASK_FIELDS.has(key)) {
|
|
193
|
+
sanitized[key] = body[key];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
res.end(JSON.stringify(options.updateTask(id, sanitized), null, 2));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (path3 === "status" && req.method === "GET") {
|
|
200
|
+
res.end(JSON.stringify({ status: "ok", tool: "annotask" }));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
res.statusCode = 404;
|
|
204
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/server/ws-server.ts
|
|
209
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
210
|
+
function createWSServer() {
|
|
211
|
+
let currentReport = null;
|
|
212
|
+
const clients = /* @__PURE__ */ new Set();
|
|
213
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
214
|
+
wss.on("connection", (ws) => {
|
|
215
|
+
clients.add(ws);
|
|
216
|
+
if (currentReport) {
|
|
217
|
+
ws.send(JSON.stringify({ event: "report:current", data: currentReport, timestamp: Date.now() }));
|
|
218
|
+
}
|
|
219
|
+
ws.on("message", (raw) => {
|
|
220
|
+
try {
|
|
221
|
+
const msg = JSON.parse(raw.toString());
|
|
222
|
+
if (msg.event === "report:updated") {
|
|
223
|
+
currentReport = msg.data;
|
|
224
|
+
for (const client of clients) {
|
|
225
|
+
if (client !== ws && client.readyState === WebSocket.OPEN) {
|
|
226
|
+
client.send(JSON.stringify({ event: "report:updated", data: msg.data, timestamp: Date.now() }));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (msg.event === "changes:cleared") {
|
|
231
|
+
currentReport = null;
|
|
232
|
+
for (const client of clients) {
|
|
233
|
+
if (client !== ws && client.readyState === WebSocket.OPEN) {
|
|
234
|
+
client.send(JSON.stringify({ event: "changes:cleared", data: null, timestamp: Date.now() }));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (msg.event === "get:report") {
|
|
239
|
+
ws.send(JSON.stringify({ event: "report:current", data: currentReport, timestamp: Date.now() }));
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
ws.on("close", () => {
|
|
245
|
+
clients.delete(ws);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
return {
|
|
249
|
+
handleUpgrade(req, socket, head) {
|
|
250
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
251
|
+
wss.emit("connection", ws, req);
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
broadcast(event, data) {
|
|
255
|
+
const msg = JSON.stringify({ event, data, timestamp: Date.now() });
|
|
256
|
+
for (const client of clients) {
|
|
257
|
+
if (client.readyState === WebSocket.OPEN) client.send(msg);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
getReport() {
|
|
261
|
+
return currentReport;
|
|
262
|
+
},
|
|
263
|
+
clients
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/server/serve-shell.ts
|
|
268
|
+
import fsp2 from "fs/promises";
|
|
269
|
+
import path from "path";
|
|
270
|
+
import { fileURLToPath } from "url";
|
|
271
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
272
|
+
function findShellDist() {
|
|
273
|
+
return path.resolve(__dirname, "shell");
|
|
274
|
+
}
|
|
275
|
+
function findVendorDist() {
|
|
276
|
+
return path.resolve(__dirname, "vendor");
|
|
277
|
+
}
|
|
278
|
+
function createShellMiddleware() {
|
|
279
|
+
const shellDist = findShellDist();
|
|
280
|
+
const vendorDist = findVendorDist();
|
|
281
|
+
return async (req, res, next) => {
|
|
282
|
+
if (!req.url?.startsWith("/__annotask")) return next();
|
|
283
|
+
const origin = req.headers.origin;
|
|
284
|
+
if (origin) {
|
|
285
|
+
try {
|
|
286
|
+
const host = new URL(origin).hostname;
|
|
287
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1") {
|
|
288
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
289
|
+
res.setHeader("Vary", "Origin");
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let filePath = req.url.replace("/__annotask", "") || "/";
|
|
295
|
+
const queryIndex = filePath.indexOf("?");
|
|
296
|
+
if (queryIndex !== -1) filePath = filePath.slice(0, queryIndex);
|
|
297
|
+
if (filePath === "/" || filePath === "") filePath = "/index.html";
|
|
298
|
+
if (filePath.startsWith("/api/") || filePath === "/ws") return next();
|
|
299
|
+
if (filePath.startsWith("/vendor/")) {
|
|
300
|
+
const vendorFile = path.join(vendorDist, filePath.replace("/vendor/", ""));
|
|
301
|
+
if (!vendorFile.startsWith(vendorDist)) {
|
|
302
|
+
res.statusCode = 403;
|
|
303
|
+
res.end("Forbidden");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const data = await fsp2.readFile(vendorFile);
|
|
308
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
309
|
+
res.setHeader("Cache-Control", "public, max-age=86400");
|
|
310
|
+
res.end(data);
|
|
311
|
+
} catch {
|
|
312
|
+
res.statusCode = 404;
|
|
313
|
+
res.end("Vendor file not found");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const fullPath = path.join(shellDist, filePath);
|
|
319
|
+
if (!fullPath.startsWith(shellDist)) {
|
|
320
|
+
res.statusCode = 403;
|
|
321
|
+
res.end("Forbidden");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const stat = await fsp2.stat(fullPath);
|
|
326
|
+
if (!stat.isFile()) throw new Error("not a file");
|
|
327
|
+
const ext = path.extname(fullPath);
|
|
328
|
+
const contentTypes = {
|
|
329
|
+
".html": "text/html",
|
|
330
|
+
".js": "application/javascript",
|
|
331
|
+
".css": "text/css",
|
|
332
|
+
".json": "application/json",
|
|
333
|
+
".svg": "image/svg+xml",
|
|
334
|
+
".png": "image/png",
|
|
335
|
+
".ico": "image/x-icon"
|
|
336
|
+
};
|
|
337
|
+
res.setHeader("Content-Type", contentTypes[ext] || "application/octet-stream");
|
|
338
|
+
const data = await fsp2.readFile(fullPath);
|
|
339
|
+
res.end(data);
|
|
340
|
+
} catch {
|
|
341
|
+
const indexPath = path.join(shellDist, "index.html");
|
|
342
|
+
try {
|
|
343
|
+
const html = await fsp2.readFile(indexPath, "utf-8");
|
|
344
|
+
res.setHeader("Content-Type", "text/html");
|
|
345
|
+
res.end(html);
|
|
346
|
+
} catch {
|
|
347
|
+
res.statusCode = 404;
|
|
348
|
+
res.end("Annotask shell not built. Run: pnpm build:shell");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/server/state.ts
|
|
355
|
+
import fs from "fs";
|
|
356
|
+
import fsp3 from "fs/promises";
|
|
357
|
+
import path2 from "path";
|
|
358
|
+
var DEFAULT_DESIGN_SPEC = {
|
|
359
|
+
initialized: false,
|
|
360
|
+
version: "1.0",
|
|
361
|
+
framework: null,
|
|
362
|
+
colors: [],
|
|
363
|
+
typography: { families: [], scale: [], weights: [] },
|
|
364
|
+
spacing: [],
|
|
365
|
+
borders: { radius: [] },
|
|
366
|
+
icons: null,
|
|
367
|
+
components: null
|
|
368
|
+
};
|
|
369
|
+
async function atomicWrite(filePath, data) {
|
|
370
|
+
const dir = path2.dirname(filePath);
|
|
371
|
+
await fsp3.mkdir(dir, { recursive: true });
|
|
372
|
+
const tmpPath = filePath + `.tmp.${process.pid}.${Date.now()}`;
|
|
373
|
+
await fsp3.writeFile(tmpPath, data, "utf-8");
|
|
374
|
+
await fsp3.rename(tmpPath, filePath);
|
|
375
|
+
}
|
|
376
|
+
function createProjectState(projectRoot, broadcast) {
|
|
377
|
+
let cachedDesignSpec = null;
|
|
378
|
+
let specWatcher = null;
|
|
379
|
+
const tasksPath = path2.join(projectRoot, ".annotask", "tasks.json");
|
|
380
|
+
let taskCache = null;
|
|
381
|
+
let writeQueue = Promise.resolve();
|
|
382
|
+
function loadTasksSync() {
|
|
383
|
+
if (taskCache) return taskCache;
|
|
384
|
+
try {
|
|
385
|
+
taskCache = JSON.parse(fs.readFileSync(tasksPath, "utf-8"));
|
|
386
|
+
} catch {
|
|
387
|
+
taskCache = { version: "1.0", tasks: [] };
|
|
388
|
+
}
|
|
389
|
+
return taskCache;
|
|
390
|
+
}
|
|
391
|
+
function flushTasks() {
|
|
392
|
+
const data = taskCache;
|
|
393
|
+
if (!data) return;
|
|
394
|
+
writeQueue = writeQueue.then(() => atomicWrite(tasksPath, JSON.stringify(data, null, 2))).catch(() => {
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function getDesignSpec() {
|
|
398
|
+
if (cachedDesignSpec !== null) return cachedDesignSpec;
|
|
399
|
+
const specPath = path2.join(projectRoot, ".annotask", "design-spec.json");
|
|
400
|
+
try {
|
|
401
|
+
cachedDesignSpec = { initialized: true, ...JSON.parse(fs.readFileSync(specPath, "utf-8")) };
|
|
402
|
+
} catch {
|
|
403
|
+
cachedDesignSpec = DEFAULT_DESIGN_SPEC;
|
|
404
|
+
}
|
|
405
|
+
if (!specWatcher) {
|
|
406
|
+
const configDir = path2.join(projectRoot, ".annotask");
|
|
407
|
+
try {
|
|
408
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
409
|
+
specWatcher = fs.watch(configDir, (_, filename) => {
|
|
410
|
+
cachedDesignSpec = null;
|
|
411
|
+
if (filename === "design-spec.json") broadcast("designspec:updated", null);
|
|
412
|
+
if (filename === "tasks.json") {
|
|
413
|
+
taskCache = null;
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
} catch {
|
|
417
|
+
cachedDesignSpec = null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return cachedDesignSpec ?? DEFAULT_DESIGN_SPEC;
|
|
421
|
+
}
|
|
422
|
+
function getConfig() {
|
|
423
|
+
const spec = getDesignSpec();
|
|
424
|
+
return { initialized: !!spec?.initialized, ...spec };
|
|
425
|
+
}
|
|
426
|
+
function addTask(task) {
|
|
427
|
+
const data = loadTasksSync();
|
|
428
|
+
const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
429
|
+
const newTask = { id, status: "pending", createdAt: Date.now(), updatedAt: Date.now(), ...task };
|
|
430
|
+
data.tasks.push(newTask);
|
|
431
|
+
flushTasks();
|
|
432
|
+
broadcast("tasks:updated", data);
|
|
433
|
+
return newTask;
|
|
434
|
+
}
|
|
435
|
+
function updateTask(id, updates) {
|
|
436
|
+
const data = loadTasksSync();
|
|
437
|
+
const task = data.tasks.find((t) => t.id === id);
|
|
438
|
+
if (!task) return { error: "Task not found" };
|
|
439
|
+
Object.assign(task, updates, { updatedAt: Date.now() });
|
|
440
|
+
if (updates.status === "accepted") {
|
|
441
|
+
if (task.screenshot) {
|
|
442
|
+
const screenshotPath = path2.join(projectRoot, ".annotask", "screenshots", task.screenshot);
|
|
443
|
+
fsp3.unlink(screenshotPath).catch(() => {
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
data.tasks = data.tasks.filter((t) => t.id !== id);
|
|
447
|
+
}
|
|
448
|
+
flushTasks();
|
|
449
|
+
broadcast("tasks:updated", data);
|
|
450
|
+
return task;
|
|
451
|
+
}
|
|
452
|
+
function dispose() {
|
|
453
|
+
if (specWatcher) {
|
|
454
|
+
specWatcher.close();
|
|
455
|
+
specWatcher = null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return { getDesignSpec, getConfig, getTasks: loadTasksSync, addTask, updateTask, dispose };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/server/index.ts
|
|
462
|
+
function createAnnotaskServer(options) {
|
|
463
|
+
const wsServer = createWSServer();
|
|
464
|
+
const state = createProjectState(options.projectRoot, wsServer.broadcast);
|
|
465
|
+
const apiMiddleware = createAPIMiddleware({
|
|
466
|
+
projectRoot: options.projectRoot,
|
|
467
|
+
getReport: () => wsServer.getReport(),
|
|
468
|
+
getConfig: () => state.getConfig(),
|
|
469
|
+
getDesignSpec: () => state.getDesignSpec(),
|
|
470
|
+
getTasks: () => state.getTasks(),
|
|
471
|
+
addTask: (task) => state.addTask(task),
|
|
472
|
+
updateTask: (id, updates) => state.updateTask(id, updates)
|
|
473
|
+
});
|
|
474
|
+
const shellMiddleware = createShellMiddleware();
|
|
475
|
+
const middleware = (req, res, next) => {
|
|
476
|
+
apiMiddleware(req, res, () => {
|
|
477
|
+
shellMiddleware(req, res, next);
|
|
478
|
+
});
|
|
479
|
+
};
|
|
480
|
+
return {
|
|
481
|
+
middleware,
|
|
482
|
+
handleUpgrade: (req, socket, head) => wsServer.handleUpgrade(req, socket, head),
|
|
483
|
+
broadcast: (event, data) => wsServer.broadcast(event, data),
|
|
484
|
+
getReport: () => wsServer.getReport(),
|
|
485
|
+
dispose: () => state.dispose()
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export {
|
|
490
|
+
createAPIMiddleware,
|
|
491
|
+
createWSServer,
|
|
492
|
+
createShellMiddleware,
|
|
493
|
+
createProjectState,
|
|
494
|
+
createAnnotaskServer
|
|
495
|
+
};
|
|
496
|
+
//# sourceMappingURL=chunk-7S23HMBH.js.map
|