@rtrentjones/greenlight 0.2.24 → 0.2.25

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.
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: provider-neon
3
+ description: How Neon works in a Greenlight setup — the `data: neon` store (serverless Postgres), git-style branch-per-env, scale-to-zero + auto-resume (so NO keepalive, unlike Supabase), pooled vs direct connection strings, the NEON_API_KEY, and migrations on a branch. Use when wiring a tool's database, choosing Neon vs Supabase, or a Neon apply.
4
+ ---
5
+
6
+ # provider-neon
7
+
8
+ `data: neon` is the **default Postgres** — for tools that need a SQL database and nothing else
9
+ bundled. One Neon **project** per tool, **a branch per env** (git-style copy-on-write): `prod` is
10
+ the project's default branch; `beta` is a child branch (separate data, instant to create). Compute
11
+ **autosuspends and auto-resumes on the next connection**, so a Neon tool needs **no keepalive** —
12
+ that's the whole reason Neon is preferred over Supabase, which pauses for 7 days and needs a manual
13
+ unpause. Choose `supabase` only when you need bundled auth + storage + realtime together.
14
+
15
+ ## Token — `NEON_API_KEY`
16
+
17
+ Console → Account settings → API keys. Account-level (configures the `neon` provider for every Neon
18
+ tool, like `CLOUDFLARE_API_TOKEN`) — **not** per-tool. `greenlight add` verifies it against
19
+ `/api/v2/projects` (HTTP 200). There is **no per-tool secret**: the role/password/connection strings
20
+ are module OUTPUTS, not inputs.
21
+
22
+ ## Terraform module — `infra/modules/neon`
23
+
24
+ Creates the project (default branch = prod) + a `neon_branch` per non-prod env (except `preview`,
25
+ which is ephemeral/per-PR — created by CI, not Terraform). Outputs two per-env maps:
26
+ - **`database_url[env]`** — the **pooled** (pgbouncer) string → `DATABASE_URL` for the serverless app.
27
+ - **`direct_url[env]`** — the **direct** string → `DIRECT_URL` for migrations.
28
+
29
+ The emitted `<name>.tf` wires `database_url["prod"]`/`["beta"]` into the Vercel env per target, so
30
+ prod and beta hit **different branches**. Pin the provider `kislerdm/neon ~> 0.13`.
31
+
32
+ ## No keepalive
33
+
34
+ Do **not** add a Neon tool to `module.keepalive.targets_json`. Neon resumes on connect — a request
35
+ just wakes it. (`doctor` does not flag `data: neon` for keepalive; that exemption is intentional.)
36
+
37
+ ## Migrations
38
+
39
+ Run migrations against the env's **branch** (`DIRECT_URL`). A PR's ephemeral branch is the safe place
40
+ to test a migration before it touches prod. Gate them with `greenlight migrations scan` (the
41
+ dangerous-SQL pre-apply check) in the tool's CI.
42
+
43
+ ## MCP
44
+
45
+ `.mcp.json` wires `neon` (hosted) with `Authorization: Bearer ${NEON_API_KEY}`. Run `/mcp` to auth.
46
+
47
+ ## Rule
48
+ The **blog must never use a database** that can pause — but Neon's auto-resume makes it safe for
49
+ *tools*; still, the apex blog stays `data: none` (D1/KV/external only). Neon is per-tool Postgres.
package/dist/bin.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  resolveUrl,
7
7
  scanSqlFiles,
8
8
  verifyAll
9
- } from "./chunk-2A7ZBBYN.js";
9
+ } from "./chunk-GPPUZ6Z5.js";
10
10
  import "./chunk-HX7VA25D.js";
11
11
  import "./chunk-N3IKUCSF.js";
12
12
  import "./chunk-KP3Y6WRU.js";
@@ -317,6 +317,37 @@ var PACKS = [
317
317
  skill: "provider-supabase",
318
318
  tfModules: ["supabase"]
319
319
  },
320
+ {
321
+ id: "neon",
322
+ name: "Neon",
323
+ appliesTo: (t) => t.data === "neon",
324
+ guide: "docs/provider-tokens.md \u2014 NEON_API_KEY (project + branch management)",
325
+ setupUrl: "https://console.neon.tech/app/settings/api-keys",
326
+ tokens: [
327
+ {
328
+ // Shared account credential (configures the neon TF provider, like CLOUDFLARE_API_TOKEN) —
329
+ // NOT per-tool. The role password + connection string are module OUTPUTS, not inputs, so
330
+ // (unlike Supabase) there is no per-tool password variable to gather.
331
+ envVar: "NEON_API_KEY",
332
+ label: "Neon API key (project + branch management)",
333
+ verify: async (t) => {
334
+ const r = await fetch("https://console.neon.tech/api/v2/projects", {
335
+ headers: { Authorization: `Bearer ${t}` }
336
+ });
337
+ return { ok: okStatus(r), detail: `HTTP ${r.status}` };
338
+ }
339
+ }
340
+ ],
341
+ mcp: {
342
+ neon: {
343
+ type: "http",
344
+ url: "https://mcp.neon.tech/mcp",
345
+ headers: { Authorization: "Bearer ${NEON_API_KEY}" }
346
+ }
347
+ },
348
+ skill: "provider-neon",
349
+ tfModules: ["neon"]
350
+ },
320
351
  {
321
352
  id: "hcp",
322
353
  name: "HCP Terraform (remote state)",
@@ -444,7 +475,7 @@ function tokensForTool(tool) {
444
475
  }
445
476
 
446
477
  // src/version.ts
447
- var MODULE_REF = "v0.2.24";
478
+ var MODULE_REF = "v0.2.25";
448
479
  var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
449
480
  function moduleSource(module, ref = MODULE_REF) {
450
481
  return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
@@ -457,6 +488,7 @@ function emitToolTf(opts) {
457
488
  const port = opts.port ?? 8e3;
458
489
  const slug = opts.slug ?? `OWNER/${name}`;
459
490
  const useSupabase = data === "supabase";
491
+ const useNeon = data === "neon";
460
492
  const useVercel = target === "vercel";
461
493
  const useOci = target === "oci";
462
494
  const supabaseOverride = opts.tokenOverrides?.SUPABASE_ACCESS_TOKEN;
@@ -467,9 +499,9 @@ function emitToolTf(opts) {
467
499
  if (useSupabase) assumes.push("var.supabase_organization_id");
468
500
  const ghcrOwner = (slug.split("/")[0] ?? "owner").toLowerCase();
469
501
  blocks.push(
470
- `# ${name} \u2014 ${lane}/${target}${useSupabase ? "/supabase" : ""}, emitted by \`greenlight add\`.
502
+ `# ${name} \u2014 ${lane}/${target}${data && data !== "none" ? `/${data}` : ""}, emitted by \`greenlight add\`.
471
503
  # Review, then commit + push: the wrapper's infra.yml (HCP-backed) runs \`terraform apply\`.
472
- # Assumes infra/main.tf declares: ${[useVercel && "vercel", useSupabase && "supabase", useOci && "oci"].filter(Boolean).join(" + ") || "cloudflare + github"} provider(s)
504
+ # Assumes infra/main.tf declares: ${[useVercel && "vercel", useSupabase && "supabase", useNeon && "neon", useOci && "oci"].filter(Boolean).join(" + ") || "cloudflare + github"} provider(s)
473
505
  # and the variables ${assumes.join(", ")}.${opts.external ? `
474
506
  # External tool: app code + deploy live in ${slug}; this manages only its infra here.` : ""}`
475
507
  );
@@ -509,6 +541,19 @@ variable "${name}_supabase_database_password" {
509
541
  sensitive = true
510
542
  default = "import-placeholder" # ignored when importing an existing project
511
543
  }${overrideBlock}`);
544
+ }
545
+ if (useNeon) {
546
+ blocks.push(`# One Neon project, a branch per env (prod = the project's default branch; beta = a child
547
+ # branch \u2014 copy-on-write, instant). Compute scales to zero and auto-resumes on the next connection,
548
+ # so a Neon tool needs NO keepalive (the reason Neon is the default Postgres). NEON_API_KEY configures
549
+ # the provider in main.tf; the connection strings are module OUTPUTS \u2014 no per-tool secret to gather.
550
+ module "${name}_neon" {
551
+ source = "${moduleSource("neon", ref)}"
552
+
553
+ name = "${name}"
554
+ region = "aws-us-east-1" # Neon region id, e.g. aws-us-east-1 / aws-us-west-2
555
+ envs = [${envList}]
556
+ }`);
512
557
  }
513
558
  if (useVercel) {
514
559
  const env = useSupabase ? `
@@ -531,6 +576,24 @@ variable "${name}_supabase_database_password" {
531
576
  supa_url_beta = module.${name}_supabase.url
532
577
  supa_anon_beta = module.${name}_supabase.anon_key
533
578
  supa_service_beta = module.${name}_supabase.service_role_key
579
+ }` : useNeon ? `
580
+ environment = {
581
+ site_url_prod = { key = "SITE_URL", target = ["production"], sensitive = false }
582
+ site_url_beta = { key = "SITE_URL", target = ["preview"], sensitive = false }
583
+ db_url_prod = { key = "DATABASE_URL", target = ["production"], sensitive = true }
584
+ db_direct_prod = { key = "DIRECT_URL", target = ["production"], sensitive = true }
585
+ db_url_beta = { key = "DATABASE_URL", target = ["preview"], sensitive = true }
586
+ db_direct_beta = { key = "DIRECT_URL", target = ["preview"], sensitive = true }
587
+ }
588
+ # Pooled (DATABASE_URL) for the serverless app; direct (DIRECT_URL) for migrations. Prod hits the
589
+ # project's default branch; beta hits the "beta" branch \u2014 separate data, instant copy-on-write.
590
+ environment_values = {
591
+ site_url_prod = "https://${name}.${domain}"
592
+ site_url_beta = "https://beta.${name}.${domain}"
593
+ db_url_prod = module.${name}_neon.database_url["prod"]
594
+ db_direct_prod = module.${name}_neon.direct_url["prod"]
595
+ db_url_beta = module.${name}_neon.database_url["beta"]
596
+ db_direct_beta = module.${name}_neon.direct_url["beta"]
534
597
  }` : `
535
598
  # No managed data store \u2014 add environment/environment_values if the app needs vars.
536
599
  environment = {}
@@ -640,10 +703,13 @@ function emitWrapperMainTf(opts) {
640
703
  req.push(' vercel = { source = "vercel/vercel", version = "~> 3.0" }');
641
704
  if (need.has("supabase"))
642
705
  req.push(' supabase = { source = "supabase/supabase", version = "~> 1.0" }');
706
+ if (need.has("neon"))
707
+ req.push(' neon = { source = "kislerdm/neon", version = "~> 0.13" }');
643
708
  if (need.has("oci")) req.push(' oci = { source = "oracle/oci", version = ">= 5.0" }');
644
709
  const providerBlocks = ['provider "cloudflare" {}', `provider "github" { owner = "${owner}" }`];
645
710
  if (need.has("vercel")) providerBlocks.push('provider "vercel" {}');
646
711
  if (need.has("supabase")) providerBlocks.push('provider "supabase" {}');
712
+ if (need.has("neon")) providerBlocks.push('provider "neon" { api_key = var.neon_api_key }');
647
713
  if (need.has("oci")) {
648
714
  providerBlocks.push(`provider "oci" {
649
715
  # trimspace guards against a trailing newline/space in a pasted secret (a malformed region
@@ -660,6 +726,9 @@ function emitWrapperMainTf(opts) {
660
726
  if (need.has("supabase")) {
661
727
  vars.push('variable "supabase_organization_id" { type = string }');
662
728
  }
729
+ if (need.has("neon")) {
730
+ vars.push('variable "neon_api_key" {\n type = string\n sensitive = true\n}');
731
+ }
663
732
  if (need.has("oci")) {
664
733
  vars.push('variable "oci_tenancy_ocid" { type = string }');
665
734
  vars.push('variable "oci_user_ocid" { type = string }');
@@ -703,6 +772,7 @@ function providersForTool(tool) {
703
772
  const out = ["cloudflare", "github"];
704
773
  if (ids.has("vercel")) out.push("vercel");
705
774
  if (ids.has("supabase")) out.push("supabase");
775
+ if (ids.has("neon")) out.push("neon");
706
776
  if (ids.has("oci")) out.push("oci");
707
777
  return out;
708
778
  }
@@ -15,13 +15,13 @@ import { createJiti } from "jiti";
15
15
  import { z } from "zod";
16
16
  var LaneEnum = z.enum(["astro", "next", "mcp"]);
17
17
  var TargetEnum = z.enum(["workers", "vercel", "oci"]);
18
- var DataEnum = z.enum(["none", "d1", "kv", "supabase"]);
18
+ var DataEnum = z.enum(["none", "d1", "kv", "supabase", "neon"]);
19
19
  var AuthEnum = z.enum(["none", "bearer", "oauth"]);
20
20
  var AccessEnum = z.enum(["public", "private"]);
21
21
  var EnvEnum = z.enum(["preview", "beta", "prod"]);
22
22
  var MATRIX = {
23
23
  astro: { targets: ["workers"], data: ["none", "d1", "kv"] },
24
- next: { targets: ["vercel"], data: ["none", "supabase"] },
24
+ next: { targets: ["vercel"], data: ["none", "supabase", "neon"] },
25
25
  mcp: { targets: ["workers", "oci"], data: ["none"] }
26
26
  };
27
27
  var ToolSchema = z.object({
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  defineConfig,
3
3
  defineVerify,
4
4
  loadConfig
5
- } from "./chunk-2A7ZBBYN.js";
5
+ } from "./chunk-GPPUZ6Z5.js";
6
6
  import "./chunk-HX7VA25D.js";
7
7
  import "./chunk-N3IKUCSF.js";
8
8
  import "./chunk-KP3Y6WRU.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtrentjones/greenlight",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
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.24",
35
- "@rtrentjones/greenlight-loop": "0.2.24",
36
- "@rtrentjones/greenlight-verify": "0.2.24",
37
- "@rtrentjones/greenlight-shared": "0.2.24"
34
+ "@rtrentjones/greenlight-adapters": "0.2.25",
35
+ "@rtrentjones/greenlight-shared": "0.2.25",
36
+ "@rtrentjones/greenlight-verify": "0.2.25",
37
+ "@rtrentjones/greenlight-loop": "0.2.25"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node scripts/copy-assets.mjs && tsup",