@rtrentjones/greenlight 0.2.26 → 0.2.27

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/dist/bin.js CHANGED
@@ -478,7 +478,7 @@ function tokensForTool(tool) {
478
478
  }
479
479
 
480
480
  // src/version.ts
481
- var MODULE_REF = "v0.2.26";
481
+ var MODULE_REF = "v0.2.27";
482
482
  var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
483
483
  function moduleSource(module, ref = MODULE_REF) {
484
484
  return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtrentjones/greenlight",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
4
4
  "description": "Greenlight CLI — setup and lifecycle for the harness.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,10 +31,10 @@
31
31
  "@anthropic-ai/sdk": "^0.69.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@rtrentjones/greenlight-adapters": "0.2.26",
35
- "@rtrentjones/greenlight-loop": "0.2.26",
36
- "@rtrentjones/greenlight-shared": "0.2.26",
37
- "@rtrentjones/greenlight-verify": "0.2.26"
34
+ "@rtrentjones/greenlight-adapters": "0.2.27",
35
+ "@rtrentjones/greenlight-loop": "0.2.27",
36
+ "@rtrentjones/greenlight-shared": "0.2.27",
37
+ "@rtrentjones/greenlight-verify": "0.2.27"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node scripts/copy-assets.mjs && tsup",
@@ -1,5 +1,27 @@
1
- # `_template-next` (placeholder)
1
+ # `_template-next` — Next.js on Vercel + Neon
2
2
 
3
- Lane template for **Next.js on Vercel** with Supabase verify mode `api + playwright`.
3
+ The lane template `greenlight add <name> --lane next --target vercel --data neon` scaffolds. A minimal
4
+ real app that **reads Neon at request time** and **migrates its schema on deploy** — the Greenlight
5
+ Neon convention end to end. Verify mode: `api` (the page runs a live query, so a broken DB 500s).
4
6
 
5
- > **Phase 0:** placeholder only. Real template content arrives when the `next` lane is exercised (HeistMind migration, **Phase 9** — docs/archive/greenlight-v1.md §16). Materialized into a tool by `greenlight add` / `greenlight adopt`.
7
+ ## How it maps to Greenlight
8
+
9
+ - **Connection strings** come from the env, wired by the `neon` Terraform module per Vercel target:
10
+ `DATABASE_URL` (pooled) for runtime reads ([lib/db.ts](lib/db.ts)), `DIRECT_URL` (direct) for
11
+ migrations. Prod build → prod branch, preview build → its own branch — same code, different data.
12
+ - **Schema as code** lives in [migrations/](migrations) (plain `.sql`). The app's build runs
13
+ [scripts/migrate.mjs](scripts/migrate.mjs) (`package.json` `build` = `node scripts/migrate.mjs &&
14
+ next build`), so a deploy creates/edits tables. A failed migration fails the build → never goes live.
15
+ - **Greenlight's gate**: run `greenlight migrations scan` in CI before the migrate (the dangerous-SQL
16
+ check). See [docs/migrations.md](../../docs/migrations.md). Greenlight does **not** run migrations.
17
+
18
+ ## Make it yours
19
+
20
+ - Prefer an ORM? Swap the plain-SQL migrations for Drizzle/Prisma — keep the build running *your*
21
+ migrate against `DIRECT_URL`, and keep `migrations scan` pointed at the generated SQL.
22
+ - Ephemeral per-PR preview branches: connect the native **Neon↔Vercel** integration (it creates a
23
+ branch + injects `DATABASE_URL` per preview); the stable prod/beta branches stay in Terraform.
24
+
25
+ ## Local dev
26
+
27
+ Set `DATABASE_URL` + `DIRECT_URL` to a Neon branch, then `pnpm migrate && pnpm dev`.
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export const metadata = {
4
+ title: 'Neon probe',
5
+ description: 'A minimal Next-on-Neon Greenlight tool.',
6
+ };
7
+
8
+ export default function RootLayout({ children }: { children: ReactNode }) {
9
+ return (
10
+ <html lang="en">
11
+ <body>{children}</body>
12
+ </html>
13
+ );
14
+ }
@@ -0,0 +1,27 @@
1
+ import { sql } from '../lib/db';
2
+
3
+ // Read fresh each request so the page reflects the DB (and so the verify gate exercises a live query —
4
+ // a broken connection / missing table would 500, not 200).
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ export default async function Page() {
8
+ const rows = await sql`SELECT id, body FROM notes ORDER BY id DESC LIMIT 20`;
9
+ return (
10
+ <main
11
+ style={{
12
+ fontFamily: 'system-ui, sans-serif',
13
+ maxWidth: 640,
14
+ margin: '3rem auto',
15
+ padding: '0 1rem',
16
+ }}
17
+ >
18
+ <h1>Notes</h1>
19
+ <p>{rows.length} note(s) from Neon.</p>
20
+ <ul>
21
+ {rows.map((r) => (
22
+ <li key={String(r.id)}>{String(r.body)}</li>
23
+ ))}
24
+ </ul>
25
+ </main>
26
+ );
27
+ }
@@ -0,0 +1,7 @@
1
+ import { neon } from '@neondatabase/serverless';
2
+
3
+ // Runtime reads use the POOLED connection (DATABASE_URL) over Neon's serverless HTTP driver — ideal
4
+ // for Vercel's per-request functions. Migrations use the DIRECT connection instead (scripts/migrate.mjs).
5
+ // Both are wired into the Vercel env per target by the Greenlight `neon` module, so prod hits the
6
+ // prod branch and preview hits its own branch.
7
+ export const sql = neon(process.env.DATABASE_URL ?? '');
@@ -0,0 +1,10 @@
1
+ -- Plain SQL migrations are the app's source of truth for the schema, applied by scripts/migrate.mjs
2
+ -- on each build (against DIRECT_URL → the env's branch). They compose with `greenlight migrations
3
+ -- scan` (the dangerous-SQL gate). Swap in Drizzle/Prisma migrations if you prefer — same convention.
4
+ CREATE TABLE IF NOT EXISTS notes (
5
+ id serial PRIMARY KEY,
6
+ body text NOT NULL,
7
+ created_at timestamptz NOT NULL DEFAULT now()
8
+ );
9
+
10
+ INSERT INTO notes (body) VALUES ('hello from Neon');
@@ -0,0 +1,4 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {};
3
+
4
+ export default nextConfig;
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "_template-next",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "node scripts/migrate.mjs && next build",
8
+ "start": "next start",
9
+ "migrate": "node scripts/migrate.mjs"
10
+ },
11
+ "dependencies": {
12
+ "@neondatabase/serverless": "^0.10.0",
13
+ "next": "^15.0.0",
14
+ "pg": "^8.13.0",
15
+ "react": "^19.0.0",
16
+ "react-dom": "^19.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.0.0",
20
+ "@types/pg": "^8.11.0",
21
+ "@types/react": "^19.0.0",
22
+ "typescript": "^5.6.0"
23
+ }
24
+ }
@@ -0,0 +1,43 @@
1
+ // The app's OWN migrate — Greenlight does not run migrations. The build runs this (see package.json
2
+ // `build`) against DIRECT_URL, so a deploy creates/edits tables on the env's Neon branch; a failed
3
+ // migration fails the build, so a broken schema never goes live. Gate it in CI with `greenlight
4
+ // migrations scan` first. Plain `pg` (TCP, transactional) for DDL; the app reads via lib/db.ts.
5
+ import { readFileSync, readdirSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import pg from 'pg';
8
+
9
+ const url = process.env.DIRECT_URL ?? process.env.DATABASE_URL;
10
+ if (!url) {
11
+ console.error('migrate: set DIRECT_URL (the direct Neon connection string)');
12
+ process.exit(1);
13
+ }
14
+
15
+ const client = new pg.Client({ connectionString: url });
16
+ await client.connect();
17
+ try {
18
+ await client.query(
19
+ 'CREATE TABLE IF NOT EXISTS _migrations (name text PRIMARY KEY, applied_at timestamptz DEFAULT now())',
20
+ );
21
+ const { rows } = await client.query('SELECT name FROM _migrations');
22
+ const applied = new Set(rows.map((r) => r.name));
23
+ const files = readdirSync('migrations')
24
+ .filter((f) => f.endsWith('.sql'))
25
+ .sort();
26
+
27
+ for (const file of files) {
28
+ if (applied.has(file)) continue;
29
+ console.log(`applying ${file}`);
30
+ await client.query('BEGIN');
31
+ try {
32
+ await client.query(readFileSync(join('migrations', file), 'utf8'));
33
+ await client.query('INSERT INTO _migrations (name) VALUES ($1)', [file]);
34
+ await client.query('COMMIT');
35
+ } catch (err) {
36
+ await client.query('ROLLBACK');
37
+ throw err;
38
+ }
39
+ }
40
+ console.log('migrations up to date');
41
+ } finally {
42
+ await client.end();
43
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "module": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "preserve",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "incremental": true,
13
+ "plugins": [{ "name": "next" }]
14
+ },
15
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
16
+ "exclude": ["node_modules"]
17
+ }
@@ -0,0 +1,8 @@
1
+ // Verify the deployed app renders AND reads Neon: the page runs a live SELECT, so a missing table or
2
+ // a bad connection 500s instead of returning the marker text. The settle absorbs Vercel propagation.
3
+ export default {
4
+ mode: 'api',
5
+ checks: [{ path: '/', status: 200, contains: 'note(s) from Neon' }],
6
+ settleRetries: 6,
7
+ settleMs: 5000,
8
+ };