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 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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ interface Io {
3
+ cwd?: string;
4
+ log?: (s: string) => void;
5
+ error?: (s: string) => void;
6
+ }
7
+ declare function runCli(argv: string[], io?: Io): number;
8
+
9
+ export { runCli };
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
+ };
@@ -0,0 +1,5 @@
1
+ import { D as DevAnnotationOptions } from '../types-BzKl2hyk.js';
2
+
3
+ declare function initDevAnnotation(options?: DevAnnotationOptions): () => void;
4
+
5
+ export { DevAnnotationOptions, initDevAnnotation };