dev-annotate 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.
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +123 -0
- package/dist/core/client.d.ts +5 -0
- package/dist/core/client.js +532 -0
- package/dist/nuxt/module.d.ts +9 -0
- package/dist/nuxt/module.js +28 -0
- package/dist/nuxt/runtime/plugin.client.d.ts +3 -0
- package/dist/nuxt/runtime/plugin.client.js +542 -0
- package/dist/nuxt/runtime/server-route.d.ts +5 -0
- package/dist/nuxt/runtime/server-route.js +57 -0
- package/dist/server/index.d.ts +25 -0
- package/dist/server/index.js +52 -0
- package/dist/types-BzKl2hyk.d.ts +19 -0
- package/package.json +36 -0
- package/templates/agent-instructions.md +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dev-annotate contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# dev-annotate
|
|
2
|
+
|
|
3
|
+
Dev-only on-screen annotation tool. Scribble (pen / text / numbered pins)
|
|
4
|
+
directly on the live page in any browser — including mobile — take an **OS
|
|
5
|
+
screenshot**, and upload it so an AI agent (or you) can read it and fix the UI.
|
|
6
|
+
|
|
7
|
+
> **Why OS screenshots?** The real pixels (horizontal table scroll, WebGL
|
|
8
|
+
> backgrounds, `backdrop-filter`, …) can't be faithfully re-rendered by
|
|
9
|
+
> html2canvas-style DOM rasterization, and mobile browsers can't use
|
|
10
|
+
> `getDisplayMedia`. So the tool never re-composites: the human takes the OS
|
|
11
|
+
> screenshot and uploads the real bytes.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
npm i -D dev-annotate
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Core (framework-agnostic)
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { initDevAnnotation } from 'dev-annotate'
|
|
23
|
+
|
|
24
|
+
// call this ONLY in development (you decide how to gate it)
|
|
25
|
+
if (import.meta.env?.DEV) {
|
|
26
|
+
const destroy = initDevAnnotation({
|
|
27
|
+
endpoint: '/api/dev/save-annotation', // where the upload is POSTed
|
|
28
|
+
// colors, sizes, shortcutKey, zIndexBase are optional
|
|
29
|
+
})
|
|
30
|
+
// call destroy() on HMR/unmount to avoid duplicate UI
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`initDevAnnotation(options?)` returns a `destroy()` function that removes all
|
|
35
|
+
injected DOM and listeners.
|
|
36
|
+
|
|
37
|
+
## Server (h3 / Nitro)
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { createSaveAnnotationHandler } from 'dev-annotate/server'
|
|
41
|
+
export default createSaveAnnotationHandler({ dir: '.playwright-mcp/design-review' })
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or use the pure writer directly:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { saveAnnotationBytes } from 'dev-annotate/server'
|
|
48
|
+
const { path, bytes } = saveAnnotationBytes(buffer, { mime: 'image/png' })
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Nuxt
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// nuxt.config.ts
|
|
55
|
+
export default defineNuxtConfig({
|
|
56
|
+
modules: ['dev-annotate/nuxt'],
|
|
57
|
+
devAnnotate: { /* endpoint, colors, sizes, dir, ... */ },
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The module only activates in dev; it registers the client plugin and a POST
|
|
62
|
+
handler at `endpoint` (default `/api/dev/save-annotation`).
|
|
63
|
+
|
|
64
|
+
## CLI — show annotations to your AI
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
npx dev-annotate latest [-n N] [--json] [--dir D] # newest path(s)
|
|
68
|
+
npx dev-annotate list [-n N] [--json] [--dir D] # all, newest first
|
|
69
|
+
npx dev-annotate watch [--json] [--dir D] # print on new file
|
|
70
|
+
npx dev-annotate clean [--keep N | --all] [--yes] # tidy up
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Drop `templates/agent-instructions.md` into your `CLAUDE.md` / `AGENTS.md` so
|
|
74
|
+
your agent knows the read → fix → verify → clean loop.
|
|
75
|
+
|
|
76
|
+
## Other frameworks (Vite / React / Next)
|
|
77
|
+
|
|
78
|
+
The core is plain DOM with zero deps. Gate it to dev and call
|
|
79
|
+
`initDevAnnotation()` once; for uploads, implement an endpoint that receives a
|
|
80
|
+
multipart `file` and writes the bytes (mirror `saveAnnotationBytes`). Adapters
|
|
81
|
+
for Vite/Next are not bundled yet — the two steps above are all you need.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { resolve as resolve2 } from "path";
|
|
5
|
+
import { realpathSync, unlinkSync, watch as fsWatch } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
// src/shared/store.ts
|
|
9
|
+
import { existsSync, readdirSync, statSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
var ANNOTATED_RE = /^annotated-.*\.(png|jpe?g|webp)$/i;
|
|
12
|
+
function listAnnotations(dir, limit) {
|
|
13
|
+
if (!existsSync(dir)) return [];
|
|
14
|
+
const files = readdirSync(dir).filter((name) => ANNOTATED_RE.test(name)).map((name) => {
|
|
15
|
+
const path = join(dir, name);
|
|
16
|
+
const st = statSync(path);
|
|
17
|
+
return { path, name, bytes: st.size, mtimeMs: st.mtimeMs };
|
|
18
|
+
}).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
19
|
+
return typeof limit === "number" ? files.slice(0, limit) : files;
|
|
20
|
+
}
|
|
21
|
+
function latestAnnotation(dir) {
|
|
22
|
+
return listAnnotations(dir, 1)[0];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/server/save.ts
|
|
26
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
27
|
+
import { resolve } from "path";
|
|
28
|
+
var DEFAULT_DIR = ".playwright-mcp/design-review";
|
|
29
|
+
|
|
30
|
+
// src/cli/index.ts
|
|
31
|
+
function parseFlags(args) {
|
|
32
|
+
const f = { json: false, all: false, yes: false };
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
const a = args[i];
|
|
35
|
+
if (a === "--json") f.json = true;
|
|
36
|
+
else if (a === "--all") f.all = true;
|
|
37
|
+
else if (a === "--yes") f.yes = true;
|
|
38
|
+
else if (a === "-n") f.n = Number(args[++i] ?? "");
|
|
39
|
+
else if (a === "--keep") f.keep = Number(args[++i] ?? "");
|
|
40
|
+
else if (a === "--dir") f.dir = args[++i] ?? void 0;
|
|
41
|
+
}
|
|
42
|
+
return f;
|
|
43
|
+
}
|
|
44
|
+
var USAGE = `usage: dev-annotate <latest|list|watch|clean> [options]
|
|
45
|
+
latest [-n N] [--json] [--dir D] newest annotation path(s)
|
|
46
|
+
list [-n N] [--json] [--dir D] all annotations, newest first
|
|
47
|
+
watch [--json] [--dir D] print path when a new annotation arrives
|
|
48
|
+
clean [--keep N | --all] [--yes] [--dir D] delete old/all annotations
|
|
49
|
+
|
|
50
|
+
--dir D annotation directory (default: ${DEFAULT_DIR})`;
|
|
51
|
+
function runCli(argv, io = {}) {
|
|
52
|
+
const log = io.log ?? ((s) => process.stdout.write(s + "\n"));
|
|
53
|
+
const error = io.error ?? ((s) => process.stderr.write(s + "\n"));
|
|
54
|
+
const cwd = io.cwd ?? process.cwd();
|
|
55
|
+
const [cmd, ...rest] = argv;
|
|
56
|
+
const flags = parseFlags(rest);
|
|
57
|
+
const dir = resolve2(cwd, flags.dir ?? DEFAULT_DIR);
|
|
58
|
+
const emit = (files) => {
|
|
59
|
+
if (flags.json) log(JSON.stringify(files.map((f) => ({ path: f.path, name: f.name, bytes: f.bytes, mtimeMs: f.mtimeMs })), null, 2));
|
|
60
|
+
else for (const f of files) log(f.path);
|
|
61
|
+
};
|
|
62
|
+
switch (cmd) {
|
|
63
|
+
case "latest": {
|
|
64
|
+
const files = listAnnotations(dir, flags.n ?? 1);
|
|
65
|
+
if (files.length === 0) {
|
|
66
|
+
error("no annotations found");
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
emit(files);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
case "list": {
|
|
73
|
+
emit(listAnnotations(dir, flags.n));
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
case "clean": {
|
|
77
|
+
const all = listAnnotations(dir);
|
|
78
|
+
const keep = flags.all ? 0 : flags.keep ?? 0;
|
|
79
|
+
const targets = all.slice(keep);
|
|
80
|
+
if (targets.length === 0) {
|
|
81
|
+
log("nothing to delete");
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
if (!flags.yes) {
|
|
85
|
+
log(`would delete ${targets.length} file(s) (pass --yes to confirm):`);
|
|
86
|
+
for (const f of targets) log(f.path);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
for (const f of targets) unlinkSync(f.path);
|
|
90
|
+
log(`deleted ${targets.length} file(s)`);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
case "watch": {
|
|
94
|
+
log(`watching ${dir} \u2026 (Ctrl+C to stop)`);
|
|
95
|
+
const latest = latestAnnotation(dir);
|
|
96
|
+
let lastMtime = latest?.mtimeMs ?? 0;
|
|
97
|
+
fsWatch(dir, () => {
|
|
98
|
+
const f = latestAnnotation(dir);
|
|
99
|
+
if (f && f.mtimeMs > lastMtime) {
|
|
100
|
+
lastMtime = f.mtimeMs;
|
|
101
|
+
if (flags.json) log(JSON.stringify({ path: f.path, name: f.name, bytes: f.bytes, mtimeMs: f.mtimeMs }));
|
|
102
|
+
else log(f.path);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
error(USAGE);
|
|
109
|
+
return 2;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
var invokedPath = process.argv[1];
|
|
113
|
+
if (invokedPath) {
|
|
114
|
+
try {
|
|
115
|
+
if (realpathSync(invokedPath) === fileURLToPath(import.meta.url)) {
|
|
116
|
+
process.exitCode = runCli(process.argv.slice(2));
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
runCli
|
|
123
|
+
};
|