apostil 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 Batzorig Tsergiinkhuu
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,169 @@
1
+ # Apostil
2
+
3
+ Pin-and-comment feedback overlay for React & Next.js. Let your team leave contextual feedback directly on the UI.
4
+
5
+ ## Features
6
+
7
+ - **Click to pin** — drop comment pins anywhere on the page
8
+ - **Smart target detection** — auto-anchors to nearest meaningful element
9
+ - **Thread-based** — replies, resolve/unresolve, delete
10
+ - **Keyboard shortcuts** — `C` to toggle comment mode, `Esc` to cancel
11
+ - **Popover-aware** — comments inside modals/popovers re-appear when reopened
12
+ - **All Pages view** — see every comment across your project in one sidebar
13
+ - **Auto z-index** — overlay detects highest z-index and sits above everything
14
+ - **SSR-safe** — works with Next.js App Router
15
+
16
+ ## Quick Start
17
+
18
+ ### 1. Install
19
+
20
+ ```bash
21
+ npm install apostil # or: pnpm add apostil / yarn add apostil
22
+ ```
23
+
24
+ ### 2. Initialize
25
+
26
+ ```bash
27
+ npx apostil init # or: pnpm exec apostil init
28
+ ```
29
+
30
+ This will:
31
+ - Create `app/api/apostil/route.ts` — API route for comment storage
32
+ - Create `components/apostil-wrapper.tsx` — pre-configured wrapper component
33
+ - Create `.apostil/` directory and add it to `.gitignore`
34
+ - **Automatically wrap `{children}` in your root layout** with `<ApostilWrapper>`
35
+
36
+ ### 3. Done
37
+
38
+ ```bash
39
+ npm run dev
40
+ ```
41
+
42
+ Press `C` on any page to start commenting. That's it.
43
+
44
+ ## How It Works
45
+
46
+ Comments are stored as JSON files in `.apostil/`:
47
+
48
+ ```
49
+ .apostil/
50
+ ├── home.json
51
+ ├── about.json
52
+ └── dashboard--settings.json
53
+ ```
54
+
55
+ The wrapper auto-detects the current page from `usePathname()` and loads the corresponding comments. Every page in your app gets commenting automatically.
56
+
57
+ ## Uninstall
58
+
59
+ ```bash
60
+ npx apostil remove
61
+ npm uninstall apostil
62
+ ```
63
+
64
+ `remove` cleans up everything — deletes the API route, wrapper component, `.apostil/` directory, removes the `<ApostilWrapper>` from your layout, and cleans `.gitignore`.
65
+
66
+ ## Keyboard Shortcuts
67
+
68
+ | Key | Action |
69
+ |-----|--------|
70
+ | `C` | Toggle comment mode |
71
+ | `Escape` | Cancel unsaved comment / exit comment mode |
72
+ | `Enter` | Submit comment |
73
+
74
+ ## Components
75
+
76
+ ### `<ApostilProvider>`
77
+
78
+ Core context provider. Use directly for custom setups:
79
+
80
+ ```tsx
81
+ <ApostilProvider pageId="my-page" storage={customAdapter}>
82
+ {children}
83
+ <CommentOverlay />
84
+ <CommentToggle />
85
+ </ApostilProvider>
86
+ ```
87
+
88
+ ### `<CommentOverlay>`
89
+
90
+ Captures clicks in comment mode. Auto-detects z-index to sit above popovers and modals.
91
+
92
+ ### `<CommentToggle>`
93
+
94
+ Floating button (bottom-right) with unresolved count badge.
95
+
96
+ ### `<CommentSidebar>`
97
+
98
+ Right panel with two tabs:
99
+ - **This Page** — comments on the current page
100
+ - **All Pages** — every comment across the project. Click to navigate.
101
+
102
+ ## Hooks
103
+
104
+ ```tsx
105
+ import { useApostil, useComments, useCommentMode } from "apostil";
106
+
107
+ const { threads, user, addThread, addReply, resolveThread } = useApostil();
108
+ const { openThreads, resolvedThreads, unresolvedCount } = useComments();
109
+ const { commentMode, toggleCommentMode, sidebarOpen, toggleSidebar } = useCommentMode();
110
+ ```
111
+
112
+ ## Storage Adapters
113
+
114
+ ### Default (file-based)
115
+
116
+ No config needed. `npx apostil init` sets up the API route that reads/writes `.apostil/` JSON files.
117
+
118
+ ### localStorage
119
+
120
+ ```tsx
121
+ import { localStorageAdapter } from "apostil/adapters/localStorage";
122
+ <ApostilProvider pageId="my-page" storage={localStorageAdapter}>
123
+ ```
124
+
125
+ ### Custom REST API
126
+
127
+ ```tsx
128
+ import { createRestAdapter } from "apostil/adapters/rest";
129
+ <ApostilProvider pageId="my-page" storage={createRestAdapter("/api/my-comments")}>
130
+ ```
131
+
132
+ ### Custom Adapter
133
+
134
+ ```tsx
135
+ const myAdapter: ApostilStorage = {
136
+ async load(pageId) { /* return threads */ },
137
+ async save(pageId, threads) { /* persist */ },
138
+ };
139
+ ```
140
+
141
+ ## Target Detection
142
+
143
+ Apostil auto-detects meaningful elements when placing comments:
144
+
145
+ 1. `data-comment-target="id"` — explicit anchor
146
+ 2. Elements with `id` or `aria-label`
147
+ 3. Semantic HTML (`section`, `nav`, `aside`)
148
+ 4. Visual panels (scrollable, bordered, shadowed)
149
+
150
+ Pins are stored as percentages relative to the target — they follow on scroll/resize.
151
+
152
+ ## Debug
153
+
154
+ ```js
155
+ // Browser console
156
+ __apostil_debug.enable()
157
+ __apostil_debug.disable()
158
+ ```
159
+
160
+ ## Requirements
161
+
162
+ - React 18+
163
+ - Next.js (App Router)
164
+ - Tailwind CSS
165
+ - lucide-react
166
+
167
+ ## License
168
+
169
+ MIT
package/bin/apostil.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli/index.js");
@@ -0,0 +1,5 @@
1
+ import { A as ApostilStorage } from '../types-oQRt3lYH.js';
2
+
3
+ declare const localStorageAdapter: ApostilStorage;
4
+
5
+ export { localStorageAdapter };
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ // src/adapters/localStorage.ts
4
+ var STORAGE_PREFIX = "apostil-";
5
+ var localStorageAdapter = {
6
+ async load(pageId) {
7
+ if (typeof window === "undefined") return [];
8
+ try {
9
+ const raw = localStorage.getItem(`${STORAGE_PREFIX}${pageId}`);
10
+ return raw ? JSON.parse(raw) : [];
11
+ } catch {
12
+ return [];
13
+ }
14
+ },
15
+ async save(pageId, threads) {
16
+ if (typeof window === "undefined") return;
17
+ localStorage.setItem(`${STORAGE_PREFIX}${pageId}`, JSON.stringify(threads));
18
+ }
19
+ };
20
+ export {
21
+ localStorageAdapter
22
+ };
23
+ //# sourceMappingURL=localStorage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/localStorage.ts"],"sourcesContent":["import type { ApostilStorage, ApostilThread } from \"../types\";\n\nconst STORAGE_PREFIX = \"apostil-\";\n\nexport const localStorageAdapter: ApostilStorage = {\n async load(pageId: string): Promise<ApostilThread[]> {\n if (typeof window === \"undefined\") return [];\n try {\n const raw = localStorage.getItem(`${STORAGE_PREFIX}${pageId}`);\n return raw ? JSON.parse(raw) : [];\n } catch {\n return [];\n }\n },\n\n async save(pageId: string, threads: ApostilThread[]): Promise<void> {\n if (typeof window === \"undefined\") return;\n localStorage.setItem(`${STORAGE_PREFIX}${pageId}`, JSON.stringify(threads));\n },\n};\n"],"mappings":";;;AAEA,IAAM,iBAAiB;AAEhB,IAAM,sBAAsC;AAAA,EACjD,MAAM,KAAK,QAA0C;AACnD,QAAI,OAAO,WAAW,YAAa,QAAO,CAAC;AAC3C,QAAI;AACF,YAAM,MAAM,aAAa,QAAQ,GAAG,cAAc,GAAG,MAAM,EAAE;AAC7D,aAAO,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAAA,IAClC,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,QAAgB,SAAyC;AAClE,QAAI,OAAO,WAAW,YAAa;AACnC,iBAAa,QAAQ,GAAG,cAAc,GAAG,MAAM,IAAI,KAAK,UAAU,OAAO,CAAC;AAAA,EAC5E;AACF;","names":[]}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Next.js API route handler for apostil comment storage.
3
+ * Stores comments as JSON files in the project's .apostil/ directory.
4
+ *
5
+ * Usage:
6
+ *
7
+ * // app/api/apostil/route.ts
8
+ * export { GET, POST } from "apostil/adapters/nextjs";
9
+ *
10
+ * Or with custom directory:
11
+ * import { createNextjsHandler } from "apostil/adapters/nextjs";
12
+ * const { GET, POST } = createNextjsHandler(".my-comments");
13
+ * export { GET, POST };
14
+ */
15
+ declare function createNextjsHandler(directory?: string): {
16
+ GET(request: Request): Promise<Response>;
17
+ POST(request: Request): Promise<Response>;
18
+ };
19
+ declare const GET: (request: Request) => Promise<Response>;
20
+ declare const POST: (request: Request) => Promise<Response>;
21
+
22
+ export { GET, POST, createNextjsHandler };
@@ -0,0 +1,75 @@
1
+ // src/adapters/nextjs.ts
2
+ function createNextjsHandler(directory = ".apostil") {
3
+ return {
4
+ async GET(request) {
5
+ const { promises: fs } = await import("fs");
6
+ const path = await import("path");
7
+ const url = new URL(request.url);
8
+ const pageId = url.searchParams.get("pageId");
9
+ const dir = path.join(process.cwd(), directory);
10
+ if (!pageId) {
11
+ try {
12
+ const files = await fs.readdir(dir);
13
+ const pages = [];
14
+ for (const file2 of files) {
15
+ if (!file2.endsWith(".json")) continue;
16
+ const id = file2.replace(".json", "");
17
+ try {
18
+ const data = await fs.readFile(path.join(dir, file2), "utf-8");
19
+ const threads = JSON.parse(data);
20
+ if (Array.isArray(threads) && threads.length > 0) {
21
+ pages.push({ pageId: id, threads });
22
+ }
23
+ } catch {
24
+ }
25
+ }
26
+ pages.sort((a, b) => {
27
+ const aLatest = Math.max(...a.threads.map((t) => new Date(t.createdAt).getTime()));
28
+ const bLatest = Math.max(...b.threads.map((t) => new Date(t.createdAt).getTime()));
29
+ return bLatest - aLatest;
30
+ });
31
+ return Response.json(pages);
32
+ } catch {
33
+ return Response.json([]);
34
+ }
35
+ }
36
+ const safeName = pageId.replace(/[^a-zA-Z0-9_-]/g, "");
37
+ const file = path.join(dir, `${safeName}.json`);
38
+ try {
39
+ const data = await fs.readFile(file, "utf-8");
40
+ return Response.json(JSON.parse(data));
41
+ } catch {
42
+ return Response.json([]);
43
+ }
44
+ },
45
+ async POST(request) {
46
+ const { promises: fs } = await import("fs");
47
+ const path = await import("path");
48
+ const url = new URL(request.url);
49
+ const pageId = url.searchParams.get("pageId");
50
+ if (!pageId) {
51
+ return Response.json({ error: "Missing pageId" }, { status: 400 });
52
+ }
53
+ const dir = path.join(process.cwd(), directory);
54
+ const safeName = pageId.replace(/[^a-zA-Z0-9_-]/g, "");
55
+ const file = path.join(dir, `${safeName}.json`);
56
+ try {
57
+ await fs.mkdir(dir, { recursive: true });
58
+ const threads = await request.json();
59
+ await fs.writeFile(file, JSON.stringify(threads, null, 2), "utf-8");
60
+ return Response.json({ ok: true });
61
+ } catch (e) {
62
+ return Response.json({ error: String(e) }, { status: 500 });
63
+ }
64
+ }
65
+ };
66
+ }
67
+ var defaultHandler = createNextjsHandler();
68
+ var GET = defaultHandler.GET;
69
+ var POST = defaultHandler.POST;
70
+ export {
71
+ GET,
72
+ POST,
73
+ createNextjsHandler
74
+ };
75
+ //# sourceMappingURL=nextjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/nextjs.ts"],"sourcesContent":["/**\n * Next.js API route handler for apostil comment storage.\n * Stores comments as JSON files in the project's .apostil/ directory.\n *\n * Usage:\n *\n * // app/api/apostil/route.ts\n * export { GET, POST } from \"apostil/adapters/nextjs\";\n *\n * Or with custom directory:\n * import { createNextjsHandler } from \"apostil/adapters/nextjs\";\n * const { GET, POST } = createNextjsHandler(\".my-comments\");\n * export { GET, POST };\n */\n\nexport function createNextjsHandler(directory: string = \".apostil\") {\n return {\n async GET(request: Request) {\n const { promises: fs } = await import(\"fs\");\n const path = await import(\"path\");\n\n const url = new URL(request.url);\n const pageId = url.searchParams.get(\"pageId\");\n const dir = path.join(process.cwd(), directory);\n\n // If no pageId, return ALL comments across all pages\n if (!pageId) {\n try {\n const files = await fs.readdir(dir);\n const pages: { pageId: string; threads: unknown[] }[] = [];\n for (const file of files) {\n if (!file.endsWith(\".json\")) continue;\n const id = file.replace(\".json\", \"\");\n try {\n const data = await fs.readFile(path.join(dir, file), \"utf-8\");\n const threads = JSON.parse(data);\n if (Array.isArray(threads) && threads.length > 0) {\n pages.push({ pageId: id, threads });\n }\n } catch {}\n }\n pages.sort((a, b) => {\n const aLatest = Math.max(...(a.threads as Array<{ createdAt: string }>).map((t) => new Date(t.createdAt).getTime()));\n const bLatest = Math.max(...(b.threads as Array<{ createdAt: string }>).map((t) => new Date(t.createdAt).getTime()));\n return bLatest - aLatest;\n });\n return Response.json(pages);\n } catch {\n return Response.json([]);\n }\n }\n\n // Single page\n const safeName = pageId.replace(/[^a-zA-Z0-9_-]/g, \"\");\n const file = path.join(dir, `${safeName}.json`);\n try {\n const data = await fs.readFile(file, \"utf-8\");\n return Response.json(JSON.parse(data));\n } catch {\n return Response.json([]);\n }\n },\n\n async POST(request: Request) {\n const { promises: fs } = await import(\"fs\");\n const path = await import(\"path\");\n\n const url = new URL(request.url);\n const pageId = url.searchParams.get(\"pageId\");\n if (!pageId) {\n return Response.json({ error: \"Missing pageId\" }, { status: 400 });\n }\n\n const dir = path.join(process.cwd(), directory);\n const safeName = pageId.replace(/[^a-zA-Z0-9_-]/g, \"\");\n const file = path.join(dir, `${safeName}.json`);\n\n try {\n await fs.mkdir(dir, { recursive: true });\n const threads = await request.json();\n await fs.writeFile(file, JSON.stringify(threads, null, 2), \"utf-8\");\n return Response.json({ ok: true });\n } catch (e) {\n return Response.json({ error: String(e) }, { status: 500 });\n }\n },\n };\n}\n\n// Default handler with \".apostil\" directory\nconst defaultHandler = createNextjsHandler();\nexport const GET = defaultHandler.GET;\nexport const POST = defaultHandler.POST;\n"],"mappings":"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","names":["file"]}
@@ -0,0 +1,9 @@
1
+ import { A as ApostilStorage } from '../types-oQRt3lYH.js';
2
+
3
+ /**
4
+ * REST API storage adapter.
5
+ * Works with any backend that implements GET/POST for threads.
6
+ */
7
+ declare function createRestAdapter(baseUrl: string): ApostilStorage;
8
+
9
+ export { createRestAdapter };
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ import {
3
+ createRestAdapter
4
+ } from "../chunk-ASP7WAEG.js";
5
+ export {
6
+ createRestAdapter
7
+ };
8
+ //# sourceMappingURL=rest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ // src/adapters/rest.ts
4
+ function createRestAdapter(baseUrl) {
5
+ return {
6
+ async load(pageId) {
7
+ const url = `${baseUrl}?pageId=${encodeURIComponent(pageId)}`;
8
+ try {
9
+ const res = await fetch(url);
10
+ if (!res.ok) {
11
+ console.warn(`[apostil] load failed: ${res.status} ${res.statusText} \u2014 ${url}`);
12
+ return [];
13
+ }
14
+ const data = await res.json();
15
+ return data;
16
+ } catch (e) {
17
+ console.warn(`[apostil] load error:`, e, `\u2014 ${url}`);
18
+ return [];
19
+ }
20
+ },
21
+ async save(pageId, threads) {
22
+ const url = `${baseUrl}?pageId=${encodeURIComponent(pageId)}`;
23
+ try {
24
+ const res = await fetch(url, {
25
+ method: "POST",
26
+ headers: { "Content-Type": "application/json" },
27
+ body: JSON.stringify(threads)
28
+ });
29
+ if (!res.ok) {
30
+ console.warn(`[apostil] save failed: ${res.status} ${res.statusText} \u2014 ${url}`);
31
+ }
32
+ } catch (e) {
33
+ console.warn(`[apostil] save error:`, e, `\u2014 ${url}`);
34
+ }
35
+ }
36
+ };
37
+ }
38
+
39
+ export {
40
+ createRestAdapter
41
+ };
42
+ //# sourceMappingURL=chunk-ASP7WAEG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/rest.ts"],"sourcesContent":["import type { ApostilStorage, ApostilThread } from \"../types\";\n\n/**\n * REST API storage adapter.\n * Works with any backend that implements GET/POST for threads.\n */\nexport function createRestAdapter(baseUrl: string): ApostilStorage {\n return {\n async load(pageId: string): Promise<ApostilThread[]> {\n const url = `${baseUrl}?pageId=${encodeURIComponent(pageId)}`;\n try {\n const res = await fetch(url);\n if (!res.ok) {\n console.warn(`[apostil] load failed: ${res.status} ${res.statusText} — ${url}`);\n return [];\n }\n const data = await res.json();\n return data;\n } catch (e) {\n console.warn(`[apostil] load error:`, e, `— ${url}`);\n return [];\n }\n },\n\n async save(pageId: string, threads: ApostilThread[]): Promise<void> {\n const url = `${baseUrl}?pageId=${encodeURIComponent(pageId)}`;\n try {\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(threads),\n });\n if (!res.ok) {\n console.warn(`[apostil] save failed: ${res.status} ${res.statusText} — ${url}`);\n }\n } catch (e) {\n console.warn(`[apostil] save error:`, e, `— ${url}`);\n }\n },\n };\n}\n"],"mappings":";;;AAMO,SAAS,kBAAkB,SAAiC;AACjE,SAAO;AAAA,IACL,MAAM,KAAK,QAA0C;AACnD,YAAM,MAAM,GAAG,OAAO,WAAW,mBAAmB,MAAM,CAAC;AAC3D,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,YAAI,CAAC,IAAI,IAAI;AACX,kBAAQ,KAAK,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,WAAM,GAAG,EAAE;AAC9E,iBAAO,CAAC;AAAA,QACV;AACA,cAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,eAAO;AAAA,MACT,SAAS,GAAG;AACV,gBAAQ,KAAK,yBAAyB,GAAG,UAAK,GAAG,EAAE;AACnD,eAAO,CAAC;AAAA,MACV;AAAA,IACF;AAAA,IAEA,MAAM,KAAK,QAAgB,SAAyC;AAClE,YAAM,MAAM,GAAG,OAAO,WAAW,mBAAmB,MAAM,CAAC;AAC3D,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,KAAK;AAAA,UAC3B,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,QAC9B,CAAC;AACD,YAAI,CAAC,IAAI,IAAI;AACX,kBAAQ,KAAK,0BAA0B,IAAI,MAAM,IAAI,IAAI,UAAU,WAAM,GAAG,EAAE;AAAA,QAChF;AAAA,MACF,SAAS,GAAG;AACV,gBAAQ,KAAK,yBAAyB,GAAG,UAAK,GAAG,EAAE;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import fs from "fs/promises";
5
+ import path from "path";
6
+ var args = process.argv.slice(2);
7
+ var command = args[0];
8
+ if (command === "init") {
9
+ init();
10
+ } else if (command === "remove") {
11
+ remove();
12
+ } else if (command === "help" || command === "--help" || command === "-h" || !command) {
13
+ printHelp();
14
+ } else {
15
+ console.log(` Unknown command: ${command}
16
+ `);
17
+ printHelp();
18
+ }
19
+ function printHelp() {
20
+ console.log(`
21
+ apostil \u2014 Pin-and-comment feedback for React & Next.js
22
+
23
+ Usage:
24
+ npx apostil init Set up apostil in your Next.js project
25
+ npx apostil remove Remove apostil from your project
26
+ npx apostil help Show this help
27
+ `);
28
+ }
29
+ async function init() {
30
+ const cwd = process.cwd();
31
+ const appDir = await findAppDir(cwd);
32
+ if (!appDir) {
33
+ console.log(" Could not find a Next.js app/ directory.");
34
+ console.log(" Make sure you're running this from your project root.\n");
35
+ process.exit(1);
36
+ }
37
+ const useSrc = appDir.includes("src/app");
38
+ console.log("\n Setting up apostil...\n");
39
+ const apiDir = path.join(appDir, "api", "apostil");
40
+ const apiFile = path.join(apiDir, "route.ts");
41
+ if (await fileExists(apiFile)) {
42
+ console.log(" \u2713 API route already exists");
43
+ } else {
44
+ await fs.mkdir(apiDir, { recursive: true });
45
+ await fs.writeFile(
46
+ apiFile,
47
+ `export { GET, POST } from "apostil/adapters/nextjs";
48
+ `,
49
+ "utf-8"
50
+ );
51
+ console.log(" \u2713 Created ${rel(cwd, apiFile)}");
52
+ }
53
+ const componentsDir = path.join(cwd, useSrc ? "src/components" : "components");
54
+ const wrapperFile = path.join(componentsDir, "apostil-wrapper.tsx");
55
+ if (await fileExists(wrapperFile)) {
56
+ console.log(" \u2713 Wrapper component already exists");
57
+ } else {
58
+ await fs.mkdir(componentsDir, { recursive: true });
59
+ await fs.writeFile(wrapperFile, getWrapperComponent(), "utf-8");
60
+ console.log(` \u2713 Created ${rel(cwd, wrapperFile)}`);
61
+ }
62
+ const commentsDir = path.join(cwd, ".apostil");
63
+ await fs.mkdir(commentsDir, { recursive: true });
64
+ console.log(" \u2713 Created .apostil/ directory");
65
+ const gitignorePath = path.join(cwd, ".gitignore");
66
+ let gitignore = "";
67
+ try {
68
+ gitignore = await fs.readFile(gitignorePath, "utf-8");
69
+ } catch {
70
+ }
71
+ if (!gitignore.includes(".apostil")) {
72
+ const entry = "\n# Apostil comments\n.apostil/\n";
73
+ await fs.appendFile(gitignorePath, entry, "utf-8");
74
+ console.log(" \u2713 Added .apostil/ to .gitignore");
75
+ } else {
76
+ console.log(" \u2713 .gitignore already configured");
77
+ }
78
+ const layoutInjected = await injectIntoLayout(appDir, useSrc);
79
+ if (layoutInjected) {
80
+ console.log(` \u2713 Added <ApostilWrapper> to root layout`);
81
+ }
82
+ console.log("\n Done! Run your dev server and press C to start commenting.\n");
83
+ }
84
+ async function remove() {
85
+ const cwd = process.cwd();
86
+ const appDir = await findAppDir(cwd);
87
+ const useSrc = appDir?.includes("src/app") ?? false;
88
+ console.log("\n Removing apostil...\n");
89
+ if (appDir) {
90
+ const apiDir = path.join(appDir, "api", "apostil");
91
+ if (await fileExists(path.join(apiDir, "route.ts"))) {
92
+ await fs.rm(apiDir, { recursive: true });
93
+ console.log(" \u2713 Removed API route");
94
+ }
95
+ }
96
+ const componentsDir = path.join(cwd, useSrc ? "src/components" : "components");
97
+ const wrapperFile = path.join(componentsDir, "apostil-wrapper.tsx");
98
+ if (await fileExists(wrapperFile)) {
99
+ await fs.rm(wrapperFile);
100
+ console.log(" \u2713 Removed wrapper component");
101
+ }
102
+ if (appDir) {
103
+ const unwrapped = await removeFromLayout(appDir);
104
+ if (unwrapped) {
105
+ console.log(" \u2713 Removed <ApostilWrapper> from root layout");
106
+ }
107
+ }
108
+ const commentsDir = path.join(cwd, ".apostil");
109
+ if (await fileExists(commentsDir)) {
110
+ await fs.rm(commentsDir, { recursive: true });
111
+ console.log(" \u2713 Removed .apostil/ directory");
112
+ }
113
+ const gitignorePath = path.join(cwd, ".gitignore");
114
+ try {
115
+ let gitignore = await fs.readFile(gitignorePath, "utf-8");
116
+ gitignore = gitignore.replace(/\n?# Apostil comments\n\.apostil\/\n?/g, "");
117
+ await fs.writeFile(gitignorePath, gitignore, "utf-8");
118
+ console.log(" \u2713 Cleaned .gitignore");
119
+ } catch {
120
+ }
121
+ console.log(`
122
+ Done! Now run: npm uninstall apostil
123
+ `);
124
+ }
125
+ function getWrapperComponent() {
126
+ return `"use client";
127
+
128
+ import { usePathname } from "next/navigation";
129
+ import {
130
+ ApostilProvider,
131
+ CommentOverlay,
132
+ CommentToggle,
133
+ CommentSidebar,
134
+ } from "apostil";
135
+
136
+ export function ApostilWrapper({ children }: { children: React.ReactNode }) {
137
+ const pathname = usePathname();
138
+ const pageId = pathname.replace(/\\//g, "--").replace(/^--/, "") || "home";
139
+
140
+ return (
141
+ <ApostilProvider pageId={pageId}>
142
+ {children}
143
+ <CommentOverlay />
144
+ <CommentSidebar />
145
+ <CommentToggle />
146
+ </ApostilProvider>
147
+ );
148
+ }
149
+ `;
150
+ }
151
+ async function injectIntoLayout(appDir, useSrc) {
152
+ const layoutPath = await findLayout(appDir);
153
+ if (!layoutPath) return false;
154
+ let content = await fs.readFile(layoutPath, "utf-8");
155
+ if (content.includes("ApostilWrapper")) {
156
+ console.log(" \u2713 Layout already has <ApostilWrapper>");
157
+ return false;
158
+ }
159
+ const importPath = useSrc ? "@/components/apostil-wrapper" : "../components/apostil-wrapper";
160
+ const importLine = `import { ApostilWrapper } from "${importPath}";
161
+ `;
162
+ const importRegex = /^import\s.+$/gm;
163
+ let lastImportIndex = 0;
164
+ let match;
165
+ while ((match = importRegex.exec(content)) !== null) {
166
+ lastImportIndex = match.index + match[0].length;
167
+ }
168
+ if (lastImportIndex > 0) {
169
+ content = content.slice(0, lastImportIndex) + "\n" + importLine + content.slice(lastImportIndex);
170
+ } else {
171
+ const useDirective = content.match(/^["']use (client|server)["'];?\n/);
172
+ const insertAt = useDirective ? useDirective[0].length : 0;
173
+ content = content.slice(0, insertAt) + importLine + content.slice(insertAt);
174
+ }
175
+ const bodyChildrenRegex = /(<body[^>]*>)([\s\S]*?)(\{[\s]*children[\s]*\})([\s\S]*?)(<\/body>)/;
176
+ const bodyMatch = content.match(bodyChildrenRegex);
177
+ if (bodyMatch) {
178
+ content = content.replace(
179
+ bodyChildrenRegex,
180
+ `$1$2<ApostilWrapper>$3</ApostilWrapper>$4$5`
181
+ );
182
+ } else {
183
+ const childrenRegex = /(\{[\s]*children[\s]*\})/;
184
+ if (childrenRegex.test(content)) {
185
+ content = content.replace(childrenRegex, `<ApostilWrapper>$1</ApostilWrapper>`);
186
+ } else {
187
+ console.log(" \u26A0 Could not find {children} in layout \u2014 add <ApostilWrapper> manually");
188
+ return false;
189
+ }
190
+ }
191
+ await fs.writeFile(layoutPath, content, "utf-8");
192
+ return true;
193
+ }
194
+ async function removeFromLayout(appDir) {
195
+ const layoutPath = await findLayout(appDir);
196
+ if (!layoutPath) return false;
197
+ let content = await fs.readFile(layoutPath, "utf-8");
198
+ if (!content.includes("ApostilWrapper")) return false;
199
+ content = content.replace(/import\s*\{[^}]*ApostilWrapper[^}]*\}\s*from\s*["'][^"']+["'];?\n?/g, "");
200
+ content = content.replace(/<ApostilWrapper>([\s\S]*?)<\/ApostilWrapper>/g, "$1");
201
+ await fs.writeFile(layoutPath, content, "utf-8");
202
+ return true;
203
+ }
204
+ async function findAppDir(cwd) {
205
+ for (const candidate of ["src/app", "app"]) {
206
+ const dir = path.join(cwd, candidate);
207
+ try {
208
+ const stat = await fs.stat(dir);
209
+ if (stat.isDirectory()) return dir;
210
+ } catch {
211
+ }
212
+ }
213
+ return null;
214
+ }
215
+ async function findLayout(appDir) {
216
+ for (const ext of ["tsx", "jsx", "ts", "js"]) {
217
+ const file = path.join(appDir, `layout.${ext}`);
218
+ if (await fileExists(file)) return file;
219
+ }
220
+ return null;
221
+ }
222
+ async function fileExists(p) {
223
+ try {
224
+ await fs.stat(p);
225
+ return true;
226
+ } catch {
227
+ return false;
228
+ }
229
+ }
230
+ function rel(cwd, filePath) {
231
+ return path.relative(cwd, filePath);
232
+ }
233
+ //# sourceMappingURL=index.js.map