@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/env/example.d.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { EnvTarget } from "@devdogsuga/env";
|
|
2
|
-
/**
|
|
3
|
-
* The sections a development init may narrow to: the four apps.
|
|
4
|
-
*
|
|
5
|
-
* Order is the PICKER's order, not the file's (that is `SECTION_ORDER`, fixed
|
|
6
|
-
* so `.env.example` stays byte-stable). The two projects most contributors
|
|
7
|
-
* join come first; `platform` follows as the shared auth infrastructure you
|
|
8
|
-
* depend on but rarely edit, and `sandbox` last as the dormant one almost
|
|
9
|
-
* nobody touches.
|
|
10
|
-
*
|
|
11
|
-
* `supabase` is NOT here and NOT optional. It is the shared database and auth
|
|
12
|
-
* layer every app runs against, so any selection implies it (the caller adds
|
|
13
|
-
* it). `devtools` is not here either, deliberately: it is a ROLE, not a
|
|
14
|
-
* project. A contributor picks what they are building; whether they also
|
|
15
|
-
* operate deploys is a separate question the picker asks separately.
|
|
16
|
-
*/
|
|
17
|
-
export declare const APP_SECTIONS: readonly ["schedule-builder", "study-group-finder", "platform", "sandbox"];
|
|
18
|
-
/**
|
|
19
|
-
* Every declared key belonging to one of the chosen sections.
|
|
20
|
-
*
|
|
21
|
-
* `some` over a key's declaring sources, so a key SHARED between a chosen and
|
|
22
|
-
* an unchosen app (API_URL, the whole connection block) is included by
|
|
23
|
-
* whichever side was picked. A narrowed file must never lose the
|
|
24
|
-
* infrastructure a chosen app boots on just because another app shares it.
|
|
25
|
-
*/
|
|
26
|
-
export declare function keysForSections(chosen: ReadonlySet<string>): Set<string>;
|
|
27
|
-
/** The committed `.env.example`: generated header + rendered registry. */
|
|
28
|
-
export declare function renderExample(): string;
|
|
29
|
-
/**
|
|
30
|
-
* A fresh env file for `env init`.
|
|
31
|
-
*
|
|
32
|
-
* Two renderings, not one with a different first line. See the module header.
|
|
33
|
-
* `development` wants every declared key with its `example` as written, which
|
|
34
|
-
* is also what `.env.example` wants; a vault target wants what a push for that
|
|
35
|
-
* target routes, blank unless the registry can derive it.
|
|
36
|
-
*/
|
|
37
|
-
export declare function renderInit(target: EnvTarget, date: string,
|
|
38
|
-
/**
|
|
39
|
-
* Development only: the sections to render, from the project picker.
|
|
40
|
-
* Callers pass the CHOSEN set plus `supabase` (the shared stack is implied
|
|
41
|
-
* by any choice); absent means everything, which is what the vault targets
|
|
42
|
-
* and every pre-picker caller get.
|
|
43
|
-
*/
|
|
44
|
-
sections?: ReadonlySet<string>): string;
|
|
45
|
-
export declare function runEnvExample(options: {
|
|
46
|
-
check?: boolean;
|
|
47
|
-
}): Promise<void>;
|
|
48
|
-
/**
|
|
49
|
-
* The development sections to render: `--apps`, else the picker, else all.
|
|
50
|
-
*
|
|
51
|
-
* `undefined` means "everything", which covers three callers at once: a script
|
|
52
|
-
* with no flag, a pipe with no terminal (the picker refuses to prompt where
|
|
53
|
-
* nobody can answer, like every other picker here), and the vault targets,
|
|
54
|
-
* which never narrow.
|
|
55
|
-
*/
|
|
56
|
-
export declare function resolveSections(apps?: string): Promise<Set<string> | undefined>;
|
|
57
|
-
export declare function runEnvInit(target: EnvTarget, apps?: string): Promise<void>;
|
|
58
|
-
/**
|
|
59
|
-
* Add newly declared keys to an existing target file without rewriting it.
|
|
60
|
-
*
|
|
61
|
-
* Vault-target derivations are evaluated against the complete routed key set,
|
|
62
|
-
* not merely the missing subset. Otherwise appending NEXT_PUBLIC_SUPABASE_URL
|
|
63
|
-
* to a file that already contains API_URL would incorrectly render a blank.
|
|
64
|
-
*/
|
|
65
|
-
export declare function renderInitAddition(target: EnvTarget, date: string, existing: string, sections?: ReadonlySet<string>): {
|
|
66
|
-
text: string;
|
|
67
|
-
count: number;
|
|
68
|
-
} | null;
|
package/dist/env/example.js
DELETED
|
@@ -1,605 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `pnpm devtools env example [--check]` and `pnpm devtools env init`.
|
|
3
|
-
*
|
|
4
|
-
* `.env.example` is OUTPUT, not a file anyone edits. The registry the
|
|
5
|
-
* manifests populate is the single source, this module renders it, and CI's
|
|
6
|
-
* `--check` fails on drift the same way it does for `database.types.ts`. A
|
|
7
|
-
* wrong line is fixed in the owning manifest's `doc`/`example`/`commented`
|
|
8
|
-
* metadata.
|
|
9
|
-
*
|
|
10
|
-
* ⚠️ Registry-only, on purpose. Nothing here touches Bitwarden, GitHub, the
|
|
11
|
-
* network, or the ambient environment, and `cli.ts` dispatches these two
|
|
12
|
-
* subcommands before any token lookup or environment prompt. The CI job that
|
|
13
|
-
* runs `--check` is deliberately credential-free, and a generator that needed
|
|
14
|
-
* a secret to describe the secrets would not be allowed there.
|
|
15
|
-
*
|
|
16
|
-
* `env init` is the same renderer pointed at a real env file. It takes any
|
|
17
|
-
* `--target` and writes THAT target's file (`.env`, `.env.preflight`,
|
|
18
|
-
* `.env.staging`, `.env.production`) from the one target table that `pull`,
|
|
19
|
-
* `push` and `audit` default their file from. When `init` mapped target to
|
|
20
|
-
* file and `push` did not, `init --env staging` created `.env.staging` and
|
|
21
|
-
* `push --env staging` uploaded `.env`.
|
|
22
|
-
*
|
|
23
|
-
* ⚠️ A VAULT TARGET'S FILE IS NOT THE DEVELOPMENT ONE WITH A DIFFERENT NAME.
|
|
24
|
-
* Rendering it as though it were produced a file worse than useless: fill in
|
|
25
|
-
* its blanks, push it, and localhost URLs and a placeholder GitHub App private
|
|
26
|
-
* key went to production, after which `env audit` reported NO DRIFT, because
|
|
27
|
-
* the stored values matched the file they came from. Two differences, both
|
|
28
|
-
* mechanical:
|
|
29
|
-
*
|
|
30
|
-
* * **Which keys.** `keysRoutedTo()`, what a push for that target would
|
|
31
|
-
* actually carry somewhere. Not the committed `scope: "default"`
|
|
32
|
-
* constants, not one contributor's `scope: "developer"` values, and not
|
|
33
|
-
* the apply-tier credentials outside production, all three of which used
|
|
34
|
-
* to ship in all three files because all three files were byte-identical
|
|
35
|
-
* but for the header. `preflight` is narrower again: a target no app boots
|
|
36
|
-
* from carries only the keys declared `narrowed`, so its file is one line
|
|
37
|
-
* long today rather than 45.
|
|
38
|
-
* * **Which values.** Blank, unless `getEnvSync().derivationOf()` says the `example` is a
|
|
39
|
-
* derivation AND every variable it expands from is in the same file. A
|
|
40
|
-
* development default and a placeholder are both non-empty, and every
|
|
41
|
-
* consumer downstream skips only EMPTY values.
|
|
42
|
-
*
|
|
43
|
-
* Nothing is commented out in one either, `commented: true` included. The flag
|
|
44
|
-
* encodes a real distinction: an EMPTY value for an enabled OAuth provider
|
|
45
|
-
* makes the Supabase CLI fail with `ProjectConfigParseError`, so "unset" and
|
|
46
|
-
* "empty" differ. `push` cannot act on it. It skips an empty value and never
|
|
47
|
-
* sees a commented line at all, so a value typed on a commented line is
|
|
48
|
-
* silently NOT pushed. That made the flag a trap here: `SUPABASE_DB_PASSWORD`,
|
|
49
|
-
* `SUPABASE_JWT_SIGNING_KEY`, `CLOUDFLARE_API_TOKEN` and all four OAuth client
|
|
50
|
-
* secrets shipped commented, so the must-fill keys were the ones that could
|
|
51
|
-
* not be filled. They ship uncommented here; the flag still governs
|
|
52
|
-
* `.env.example` and the development `.env`, where the CLI reads the file and
|
|
53
|
-
* nothing pushes it.
|
|
54
|
-
*
|
|
55
|
-
* Re-running it is ADDITIVE: it appends only newly declared keys that the
|
|
56
|
-
* target routes and leaves every existing line byte-for-byte. There is no
|
|
57
|
-
* `--force` path that replaces a whole file with blanks: `env reset` blanks
|
|
58
|
-
* values recoverably, and `env pull` updates stored values in place.
|
|
59
|
-
*/
|
|
60
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
61
|
-
import { resolve } from "node:path";
|
|
62
|
-
import { log, multiselect, note } from "@clack/prompts";
|
|
63
|
-
import { EnvDocument } from "./document.js";
|
|
64
|
-
import { unwrap } from "../ui.js";
|
|
65
|
-
import { assertRegistryLoaded } from "./discovery.js";
|
|
66
|
-
import { getEnvSync } from "../repo/peers.js";
|
|
67
|
-
import { recordResolved } from "../invocation.js";
|
|
68
|
-
import { keysRoutedTo } from "./selection.js";
|
|
69
|
-
import { findRepoRoot } from "../repo/root.js";
|
|
70
|
-
import { explain } from "../ui.js";
|
|
71
|
-
/**
|
|
72
|
-
* Section order is FIXED here rather than inherited from manifest import
|
|
73
|
-
* order, and a key declared by several manifests renders once, under the
|
|
74
|
-
* earliest of its sources in this list. Both choices keep the output a pure
|
|
75
|
-
* function of the declarations, so `--check` compares content rather than
|
|
76
|
-
* filesystem enumeration accidents.
|
|
77
|
-
*/
|
|
78
|
-
const SECTION_ORDER = [
|
|
79
|
-
"platform",
|
|
80
|
-
"schedule-builder",
|
|
81
|
-
"study-group-finder",
|
|
82
|
-
"sandbox",
|
|
83
|
-
"supabase",
|
|
84
|
-
"devtools",
|
|
85
|
-
];
|
|
86
|
-
const SECTION_LABELS = {
|
|
87
|
-
platform: "platform (apps/platform/src/env.ts)",
|
|
88
|
-
"schedule-builder": "schedule-builder (apps/schedule-builder/src/env.ts)",
|
|
89
|
-
"study-group-finder": "study-group-finder (apps/study-group-finder/env.ts)",
|
|
90
|
-
sandbox: "sandbox — a dormant Worker's bindings; nothing provisions or deploys " +
|
|
91
|
-
"it from the platform side any more (apps/sandbox/env.ts)",
|
|
92
|
-
supabase: "supabase — read by config.toml and the Supabase CLI (supabase/env.ts)",
|
|
93
|
-
devtools: "devtools — operator tooling, no app reads these (@devdogsuga/devtools)",
|
|
94
|
-
};
|
|
95
|
-
/** `study-group-finder:tooling` and friends fold into their app's section. */
|
|
96
|
-
function sectionOf(source) {
|
|
97
|
-
return source.split(":")[0];
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* The sections a development init may narrow to: the four apps.
|
|
101
|
-
*
|
|
102
|
-
* Order is the PICKER's order, not the file's (that is `SECTION_ORDER`, fixed
|
|
103
|
-
* so `.env.example` stays byte-stable). The two projects most contributors
|
|
104
|
-
* join come first; `platform` follows as the shared auth infrastructure you
|
|
105
|
-
* depend on but rarely edit, and `sandbox` last as the dormant one almost
|
|
106
|
-
* nobody touches.
|
|
107
|
-
*
|
|
108
|
-
* `supabase` is NOT here and NOT optional. It is the shared database and auth
|
|
109
|
-
* layer every app runs against, so any selection implies it (the caller adds
|
|
110
|
-
* it). `devtools` is not here either, deliberately: it is a ROLE, not a
|
|
111
|
-
* project. A contributor picks what they are building; whether they also
|
|
112
|
-
* operate deploys is a separate question the picker asks separately.
|
|
113
|
-
*/
|
|
114
|
-
export const APP_SECTIONS = [
|
|
115
|
-
"schedule-builder",
|
|
116
|
-
"study-group-finder",
|
|
117
|
-
"platform",
|
|
118
|
-
"sandbox",
|
|
119
|
-
];
|
|
120
|
-
/**
|
|
121
|
-
* Every declared key belonging to one of the chosen sections.
|
|
122
|
-
*
|
|
123
|
-
* `some` over a key's declaring sources, so a key SHARED between a chosen and
|
|
124
|
-
* an unchosen app (API_URL, the whole connection block) is included by
|
|
125
|
-
* whichever side was picked. A narrowed file must never lose the
|
|
126
|
-
* infrastructure a chosen app boots on just because another app shares it.
|
|
127
|
-
*/
|
|
128
|
-
export function keysForSections(chosen) {
|
|
129
|
-
assertRegistryLoaded();
|
|
130
|
-
const keys = new Set();
|
|
131
|
-
for (const [key, entries] of getEnvSync().variables()) {
|
|
132
|
-
if (entries.some((entry) => chosen.has(sectionOf(entry.source)))) {
|
|
133
|
-
keys.add(key);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return keys;
|
|
137
|
-
}
|
|
138
|
-
function sectionRank(source) {
|
|
139
|
-
const index = SECTION_ORDER.indexOf(sectionOf(source));
|
|
140
|
-
return index === -1 ? SECTION_ORDER.length : index;
|
|
141
|
-
}
|
|
142
|
-
/** Greedy wrap into `# `-prefixed lines that stay under 80 columns. */
|
|
143
|
-
function comment(text, width = 77) {
|
|
144
|
-
const lines = [];
|
|
145
|
-
let line = "";
|
|
146
|
-
for (const word of text.split(/\s+/)) {
|
|
147
|
-
if (line !== "" && line.length + 1 + word.length > width) {
|
|
148
|
-
lines.push(`# ${line}`);
|
|
149
|
-
line = word;
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
line = line === "" ? word : `${line} ${word}`;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (line !== "")
|
|
156
|
-
lines.push(`# ${line}`);
|
|
157
|
-
return lines;
|
|
158
|
-
}
|
|
159
|
-
const RULE = `# ${"-".repeat(75)}`;
|
|
160
|
-
/** One block per key, grouped by owning section, in declaration order. */
|
|
161
|
-
function sections() {
|
|
162
|
-
assertRegistryLoaded();
|
|
163
|
-
const bySection = new Map();
|
|
164
|
-
for (const entries of getEnvSync().variables().values()) {
|
|
165
|
-
// The stable sort keeps declaration order among same-section duplicates,
|
|
166
|
-
// so the owner is the canonical section's own declaration.
|
|
167
|
-
const owner = [...entries].sort((a, b) => sectionRank(a.source) - sectionRank(b.source))[0];
|
|
168
|
-
const own = sectionOf(owner.source);
|
|
169
|
-
const sharedWith = [
|
|
170
|
-
...new Set(entries.map((e) => sectionOf(e.source)).filter((s) => s !== own)),
|
|
171
|
-
];
|
|
172
|
-
const blocks = bySection.get(own) ?? [];
|
|
173
|
-
blocks.push({ entry: owner, sharedWith });
|
|
174
|
-
bySection.set(own, blocks);
|
|
175
|
-
}
|
|
176
|
-
return [...bySection.entries()]
|
|
177
|
-
.sort(([a], [b]) => sectionRank(a) - sectionRank(b) || a.localeCompare(b))
|
|
178
|
-
.map(([name, blocks]) => ({ name, blocks }));
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* The value a vault target's file carries for a key: a derivation, or nothing.
|
|
182
|
-
*
|
|
183
|
-
* `routed` is the rest of the file, and passing it is the whole check. A
|
|
184
|
-
* formula is only a value if the file can expand it. `$BASE_URL/auth/callback`
|
|
185
|
-
* with no `BASE_URL` line above it is not "structure worth keeping", it is a
|
|
186
|
-
* literal dollar sign about to be pushed to Bitwarden, stored, synced to
|
|
187
|
-
* GitHub, and written into a deployed environment verbatim.
|
|
188
|
-
*/
|
|
189
|
-
function derivedValue(meta, routed) {
|
|
190
|
-
const derivation = getEnvSync().derivationOf(meta);
|
|
191
|
-
if (derivation === null)
|
|
192
|
-
return "";
|
|
193
|
-
return getEnvSync()
|
|
194
|
-
.envReferences(derivation)
|
|
195
|
-
.every((ref) => routed.has(ref))
|
|
196
|
-
? derivation
|
|
197
|
-
: "";
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* `routed` is absent for `.env.example` and the development `.env`, the two
|
|
201
|
-
* renderings that want every declared key with its `example` as written. It is
|
|
202
|
-
* present for a vault target, where it is both the filter and the set a
|
|
203
|
-
* derivation's references have to resolve within.
|
|
204
|
-
*/
|
|
205
|
-
function renderBlock({ entry, sharedWith }, routed) {
|
|
206
|
-
const { key, meta } = entry;
|
|
207
|
-
const lines = comment(meta.doc);
|
|
208
|
-
// Development-only, and actively misleading in a deployed file: staging has
|
|
209
|
-
// no local stack to supply anything, which is why the five keys carrying
|
|
210
|
-
// this flag are still in `variableKeys()` and still land here.
|
|
211
|
-
if (meta.localStack && routed === undefined) {
|
|
212
|
-
lines.push("# (the running local Supabase stack supplies this via .env.generated)");
|
|
213
|
-
}
|
|
214
|
-
if (sharedWith.length > 0) {
|
|
215
|
-
lines.push(`# (shared with ${sharedWith.join(", ")})`);
|
|
216
|
-
}
|
|
217
|
-
// A never-store credential ships COMMENTED. "Never store" is about REMOTE
|
|
218
|
-
// stores: push refuses these by name, pull will not write them back, and
|
|
219
|
-
// audit errors on any remote copy. The operator's own gitignored .env may
|
|
220
|
-
// hold one (since 2026-08-19, by decision), and the BWS prompts offer to
|
|
221
|
-
// save there. The commented line is the documented home that offer revives.
|
|
222
|
-
// A vault target's file never renders these at all, because
|
|
223
|
-
// `keysRoutedTo()` refuses them.
|
|
224
|
-
if (meta.secrecy === "never-store") {
|
|
225
|
-
return [...lines, `# ${key}=""`];
|
|
226
|
-
}
|
|
227
|
-
// A minted credential gets the same treatment, for a different reason. There
|
|
228
|
-
// is no value to write: it is signed at deploy time and lives only on the
|
|
229
|
-
// deploy target. A blank `SANDBOX_PROXY_TOKEN=` line would read as a field
|
|
230
|
-
// awaiting a paste, and the paste would be a hand-made token that never
|
|
231
|
-
// rotates, the exact failure minting exists to remove.
|
|
232
|
-
if (meta.minted) {
|
|
233
|
-
return [
|
|
234
|
-
`# ${key}:`,
|
|
235
|
-
...lines,
|
|
236
|
-
...comment(`(no ${key}= line on purpose — this one is minted at deploy time, so ` +
|
|
237
|
-
"there is no value to fill in and a hand-pasted one would never " +
|
|
238
|
-
"rotate)"),
|
|
239
|
-
];
|
|
240
|
-
}
|
|
241
|
-
// A vault target's file: a derivation or a blank, always assignable. See the
|
|
242
|
-
// module header for why `commented` stops applying here and why every other
|
|
243
|
-
// `example` is dropped rather than carried across.
|
|
244
|
-
if (routed !== undefined) {
|
|
245
|
-
lines.push(`${key}="${derivedValue(meta, routed)}"`);
|
|
246
|
-
return lines;
|
|
247
|
-
}
|
|
248
|
-
const value = `${key}="${meta.example ?? ""}"`;
|
|
249
|
-
// `developer` keys are always commented: they are one contributor's own
|
|
250
|
-
// values, and an uncommented empty line reads as a blank everybody fills in.
|
|
251
|
-
const commented = meta.commented === true || meta.scope === "developer";
|
|
252
|
-
lines.push(commented ? `# ${value}` : value);
|
|
253
|
-
return lines;
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* `routed` absent renders every declared key; present renders that set only.
|
|
257
|
-
*
|
|
258
|
-
* A section whose keys are all filtered out is dropped with them. A heading
|
|
259
|
-
* over nothing reads as "this app needs nothing here", a different and false
|
|
260
|
-
* claim.
|
|
261
|
-
*/
|
|
262
|
-
function renderBody(routed, derivationScope = routed) {
|
|
263
|
-
const lines = [];
|
|
264
|
-
for (const { name, blocks } of sections()) {
|
|
265
|
-
const included = routed === undefined
|
|
266
|
-
? blocks
|
|
267
|
-
: blocks.filter(({ entry }) => routed.has(entry.key));
|
|
268
|
-
if (included.length === 0)
|
|
269
|
-
continue;
|
|
270
|
-
lines.push("", RULE, `# ${SECTION_LABELS[name] ?? name}`, RULE);
|
|
271
|
-
for (const block of included) {
|
|
272
|
-
lines.push("", ...renderBlock(block, derivationScope));
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
return lines;
|
|
276
|
-
}
|
|
277
|
-
/** The committed `.env.example`: generated header + rendered registry. */
|
|
278
|
-
export function renderExample() {
|
|
279
|
-
const header = [
|
|
280
|
-
"# .env.example — every declared environment variable, with its documentation.",
|
|
281
|
-
"#",
|
|
282
|
-
"# GENERATED by `pnpm devtools env example`. Do not edit this file: edit",
|
|
283
|
-
"# the owning manifest (src/env.ts, env.ts, supabase/env.ts) and regenerate.",
|
|
284
|
-
"# CI runs `env example --check` and fails on drift, the same pattern as",
|
|
285
|
-
"# database.types.ts.",
|
|
286
|
-
"#",
|
|
287
|
-
"# One env file per target, each standalone — no shared base, so a key",
|
|
288
|
-
"# missing from a deployed file is a loud validation error rather than a",
|
|
289
|
-
"# silent fallthrough to a development value:",
|
|
290
|
-
"#",
|
|
291
|
-
"# .env IS development (the unsuffixed file, read by default)",
|
|
292
|
-
"# .env.preflight `pnpm devtools env pull --target preflight`",
|
|
293
|
-
"# .env.staging `pnpm devtools env pull --target staging`",
|
|
294
|
-
"# .env.production `pnpm devtools env pull --target production`",
|
|
295
|
-
"#",
|
|
296
|
-
"# .env.preflight is a staging area for pushing credentials, never a file an",
|
|
297
|
-
"# app boots from: DEPLOY_ENV=preflight is refused on purpose.",
|
|
298
|
-
"#",
|
|
299
|
-
"# `pnpm devtools setup` (or `pnpm devtools env init`) materialises a",
|
|
300
|
-
"# fresh .env from the same registry that generated this file. The local",
|
|
301
|
-
"# Docker stack is detected by a port probe, not a flag: while it is running,",
|
|
302
|
-
"# .env.generated (written by start-local-stack) overlays the connection",
|
|
303
|
-
"# block — in development only. `supabase stop` is the toggle back.",
|
|
304
|
-
"#",
|
|
305
|
-
"# SECTIONS ARE GROUPED BY WHO READS THE VARIABLE, and two of the readers",
|
|
306
|
-
"# are not apps — that is deliberate, not clutter. The four app sections",
|
|
307
|
-
"# come first. Then `supabase`: values config.toml and the Supabase CLI",
|
|
308
|
-
"# read (the shared auth server serves every app, so no one app can own",
|
|
309
|
-
"# them). Then `devtools`: operator credentials no app reads at all. They",
|
|
310
|
-
"# cannot be moved into an app's manifest, because an app's manifest is",
|
|
311
|
-
"# what `deploy secrets-file` uploads to that app's Worker — filing",
|
|
312
|
-
"# CLOUDFLARE_API_TOKEN or an OAuth client secret under an app would put",
|
|
313
|
-
"# it on an internet-facing Worker that never asked for it. A key an app",
|
|
314
|
-
"# only needs at dev/build time uses the `<app>:tooling` suffix instead,",
|
|
315
|
-
"# which keeps the app grouping without the upload.",
|
|
316
|
-
];
|
|
317
|
-
return [...header, ...renderBody(), ""].join("\n");
|
|
318
|
-
}
|
|
319
|
-
/** The prose above a vault target's blanks, explaining why they are blank. */
|
|
320
|
-
function targetHeader(target, count) {
|
|
321
|
-
return [
|
|
322
|
-
"#",
|
|
323
|
-
...comment(`The ${count} key${count === 1 ? "" : "s"} a ` +
|
|
324
|
-
`\`pnpm devtools env push --target ${target}\` ` +
|
|
325
|
-
"routes, and nothing else. The committed defaults and the " +
|
|
326
|
-
"per-developer values go nowhere, and a minted credential is signed " +
|
|
327
|
-
"at deploy time rather than stored, so none of the three has a line " +
|
|
328
|
-
"here to fill in."),
|
|
329
|
-
"#",
|
|
330
|
-
...comment(target === "production"
|
|
331
|
-
? "The apply-tier credential IS here — SUPABASE_ACCESS_TOKEN reaches " +
|
|
332
|
-
"the production-apply GitHub environment, and no other target " +
|
|
333
|
-
"carries it."
|
|
334
|
-
: "The apply-tier credential is NOT here. It exists to reshape " +
|
|
335
|
-
`production, so a copy in ${target} would be a second ` +
|
|
336
|
-
"write-capable token to rotate for no benefit — and `env push` " +
|
|
337
|
-
"skips it outside production anyway."),
|
|
338
|
-
"#",
|
|
339
|
-
...comment("Fill the values in, or run " +
|
|
340
|
-
`\`pnpm devtools env pull --target ${target}\` to fetch what ` +
|
|
341
|
-
"Bitwarden already holds. Nothing is commented out: push reads " +
|
|
342
|
-
"assignments, and a commented line is not one — a value typed on it " +
|
|
343
|
-
"would be silently skipped."),
|
|
344
|
-
"#",
|
|
345
|
-
...comment("The blanks are blank ON PURPOSE. A key's `example` metadata is a " +
|
|
346
|
-
"development default (`http://localhost:3000`) or an .env.example " +
|
|
347
|
-
"placeholder (`000000`, a fake private key), and every consumer " +
|
|
348
|
-
"downstream skips only EMPTY values — so a prefilled one would push " +
|
|
349
|
-
"cleanly and then be reported as no drift, because the stored value " +
|
|
350
|
-
"would match this file. The `$VAR` lines that DID survive are " +
|
|
351
|
-
"derivations: how the value is built, from another line in this same " +
|
|
352
|
-
"file. Leave them alone unless the target genuinely differs."),
|
|
353
|
-
...(target === "preflight"
|
|
354
|
-
? [
|
|
355
|
-
"#",
|
|
356
|
-
...comment("⚠️ PREFLIGHT IS DELIBERATELY TINY, and a short file here is the " +
|
|
357
|
-
"correct output rather than a truncated one. Nothing boots from " +
|
|
358
|
-
"this target: it exists to feed CI's migration DRY RUN, which " +
|
|
359
|
-
"reads and changes nothing, so it carries only the keys whose " +
|
|
360
|
-
"declaration opts in with `narrowed` — today, a Postgres role " +
|
|
361
|
-
"that sees only the migrations table and nothing wider. Every " +
|
|
362
|
-
"other key is absent ON PURPOSE: this project's GitHub " +
|
|
363
|
-
"environment is reachable from `main`, and listing every " +
|
|
364
|
-
"routable key there, the JWT signing key included, would " +
|
|
365
|
-
"defeat the point of a narrowed preflight role."),
|
|
366
|
-
]
|
|
367
|
-
: []),
|
|
368
|
-
];
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* A fresh env file for `env init`.
|
|
372
|
-
*
|
|
373
|
-
* Two renderings, not one with a different first line. See the module header.
|
|
374
|
-
* `development` wants every declared key with its `example` as written, which
|
|
375
|
-
* is also what `.env.example` wants; a vault target wants what a push for that
|
|
376
|
-
* target routes, blank unless the registry can derive it.
|
|
377
|
-
*/
|
|
378
|
-
export function renderInit(target, date,
|
|
379
|
-
/**
|
|
380
|
-
* Development only: the sections to render, from the project picker.
|
|
381
|
-
* Callers pass the CHOSEN set plus `supabase` (the shared stack is implied
|
|
382
|
-
* by any choice); absent means everything, which is what the vault targets
|
|
383
|
-
* and every pre-picker caller get.
|
|
384
|
-
*/
|
|
385
|
-
sections) {
|
|
386
|
-
const file = getEnvSync().fileFor(target);
|
|
387
|
-
const stamp = `# ${file} (${target}) — created by \`pnpm devtools env init\` on ${date}.`;
|
|
388
|
-
if (getEnvSync().isVaultTarget(target)) {
|
|
389
|
-
const routed = keysRoutedTo(target);
|
|
390
|
-
return [
|
|
391
|
-
stamp,
|
|
392
|
-
...targetHeader(target, routed.size),
|
|
393
|
-
...renderBody(routed),
|
|
394
|
-
"",
|
|
395
|
-
].join("\n");
|
|
396
|
-
}
|
|
397
|
-
const narrowed = sections &&
|
|
398
|
-
SECTION_ORDER.some((s) => !sections.has(s));
|
|
399
|
-
return [
|
|
400
|
-
stamp,
|
|
401
|
-
...(narrowed
|
|
402
|
-
? [
|
|
403
|
-
`# Narrowed to: ${[...sections].sort().join(", ")} — the projects picked at`,
|
|
404
|
-
"# init. Nothing else is missing by accident: re-run",
|
|
405
|
-
"# `pnpm devtools env init --target development` any time to add the",
|
|
406
|
-
"# sections for another project (or the operator tooling), appended",
|
|
407
|
-
"# without touching a value you have already filled in.",
|
|
408
|
-
]
|
|
409
|
-
: []),
|
|
410
|
-
"# Fill in the values below. The local Supabase stack supplies the whole",
|
|
411
|
-
"# connection block: `pnpm devtools db start` starts it and writes",
|
|
412
|
-
"# .env.generated, which overlays this file while the stack is running.",
|
|
413
|
-
...renderBody(sections ? keysForSections(sections) : undefined),
|
|
414
|
-
"",
|
|
415
|
-
].join("\n");
|
|
416
|
-
}
|
|
417
|
-
// ── commands ─────────────────────────────────────────────────────────────────
|
|
418
|
-
const EXAMPLE_PATH = ".env.example";
|
|
419
|
-
/** `KEY=`, `# KEY=`: the assignable (or deliberately commented) lines. */
|
|
420
|
-
const ASSIGNMENT = /^(#\s?)?([A-Z][A-Z0-9_]*)=.*$/;
|
|
421
|
-
/** key → full line, for the key-level diff summary. */
|
|
422
|
-
function assignments(text) {
|
|
423
|
-
const map = new Map();
|
|
424
|
-
for (const line of text.split("\n")) {
|
|
425
|
-
const match = ASSIGNMENT.exec(line);
|
|
426
|
-
if (match && !map.has(match[2]))
|
|
427
|
-
map.set(match[2], line);
|
|
428
|
-
}
|
|
429
|
-
return map;
|
|
430
|
-
}
|
|
431
|
-
export async function runEnvExample(options) {
|
|
432
|
-
const path = resolve(findRepoRoot(), EXAMPLE_PATH);
|
|
433
|
-
const generated = renderExample();
|
|
434
|
-
if (!options.check) {
|
|
435
|
-
await writeFile(path, generated);
|
|
436
|
-
log.success(`Wrote ${EXAMPLE_PATH} from the registry ` +
|
|
437
|
-
`(${assignments(generated).size} assignable keys).`);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
const committed = await readFile(path, "utf8").catch(() => null);
|
|
441
|
-
if (committed === generated) {
|
|
442
|
-
log.success(`${EXAMPLE_PATH} matches the env manifests.`);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
// Key-level, not a full diff: the file is mostly prose, and the useful
|
|
446
|
-
// answer is WHICH declarations moved, not a wall of comment churn.
|
|
447
|
-
const ours = assignments(generated);
|
|
448
|
-
const theirs = committed === null ? new Map() : assignments(committed);
|
|
449
|
-
const drift = [];
|
|
450
|
-
for (const key of ours.keys()) {
|
|
451
|
-
if (!theirs.has(key))
|
|
452
|
-
drift.push(`+ ${key} declared, missing from ${EXAMPLE_PATH}`);
|
|
453
|
-
else if (theirs.get(key) !== ours.get(key))
|
|
454
|
-
drift.push(`~ ${key} line differs`);
|
|
455
|
-
}
|
|
456
|
-
for (const key of theirs.keys()) {
|
|
457
|
-
if (!ours.has(key))
|
|
458
|
-
drift.push(`- ${key} in ${EXAMPLE_PATH}, no longer generated`);
|
|
459
|
-
}
|
|
460
|
-
if (drift.length === 0) {
|
|
461
|
-
drift.push("(no key drifted — the header or a doc comment changed)");
|
|
462
|
-
}
|
|
463
|
-
note(drift.join("\n"), `${EXAMPLE_PATH} is out of date`);
|
|
464
|
-
explain(`${EXAMPLE_PATH} does not match the env manifests.`, "", [
|
|
465
|
-
"Regenerate it with `pnpm devtools env example` and commit the result.",
|
|
466
|
-
"The manifests are the source of truth; never edit the file by hand.",
|
|
467
|
-
]);
|
|
468
|
-
process.exitCode = 1;
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* The development sections to render: `--apps`, else the picker, else all.
|
|
472
|
-
*
|
|
473
|
-
* `undefined` means "everything", which covers three callers at once: a script
|
|
474
|
-
* with no flag, a pipe with no terminal (the picker refuses to prompt where
|
|
475
|
-
* nobody can answer, like every other picker here), and the vault targets,
|
|
476
|
-
* which never narrow.
|
|
477
|
-
*/
|
|
478
|
-
export async function resolveSections(apps) {
|
|
479
|
-
if (apps !== undefined) {
|
|
480
|
-
const chosen = apps
|
|
481
|
-
.split(",")
|
|
482
|
-
.map((name) => name.trim())
|
|
483
|
-
.filter((name) => name !== "");
|
|
484
|
-
const valid = [...APP_SECTIONS, "devtools"];
|
|
485
|
-
for (const name of chosen) {
|
|
486
|
-
if (!valid.includes(name)) {
|
|
487
|
-
throw new Error(`--apps: "${name}" is not a section. Apps: ${APP_SECTIONS.join(", ")}; ` +
|
|
488
|
-
"plus `devtools` for the operator tooling. `supabase` is implied " +
|
|
489
|
-
"by any choice and is not an option.");
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
if (chosen.length === 0)
|
|
493
|
-
return undefined;
|
|
494
|
-
return new Set([...chosen, "supabase"]);
|
|
495
|
-
}
|
|
496
|
-
if (!process.stdin.isTTY)
|
|
497
|
-
return undefined;
|
|
498
|
-
// The platform hint frames it as shared infrastructure: keep it in reach
|
|
499
|
-
// (running the local auth server yourself needs its keys), but say plainly
|
|
500
|
-
// that most contributors are not editing it. Sandbox gets the opposite
|
|
501
|
-
// framing: it is dormant, kept in the registry only so its own bindings
|
|
502
|
-
// stay visible, not because anyone is likely to run it.
|
|
503
|
-
const APP_HINTS = {
|
|
504
|
-
"schedule-builder": "the DogDays course-schedule planner (Next.js)",
|
|
505
|
-
"study-group-finder": "the Dog Pack study-group app (Flutter)",
|
|
506
|
-
platform: "shared auth server — needed only if you run it locally for sign-in; usually you don't edit it",
|
|
507
|
-
sandbox: "dormant Worker — no platform integration left; almost never touched",
|
|
508
|
-
};
|
|
509
|
-
const chosen = unwrap(await multiselect({
|
|
510
|
-
message: "Which projects are you working on? (The shared Supabase stack is " +
|
|
511
|
-
"always included; re-run init later to add more.)",
|
|
512
|
-
options: [
|
|
513
|
-
...APP_SECTIONS.map((app) => ({
|
|
514
|
-
value: app,
|
|
515
|
-
label: app,
|
|
516
|
-
hint: APP_HINTS[app],
|
|
517
|
-
})),
|
|
518
|
-
{
|
|
519
|
-
value: "devtools",
|
|
520
|
-
label: "deploy & officer tooling",
|
|
521
|
-
hint: "a role, not an app — operator credentials, all commented out; most contributors never need these",
|
|
522
|
-
},
|
|
523
|
-
],
|
|
524
|
-
initialValues: [...APP_SECTIONS],
|
|
525
|
-
required: true,
|
|
526
|
-
}));
|
|
527
|
-
return new Set([...chosen, "supabase"]);
|
|
528
|
-
}
|
|
529
|
-
export async function runEnvInit(target, apps) {
|
|
530
|
-
const file = getEnvSync().fileFor(target);
|
|
531
|
-
const path = resolve(findRepoRoot(), file);
|
|
532
|
-
const date = new Date().toISOString().slice(0, 10);
|
|
533
|
-
const sections = target === "development" ? await resolveSections(apps) : undefined;
|
|
534
|
-
// When the projects came from the picker rather than `--apps`, record them
|
|
535
|
-
// so the rerun line can reproduce the selection without prompting. `supabase`
|
|
536
|
-
// is always implied, never an `--apps` value, so it is dropped here.
|
|
537
|
-
if (apps === undefined && sections !== undefined) {
|
|
538
|
-
const picked = [...sections].filter((s) => s !== "supabase").sort();
|
|
539
|
-
if (picked.length > 0)
|
|
540
|
-
recordResolved("--apps", picked.join(","));
|
|
541
|
-
}
|
|
542
|
-
try {
|
|
543
|
-
// `wx` makes the existence check and the write one atomic operation, so
|
|
544
|
-
// two concurrent inits cannot both pass a stat and then clobber.
|
|
545
|
-
await writeFile(path, renderInit(target, date, sections), { flag: "wx" });
|
|
546
|
-
}
|
|
547
|
-
catch (err) {
|
|
548
|
-
if (err.code === "EEXIST") {
|
|
549
|
-
// A re-run is ADDITIVE for every target. Only keys the file does not
|
|
550
|
-
// mention at all are appended. An active line holds somebody's value
|
|
551
|
-
// and a commented one holds their decision; both survive byte-for-byte.
|
|
552
|
-
const existing = await readFile(path, "utf8");
|
|
553
|
-
const addition = renderInitAddition(target, date, existing, sections);
|
|
554
|
-
if (addition === null) {
|
|
555
|
-
log.info(`${file} already covers that target — nothing to add.`);
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
await writeFile(path, addition.text);
|
|
559
|
-
log.success(`Appended ${addition.count} key${addition.count === 1 ? "" : "s"} to ${file}; existing lines untouched.`);
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
throw err;
|
|
563
|
-
}
|
|
564
|
-
log.success(`Created ${file} (${target}).`);
|
|
565
|
-
log.info(target === "development"
|
|
566
|
-
? "Fill in the values, or run `pnpm devtools db start` to start the local " +
|
|
567
|
-
"stack — it supplies the whole connection block via .env.generated."
|
|
568
|
-
: "Every value is blank but the `$VAR` derivations, which are how those " +
|
|
569
|
-
"values are built rather than a guess at them. " +
|
|
570
|
-
`\`pnpm devtools env pull --target ${target}\` fills the rest from ` +
|
|
571
|
-
"Bitwarden.");
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Add newly declared keys to an existing target file without rewriting it.
|
|
575
|
-
*
|
|
576
|
-
* Vault-target derivations are evaluated against the complete routed key set,
|
|
577
|
-
* not merely the missing subset. Otherwise appending NEXT_PUBLIC_SUPABASE_URL
|
|
578
|
-
* to a file that already contains API_URL would incorrectly render a blank.
|
|
579
|
-
*/
|
|
580
|
-
export function renderInitAddition(target, date, existing, sections) {
|
|
581
|
-
const doc = EnvDocument.parse(existing);
|
|
582
|
-
const wanted = getEnvSync().isVaultTarget(target)
|
|
583
|
-
? keysRoutedTo(target)
|
|
584
|
-
: sections
|
|
585
|
-
? keysForSections(sections)
|
|
586
|
-
: new Set(getEnvSync().variables().keys());
|
|
587
|
-
const missing = new Set([...wanted].filter((key) => !doc.has(key) && !doc.isCommented(key)));
|
|
588
|
-
if (missing.size === 0)
|
|
589
|
-
return null;
|
|
590
|
-
const label = getEnvSync().isVaultTarget(target)
|
|
591
|
-
? target
|
|
592
|
-
: sections
|
|
593
|
-
? [...sections].sort().join(", ")
|
|
594
|
-
: "everything";
|
|
595
|
-
const appended = [
|
|
596
|
-
"",
|
|
597
|
-
`# --- added by \`pnpm devtools env init\` on ${date} for: ${label} ---`,
|
|
598
|
-
...renderBody(missing, getEnvSync().isVaultTarget(target) ? wanted : undefined),
|
|
599
|
-
"",
|
|
600
|
-
].join("\n");
|
|
601
|
-
return {
|
|
602
|
-
text: existing.replace(/\n?$/, "\n") + appended,
|
|
603
|
-
count: missing.size,
|
|
604
|
-
};
|
|
605
|
-
}
|