@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-
|
|
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.
|
|
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}${
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtrentjones/greenlight",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
35
|
-
"@rtrentjones/greenlight-
|
|
36
|
-
"@rtrentjones/greenlight-verify": "0.2.
|
|
37
|
-
"@rtrentjones/greenlight-
|
|
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",
|