create-snagset 0.0.1

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 Eurico Sá Fernandes
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,67 @@
1
+ # `create-snagset`
2
+
3
+ Scaffold a self-hosted **[Snagset](https://github.com/snagset/snagset)** instance
4
+ — click-to-comment review for your clients' live sites.
5
+
6
+ ```bash
7
+ npx create-snagset my-review
8
+ cd my-review
9
+ docker compose up
10
+ ```
11
+
12
+ Postgres is included. Open `http://localhost:8080` and follow the claim URL in
13
+ the logs. Already have a database? Put its URL in `.env`, then `npm install &&
14
+ npm start`.
15
+
16
+ ## What you get
17
+
18
+ A directory with everything an instance needs and nothing it does not:
19
+
20
+ | | |
21
+ |---|---|
22
+ | `compose.yml` | The server and a Postgres, for `docker compose up` |
23
+ | `.env` | Configuration. **`DATABASE_URL` is the only required variable** — a signing key is generated into it for you |
24
+ | `package.json` | `@snagset/server` as a dependency, with `start`, `migrate` and `env` scripts |
25
+ | `vercel.json` + `api/index.js` | Deploy to Vercel instead, if you would rather not run anything |
26
+ | `README.md` | The operating manual, written for the instance you just made |
27
+
28
+ ## Then what
29
+
30
+ Sign in, add a site, and paste the tag it gives you into the site you want
31
+ reviewed:
32
+
33
+ ```html
34
+ <script src="http://localhost:8080/snag.js" data-snag-site="prj_…" defer></script>
35
+ ```
36
+
37
+ Your client opens a review link, clicks the thing that's wrong, and types.
38
+ **No account, no extension, no login** — the link is the whole identity. You
39
+ get the pin, the screenshot, the console and the route.
40
+
41
+ Prefer an import? `npm i snagset` gives you the same tag from
42
+ `snagset.init({ host, site })`, or the framework subpaths — `snagset/react`,
43
+ `snagset/vue`, `snagset/next`, `snagset/vite` (which covers Nuxt and
44
+ SvelteKit too), `snagset/astro`.
45
+
46
+ ## What it costs to run
47
+
48
+ A Node process and a Postgres. Screenshots want an S3-compatible bucket (R2,
49
+ Spaces, MinIO, AWS) and notifications want a mailer — **both optional**, and
50
+ the product is honest without them: no bucket means annotation mode and the
51
+ widget says screenshots are unavailable rather than offering one that fails.
52
+
53
+ ## Back up the signing key
54
+
55
+ If you did not set `SNAGSET_SECRET_KEY`, one was generated into your `.env`.
56
+ **Treat it as a password.** Without it every reviewer session is dead and any
57
+ stored credential is unreadable ciphertext.
58
+
59
+ ---
60
+
61
+ **Early release.** The install path is tested end to end, but this is a `0.0.x`
62
+ line: expect the odd rough edge, and pin the version if you need stability.
63
+ Issues and notes are welcome at
64
+ [snagset/snagset](https://github.com/snagset/snagset).
65
+
66
+ AGPL-3.0-only. The widget you put on a client's site is
67
+ [`snagset`](https://www.npmjs.com/package/snagset), and it is MIT.
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { scaffold } from "./scaffold.js";
3
+ /**
4
+ * The bin. Argument handling and exit codes only — everything that touches the
5
+ * filesystem is in `scaffold.ts`, which has no side effects at import time and
6
+ * is therefore testable.
7
+ */
8
+ const target = process.argv[2];
9
+ if (!target || target.startsWith("-")) {
10
+ process.stderr.write("usage: npx create-snagset <directory>\n\n" +
11
+ " npx create-snagset my-review\n" +
12
+ " cd my-review && docker compose up\n");
13
+ process.exit(2);
14
+ }
15
+ try {
16
+ scaffold(target);
17
+ }
18
+ catch (e) {
19
+ process.stderr.write(`✗ ${e instanceof Error ? e.message : String(e)}\n`);
20
+ process.exit(2);
21
+ }
22
+ const name = target.split("/").pop() || "snagset";
23
+ process.stdout.write(`\n ✓ ${name}\n\n` +
24
+ ` cd ${target}\n` +
25
+ ` docker compose up\n\n` +
26
+ ` Then open http://localhost:8080 and follow the claim URL in the logs.\n` +
27
+ ` No Docker? Put a Postgres URL in .env, then \`npm install && npm start\`.\n\n` +
28
+ ` A signing key was generated into .env. Back it up — without it every\n` +
29
+ ` session and stored credential is unrecoverable.\n\n`);
@@ -0,0 +1,106 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { randomBytes } from "node:crypto";
5
+ /**
6
+ * `npx create-snagset` — D63's self-host, the `create-next-app` model.
7
+ *
8
+ * ── WHY A SCAFFOLDER AND NOT `git clone` ──
9
+ *
10
+ * FasterFixes ships `git clone` + `pnpm install` + a build, and it is the
11
+ * weakest part of their docs: it makes the operator your build engineer. They
12
+ * own the Node version, the lockfile and the build step, and every upgrade is
13
+ * a pull, a reinstall and a rebuild against a tree that has moved underneath
14
+ * them. Sixteen environment variables and four mandatory external services
15
+ * before the first run.
16
+ *
17
+ * What this writes instead is a small repository the operator OWNS — their
18
+ * `.env`, their compose file, their deploy config — that depends on
19
+ * `@snagset/server` like any other package. Upgrading is `npm update`. There
20
+ * is no build. Our source is not in their tree unless they want it, and when
21
+ * they do, it is AGPL and the repository is public.
22
+ *
23
+ * ── WHY NOT `npm install @snagset/server` INTO A DIRECTORY THEY INVENTED ──
24
+ *
25
+ * Because the interesting files are not the dependency. A working self-host is
26
+ * a compose file with Postgres in it, a `.env` with the one required variable
27
+ * already spelled, a Vercel config, a serverless entry point, and a README
28
+ * that says which of those matter. `npm install` gives you a `node_modules`
29
+ * and a blank page; measured on a cold room, the install is 5 seconds and the
30
+ * remaining friction is entirely "what do I write, and where".
31
+ */
32
+ const HERE = dirname(fileURLToPath(import.meta.url));
33
+ /**
34
+ * `dist/template` beside the compiled entry (see scripts/copy-template.mjs),
35
+ * falling back to `../template` so the package runs from source in this repo.
36
+ *
37
+ * The fallback is the honest version of "it works on my machine": without it
38
+ * every local run and every test would read a directory that only exists after
39
+ * a build, and the first person to try `node src/index.ts` would get an empty
40
+ * scaffold with no error.
41
+ */
42
+ const TEMPLATE = existsSync(join(HERE, "template"))
43
+ ? join(HERE, "template")
44
+ : join(HERE, "..", "template");
45
+ /**
46
+ * `_package.json` on disk, `package.json` when written.
47
+ *
48
+ * npm STRIPS a `.gitignore` from a published tarball and rewrites a nested
49
+ * `package.json`, which is why these two ship under other names. The `_`
50
+ * prefix is the convention `create-next-app` and `create-vite` both use, for
51
+ * exactly this reason, and a template that ships without its `.gitignore`
52
+ * produces a scaffold whose first `git add .` commits `node_modules` and a
53
+ * `.env` holding a database password.
54
+ */
55
+ export const RENAME = {
56
+ "_package.json": "package.json",
57
+ _gitignore: ".gitignore",
58
+ "_env.example": ".env.example",
59
+ "api_index.js": "api/index.js",
60
+ };
61
+ /**
62
+ * Scaffold into `dir`. Pure enough to test: it takes and returns paths, throws
63
+ * a sentence rather than calling `process.exit`, and writes nothing outside
64
+ * the directory it was given.
65
+ *
66
+ * Split from the bin for one specific reason — the first version guarded
67
+ * `main()` with `if (/create-snagset/.test(process.argv[1]))` so a test could
68
+ * import it, and the test file's own path contains `create-snagset`, so
69
+ * importing it would have scaffolded into the test runner's working directory.
70
+ * A module with side effects at import time cannot be tested; the fix is not a
71
+ * cleverer guard.
72
+ */
73
+ export function scaffold(target, cwd = process.cwd()) {
74
+ const dir = resolve(cwd, target);
75
+ // A non-empty directory is refused rather than merged into. Merging is how a
76
+ // scaffolder overwrites somebody's `.env`, and there is no undo for that.
77
+ if (existsSync(dir) && readdirSync(dir).length > 0) {
78
+ throw new Error(`${target} already exists and is not empty.`);
79
+ }
80
+ mkdirSync(join(dir, "api"), { recursive: true });
81
+ const name = target.split("/").pop() || "snagset";
82
+ // Pinned to THIS scaffolder's version, so `create-snagset@1.4.2` produces a
83
+ // repo on `@snagset/server@1.4.2`. A caret here would let a fresh scaffold
84
+ // resolve to a server this template has never been run against.
85
+ const version = JSON.parse(readFileSync(join(HERE, "..", "package.json"), "utf8")).version;
86
+ for (const entry of readdirSync(TEMPLATE)) {
87
+ const out = RENAME[entry] ?? entry;
88
+ const body = readFileSync(join(TEMPLATE, entry), "utf8")
89
+ .replaceAll("__NAME__", name)
90
+ .replaceAll("__VERSION__", version);
91
+ writeFileSync(join(dir, out), body);
92
+ }
93
+ /**
94
+ * `.env` is written, not just `.env.example`, and with a REAL signing key.
95
+ *
96
+ * The alternative — ship an example and tell them to copy it — adds a step
97
+ * between the scaffold and the first run, which is the step where an install
98
+ * stalls. And generating the key here rather than letting the server do it
99
+ * on first boot matters for the deploy case: 08 §4.2's generate-to-disk is
100
+ * bin-only, so an operator who scaffolds locally and deploys to a platform
101
+ * would otherwise discover on the far side that they had no key to set.
102
+ */
103
+ const env = readFileSync(join(dir, ".env.example"), "utf8").replace("# SNAGSET_SECRET_KEY=", `SNAGSET_SECRET_KEY=${randomBytes(32).toString("base64url")}`);
104
+ writeFileSync(join(dir, ".env"), env);
105
+ return dir;
106
+ }
@@ -0,0 +1,82 @@
1
+ # __NAME__
2
+
3
+ A self-hosted [Snagset](https://github.com/snagset/snagset) instance. Click-to-comment
4
+ review for your clients' live sites.
5
+
6
+ ## Run it
7
+
8
+ ```bash
9
+ docker compose up
10
+ ```
11
+
12
+ Postgres is included. Open http://localhost:8080 and follow the claim URL
13
+ printed in the logs.
14
+
15
+ **Already have a database?** Put its URL in `.env`, then:
16
+
17
+ ```bash
18
+ npm install
19
+ npm start
20
+ ```
21
+
22
+ ## Add a site
23
+
24
+ Sign in, open **Sites**, and add one. You get a script tag:
25
+
26
+ ```html
27
+ <script src="http://localhost:8080/snag.js" data-snag-site="prj_…" defer></script>
28
+ ```
29
+
30
+ Paste it into the site you want reviewed, before `</body>`. Or install the npm
31
+ package — `npm i snagset` — and `snagset.init({ host, site })`, or one of the
32
+ framework subpaths: `snagset/react`, `snagset/vue`, `snagset/next`,
33
+ `snagset/vite` (which also covers Nuxt and SvelteKit), `snagset/astro`. They
34
+ all write the same tag.
35
+
36
+ **The site's address has to be on the allow-list.** That is the field beside
37
+ the name when you add it. A tag on an address that is not listed loads and is
38
+ ignored, which is the hardest failure here to spot from the outside.
39
+
40
+ ## Configure
41
+
42
+ `.env`. Only `DATABASE_URL` is required.
43
+
44
+ ```bash
45
+ npm run env # every variable this build accepts, with its default
46
+ npm run env MAIL # just the ones matching MAIL
47
+ ```
48
+
49
+ Screenshots need an S3-compatible bucket (R2, Spaces, MinIO, AWS).
50
+ Notifications need a mailer. Both are optional: with no bucket you get
51
+ annotation mode and the widget says screenshots are unavailable rather than
52
+ offering one that fails; with no mailer, notifications are off and the
53
+ dashboard says so.
54
+
55
+ ## Deploy it
56
+
57
+ **Vercel.** `vercel.json` and `api/index.js` are here already. Add a Postgres
58
+ from the marketplace, set `SNAGSET_SECRET_KEY` (required on serverless — the
59
+ filesystem is ephemeral, so a generated key would differ per instance and
60
+ nothing would decrypt), and add a release command:
61
+
62
+ ```
63
+ npx snagset migrate
64
+ ```
65
+
66
+ **Anything that runs Node.** `npm start`. It serves the API, the dashboard and
67
+ the widget on one port.
68
+
69
+ ## Upgrading
70
+
71
+ ```bash
72
+ npm update @snagset/server
73
+ ```
74
+
75
+ Migrations run on boot. There is no build step to redo.
76
+
77
+ ## Back up your signing key
78
+
79
+ If you did not set `SNAGSET_SECRET_KEY`, the server generated one at
80
+ `.snagset/secret.key` on first boot. **Treat it as a password.** Without it,
81
+ every reviewer session is dead and any stored credential is unreadable
82
+ ciphertext.
@@ -0,0 +1,27 @@
1
+ # ── Required ───────────────────────────────────────────────────────────
2
+ # Any Postgres 14+. `docker compose up` provides one at this URL.
3
+ DATABASE_URL=postgres://snagset:snagset@localhost:5432/snagset
4
+
5
+ # ── Strongly recommended ───────────────────────────────────────────────
6
+ # 32 random bytes, base64url. Generated to .snagset/secret.key on first boot
7
+ # if unset — which is fine on one machine and wrong everywhere else, because
8
+ # each instance would invent a different one.
9
+ #
10
+ # node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
11
+ # SNAGSET_SECRET_KEY=
12
+
13
+ # The public origin. Review links and the claim URL are built from it.
14
+ # SNAGSET_BASE_URL=http://localhost:8080
15
+
16
+ # ── Claim it without a browser (optional) ──────────────────────────────
17
+ # Set these and the first `npm start` creates the owner and the first site.
18
+ # All of it is ignored once an owner exists, so it is safe to leave here.
19
+ # SNAGSET_BOOTSTRAP_OWNER_EMAIL=you@example.com
20
+ # SNAGSET_BOOTSTRAP_OWNER_PASSWORD_FILE=./owner-password
21
+ # SNAGSET_BOOTSTRAP_PROJECT_NAME=First site
22
+ # SNAGSET_BOOTSTRAP_PROJECT_ORIGINS=https://staging.example.com,http://localhost:3000
23
+
24
+ # ── Everything else ────────────────────────────────────────────────────
25
+ # `npm run env` prints every variable this build accepts, with its default.
26
+ # Screenshots need an S3-compatible bucket; notifications need a mailer.
27
+ # Both are optional and the product is honest about running without them.
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ # Your database URL and your signing key live here. Never commit it.
3
+ .env
4
+ .env.local
5
+ # The signing key the server generates on first boot if you did not set one.
6
+ # Back this up; without it every session and encrypted credential is lost.
7
+ .snagset/
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "__NAME__",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "start": "snagset serve",
7
+ "migrate": "snagset migrate",
8
+ "bootstrap": "snagset bootstrap",
9
+ "env": "snagset env",
10
+ "sites": "snagset projects"
11
+ },
12
+ "dependencies": {
13
+ "@snagset/server": "__VERSION__"
14
+ },
15
+ "engines": { "node": ">=22" }
16
+ }
@@ -0,0 +1,8 @@
1
+ // The serverless entry — one line, because `@snagset/server` exports the
2
+ // Web-standard `fetch(Request) => Response` handler every platform accepts.
3
+ //
4
+ // Vercel, Netlify Functions, Cloudflare Workers and Deno Deploy all take this
5
+ // shape. Migrations run as a DEPLOY step, not on cold start (see README) —
6
+ // migrating on a request's critical path would put every instance behind one
7
+ // applying DDL, on somebody's first page load.
8
+ export { default } from "@snagset/server/serverless";
@@ -0,0 +1,40 @@
1
+ # One command: `docker compose up`.
2
+ #
3
+ # Postgres is included so there is nothing to provision before the first run.
4
+ # Point DATABASE_URL at a managed database instead — Neon, Supabase, RDS — and
5
+ # delete the `db` service; nothing else changes.
6
+ services:
7
+ db:
8
+ image: postgres:18-alpine
9
+ environment:
10
+ POSTGRES_USER: snagset
11
+ POSTGRES_PASSWORD: snagset
12
+ POSTGRES_DB: snagset
13
+ ports: ["5432:5432"]
14
+ volumes: ["snagset-db:/var/lib/postgresql/data"]
15
+ healthcheck:
16
+ # The server migrates on boot, so it must not start against a database
17
+ # that is still initialising — the advisory lock would be taken against
18
+ # a socket that is not accepting connections yet.
19
+ test: ["CMD-SHELL", "pg_isready -U snagset"]
20
+ interval: 2s
21
+ timeout: 3s
22
+ retries: 20
23
+
24
+ snagset:
25
+ image: node:22-alpine
26
+ working_dir: /app
27
+ volumes: ["./:/app"]
28
+ command: sh -c "npm install --omit=dev && npm start"
29
+ env_file: [.env]
30
+ environment:
31
+ # Inside the compose network the database is `db`, not `localhost` —
32
+ # overridden here so the same .env works for `npm start` on the host.
33
+ DATABASE_URL: postgres://snagset:snagset@db:5432/snagset
34
+ SNAGSET_BIND_ADDRESS: 0.0.0.0
35
+ ports: ["8080:8080"]
36
+ depends_on:
37
+ db: { condition: service_healthy }
38
+
39
+ volumes:
40
+ snagset-db:
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://openapi.vercel.sh/vercel.json",
3
+ "crons": [
4
+ { "path": "/api/jobs/run", "schedule": "* * * * *" }
5
+ ]
6
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "create-snagset",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Scaffold a self-hosted Snagset instance — a repository you own, with Docker Compose, a Vercel config, and one variable to fill in.",
7
+ "license": "AGPL-3.0-only",
8
+ "bin": {
9
+ "create-snagset": "./dist/index.js"
10
+ },
11
+ "scripts": {
12
+ "typecheck": "tsc -p tsconfig.json --noEmit",
13
+ "test": "node --experimental-strip-types --test test/*.test.ts",
14
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-template.mjs",
15
+ "prepack": "tsc -p tsconfig.build.json && node scripts/copy-template.mjs && node ../../tools/publish-manifest.mjs strip",
16
+ "postpack": "node ../../tools/publish-manifest.mjs restore"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/snagset/snagset.git",
32
+ "directory": "packages/create-snagset"
33
+ },
34
+ "homepage": "https://github.com/snagset/snagset",
35
+ "bugs": {
36
+ "url": "https://github.com/snagset/snagset/issues"
37
+ }
38
+ }