@devdogsuga/devtools 0.1.25 → 0.1.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/README.md +72 -11
- package/bin/devtools.mjs +4 -4
- package/dist/cli-CZKkNHK5.js +6619 -0
- package/dist/failure-log-BBPGejQv.js +256 -0
- package/dist/launch.js +267 -281
- package/dist/local-env-D8HC2JK_.js +351 -0
- package/dist/local-env-DrBCXqD8.js +2 -0
- package/dist/menu-CQBsQRgH.js +1684 -0
- package/dist/peer-redirect-hooks.js +15 -0
- package/dist/peers-RBplwovA.js +229 -0
- package/dist/root-C2OmZDwD.js +94 -0
- package/dist/stack-Bx-55oi_.js +2 -0
- package/dist/stack-BynCgRVS.js +181 -0
- package/dist/supabase-project-0knM0mqd.js +426 -0
- package/dist/telemetry-C4foD8MD.js +249 -0
- package/dist/telemetry.js +2 -148
- package/package.json +10 -29
- package/bin/devtools-ci-bare.mjs +0 -48
- package/bin/devtools-ci.mjs +0 -31
- package/dist/args.d.ts +0 -26
- package/dist/args.js +0 -78
- package/dist/bws/bw.d.ts +0 -18
- package/dist/bws/bw.js +0 -70
- package/dist/bws/client.d.ts +0 -43
- package/dist/bws/client.js +0 -363
- package/dist/bws/environments.d.ts +0 -96
- package/dist/bws/environments.js +0 -111
- package/dist/bws/pace.d.ts +0 -25
- package/dist/bws/pace.js +0 -43
- package/dist/bws/retry.d.ts +0 -12
- package/dist/bws/retry.js +0 -39
- package/dist/bws/store.d.ts +0 -1
- package/dist/bws/store.js +0 -32
- package/dist/bws/token.d.ts +0 -31
- package/dist/bws/token.js +0 -88
- package/dist/bws/vault.d.ts +0 -29
- package/dist/bws/vault.js +0 -200
- package/dist/catalog.d.ts +0 -57
- package/dist/catalog.js +0 -57
- package/dist/cf/build.d.ts +0 -13
- package/dist/cf/build.js +0 -65
- package/dist/cf/commands.d.ts +0 -1
- package/dist/cf/commands.js +0 -172
- package/dist/cf/local-env.d.ts +0 -58
- package/dist/cf/local-env.js +0 -177
- package/dist/ci.d.ts +0 -1
- package/dist/ci.js +0 -328
- package/dist/cli.d.ts +0 -11
- package/dist/cli.js +0 -1094
- package/dist/commands.d.ts +0 -261
- package/dist/commands.js +0 -1318
- package/dist/completions.d.ts +0 -4
- package/dist/completions.js +0 -110
- package/dist/cron/commands.d.ts +0 -47
- package/dist/cron/commands.js +0 -372
- package/dist/cron/discovery.d.ts +0 -55
- package/dist/cron/discovery.js +0 -85
- package/dist/cron/schema.d.ts +0 -45
- package/dist/cron/schema.js +0 -35
- package/dist/db/config-push.d.ts +0 -8
- package/dist/db/config-push.js +0 -11
- package/dist/db/connection.d.ts +0 -79
- package/dist/db/connection.js +0 -114
- package/dist/db/exec.d.ts +0 -1
- package/dist/db/exec.js +0 -11
- package/dist/db/generate-types.d.ts +0 -1
- package/dist/db/generate-types.js +0 -4
- package/dist/db/generated-env.d.ts +0 -69
- package/dist/db/generated-env.js +0 -113
- package/dist/db/introspect.d.ts +0 -1
- package/dist/db/introspect.js +0 -122
- package/dist/db/local-env.d.ts +0 -65
- package/dist/db/local-env.js +0 -23
- package/dist/db/new-migration.d.ts +0 -8
- package/dist/db/new-migration.js +0 -69
- package/dist/db/pooler.d.ts +0 -23
- package/dist/db/pooler.js +0 -55
- package/dist/db/run.d.ts +0 -68
- package/dist/db/run.js +0 -156
- package/dist/db/seed-buckets.d.ts +0 -2
- package/dist/db/seed-buckets.js +0 -4
- package/dist/db/seed-production.d.ts +0 -8
- package/dist/db/seed-production.js +0 -79
- package/dist/db/seed-roles.d.ts +0 -7
- package/dist/db/seed-roles.js +0 -33
- package/dist/db/session-refresh.d.ts +0 -43
- package/dist/db/session-refresh.js +0 -40
- package/dist/deploy/migrations.d.ts +0 -20
- package/dist/deploy/migrations.js +0 -68
- package/dist/deploy/orphans.d.ts +0 -20
- package/dist/deploy/orphans.js +0 -177
- package/dist/deploy/preflight.d.ts +0 -53
- package/dist/deploy/preflight.js +0 -189
- package/dist/deploy/report.d.ts +0 -26
- package/dist/deploy/report.js +0 -70
- package/dist/deploy/require-planner.d.ts +0 -2
- package/dist/deploy/require-planner.js +0 -60
- package/dist/deploy/require-token.d.ts +0 -15
- package/dist/deploy/require-token.js +0 -78
- package/dist/deploy/secrets-file.d.ts +0 -17
- package/dist/deploy/secrets-file.js +0 -166
- package/dist/deploy/write-env.d.ts +0 -40
- package/dist/deploy/write-env.js +0 -395
- package/dist/docs/index-pages.d.ts +0 -38
- package/dist/docs/index-pages.js +0 -186
- package/dist/emails/commands.d.ts +0 -15
- package/dist/emails/commands.js +0 -130
- package/dist/emails/email-types.d.ts +0 -13
- package/dist/emails/email-types.js +0 -1
- package/dist/emails/fixtures.d.ts +0 -20
- package/dist/emails/fixtures.js +0 -20
- package/dist/env/audit.d.ts +0 -206
- package/dist/env/audit.js +0 -436
- package/dist/env/cloudflare.d.ts +0 -21
- package/dist/env/cloudflare.js +0 -61
- package/dist/env/commands.d.ts +0 -99
- package/dist/env/commands.js +0 -613
- package/dist/env/discovery.d.ts +0 -15
- package/dist/env/discovery.js +0 -239
- package/dist/env/document.d.ts +0 -119
- package/dist/env/document.js +0 -323
- package/dist/env/example.d.ts +0 -68
- package/dist/env/example.js +0 -605
- package/dist/env/selection.d.ts +0 -105
- package/dist/env/selection.js +0 -228
- package/dist/env-entry.d.ts +0 -49
- package/dist/env-entry.js +0 -204
- package/dist/environment-doctor.d.ts +0 -45
- package/dist/environment-doctor.js +0 -429
- package/dist/environment.d.ts +0 -92
- package/dist/environment.js +0 -185
- package/dist/fingerprint.d.ts +0 -10
- package/dist/fingerprint.js +0 -18
- package/dist/gen/campus-map.d.ts +0 -1
- package/dist/gen/campus-map.js +0 -473
- package/dist/gen/commands.d.ts +0 -1
- package/dist/gen/commands.js +0 -25
- package/dist/gen/hypno.d.ts +0 -1
- package/dist/gen/hypno.js +0 -134
- package/dist/gh/client.d.ts +0 -80
- package/dist/gh/client.js +0 -259
- package/dist/gh/environments.d.ts +0 -83
- package/dist/gh/environments.js +0 -178
- package/dist/gh/rulesets/actors.d.ts +0 -14
- package/dist/gh/rulesets/actors.js +0 -69
- package/dist/gh/rulesets/api.d.ts +0 -15
- package/dist/gh/rulesets/api.js +0 -104
- package/dist/gh/rulesets/commands.d.ts +0 -3
- package/dist/gh/rulesets/commands.js +0 -203
- package/dist/gh/rulesets/desired.d.ts +0 -112
- package/dist/gh/rulesets/desired.js +0 -271
- package/dist/gh/rulesets/diff.d.ts +0 -65
- package/dist/gh/rulesets/diff.js +0 -170
- package/dist/gh/rulesets/types.d.ts +0 -76
- package/dist/gh/rulesets/types.js +0 -13
- package/dist/gh/settings/api.d.ts +0 -40
- package/dist/gh/settings/api.js +0 -180
- package/dist/gh/settings/commands.d.ts +0 -1
- package/dist/gh/settings/commands.js +0 -254
- package/dist/gh/settings/desired.d.ts +0 -53
- package/dist/gh/settings/desired.js +0 -43
- package/dist/gh/settings/diff.d.ts +0 -57
- package/dist/gh/settings/diff.js +0 -161
- package/dist/gh/settings/types.d.ts +0 -92
- package/dist/gh/settings/types.js +0 -11
- package/dist/gh/settings/workflows.d.ts +0 -41
- package/dist/gh/settings/workflows.js +0 -128
- package/dist/grantRoot.d.ts +0 -42
- package/dist/grantRoot.js +0 -89
- package/dist/help.d.ts +0 -18
- package/dist/help.js +0 -169
- package/dist/images/commands.d.ts +0 -32
- package/dist/images/commands.js +0 -231
- package/dist/images/events.d.ts +0 -47
- package/dist/images/events.js +0 -116
- package/dist/images/graphics.d.ts +0 -70
- package/dist/images/graphics.js +0 -234
- package/dist/images/open-graph-types.d.ts +0 -75
- package/dist/images/open-graph-types.js +0 -1
- package/dist/images/prompts.d.ts +0 -34
- package/dist/images/prompts.js +0 -100
- package/dist/images/render.d.ts +0 -21
- package/dist/images/render.js +0 -61
- package/dist/images/select.d.ts +0 -64
- package/dist/images/select.js +0 -96
- package/dist/instance.d.ts +0 -69
- package/dist/instance.js +0 -102
- package/dist/invocation.d.ts +0 -49
- package/dist/invocation.js +0 -78
- package/dist/launch-ci.d.ts +0 -1
- package/dist/launch-ci.js +0 -90
- package/dist/launch.d.ts +0 -18
- package/dist/menu.d.ts +0 -43
- package/dist/menu.js +0 -300
- package/dist/moderation.d.ts +0 -70
- package/dist/moderation.js +0 -128
- package/dist/oauth/browser.d.ts +0 -1
- package/dist/oauth/browser.js +0 -13
- package/dist/oauth/config.d.ts +0 -24
- package/dist/oauth/config.js +0 -24
- package/dist/oauth/connect-url.d.ts +0 -17
- package/dist/oauth/connect-url.js +0 -23
- package/dist/oauth/db.d.ts +0 -37
- package/dist/oauth/db.js +0 -108
- package/dist/oauth/device.d.ts +0 -93
- package/dist/oauth/device.js +0 -198
- package/dist/oauth/discovery.d.ts +0 -28
- package/dist/oauth/discovery.js +0 -54
- package/dist/oauth/env-file.d.ts +0 -6
- package/dist/oauth/env-file.js +0 -35
- package/dist/oauth/exchange.d.ts +0 -43
- package/dist/oauth/exchange.js +0 -93
- package/dist/oauth/label.d.ts +0 -1
- package/dist/oauth/label.js +0 -25
- package/dist/oauth/loopback.d.ts +0 -52
- package/dist/oauth/loopback.js +0 -168
- package/dist/oauth/pkce.d.ts +0 -6
- package/dist/oauth/pkce.js +0 -34
- package/dist/oauth/target.d.ts +0 -54
- package/dist/oauth/target.js +0 -102
- package/dist/oauth/transport.d.ts +0 -41
- package/dist/oauth/transport.js +0 -32
- package/dist/oauth/wizard.d.ts +0 -40
- package/dist/oauth/wizard.js +0 -578
- package/dist/persona.d.ts +0 -18
- package/dist/persona.js +0 -221
- package/dist/pick.d.ts +0 -6
- package/dist/pick.js +0 -64
- package/dist/pipes.d.ts +0 -1
- package/dist/pipes.js +0 -25
- package/dist/planner/checks.d.ts +0 -26
- package/dist/planner/checks.js +0 -104
- package/dist/planner/commands.d.ts +0 -10
- package/dist/planner/commands.js +0 -367
- package/dist/planner/db.d.ts +0 -15
- package/dist/planner/db.js +0 -33
- package/dist/planner/role.d.ts +0 -33
- package/dist/planner/role.js +0 -68
- package/dist/repo/peer-redirect-hooks.d.ts +0 -16
- package/dist/repo/peer-redirect-hooks.js +0 -10
- package/dist/repo/peer-redirect.d.ts +0 -4
- package/dist/repo/peer-redirect.js +0 -26
- package/dist/repo/peers.d.ts +0 -39
- package/dist/repo/peers.js +0 -139
- package/dist/repo/resolve.d.ts +0 -41
- package/dist/repo/resolve.js +0 -152
- package/dist/repo/root.d.ts +0 -23
- package/dist/repo/root.js +0 -91
- package/dist/repo/source.d.ts +0 -14
- package/dist/repo/source.js +0 -54
- package/dist/repo/supabase-project.d.ts +0 -60
- package/dist/repo/supabase-project.js +0 -124
- package/dist/repo/tsx-loader.d.ts +0 -6
- package/dist/repo/tsx-loader.js +0 -41
- package/dist/run/pick.d.ts +0 -90
- package/dist/run/pick.js +0 -664
- package/dist/setup.d.ts +0 -1
- package/dist/setup.js +0 -280
- package/dist/stack.d.ts +0 -48
- package/dist/stack.js +0 -403
- package/dist/telemetry.d.ts +0 -59
- package/dist/tier.d.ts +0 -40
- package/dist/tier.js +0 -96
- package/dist/ui.d.ts +0 -40
- package/dist/ui.js +0 -71
- package/dist/version.d.ts +0 -7
- package/dist/version.js +0 -17
- package/dist/workers.d.ts +0 -5
- package/dist/workers.js +0 -33
- package/dist/workflows/commands.d.ts +0 -54
- package/dist/workflows/commands.js +0 -763
package/dist/planner/commands.js
DELETED
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `pnpm devtools db planner <status|create|reset-password|drop>`, the operator
|
|
3
|
-
* side of the `migration_planner` role.
|
|
4
|
-
*
|
|
5
|
-
* Four commands because the role has exactly four lifecycle moments:
|
|
6
|
-
*
|
|
7
|
-
* status does it exist, can it log in, does it hold the two
|
|
8
|
-
* validated grants and nothing that reaches further
|
|
9
|
-
* create mint it: role + grants + a generated password, verified
|
|
10
|
-
* live, written into .env.preflight
|
|
11
|
-
* reset-password new password on the existing role, same verification,
|
|
12
|
-
* same write
|
|
13
|
-
* drop remove it: the recovery path for a role in a shape
|
|
14
|
-
* create refuses to repair, and the retirement path if
|
|
15
|
-
* the tier is ever redesigned
|
|
16
|
-
*
|
|
17
|
-
* There is no `retrieve`. A Postgres password is not retrievable, the server
|
|
18
|
-
* holds a hash, and the composed DB_URL's home is `.env.preflight`, synced
|
|
19
|
-
* outward by `env push --target preflight` and back by `env pull`. Losing the
|
|
20
|
-
* local copy is what `pull` is for; losing every copy is what
|
|
21
|
-
* `reset-password` is for. Printing it here would put a live credential in a
|
|
22
|
-
* terminal scrollback for no state those two do not already cover.
|
|
23
|
-
*
|
|
24
|
-
* ## Where the admin connection comes from
|
|
25
|
-
*
|
|
26
|
-
* Creating a role needs a privileged connection to the PRODUCTION database.
|
|
27
|
-
* The planner exists so `main` can ask "what would these migrations do to
|
|
28
|
-
* production". That connection is read from `.env.production`'s `DB_URL`
|
|
29
|
-
* (fetch it with `env pull --target production`), or passed as `--db-url` for
|
|
30
|
-
* the bootstrap case where that file is not populated yet. Both paths write to
|
|
31
|
-
* production, so both confirm first: `production` is a guarded target, and a
|
|
32
|
-
* role created by reflex is a credential nobody meant to mint.
|
|
33
|
-
*
|
|
34
|
-
* ## Verified live
|
|
35
|
-
*
|
|
36
|
-
* `create` and `reset-password` end by opening a SECOND connection as the
|
|
37
|
-
* planner and running the same three checks CI's `deploy require-planner`
|
|
38
|
-
* runs. "The role works and cannot overreach" is observed before the command
|
|
39
|
-
* says done: the property the security plan records as "validated working",
|
|
40
|
-
* re-established on every mint instead of remembered from one.
|
|
41
|
-
*/
|
|
42
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
43
|
-
import { resolve } from "node:path";
|
|
44
|
-
import { confirm, log, note } from "@clack/prompts";
|
|
45
|
-
import { fingerprint } from "../fingerprint.js";
|
|
46
|
-
import { findRepoRoot } from "../repo/root.js";
|
|
47
|
-
import { loadEnv } from "../repo/peers.js";
|
|
48
|
-
import { bail, unwrap } from "../ui.js";
|
|
49
|
-
import { EnvDocument } from "../env/document.js";
|
|
50
|
-
import { checkPlanner } from "./checks.js";
|
|
51
|
-
import { connectDb } from "./db.js";
|
|
52
|
-
import { PLANNER_ROLE, createRoleSql, generatePassword, plannerUrlFrom, resetPasswordSql, } from "./role.js";
|
|
53
|
-
/** Constants, like `checks.ts`: `PlannerDb.run` never sees a composed string. */
|
|
54
|
-
const QUERY_ROLE = `select rolcanlogin from pg_roles where rolname = '${PLANNER_ROLE}'`;
|
|
55
|
-
/**
|
|
56
|
-
* The schema the grants attach to. The Supabase CLI creates it the first time
|
|
57
|
-
* it records migration history on a database, so on a database that has never
|
|
58
|
-
* been CLI-migrated, or baselined with `supabase migration repair`, there is
|
|
59
|
-
* nothing to grant on. `create` has to say that BEFORE issuing CREATE ROLE, or
|
|
60
|
-
* it strands a grant-less role behind a refusal to re-run.
|
|
61
|
-
*/
|
|
62
|
-
const QUERY_SCHEMA = "select 1 from pg_namespace where nspname = 'supabase_migrations'";
|
|
63
|
-
const QUERY_GRANTS = `select has_schema_privilege('${PLANNER_ROLE}', 'supabase_migrations', 'usage') as schema_usage, ` +
|
|
64
|
-
`has_table_privilege('${PLANNER_ROLE}', 'supabase_migrations.schema_migrations', 'select') as table_select`;
|
|
65
|
-
/**
|
|
66
|
-
* Membership is the quiet way a "narrow" role widens: `grant postgres to
|
|
67
|
-
* migration_planner` leaves both privilege queries above looking correct.
|
|
68
|
-
*/
|
|
69
|
-
const QUERY_MEMBERSHIPS = "select r.rolname from pg_auth_members m " +
|
|
70
|
-
"join pg_roles r on r.oid = m.roleid " +
|
|
71
|
-
"join pg_roles g on g.oid = m.member " +
|
|
72
|
-
`where g.rolname = '${PLANNER_ROLE}' order by r.rolname`;
|
|
73
|
-
/**
|
|
74
|
-
* The schemas a dry run has no business seeing. `public` is absent on
|
|
75
|
-
* purpose: PUBLIC-role defaults give every login usage there, so listing it
|
|
76
|
-
* would make this red on a correctly minted role.
|
|
77
|
-
*/
|
|
78
|
-
const QUERY_OVERREACH = "select nspname from pg_namespace " +
|
|
79
|
-
"where nspname in ('platform', 'schedule_builder', 'study_group_finder', 'auth', 'storage') " +
|
|
80
|
-
`and has_schema_privilege('${PLANNER_ROLE}', nspname, 'usage') order by nspname`;
|
|
81
|
-
async function preflightPath() {
|
|
82
|
-
const { fileFor } = await loadEnv();
|
|
83
|
-
return resolve(findRepoRoot(), fileFor("preflight"));
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* The admin connection string: `--db-url`, else `.env.production`'s `DB_URL`.
|
|
87
|
-
*
|
|
88
|
-
* Read from the FILE rather than `process.env` so that running this under
|
|
89
|
-
* `with-env` (which loads the development `.env`) cannot silently point a
|
|
90
|
-
* CREATE ROLE at the shared dev database. The wrong database succeeding is
|
|
91
|
-
* worse than the right one refusing.
|
|
92
|
-
*/
|
|
93
|
-
async function adminUrl(options) {
|
|
94
|
-
if (options.dbUrl)
|
|
95
|
-
return options.dbUrl;
|
|
96
|
-
const path = resolve(findRepoRoot(), (await loadEnv()).fileFor("production"));
|
|
97
|
-
let doc;
|
|
98
|
-
try {
|
|
99
|
-
doc = EnvDocument.parse(await readFile(path, "utf8"));
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
doc = EnvDocument.empty();
|
|
103
|
-
}
|
|
104
|
-
const url = doc.get("DB_URL");
|
|
105
|
-
if (!url) {
|
|
106
|
-
bail("No DB_URL in .env.production. The planner lives on the production " +
|
|
107
|
-
"database, so this needs its full connection string: run " +
|
|
108
|
-
"`pnpm devtools env pull --target production`, or pass --db-url.");
|
|
109
|
-
}
|
|
110
|
-
return url;
|
|
111
|
-
}
|
|
112
|
-
async function withDb(connect, url, body) {
|
|
113
|
-
const db = connect(url);
|
|
114
|
-
try {
|
|
115
|
-
return await body(db);
|
|
116
|
-
}
|
|
117
|
-
finally {
|
|
118
|
-
await db.end();
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
async function roleExists(db) {
|
|
122
|
-
return (await db.run(QUERY_ROLE)).length > 0;
|
|
123
|
-
}
|
|
124
|
-
async function migrationSchemaExists(db) {
|
|
125
|
-
return (await db.run(QUERY_SCHEMA)).length > 0;
|
|
126
|
-
}
|
|
127
|
-
/** Postgres 42501 (insufficient_privilege), with a text fallback. */
|
|
128
|
-
function isPermissionDenied(error) {
|
|
129
|
-
const e = error;
|
|
130
|
-
return e.code === "42501" || /permission denied/i.test(e.message ?? "");
|
|
131
|
-
}
|
|
132
|
-
function errorText(error) {
|
|
133
|
-
return error instanceof Error ? error.message : String(error);
|
|
134
|
-
}
|
|
135
|
-
function bailMissingSchema() {
|
|
136
|
-
bail("The supabase_migrations schema does not exist on this database, so " +
|
|
137
|
-
"there is nothing to grant on — and nothing for the §3.5 dry run to " +
|
|
138
|
-
"plan against. The Supabase CLI creates it the first time it records " +
|
|
139
|
-
"migration history here: `supabase db push` on a fresh database, or " +
|
|
140
|
-
"`supabase migration repair` to baseline one that already has its " +
|
|
141
|
-
"objects. Initialize the history first, then re-run this command.");
|
|
142
|
-
}
|
|
143
|
-
export async function runPlannerStatus(options = {}) {
|
|
144
|
-
const connect = options.connect ?? connectDb;
|
|
145
|
-
await withDb(connect, await adminUrl(options), async (db) => {
|
|
146
|
-
// First, because it recolors everything after it: on a database with no
|
|
147
|
-
// migration history, the story is the missing history, not the grants.
|
|
148
|
-
const schemaReady = await migrationSchemaExists(db);
|
|
149
|
-
const [role] = await db.run(QUERY_ROLE);
|
|
150
|
-
if (!role) {
|
|
151
|
-
log.warn(schemaReady
|
|
152
|
-
? `${PLANNER_ROLE} does not exist. Mint it with \`pnpm devtools db planner create\`.`
|
|
153
|
-
: `${PLANNER_ROLE} does not exist — and neither does the ` +
|
|
154
|
-
"supabase_migrations schema, so `planner create` would refuse: " +
|
|
155
|
-
"initialize this database's migration history first " +
|
|
156
|
-
"(`supabase db push`, or `supabase migration repair` to " +
|
|
157
|
-
"baseline an existing database).");
|
|
158
|
-
process.exitCode = 1;
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const lines = [
|
|
162
|
-
`exists, ${role.rolcanlogin ? "can log in" : "⚠️ CANNOT log in (nologin)"}`,
|
|
163
|
-
];
|
|
164
|
-
if (!schemaReady) {
|
|
165
|
-
lines.push("⚠️ the supabase_migrations schema DOES NOT EXIST on this database " +
|
|
166
|
-
"— the grants below cannot hold and the dry run cannot plan. " +
|
|
167
|
-
"Initialize the migration history, then `planner reset-password` " +
|
|
168
|
-
"will re-verify (or `planner drop` and re-run `planner create`).");
|
|
169
|
-
}
|
|
170
|
-
// Skipped when the schema is absent: has_schema_privilege raises on a
|
|
171
|
-
// name that does not exist, and the schema line above already carries
|
|
172
|
-
// the diagnosis.
|
|
173
|
-
if (schemaReady) {
|
|
174
|
-
const [grants] = await db.run(QUERY_GRANTS);
|
|
175
|
-
lines.push(grants?.schema_usage
|
|
176
|
-
? "has usage on supabase_migrations"
|
|
177
|
-
: "⚠️ MISSING usage on supabase_migrations", grants?.table_select
|
|
178
|
-
? "has select on supabase_migrations.schema_migrations"
|
|
179
|
-
: "⚠️ MISSING select on supabase_migrations.schema_migrations");
|
|
180
|
-
}
|
|
181
|
-
const memberships = await db.run(QUERY_MEMBERSHIPS);
|
|
182
|
-
lines.push(memberships.length === 0
|
|
183
|
-
? "member of no other role"
|
|
184
|
-
: `⚠️ MEMBER OF: ${memberships.map((m) => String(m.rolname)).join(", ")} — inherited privileges reach past the validated pair`);
|
|
185
|
-
const overreach = await db.run(QUERY_OVERREACH);
|
|
186
|
-
lines.push(overreach.length === 0
|
|
187
|
-
? "no usage on any application schema (platform, schedule_builder, study_group_finder, auth, storage)"
|
|
188
|
-
: `⚠️ CAN REACH: ${overreach.map((s) => String(s.nspname)).join(", ")} — \`planner drop\`, then \`planner create\``);
|
|
189
|
-
const healthy = !lines.some((l) => l.includes("⚠️"));
|
|
190
|
-
note(lines.map((l) => `• ${l}`).join("\n"), PLANNER_ROLE);
|
|
191
|
-
if (!healthy) {
|
|
192
|
-
log.error("The role does not match the validated shape. `planner create` " +
|
|
193
|
-
"refuses to touch an existing role — `planner drop` and re-run " +
|
|
194
|
-
"create, or repair the grants by hand (planner/role.ts holds the " +
|
|
195
|
-
"pair).");
|
|
196
|
-
process.exitCode = 1;
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
/** Shared tail of create and reset-password: verify live, write the file. */
|
|
201
|
-
async function verifyAndStore(connect, adminDbUrl, password) {
|
|
202
|
-
const { fileFor } = await loadEnv();
|
|
203
|
-
const url = plannerUrlFrom(adminDbUrl, password);
|
|
204
|
-
// The same three checks `deploy require-planner` runs in CI. Observed here,
|
|
205
|
-
// at minting time, so the first workflow run is a repeat rather than a
|
|
206
|
-
// premiere.
|
|
207
|
-
const verdict = await withDb(connect, url, (db) => checkPlanner(db));
|
|
208
|
-
for (const line of verdict.lines)
|
|
209
|
-
log.info(line);
|
|
210
|
-
if (!verdict.ok) {
|
|
211
|
-
bail(`The freshly minted credential failed verification: ${verdict.problem}`);
|
|
212
|
-
}
|
|
213
|
-
const path = await preflightPath();
|
|
214
|
-
let doc;
|
|
215
|
-
try {
|
|
216
|
-
doc = EnvDocument.parse(await readFile(path, "utf8"));
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
doc = EnvDocument.empty();
|
|
220
|
-
}
|
|
221
|
-
doc.set("DB_URL", url);
|
|
222
|
-
await writeFile(path, doc.toString());
|
|
223
|
-
log.success(`Wrote DB_URL (${fingerprint(url)}) to ${fileFor("preflight")}. ` +
|
|
224
|
-
"Next: `pnpm devtools env push --target preflight`.");
|
|
225
|
-
}
|
|
226
|
-
export async function runPlannerCreate(options = {}) {
|
|
227
|
-
const connect = options.connect ?? connectDb;
|
|
228
|
-
const admin = await adminUrl(options);
|
|
229
|
-
const go = unwrap(await confirm({
|
|
230
|
-
message: `Create role ${PLANNER_ROLE} on the PRODUCTION database, with its ` +
|
|
231
|
-
"two grants and a generated password?",
|
|
232
|
-
initialValue: false,
|
|
233
|
-
}));
|
|
234
|
-
if (!go)
|
|
235
|
-
bail();
|
|
236
|
-
await withDb(connect, admin, async (db) => {
|
|
237
|
-
// Before CREATE ROLE, so a database with no migration history refuses
|
|
238
|
-
// cleanly instead of stranding a grant-less role behind the
|
|
239
|
-
// already-exists refusal below. That happened the first time this ran
|
|
240
|
-
// against a database the CLI had never migrated.
|
|
241
|
-
if (!(await migrationSchemaExists(db)))
|
|
242
|
-
bailMissingSchema();
|
|
243
|
-
if (await roleExists(db)) {
|
|
244
|
-
// Refused rather than repaired: an existing role may hold grants this
|
|
245
|
-
// command did not give it, and layering the validated pair on top would
|
|
246
|
-
// report success over an unvalidated whole. `status` shows the shape;
|
|
247
|
-
// `reset-password` rotates the credential without touching grants.
|
|
248
|
-
bail(`${PLANNER_ROLE} already exists. Run \`planner status\` to check its ` +
|
|
249
|
-
"shape, `planner reset-password` to rotate it — or, for a " +
|
|
250
|
-
"from-scratch mint, `planner drop` and re-run create.");
|
|
251
|
-
}
|
|
252
|
-
const password = generatePassword();
|
|
253
|
-
// One transaction: the role and its grants exist together or not at all.
|
|
254
|
-
// A mid-flight failure used to leave the role behind with no grants,
|
|
255
|
-
// which the already-exists refusal then protected like a real one.
|
|
256
|
-
await db.run("begin");
|
|
257
|
-
try {
|
|
258
|
-
for (const statement of createRoleSql(password)) {
|
|
259
|
-
await db.run(statement);
|
|
260
|
-
}
|
|
261
|
-
await db.run("commit");
|
|
262
|
-
}
|
|
263
|
-
catch (error) {
|
|
264
|
-
await db.run("rollback").catch(() => undefined);
|
|
265
|
-
throw error;
|
|
266
|
-
}
|
|
267
|
-
log.success(`Created ${PLANNER_ROLE} with the two validated grants.`);
|
|
268
|
-
await verifyAndStore(connect, admin, password);
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
export async function runPlannerDrop(options = {}) {
|
|
272
|
-
const { fileFor } = await loadEnv();
|
|
273
|
-
const connect = options.connect ?? connectDb;
|
|
274
|
-
const admin = await adminUrl(options);
|
|
275
|
-
const go = unwrap(await confirm({
|
|
276
|
-
message: `Drop role ${PLANNER_ROLE} from the PRODUCTION database? The ` +
|
|
277
|
-
"preflight credential stops working the moment this runs.",
|
|
278
|
-
initialValue: false,
|
|
279
|
-
}));
|
|
280
|
-
if (!go)
|
|
281
|
-
bail();
|
|
282
|
-
await withDb(connect, admin, async (db) => {
|
|
283
|
-
if (!(await roleExists(db))) {
|
|
284
|
-
bail(`${PLANNER_ROLE} does not exist — nothing to drop.`);
|
|
285
|
-
}
|
|
286
|
-
// Three statements, one transaction, and the order is load-bearing twice:
|
|
287
|
-
//
|
|
288
|
-
// * DROP OWNED before DROP ROLE. DROP ROLE alone fails on a role that
|
|
289
|
-
// still holds grants ("some objects depend on it"), and a role being
|
|
290
|
-
// dropped for the WRONG shape is exactly the one whose grants this
|
|
291
|
-
// command cannot enumerate: stranded grants, or an overreaching grant
|
|
292
|
-
// somebody added by hand. The role owns no objects by design, so DROP
|
|
293
|
-
// OWNED only revokes; if somebody ever made it own something,
|
|
294
|
-
// dropping that too is what "drop" has to mean for a from-scratch
|
|
295
|
-
// re-mint.
|
|
296
|
-
// * GRANT before DROP OWNED. Supabase's `postgres` is CREATEROLE, not
|
|
297
|
-
// superuser, and DROP OWNED requires MEMBERSHIP in the role. Having
|
|
298
|
-
// created it, which confers ADMIN on the role and the right to drop
|
|
299
|
-
// it, is not enough: the first real run failed here with "permission
|
|
300
|
-
// denied". Granting the role to ourselves is what a non-superuser
|
|
301
|
-
// admin has to do, it is idempotent (a re-grant is a NOTICE), and the
|
|
302
|
-
// membership vanishes with the role two statements later, or with the
|
|
303
|
-
// rollback.
|
|
304
|
-
await db.run("begin");
|
|
305
|
-
try {
|
|
306
|
-
await db.run(`grant ${PLANNER_ROLE} to current_user`);
|
|
307
|
-
await db.run(`drop owned by ${PLANNER_ROLE}`);
|
|
308
|
-
await db.run(`drop role ${PLANNER_ROLE}`);
|
|
309
|
-
await db.run("commit");
|
|
310
|
-
}
|
|
311
|
-
catch (error) {
|
|
312
|
-
await db.run("rollback").catch(() => undefined);
|
|
313
|
-
if (isPermissionDenied(error)) {
|
|
314
|
-
bail(`Postgres refused: ${errorText(error)}. Dropping ${PLANNER_ROLE} ` +
|
|
315
|
-
"needs a connection that can administer it — on Supabase that " +
|
|
316
|
-
"is the `postgres` user's own connection string (what " +
|
|
317
|
-
"`.env.production`'s DB_URL should hold), not a lesser role. " +
|
|
318
|
-
"If postgres itself is refused, the role was created by " +
|
|
319
|
-
"somebody else's admin role; drop it from the dashboard's SQL " +
|
|
320
|
-
"editor, which runs as postgres.");
|
|
321
|
-
}
|
|
322
|
-
throw error;
|
|
323
|
-
}
|
|
324
|
-
log.success(`Dropped ${PLANNER_ROLE}.`);
|
|
325
|
-
});
|
|
326
|
-
// The stored credential died with the role, and a dead value left in place
|
|
327
|
-
// would push cleanly and audit green. Blanked rather than deleted, because a
|
|
328
|
-
// blank line is the file's "fill me in" convention. Only when the value is
|
|
329
|
-
// actually the planner's: a hand-set URL under this key is somebody's
|
|
330
|
-
// deliberate state.
|
|
331
|
-
const path = await preflightPath();
|
|
332
|
-
let doc;
|
|
333
|
-
try {
|
|
334
|
-
doc = EnvDocument.parse(await readFile(path, "utf8"));
|
|
335
|
-
}
|
|
336
|
-
catch {
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
const stored = doc.get("DB_URL");
|
|
340
|
-
if (stored?.includes(PLANNER_ROLE)) {
|
|
341
|
-
doc.set("DB_URL", "");
|
|
342
|
-
await writeFile(path, doc.toString());
|
|
343
|
-
log.info(`Blanked the dead DB_URL in ${fileFor("preflight")}. After the next ` +
|
|
344
|
-
"`planner create`, push it with `env push --target preflight`.");
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
export async function runPlannerResetPassword(options = {}) {
|
|
348
|
-
const connect = options.connect ?? connectDb;
|
|
349
|
-
const admin = await adminUrl(options);
|
|
350
|
-
const go = unwrap(await confirm({
|
|
351
|
-
message: `Reset ${PLANNER_ROLE}'s password on the PRODUCTION database? The ` +
|
|
352
|
-
"old connection string stops working the moment this runs.",
|
|
353
|
-
initialValue: false,
|
|
354
|
-
}));
|
|
355
|
-
if (!go)
|
|
356
|
-
bail();
|
|
357
|
-
await withDb(connect, admin, async (db) => {
|
|
358
|
-
if (!(await roleExists(db))) {
|
|
359
|
-
bail(`${PLANNER_ROLE} does not exist — nothing to reset. Mint it with ` +
|
|
360
|
-
"`pnpm devtools db planner create`.");
|
|
361
|
-
}
|
|
362
|
-
const password = generatePassword();
|
|
363
|
-
await db.run(resetPasswordSql(password));
|
|
364
|
-
log.success(`Reset ${PLANNER_ROLE}'s password.`);
|
|
365
|
-
await verifyAndStore(connect, admin, password);
|
|
366
|
-
});
|
|
367
|
-
}
|
package/dist/planner/db.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export interface PlannerDb {
|
|
2
|
-
run(query: string): Promise<Record<string, unknown>[]>;
|
|
3
|
-
end(): Promise<void>;
|
|
4
|
-
}
|
|
5
|
-
/** How a caller opens one; injectable so tests never touch the network. */
|
|
6
|
-
export type Connect = (url: string) => PlannerDb;
|
|
7
|
-
/**
|
|
8
|
-
* Opens the single connection the planner commands run through.
|
|
9
|
-
*
|
|
10
|
-
* `max: 1` because every use here is a handful of sequential statements, and
|
|
11
|
-
* `prepare: false` because the session pooler tolerates prepared statements
|
|
12
|
-
* but gains nothing from them for one-shot queries. The timeout keeps a
|
|
13
|
-
* wrong-host URL from hanging a CI job for its full step timeout.
|
|
14
|
-
*/
|
|
15
|
-
export declare function connectDb(url: string): PlannerDb;
|
package/dist/planner/db.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The one seam between the planner commands and a real Postgres.
|
|
3
|
-
*
|
|
4
|
-
* Deliberately tiny: a runner of constant SQL strings and an `end()`. Every
|
|
5
|
-
* query in this feature is a string literal. Nothing user-shaped is ever
|
|
6
|
-
* interpolated except the generated password, whose alphabet `role.ts`
|
|
7
|
-
* constrains. That lets the interface stay `run(text)`, and lets tests hand in
|
|
8
|
-
* a map from query to rows instead of mocking a client library.
|
|
9
|
-
*/
|
|
10
|
-
import postgres from "postgres";
|
|
11
|
-
/**
|
|
12
|
-
* Opens the single connection the planner commands run through.
|
|
13
|
-
*
|
|
14
|
-
* `max: 1` because every use here is a handful of sequential statements, and
|
|
15
|
-
* `prepare: false` because the session pooler tolerates prepared statements
|
|
16
|
-
* but gains nothing from them for one-shot queries. The timeout keeps a
|
|
17
|
-
* wrong-host URL from hanging a CI job for its full step timeout.
|
|
18
|
-
*/
|
|
19
|
-
export function connectDb(url) {
|
|
20
|
-
const sql = postgres(url, {
|
|
21
|
-
max: 1,
|
|
22
|
-
prepare: false,
|
|
23
|
-
connect_timeout: 15,
|
|
24
|
-
});
|
|
25
|
-
return {
|
|
26
|
-
async run(query) {
|
|
27
|
-
return await sql.unsafe(query);
|
|
28
|
-
},
|
|
29
|
-
async end() {
|
|
30
|
-
await sql.end({ timeout: 5 });
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
package/dist/planner/role.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export declare const PLANNER_ROLE = "migration_planner";
|
|
2
|
-
/**
|
|
3
|
-
* The statements `planner create` runs, in order. Exported so the test can
|
|
4
|
-
* assert the grants byte-for-byte against the security plan's validated pair.
|
|
5
|
-
* A paraphrase here would be a second copy that can drift.
|
|
6
|
-
*
|
|
7
|
-
* The password is interpolated rather than bound because Postgres accepts no
|
|
8
|
-
* bind parameters in a utility statement like CREATE ROLE. That is safe here
|
|
9
|
-
* ONLY because `generatePassword()` emits the base64url alphabet: no quote,
|
|
10
|
-
* no backslash, nothing needing escape. Do not widen the alphabet without
|
|
11
|
-
* revisiting this.
|
|
12
|
-
*/
|
|
13
|
-
export declare function createRoleSql(password: string): string[];
|
|
14
|
-
/** ALTER ROLE, same interpolation contract as `createRoleSql`. */
|
|
15
|
-
export declare function resetPasswordSql(password: string): string;
|
|
16
|
-
/**
|
|
17
|
-
* 192 bits, base64url. The alphabet is load-bearing twice over: it needs no
|
|
18
|
-
* escaping inside the SQL literal above, and no percent-encoding inside the
|
|
19
|
-
* userinfo of the connection URL the password ends up in.
|
|
20
|
-
*/
|
|
21
|
-
export declare function generatePassword(): string;
|
|
22
|
-
/**
|
|
23
|
-
* The preflight `DB_URL`, derived from the admin one.
|
|
24
|
-
*
|
|
25
|
-
* Everything but the identity is kept, including host, port, and database,
|
|
26
|
-
* because the planner connects to the same place as everyone else. Only the userinfo
|
|
27
|
-
* changes, and the username's shape depends on the route in a way worth
|
|
28
|
-
* spelling out: through Supabase's session pooler the username is
|
|
29
|
-
* `<role>.<project-ref>`, so the ref (everything after the first dot of the
|
|
30
|
-
* admin username) survives the swap; on a direct connection the username is
|
|
31
|
-
* the bare role.
|
|
32
|
-
*/
|
|
33
|
-
export declare function plannerUrlFrom(adminUrl: string, password: string): string;
|
package/dist/planner/role.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `migration_planner` role: the one Postgres credential `main` may hold.
|
|
3
|
-
*
|
|
4
|
-
* §3.5's stage-1 dry run answers "what would these migrations do to
|
|
5
|
-
* production" from `main`, so whatever it authenticates with is reachable
|
|
6
|
-
* from the `main` trust tier. The plan refuses even a general read-only role
|
|
7
|
-
* there, because that puts every row of production data behind that boundary.
|
|
8
|
-
* What is left is a role that can read the migrations-history table and
|
|
9
|
-
* nothing else. Two grants are the whole of it:
|
|
10
|
-
*
|
|
11
|
-
* grant usage on schema supabase_migrations to migration_planner;
|
|
12
|
-
* grant select on supabase_migrations.schema_migrations to migration_planner;
|
|
13
|
-
*
|
|
14
|
-
* This module is the pure half: names, SQL text, password generation, URL
|
|
15
|
-
* derivation. Everything that talks to a database lives beside it.
|
|
16
|
-
*/
|
|
17
|
-
import { randomBytes } from "node:crypto";
|
|
18
|
-
export const PLANNER_ROLE = "migration_planner";
|
|
19
|
-
/**
|
|
20
|
-
* The statements `planner create` runs, in order. Exported so the test can
|
|
21
|
-
* assert the grants byte-for-byte against the security plan's validated pair.
|
|
22
|
-
* A paraphrase here would be a second copy that can drift.
|
|
23
|
-
*
|
|
24
|
-
* The password is interpolated rather than bound because Postgres accepts no
|
|
25
|
-
* bind parameters in a utility statement like CREATE ROLE. That is safe here
|
|
26
|
-
* ONLY because `generatePassword()` emits the base64url alphabet: no quote,
|
|
27
|
-
* no backslash, nothing needing escape. Do not widen the alphabet without
|
|
28
|
-
* revisiting this.
|
|
29
|
-
*/
|
|
30
|
-
export function createRoleSql(password) {
|
|
31
|
-
return [
|
|
32
|
-
`create role ${PLANNER_ROLE} login password '${password}'`,
|
|
33
|
-
`grant usage on schema supabase_migrations to ${PLANNER_ROLE}`,
|
|
34
|
-
`grant select on supabase_migrations.schema_migrations to ${PLANNER_ROLE}`,
|
|
35
|
-
];
|
|
36
|
-
}
|
|
37
|
-
/** ALTER ROLE, same interpolation contract as `createRoleSql`. */
|
|
38
|
-
export function resetPasswordSql(password) {
|
|
39
|
-
return `alter role ${PLANNER_ROLE} password '${password}'`;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* 192 bits, base64url. The alphabet is load-bearing twice over: it needs no
|
|
43
|
-
* escaping inside the SQL literal above, and no percent-encoding inside the
|
|
44
|
-
* userinfo of the connection URL the password ends up in.
|
|
45
|
-
*/
|
|
46
|
-
export function generatePassword() {
|
|
47
|
-
return randomBytes(24).toString("base64url");
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* The preflight `DB_URL`, derived from the admin one.
|
|
51
|
-
*
|
|
52
|
-
* Everything but the identity is kept, including host, port, and database,
|
|
53
|
-
* because the planner connects to the same place as everyone else. Only the userinfo
|
|
54
|
-
* changes, and the username's shape depends on the route in a way worth
|
|
55
|
-
* spelling out: through Supabase's session pooler the username is
|
|
56
|
-
* `<role>.<project-ref>`, so the ref (everything after the first dot of the
|
|
57
|
-
* admin username) survives the swap; on a direct connection the username is
|
|
58
|
-
* the bare role.
|
|
59
|
-
*/
|
|
60
|
-
export function plannerUrlFrom(adminUrl, password) {
|
|
61
|
-
const url = new URL(adminUrl);
|
|
62
|
-
const ref = url.username.includes(".")
|
|
63
|
-
? url.username.slice(url.username.indexOf("."))
|
|
64
|
-
: "";
|
|
65
|
-
url.username = `${PLANNER_ROLE}${ref}`;
|
|
66
|
-
url.password = password;
|
|
67
|
-
return url.toString();
|
|
68
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Module-loader hooks for `repo/peer-redirect.ts`. Runs on Node's loader
|
|
3
|
-
* thread, not the main one, so nothing here can import devtools state: the
|
|
4
|
-
* redirects arrive once, as `initialize()`'s data, and are never updated.
|
|
5
|
-
*/
|
|
6
|
-
import type { InitializeHook, ResolveHook } from "node:module";
|
|
7
|
-
export interface PeerRedirect {
|
|
8
|
-
/** The exact file URL of the importing module the redirect applies to. */
|
|
9
|
-
parentURL: string;
|
|
10
|
-
/** The bare specifier it imports, e.g. `@devdogsuga/env`. */
|
|
11
|
-
specifier: string;
|
|
12
|
-
/** Where that specifier should resolve instead. */
|
|
13
|
-
url: string;
|
|
14
|
-
}
|
|
15
|
-
export declare const initialize: InitializeHook<PeerRedirect[]>;
|
|
16
|
-
export declare const resolve: ResolveHook;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
let redirects = [];
|
|
2
|
-
export const initialize = (data) => {
|
|
3
|
-
redirects = data;
|
|
4
|
-
};
|
|
5
|
-
export const resolve = (specifier, context, nextResolve) => {
|
|
6
|
-
const redirect = redirects.find((r) => r.specifier === specifier && r.parentURL === context.parentURL);
|
|
7
|
-
if (redirect)
|
|
8
|
-
return { url: redirect.url, shortCircuit: true };
|
|
9
|
-
return nextResolve(specifier, context);
|
|
10
|
-
};
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { PeerRedirect } from "./peer-redirect-hooks.js";
|
|
2
|
-
/** Registers `redirect`. Hooks cannot be unregistered, so each call adds a
|
|
3
|
-
* loader for the rest of the process; call it once per redirect. */
|
|
4
|
-
export declare function redirectPeer(redirect: PeerRedirect): void;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Points one module's bare import of a peer at the copy the target repo
|
|
3
|
-
* resolved (`repo/peers.ts`).
|
|
4
|
-
*
|
|
5
|
-
* devtools' own `env.ts` manifest imports `@devdogsuga/env` by bare
|
|
6
|
-
* specifier, and from an installed copy that specifier resolves relative to
|
|
7
|
-
* the manifest's own location: inside the pnpm store, next to devtools,
|
|
8
|
-
* where an optional peer is never installed under `pnpm dlx`. The import
|
|
9
|
-
* failed with ERR_MODULE_NOT_FOUND, and every `env` command with it. Even
|
|
10
|
-
* where the specifier does resolve (devtools installed as a devDependency),
|
|
11
|
-
* the only copy that keeps the registry's module identity is the one the
|
|
12
|
-
* repo's manifests use, so the redirect is applied whenever that copy is
|
|
13
|
-
* known rather than only as a fallback.
|
|
14
|
-
*
|
|
15
|
-
* Node resolve hooks (`module.register`) rather than rewriting the file: the
|
|
16
|
-
* manifest also imports `zod`, which has to keep resolving from devtools'
|
|
17
|
-
* own location, and a copy written anywhere else would lose that.
|
|
18
|
-
*/
|
|
19
|
-
import { register } from "node:module";
|
|
20
|
-
/** Registers `redirect`. Hooks cannot be unregistered, so each call adds a
|
|
21
|
-
* loader for the rest of the process; call it once per redirect. */
|
|
22
|
-
export function redirectPeer(redirect) {
|
|
23
|
-
register("./peer-redirect-hooks.js", import.meta.url, {
|
|
24
|
-
data: [redirect],
|
|
25
|
-
});
|
|
26
|
-
}
|
package/dist/repo/peers.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/** Test-only: clears the memoized peer modules so a test can re-resolve with a fresh mock. */
|
|
2
|
-
export declare function resetPeerCacheForTests(): void;
|
|
3
|
-
/**
|
|
4
|
-
* The file URL `specifier` was loaded from through the target repo, or
|
|
5
|
-
* undefined if it has not been loaded yet or came from a plain bare-specifier
|
|
6
|
-
* `import()` (the test-suite and not-in-a-repo fallbacks above).
|
|
7
|
-
*
|
|
8
|
-
* For code devtools imports that itself names a peer by bare specifier --
|
|
9
|
-
* today only devtools' own `env.ts` manifest. From an installed copy that
|
|
10
|
-
* specifier cannot resolve at all (an optional peer is never installed next
|
|
11
|
-
* to devtools under `pnpm dlx`), and even where it can, the only copy that
|
|
12
|
-
* keeps module identity is this one. `repo/peer-redirect.ts` uses it to
|
|
13
|
-
* point the import here.
|
|
14
|
-
*/
|
|
15
|
-
export declare function repoPeerUrl(specifier: string): string | undefined;
|
|
16
|
-
import type * as EnvModule from "@devdogsuga/env";
|
|
17
|
-
import type * as EnvLoadModule from "@devdogsuga/env/load";
|
|
18
|
-
import type * as EnvSessionModule from "@devdogsuga/env/session";
|
|
19
|
-
export declare function loadEnv(): Promise<typeof EnvModule>;
|
|
20
|
-
/**
|
|
21
|
-
* The synchronous companion to `loadEnv()`, for the handful of call sites
|
|
22
|
-
* (`gh/environments.ts`'s `GITHUB_ENVIRONMENT_SPECS` getters) that cannot
|
|
23
|
-
* become async without changing their own callers' contract — they read a
|
|
24
|
-
* plain array off a getter, not a Promise. Safe ONLY after `loadEnv()` has
|
|
25
|
-
* resolved at least once; every caller of these getters already requires
|
|
26
|
-
* `env/discovery.ts`'s `loadRegistry()` (which calls `loadEnv()` itself) to
|
|
27
|
-
* have run first, via `assertRegistryLoaded()`'s own guard.
|
|
28
|
-
*/
|
|
29
|
-
export declare function getEnvSync(): typeof EnvModule;
|
|
30
|
-
/** Test-only: clears the sync cache alongside `resetPeerCacheForTests()`. */
|
|
31
|
-
export declare function resetEnvSyncCacheForTests(): void;
|
|
32
|
-
export declare function loadEnvLoad(): Promise<typeof EnvLoadModule>;
|
|
33
|
-
export declare function loadEnvSession(): Promise<typeof EnvSessionModule>;
|
|
34
|
-
import type * as DbTypegenModule from "@devdogsuga/db/typegen";
|
|
35
|
-
export declare function loadDbTypegen(): Promise<typeof DbTypegenModule>;
|
|
36
|
-
import type * as BrandEventModule from "@devdogsuga/brand/event";
|
|
37
|
-
export declare function loadBrandEvent(): Promise<typeof BrandEventModule>;
|
|
38
|
-
import type * as EventsModule from "@devdogsuga/events";
|
|
39
|
-
export declare function loadEvents(): Promise<typeof EventsModule>;
|