@rtrentjones/greenlight 0.2.24 → 0.2.26
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/assets/skills/provider-neon/SKILL.md +65 -0
- package/dist/bin.js +120 -12
- package/dist/{chunk-2A7ZBBYN.js → chunk-HMU7D7R2.js} +44 -3
- package/dist/index.js +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
## Schema as code / migrations
|
|
38
|
+
|
|
39
|
+
**Greenlight does NOT run migrations — by design.** The split:
|
|
40
|
+
- **Schema** lives in the tool (an ORM — Drizzle/Prisma — or plain `.sql` migrations).
|
|
41
|
+
- **Branch-per-env**: the TF module owns stable `prod`/`beta`; the **native Neon↔Vercel integration**
|
|
42
|
+
owns ephemeral per-PR preview branches (+ auto-injects `DATABASE_URL`). Don't put ephemeral branches
|
|
43
|
+
in Terraform.
|
|
44
|
+
- **Execution**: the app's own build runs its migrate (`drizzle-kit migrate` / `prisma migrate deploy`)
|
|
45
|
+
against the wired **`DIRECT_URL`** — prod build → prod branch, preview build → preview branch. A
|
|
46
|
+
failed migrate fails the build = a natural gate.
|
|
47
|
+
- **Greenlight's role**: the **dangerous-SQL gate**. Run `greenlight migrations scan` (no `<dir>` →
|
|
48
|
+
it auto-detects `supabase/migrations | migrations | drizzle/migrations | …`) in CI before the migrate.
|
|
49
|
+
|
|
50
|
+
See [docs/migrations.md](../../../docs/migrations.md).
|
|
51
|
+
|
|
52
|
+
## Sharing one DB + multi-account
|
|
53
|
+
|
|
54
|
+
- **One DB, many services**: a second tool sets `dataShareWith: '<owner>'` (or `add … --share <owner>`)
|
|
55
|
+
— it creates no project and wires the owner's connection strings.
|
|
56
|
+
- **A second Neon account**: `tokenOverrides: { NEON_API_KEY: 'NEON_API_KEY_X' }` → an aliased `neon`
|
|
57
|
+
provider authenticates that account. (A sharer can't also override — it uses the owner's account.)
|
|
58
|
+
|
|
59
|
+
## MCP
|
|
60
|
+
|
|
61
|
+
`.mcp.json` wires `neon` (hosted) with `Authorization: Bearer ${NEON_API_KEY}`. Run `/mcp` to auth.
|
|
62
|
+
|
|
63
|
+
## Rule
|
|
64
|
+
The **blog must never use a database** that can pause — but Neon's auto-resume makes it safe for
|
|
65
|
+
*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-HMU7D7R2.js";
|
|
10
10
|
import "./chunk-HX7VA25D.js";
|
|
11
11
|
import "./chunk-N3IKUCSF.js";
|
|
12
12
|
import "./chunk-KP3Y6WRU.js";
|
|
@@ -64,6 +64,7 @@ function serializeTool(t) {
|
|
|
64
64
|
const ov = Object.entries(t.tokenOverrides).map(([k, v]) => `${k}: ${q(v)}`).join(", ");
|
|
65
65
|
parts.push(`tokenOverrides: { ${ov} }`);
|
|
66
66
|
}
|
|
67
|
+
if (t.dataShareWith !== void 0) parts.push(`dataShareWith: ${q(t.dataShareWith)}`);
|
|
67
68
|
return ` { ${parts.join(", ")} },`;
|
|
68
69
|
}
|
|
69
70
|
function serializeConfig(c) {
|
|
@@ -111,7 +112,8 @@ function addTool(config, t) {
|
|
|
111
112
|
...t.port !== void 0 ? { port: t.port } : {},
|
|
112
113
|
...t.preview ? { preview: t.preview } : {},
|
|
113
114
|
...t.tokens?.length ? { tokens: t.tokens } : {},
|
|
114
|
-
...t.tokenOverrides && Object.keys(t.tokenOverrides).length ? { tokenOverrides: t.tokenOverrides } : {}
|
|
115
|
+
...t.tokenOverrides && Object.keys(t.tokenOverrides).length ? { tokenOverrides: t.tokenOverrides } : {},
|
|
116
|
+
...t.dataShareWith ? { dataShareWith: t.dataShareWith } : {}
|
|
115
117
|
}
|
|
116
118
|
]
|
|
117
119
|
};
|
|
@@ -139,7 +141,8 @@ function upsertTool(config, t) {
|
|
|
139
141
|
...t.port !== void 0 ? { port: t.port } : {},
|
|
140
142
|
...t.preview ? { preview: t.preview } : {},
|
|
141
143
|
...t.tokens?.length ? { tokens: t.tokens } : {},
|
|
142
|
-
...t.tokenOverrides && Object.keys(t.tokenOverrides).length ? { tokenOverrides: t.tokenOverrides } : {}
|
|
144
|
+
...t.tokenOverrides && Object.keys(t.tokenOverrides).length ? { tokenOverrides: t.tokenOverrides } : {},
|
|
145
|
+
...t.dataShareWith ? { dataShareWith: t.dataShareWith } : {}
|
|
143
146
|
};
|
|
144
147
|
const tools = config.tools.some((x) => x.name === t.name) ? config.tools.map((x) => x.name === t.name ? entry : x) : [...config.tools, entry];
|
|
145
148
|
const result = ConfigSchema.safeParse({ ...config, tools });
|
|
@@ -317,6 +320,37 @@ var PACKS = [
|
|
|
317
320
|
skill: "provider-supabase",
|
|
318
321
|
tfModules: ["supabase"]
|
|
319
322
|
},
|
|
323
|
+
{
|
|
324
|
+
id: "neon",
|
|
325
|
+
name: "Neon",
|
|
326
|
+
appliesTo: (t) => t.data === "neon",
|
|
327
|
+
guide: "docs/provider-tokens.md \u2014 NEON_API_KEY (project + branch management)",
|
|
328
|
+
setupUrl: "https://console.neon.tech/app/settings/api-keys",
|
|
329
|
+
tokens: [
|
|
330
|
+
{
|
|
331
|
+
// Shared account credential (configures the neon TF provider, like CLOUDFLARE_API_TOKEN) —
|
|
332
|
+
// NOT per-tool. The role password + connection string are module OUTPUTS, not inputs, so
|
|
333
|
+
// (unlike Supabase) there is no per-tool password variable to gather.
|
|
334
|
+
envVar: "NEON_API_KEY",
|
|
335
|
+
label: "Neon API key (project + branch management)",
|
|
336
|
+
verify: async (t) => {
|
|
337
|
+
const r = await fetch("https://console.neon.tech/api/v2/projects", {
|
|
338
|
+
headers: { Authorization: `Bearer ${t}` }
|
|
339
|
+
});
|
|
340
|
+
return { ok: okStatus(r), detail: `HTTP ${r.status}` };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
],
|
|
344
|
+
mcp: {
|
|
345
|
+
neon: {
|
|
346
|
+
type: "http",
|
|
347
|
+
url: "https://mcp.neon.tech/mcp",
|
|
348
|
+
headers: { Authorization: "Bearer ${NEON_API_KEY}" }
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
skill: "provider-neon",
|
|
352
|
+
tfModules: ["neon"]
|
|
353
|
+
},
|
|
320
354
|
{
|
|
321
355
|
id: "hcp",
|
|
322
356
|
name: "HCP Terraform (remote state)",
|
|
@@ -444,7 +478,7 @@ function tokensForTool(tool) {
|
|
|
444
478
|
}
|
|
445
479
|
|
|
446
480
|
// src/version.ts
|
|
447
|
-
var MODULE_REF = "v0.2.
|
|
481
|
+
var MODULE_REF = "v0.2.26";
|
|
448
482
|
var MODULE_SOURCE_BASE = "git::https://github.com/RTrentJones/greenlight.git//infra/modules";
|
|
449
483
|
function moduleSource(module, ref = MODULE_REF) {
|
|
450
484
|
return `${MODULE_SOURCE_BASE}/${module}?ref=${ref}`;
|
|
@@ -457,9 +491,12 @@ function emitToolTf(opts) {
|
|
|
457
491
|
const port = opts.port ?? 8e3;
|
|
458
492
|
const slug = opts.slug ?? `OWNER/${name}`;
|
|
459
493
|
const useSupabase = data === "supabase";
|
|
494
|
+
const useNeon = data === "neon";
|
|
460
495
|
const useVercel = target === "vercel";
|
|
461
496
|
const useOci = target === "oci";
|
|
462
497
|
const supabaseOverride = opts.tokenOverrides?.SUPABASE_ACCESS_TOKEN;
|
|
498
|
+
const neonOverride = opts.tokenOverrides?.NEON_API_KEY;
|
|
499
|
+
const neonOwner = opts.dataShareWith ?? name;
|
|
463
500
|
const envList = envs.map((e) => `"${e}"`).join(", ");
|
|
464
501
|
const blocks = [];
|
|
465
502
|
const assumes = ["var.cloudflare_zone_id"];
|
|
@@ -467,9 +504,9 @@ function emitToolTf(opts) {
|
|
|
467
504
|
if (useSupabase) assumes.push("var.supabase_organization_id");
|
|
468
505
|
const ghcrOwner = (slug.split("/")[0] ?? "owner").toLowerCase();
|
|
469
506
|
blocks.push(
|
|
470
|
-
`# ${name} \u2014 ${lane}/${target}${
|
|
507
|
+
`# ${name} \u2014 ${lane}/${target}${data && data !== "none" ? `/${data}` : ""}, emitted by \`greenlight add\`.
|
|
471
508
|
# 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)
|
|
509
|
+
# Assumes infra/main.tf declares: ${[useVercel && "vercel", useSupabase && "supabase", useNeon && "neon", useOci && "oci"].filter(Boolean).join(" + ") || "cloudflare + github"} provider(s)
|
|
473
510
|
# and the variables ${assumes.join(", ")}.${opts.external ? `
|
|
474
511
|
# External tool: app code + deploy live in ${slug}; this manages only its infra here.` : ""}`
|
|
475
512
|
);
|
|
@@ -508,6 +545,38 @@ variable "${name}_supabase_database_password" {
|
|
|
508
545
|
type = string
|
|
509
546
|
sensitive = true
|
|
510
547
|
default = "import-placeholder" # ignored when importing an existing project
|
|
548
|
+
}${overrideBlock}`);
|
|
549
|
+
}
|
|
550
|
+
if (useNeon && opts.dataShareWith) {
|
|
551
|
+
blocks.push(`# Shares the Neon project owned by "${opts.dataShareWith}" (one DB, many services).
|
|
552
|
+
# No neon module here \u2014 the env wiring below reads module.${neonOwner}_neon.* (its prod/beta branches).`);
|
|
553
|
+
} else if (useNeon) {
|
|
554
|
+
const providersLine = neonOverride ? `
|
|
555
|
+
providers = { neon = neon.${name} }` : "";
|
|
556
|
+
const overrideBlock = neonOverride ? `
|
|
557
|
+
|
|
558
|
+
# Multi-account: ${name}'s Neon lives in a SECOND account \u2014 an aliased provider authenticates with
|
|
559
|
+
# its own token. In infra.yml: TF_VAR_${name}_neon_api_key: \${{ secrets.${neonOverride} }}
|
|
560
|
+
provider "neon" {
|
|
561
|
+
alias = "${name}"
|
|
562
|
+
api_key = var.${name}_neon_api_key
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
variable "${name}_neon_api_key" {
|
|
566
|
+
type = string
|
|
567
|
+
sensitive = true
|
|
568
|
+
description = "Neon API key for ${name}'s account (scoped secret ${neonOverride})."
|
|
569
|
+
}` : "";
|
|
570
|
+
blocks.push(`# One Neon project, a branch per env (prod = the project's default branch; beta = a child
|
|
571
|
+
# branch \u2014 copy-on-write, instant). Compute scales to zero and auto-resumes on the next connection,
|
|
572
|
+
# so a Neon tool needs NO keepalive (the reason Neon is the default Postgres). NEON_API_KEY configures
|
|
573
|
+
# the provider in main.tf; the connection strings are module OUTPUTS \u2014 no per-tool secret to gather.
|
|
574
|
+
module "${name}_neon" {
|
|
575
|
+
source = "${moduleSource("neon", ref)}"${providersLine}
|
|
576
|
+
|
|
577
|
+
name = "${name}"
|
|
578
|
+
region = "aws-us-east-1" # Neon region id, e.g. aws-us-east-1 / aws-us-west-2
|
|
579
|
+
envs = [${envList}]
|
|
511
580
|
}${overrideBlock}`);
|
|
512
581
|
}
|
|
513
582
|
if (useVercel) {
|
|
@@ -531,6 +600,24 @@ variable "${name}_supabase_database_password" {
|
|
|
531
600
|
supa_url_beta = module.${name}_supabase.url
|
|
532
601
|
supa_anon_beta = module.${name}_supabase.anon_key
|
|
533
602
|
supa_service_beta = module.${name}_supabase.service_role_key
|
|
603
|
+
}` : useNeon ? `
|
|
604
|
+
environment = {
|
|
605
|
+
site_url_prod = { key = "SITE_URL", target = ["production"], sensitive = false }
|
|
606
|
+
site_url_beta = { key = "SITE_URL", target = ["preview"], sensitive = false }
|
|
607
|
+
db_url_prod = { key = "DATABASE_URL", target = ["production"], sensitive = true }
|
|
608
|
+
db_direct_prod = { key = "DIRECT_URL", target = ["production"], sensitive = true }
|
|
609
|
+
db_url_beta = { key = "DATABASE_URL", target = ["preview"], sensitive = true }
|
|
610
|
+
db_direct_beta = { key = "DIRECT_URL", target = ["preview"], sensitive = true }
|
|
611
|
+
}
|
|
612
|
+
# Pooled (DATABASE_URL) for the serverless app; direct (DIRECT_URL) for migrations. Prod hits the
|
|
613
|
+
# project's default branch; beta hits the "beta" branch \u2014 separate data, instant copy-on-write.
|
|
614
|
+
environment_values = {
|
|
615
|
+
site_url_prod = "https://${name}.${domain}"
|
|
616
|
+
site_url_beta = "https://beta.${name}.${domain}"
|
|
617
|
+
db_url_prod = module.${neonOwner}_neon.database_url["prod"]
|
|
618
|
+
db_direct_prod = module.${neonOwner}_neon.direct_url["prod"]
|
|
619
|
+
db_url_beta = module.${neonOwner}_neon.database_url["beta"]
|
|
620
|
+
db_direct_beta = module.${neonOwner}_neon.direct_url["beta"]
|
|
534
621
|
}` : `
|
|
535
622
|
# No managed data store \u2014 add environment/environment_values if the app needs vars.
|
|
536
623
|
environment = {}
|
|
@@ -640,10 +727,13 @@ function emitWrapperMainTf(opts) {
|
|
|
640
727
|
req.push(' vercel = { source = "vercel/vercel", version = "~> 3.0" }');
|
|
641
728
|
if (need.has("supabase"))
|
|
642
729
|
req.push(' supabase = { source = "supabase/supabase", version = "~> 1.0" }');
|
|
730
|
+
if (need.has("neon"))
|
|
731
|
+
req.push(' neon = { source = "kislerdm/neon", version = "~> 0.13" }');
|
|
643
732
|
if (need.has("oci")) req.push(' oci = { source = "oracle/oci", version = ">= 5.0" }');
|
|
644
733
|
const providerBlocks = ['provider "cloudflare" {}', `provider "github" { owner = "${owner}" }`];
|
|
645
734
|
if (need.has("vercel")) providerBlocks.push('provider "vercel" {}');
|
|
646
735
|
if (need.has("supabase")) providerBlocks.push('provider "supabase" {}');
|
|
736
|
+
if (need.has("neon")) providerBlocks.push('provider "neon" { api_key = var.neon_api_key }');
|
|
647
737
|
if (need.has("oci")) {
|
|
648
738
|
providerBlocks.push(`provider "oci" {
|
|
649
739
|
# trimspace guards against a trailing newline/space in a pasted secret (a malformed region
|
|
@@ -660,6 +750,9 @@ function emitWrapperMainTf(opts) {
|
|
|
660
750
|
if (need.has("supabase")) {
|
|
661
751
|
vars.push('variable "supabase_organization_id" { type = string }');
|
|
662
752
|
}
|
|
753
|
+
if (need.has("neon")) {
|
|
754
|
+
vars.push('variable "neon_api_key" {\n type = string\n sensitive = true\n}');
|
|
755
|
+
}
|
|
663
756
|
if (need.has("oci")) {
|
|
664
757
|
vars.push('variable "oci_tenancy_ocid" { type = string }');
|
|
665
758
|
vars.push('variable "oci_user_ocid" { type = string }');
|
|
@@ -703,6 +796,7 @@ function providersForTool(tool) {
|
|
|
703
796
|
const out = ["cloudflare", "github"];
|
|
704
797
|
if (ids.has("vercel")) out.push("vercel");
|
|
705
798
|
if (ids.has("supabase")) out.push("supabase");
|
|
799
|
+
if (ids.has("neon")) out.push("neon");
|
|
706
800
|
if (ids.has("oci")) out.push("oci");
|
|
707
801
|
return out;
|
|
708
802
|
}
|
|
@@ -1057,7 +1151,7 @@ async function addCommand(args) {
|
|
|
1057
1151
|
const name = args[0];
|
|
1058
1152
|
if (!name || name.startsWith("-")) {
|
|
1059
1153
|
throw new Error(
|
|
1060
|
-
"usage: greenlight add <name> --lane <lane> --target <target> [--data <d>] [--auth <a>] [--envs beta,prod] [--port 8000]"
|
|
1154
|
+
"usage: greenlight add <name> --lane <lane> --target <target> [--data <d>] [--auth <a>] [--envs beta,prod] [--port 8000] [--share <owner>]"
|
|
1061
1155
|
);
|
|
1062
1156
|
}
|
|
1063
1157
|
const lane = flag2(args, "--lane");
|
|
@@ -1075,7 +1169,9 @@ async function addCommand(args) {
|
|
|
1075
1169
|
data: flag2(args, "--data"),
|
|
1076
1170
|
auth: flag2(args, "--auth"),
|
|
1077
1171
|
envs: flag2(args, "--envs")?.split(","),
|
|
1078
|
-
port: portFlag ? Number(portFlag) : void 0
|
|
1172
|
+
port: portFlag ? Number(portFlag) : void 0,
|
|
1173
|
+
// --share <owner>: this tool reads the owner's Neon DB instead of creating its own (one DB, many services).
|
|
1174
|
+
dataShareWith: flag2(args, "--share")
|
|
1079
1175
|
});
|
|
1080
1176
|
const entry = next.tools.find((t) => t.name === name);
|
|
1081
1177
|
const data = entry?.data ?? "none";
|
|
@@ -1124,7 +1220,8 @@ async function addCommand(args) {
|
|
|
1124
1220
|
data,
|
|
1125
1221
|
envs,
|
|
1126
1222
|
port: entry?.port,
|
|
1127
|
-
tokenOverrides: entry?.tokenOverrides
|
|
1223
|
+
tokenOverrides: entry?.tokenOverrides,
|
|
1224
|
+
dataShareWith: entry?.dataShareWith
|
|
1128
1225
|
})
|
|
1129
1226
|
);
|
|
1130
1227
|
console.log(`\u2714 wrote infra/${name}.tf (modules: ${providers.join(", ")})`);
|
|
@@ -2457,19 +2554,30 @@ Next:
|
|
|
2457
2554
|
}
|
|
2458
2555
|
|
|
2459
2556
|
// src/commands/migrations.ts
|
|
2460
|
-
import { readFileSync as readFileSync7, readdirSync as readdirSync3 } from "fs";
|
|
2557
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, readdirSync as readdirSync3 } from "fs";
|
|
2461
2558
|
import { join as join5 } from "path";
|
|
2462
2559
|
var DEFAULT_DIR = "supabase/migrations";
|
|
2560
|
+
var CANDIDATE_DIRS = [
|
|
2561
|
+
DEFAULT_DIR,
|
|
2562
|
+
"migrations",
|
|
2563
|
+
"drizzle/migrations",
|
|
2564
|
+
"drizzle",
|
|
2565
|
+
"db/migrations"
|
|
2566
|
+
];
|
|
2567
|
+
function resolveMigrationsDir(explicit, root = process.cwd()) {
|
|
2568
|
+
if (explicit) return explicit;
|
|
2569
|
+
return CANDIDATE_DIRS.find((d) => existsSync10(join5(root, d))) ?? DEFAULT_DIR;
|
|
2570
|
+
}
|
|
2463
2571
|
async function migrationsCommand(args) {
|
|
2464
2572
|
if (args[0] !== "scan") {
|
|
2465
2573
|
console.log(
|
|
2466
2574
|
`usage: greenlight migrations scan [<dir>] [--strict]
|
|
2467
2575
|
scan SQL migrations for data-destroying / lock-heavy statements (the pre-apply gate).
|
|
2468
|
-
|
|
2576
|
+
no <dir> \u2192 auto-detects ${CANDIDATE_DIRS.join(" | ")}. Acknowledge an intentional op with \`-- greenlight:allow\`.`
|
|
2469
2577
|
);
|
|
2470
2578
|
process.exit(args[0] ? 1 : 0);
|
|
2471
2579
|
}
|
|
2472
|
-
const dir = args.slice(1).find((a) => !a.startsWith("-"))
|
|
2580
|
+
const dir = resolveMigrationsDir(args.slice(1).find((a) => !a.startsWith("-")));
|
|
2473
2581
|
const strict = args.includes("--strict");
|
|
2474
2582
|
let names;
|
|
2475
2583
|
try {
|
|
@@ -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({
|
|
@@ -65,7 +65,12 @@ var ToolSchema = z.object({
|
|
|
65
65
|
// var to an alternate secret name, so this tool authenticates that provider with a SECOND account
|
|
66
66
|
// — e.g. { SUPABASE_ACCESS_TOKEN: 'SUPABASE_ACCESS_TOKEN_HEISTMIND' }. Absent ⇒ unchanged (the
|
|
67
67
|
// default token). `add`/`adopt` emit an aliased provider + scoped var/secret for an overridden token.
|
|
68
|
-
tokenOverrides: z.record(z.string(), z.string()).optional()
|
|
68
|
+
tokenOverrides: z.record(z.string(), z.string()).optional(),
|
|
69
|
+
// Share another tool's data store instead of creating one (multiple services on one Neon DB).
|
|
70
|
+
// The value is the OWNER tool's name; this tool emits no data module and wires the owner's
|
|
71
|
+
// connection strings. Cross-tool validity (owner exists, same data, no chains) is checked on
|
|
72
|
+
// the whole config below.
|
|
73
|
+
dataShareWith: z.string().optional()
|
|
69
74
|
}).superRefine((tool, ctx) => {
|
|
70
75
|
const rule = MATRIX[tool.lane];
|
|
71
76
|
if (!rule.targets.includes(tool.target)) {
|
|
@@ -89,6 +94,13 @@ var ToolSchema = z.object({
|
|
|
89
94
|
message: 'private tools must set auth to "bearer" or "oauth", never "none"'
|
|
90
95
|
});
|
|
91
96
|
}
|
|
97
|
+
if (tool.dataShareWith && tool.data !== "neon") {
|
|
98
|
+
ctx.addIssue({
|
|
99
|
+
code: z.ZodIssueCode.custom,
|
|
100
|
+
path: ["dataShareWith"],
|
|
101
|
+
message: 'dataShareWith currently supports data: "neon" only'
|
|
102
|
+
});
|
|
103
|
+
}
|
|
92
104
|
});
|
|
93
105
|
var BlogSchema = z.object({
|
|
94
106
|
lane: z.literal("astro"),
|
|
@@ -104,6 +116,35 @@ var ConfigSchema = z.object({
|
|
|
104
116
|
// Optional: a tool-only repo (a poly-repo consumer) has no blog.
|
|
105
117
|
blog: BlogSchema.optional(),
|
|
106
118
|
tools: z.array(ToolSchema).default([])
|
|
119
|
+
}).superRefine((config, ctx) => {
|
|
120
|
+
for (const [i, tool] of config.tools.entries()) {
|
|
121
|
+
if (!tool.dataShareWith) continue;
|
|
122
|
+
const issue = (message) => ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["tools", i, "dataShareWith"], message });
|
|
123
|
+
if (tool.dataShareWith === tool.name) {
|
|
124
|
+
issue(`"${tool.name}" cannot share a data store with itself`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const owner = config.tools.find((t) => t.name === tool.dataShareWith);
|
|
128
|
+
if (!owner) {
|
|
129
|
+
issue(`dataShareWith "${tool.dataShareWith}" is not a tool in this manifest`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (owner.data !== tool.data) {
|
|
133
|
+
issue(
|
|
134
|
+
`"${tool.name}" (data: ${tool.data}) must share a tool with the same data \u2014 "${owner.name}" is ${owner.data}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (owner.dataShareWith) {
|
|
138
|
+
issue(
|
|
139
|
+
`cannot share with "${owner.name}" \u2014 it is itself a sharer (no chains); point at the owner`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (tool.tokenOverrides?.NEON_API_KEY) {
|
|
143
|
+
issue(
|
|
144
|
+
`a sharer uses the owner's Neon account \u2014 remove the NEON_API_KEY override from "${tool.name}"`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
107
148
|
});
|
|
108
149
|
|
|
109
150
|
// ../packages/shared/src/load.ts
|
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.26",
|
|
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-loop": "0.2.
|
|
36
|
-
"@rtrentjones/greenlight-
|
|
37
|
-
"@rtrentjones/greenlight-
|
|
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"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node scripts/copy-assets.mjs && tsup",
|