@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/commands.js
DELETED
|
@@ -1,613 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `pnpm devtools env <pull|push|audit> --target <target>`
|
|
3
|
-
*
|
|
4
|
-
* One local env file per target, three remote stores, and a source of truth:
|
|
5
|
-
*
|
|
6
|
-
* pull Bitwarden into your env file
|
|
7
|
-
* push your env file to Bitwarden, then to GitHub secrets AND variables
|
|
8
|
-
* audit compare the file, Bitwarden, GitHub and Cloudflare
|
|
9
|
-
*
|
|
10
|
-
* ⚠️ THE FILE IS DERIVED FROM THE TARGET, and that one line was this
|
|
11
|
-
* module's bug. `pathFor()` used to default to the root `.env` regardless of
|
|
12
|
-
* target and honour only an explicit `--file`, while `init` mapped target to
|
|
13
|
-
* file the way the table always said. So `push --env staging` uploaded the
|
|
14
|
-
* DEVELOPMENT file to the staging project and reported success. Neither
|
|
15
|
-
* subcommand was internally wrong; they read two different enums behind one
|
|
16
|
-
* flag name. `--file` remains, as an override somebody types on purpose.
|
|
17
|
-
*
|
|
18
|
-
* Four rules shape all of it:
|
|
19
|
-
*
|
|
20
|
-
* * **Nothing is overwritten or removed without being asked.** Every
|
|
21
|
-
* destructive change is listed in fingerprints and confirmed. `push` sends
|
|
22
|
-
* to Bitwarden AND GitHub because a value in one and not the other is the
|
|
23
|
-
* failure this design has.
|
|
24
|
-
* * **Bitwarden holds an entire target, not its secret half.** The public
|
|
25
|
-
* per-environment values (`PROJECT_REF`, `BASE_URL`, `PUBLISHABLE_KEY`,
|
|
26
|
-
* and so on) are stored there too and pushed on to GitHub as *variables*
|
|
27
|
-
* rather than secrets. Storing them as secrets would mask them by
|
|
28
|
-
* substring in every log line they appear in and make their values
|
|
29
|
-
* unreadable to `audit`; leaving them out of Bitwarden would mean `pull`
|
|
30
|
-
* reconstructs a file that cannot boot an app.
|
|
31
|
-
* * **Nothing is deleted from the file.** A key that should go away is
|
|
32
|
-
* commented out, so the previous value stays recoverable from the file.
|
|
33
|
-
* * **Values are never printed.** Fingerprints tell a rotation from a paste
|
|
34
|
-
* error and cannot be used to reconstruct anything.
|
|
35
|
-
*/
|
|
36
|
-
import { chmod, readFile, writeFile } from "node:fs/promises";
|
|
37
|
-
import { resolve } from "node:path";
|
|
38
|
-
import { confirm, log, note } from "@clack/prompts";
|
|
39
|
-
import { getEnvSync } from "../repo/peers.js";
|
|
40
|
-
import { createSecret, listSecrets as listBwsSecrets, byKey, projectIdFor, updateSecret, } from "../bws/client.js";
|
|
41
|
-
import { environmentSpecs, assertVaultTarget, } from "../bws/environments.js";
|
|
42
|
-
import { fingerprint } from "../fingerprint.js";
|
|
43
|
-
import { listSecrets as listGhSecrets, listVariables as listGhVariables, listRepositoryVariables, setSecret, setVariable, } from "../gh/client.js";
|
|
44
|
-
import { GITHUB_ENVIRONMENT_SPECS, accepts, acceptsKey, githubTargets, routeTo, } from "../gh/environments.js";
|
|
45
|
-
import { audit, hasErrors, renderFindings, } from "./audit.js";
|
|
46
|
-
import { listWorkerSecrets } from "./cloudflare.js";
|
|
47
|
-
import { EnvDocument } from "./document.js";
|
|
48
|
-
import { ignoredFor, minted, neverStore, pushableVariables, selectForPush, } from "./selection.js";
|
|
49
|
-
import { findRepoRoot } from "../repo/root.js";
|
|
50
|
-
import { bail, errorMessage, explain, unwrap } from "../ui.js";
|
|
51
|
-
async function readDocument(path) {
|
|
52
|
-
try {
|
|
53
|
-
return EnvDocument.parse(await readFile(path, "utf8"));
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return EnvDocument.empty();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* The file a command works on: the target's, unless `--file` overrides it.
|
|
61
|
-
*
|
|
62
|
-
* The default is the whole fix. `--target staging` now reads and writes
|
|
63
|
-
* `.env.staging` for pull, push AND audit, where it used to reach the root
|
|
64
|
-
* `.env`. That meant pushing "staging" uploaded the development values, and
|
|
65
|
-
* pulling "staging" overwrote the development file. Every caller passes the
|
|
66
|
-
* target explicitly so no future one can inherit `.env` by omission.
|
|
67
|
-
*/
|
|
68
|
-
function pathFor(target, file) {
|
|
69
|
-
return resolve(findRepoRoot(), file ?? getEnvSync().fileFor(target));
|
|
70
|
-
}
|
|
71
|
-
/** Today, as an ISO date. Separated so the document layer stays testable. */
|
|
72
|
-
function stampFor(target, action) {
|
|
73
|
-
return {
|
|
74
|
-
environment: target,
|
|
75
|
-
action,
|
|
76
|
-
date: new Date().toISOString().slice(0, 10),
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Writes the document, tidying first.
|
|
81
|
-
*
|
|
82
|
-
* `group()` runs on every write rather than as its own command: files drift a
|
|
83
|
-
* line at a time, and a tidy pass nobody remembers to run is a tidy pass that
|
|
84
|
-
* never happens.
|
|
85
|
-
*
|
|
86
|
-
* Env files contain credentials and must not be world-readable. The mode
|
|
87
|
-
* parameter only applies on creation, so existing world-readable files are
|
|
88
|
-
* tightened with chmod after each write.
|
|
89
|
-
*/
|
|
90
|
-
export async function save(path, doc) {
|
|
91
|
-
const moved = doc.group();
|
|
92
|
-
await writeFile(path, doc.toString(), { mode: 0o600 });
|
|
93
|
-
await chmod(path, 0o600);
|
|
94
|
-
return moved;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Names every key the file holds that no manifest declares.
|
|
98
|
-
*
|
|
99
|
-
* Loud on purpose, and per key: an undeclared key used to be pushed as a secret
|
|
100
|
-
* BY OMISSION, so a typo'd name uploaded garbage under the wrong key and a
|
|
101
|
-
* stray local variable uploaded something private. Now it is skipped, and the
|
|
102
|
-
* person who typed the line has to hear that. Silence would read as "stored"
|
|
103
|
-
* right up until a deploy goes looking for it.
|
|
104
|
-
*/
|
|
105
|
-
function warnUnknown(unknown) {
|
|
106
|
-
for (const key of unknown) {
|
|
107
|
-
log.warn(`${key} is declared in no env manifest, so it was NOT pushed. If it is ` +
|
|
108
|
-
`real, declare it with define() in the owning package's env.ts so it ` +
|
|
109
|
-
`can be classified and routed; if it is a typo or a stray local ` +
|
|
110
|
-
`variable, fix or remove the line.`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Says out loud that a refused credential was left behind.
|
|
115
|
-
*
|
|
116
|
-
* A warning rather than a hard stop, because a never-store key can be
|
|
117
|
-
* legitimately in `.env` for an operator's own use, and blocking the whole
|
|
118
|
-
* push then would be wrong. But never silence: somebody who put a token in
|
|
119
|
-
* the file expecting it to sync has to learn that it did not.
|
|
120
|
-
*/
|
|
121
|
-
function warnRefused(refused) {
|
|
122
|
-
for (const key of refused) {
|
|
123
|
-
if (key === "BWS_ACCESS_TOKEN") {
|
|
124
|
-
log.warn(`${key} was NOT uploaded, and must not be — it unlocks all three ` +
|
|
125
|
-
`Bitwarden projects, so storing it in one is a key locked inside the ` +
|
|
126
|
-
`box it opens. ⚠️ Remove it from your .env; export it per shell from ` +
|
|
127
|
-
`the Password Manager vault instead.`);
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
log.warn(`${key} was NOT uploaded, and must not be. See its define() doc in ` +
|
|
131
|
-
`the owning package's env.ts for why.`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Says that some lines were left to the registry rather than stored.
|
|
137
|
-
*
|
|
138
|
-
* `info`, not `warn`: this is the correct outcome, and the eight lines it
|
|
139
|
-
* covers are ones `env init --target` WROTE. Silence would be the problem.
|
|
140
|
-
* Somebody who filled in a file and pushed it has to be able to tell "your
|
|
141
|
-
* derivations were left alone, on purpose" from "your derivations were
|
|
142
|
-
* dropped", and before this the tool said neither.
|
|
143
|
-
*/
|
|
144
|
-
function noteDerived(derived) {
|
|
145
|
-
if (derived.length === 0)
|
|
146
|
-
return;
|
|
147
|
-
log.info(`${derived.length} value(s) were left to the registry: ${derived.join(", ")}. ` +
|
|
148
|
-
"Each is still exactly the derivation its declaration gives — a shape " +
|
|
149
|
-
"rather than a value — so storing it would freeze today's formula into " +
|
|
150
|
-
"Bitwarden and GitHub and stop the registry from ever changing it. The " +
|
|
151
|
-
"deploy expands them at compose time instead. Replace one with a real " +
|
|
152
|
-
"value if this target genuinely differs, and it will push.");
|
|
153
|
-
}
|
|
154
|
-
// ── pull ─────────────────────────────────────────────────────────────────────
|
|
155
|
-
/**
|
|
156
|
-
* Brings Bitwarden's values into the target's env file, in place.
|
|
157
|
-
*
|
|
158
|
-
* ⚠️ With `--file .env` (or `--target development`, which is refused) this is
|
|
159
|
-
* the file `pnpm dev` reads. Pulling `production` into it points local
|
|
160
|
-
* development at production, which is why that target warns and defaults its
|
|
161
|
-
* confirmation to no.
|
|
162
|
-
*/
|
|
163
|
-
export async function runEnvPull(options) {
|
|
164
|
-
assertVaultTarget(options.target);
|
|
165
|
-
const target = options.target;
|
|
166
|
-
const spec = environmentSpecs()[target];
|
|
167
|
-
const path = pathFor(target, options.file);
|
|
168
|
-
const projectId = await projectIdFor(spec.project);
|
|
169
|
-
const refused = neverStore();
|
|
170
|
-
const all = await listBwsSecrets(projectId);
|
|
171
|
-
// If one of these is in the project it should not be, and writing it into the
|
|
172
|
-
// file people run `pnpm dev` against would spread it further. The audit says
|
|
173
|
-
// so loudly; here it is simply not written.
|
|
174
|
-
const remote = new Map(all.filter((s) => !refused.has(s.key)).map((s) => [s.key, s.value]));
|
|
175
|
-
for (const s of all) {
|
|
176
|
-
if (refused.has(s.key)) {
|
|
177
|
-
log.warn(`${s.key} is in ${spec.project} and must not be. Not written to ${path}. ` +
|
|
178
|
-
`Delete it there — run \`pnpm devtools env audit --target ${target}\`.`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
if (remote.size === 0) {
|
|
182
|
-
log.warn(`${spec.project} contains no secrets. Writing nothing, because an empty ` +
|
|
183
|
-
`result and a successful pull look identical afterwards.`);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const doc = await readDocument(path);
|
|
187
|
-
const changes = [];
|
|
188
|
-
for (const [key, value] of remote) {
|
|
189
|
-
const current = doc.get(key);
|
|
190
|
-
if (current === value)
|
|
191
|
-
continue;
|
|
192
|
-
changes.push(current === undefined
|
|
193
|
-
? `+ ${key} (${fingerprint(value)})${doc.isCommented(key) ? " — uncommenting" : ""}`
|
|
194
|
-
: `~ ${key} ${fingerprint(current)} → ${fingerprint(value)}`);
|
|
195
|
-
}
|
|
196
|
-
if (changes.length === 0) {
|
|
197
|
-
log.success(`${path} already matches ${spec.project}.`);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
note(changes.join("\n"), `${path} would change`);
|
|
201
|
-
if (spec.guarded) {
|
|
202
|
-
log.warn(`This writes PRODUCTION values into ${path}. Anything that loads that ` +
|
|
203
|
-
`file — or a stray \`--file .env\` — is then pointed at production.`);
|
|
204
|
-
}
|
|
205
|
-
if (!options.yes) {
|
|
206
|
-
const ok = unwrap(await confirm({
|
|
207
|
-
message: `Apply ${changes.length} change(s) to ${path}?`,
|
|
208
|
-
initialValue: !spec.guarded,
|
|
209
|
-
}));
|
|
210
|
-
if (!ok)
|
|
211
|
-
bail("Nothing written.");
|
|
212
|
-
}
|
|
213
|
-
const stamp = stampFor(target, "pulled");
|
|
214
|
-
for (const [key, value] of remote)
|
|
215
|
-
doc.set(key, value, stamp);
|
|
216
|
-
const moved = await save(path, doc);
|
|
217
|
-
log.success(`Updated ${changes.length} value(s) in ${path}` +
|
|
218
|
-
(moved ? ", and grouped same-named lines together." : "."));
|
|
219
|
-
}
|
|
220
|
-
// ── push ─────────────────────────────────────────────────────────────────────
|
|
221
|
-
/**
|
|
222
|
-
* Sends the target's env file to Bitwarden and then on to GitHub.
|
|
223
|
-
*
|
|
224
|
-
* Both, always. Bitwarden alone is the failure this design has: the source of
|
|
225
|
-
* truth moves, the deploy does not, and everything looks healthy until the old
|
|
226
|
-
* credential is revoked.
|
|
227
|
-
*
|
|
228
|
-
* ⚠️ The file comes from the TARGET. `--target staging` reads `.env.staging`,
|
|
229
|
-
* not the root `.env`. That defaulting mistake meant this command uploaded a
|
|
230
|
-
* developer's own values to a shared project and said "3 created, 12 updated"
|
|
231
|
-
* about it.
|
|
232
|
-
*
|
|
233
|
-
* ⚠️ Two GitHub stores on the far side, and only one Bitwarden project. The
|
|
234
|
-
* public per-environment values become GitHub *variables* rather than secrets
|
|
235
|
-
* (see `gh/client.ts` for why the store matters), but they are stored in
|
|
236
|
-
* Bitwarden alongside the secrets, because "Bitwarden is the source of truth"
|
|
237
|
-
* has to be true of a WHOLE target for `pull` to rebuild a working env file.
|
|
238
|
-
* Splitting them would make GitHub the only home for 27 keys and the second
|
|
239
|
-
* source of truth for their values, which is the arrangement this tool exists
|
|
240
|
-
* to remove.
|
|
241
|
-
*/
|
|
242
|
-
export async function runEnvPush(options) {
|
|
243
|
-
assertVaultTarget(options.target);
|
|
244
|
-
const target = options.target;
|
|
245
|
-
const spec = environmentSpecs()[target];
|
|
246
|
-
const path = pathFor(target, options.file);
|
|
247
|
-
const doc = await readDocument(path);
|
|
248
|
-
const skip = ignoredFor(target);
|
|
249
|
-
const { push: secrets, variables: publicValues, refused, unknown, derived, } = selectForPush(doc.entries(), target);
|
|
250
|
-
warnRefused(refused);
|
|
251
|
-
warnUnknown(unknown);
|
|
252
|
-
noteDerived(derived);
|
|
253
|
-
// What Bitwarden holds: both halves, keyed together. The two are disjoint by
|
|
254
|
-
// construction (see `selection.ts`), so this loses nothing.
|
|
255
|
-
const stored = new Map([...secrets, ...publicValues]);
|
|
256
|
-
if (stored.size === 0) {
|
|
257
|
-
explain(`No pushable values in ${path}.`, "", [
|
|
258
|
-
"Committed defaults, per-developer values and empty values are skipped.",
|
|
259
|
-
derived.length > 0
|
|
260
|
-
? `The ${derived.length} line(s) named above are still their declared ` +
|
|
261
|
-
"derivations, which the registry expands at deploy time."
|
|
262
|
-
: "Nothing else was found.",
|
|
263
|
-
]);
|
|
264
|
-
process.exitCode = 1;
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
const projectId = await projectIdFor(spec.project);
|
|
268
|
-
const existing = await listBwsSecrets(projectId);
|
|
269
|
-
const index = byKey(existing);
|
|
270
|
-
const created = [];
|
|
271
|
-
const updated = [];
|
|
272
|
-
for (const [key, value] of stored) {
|
|
273
|
-
const current = index.get(key);
|
|
274
|
-
if (!current)
|
|
275
|
-
created.push(key);
|
|
276
|
-
else if (current.value !== value)
|
|
277
|
-
updated.push(key);
|
|
278
|
-
}
|
|
279
|
-
// Present in the project and absent from the file. NEVER removed, only
|
|
280
|
-
// reported: a key missing from a file is far more often an incomplete edit
|
|
281
|
-
// than an intentional deletion.
|
|
282
|
-
const orphaned = existing
|
|
283
|
-
.map((s) => s.key)
|
|
284
|
-
.filter((k) => !stored.has(k) && !skip.has(k));
|
|
285
|
-
if (created.length === 0 && updated.length === 0) {
|
|
286
|
-
log.success(`${spec.project} already matches ${path}.`);
|
|
287
|
-
if (orphaned.length > 0) {
|
|
288
|
-
log.warn(`${orphaned.length} secret(s) exist in the project and not in ${path}: ` +
|
|
289
|
-
`${orphaned.join(", ")}. They were left alone.`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
// Fingerprints for the public values too, not their plaintext. They are
|
|
294
|
-
// safe to print, but "values are never printed" is a rule worth stating
|
|
295
|
-
// without an exception, and a fingerprint answers the question anybody is
|
|
296
|
-
// asking here: rotation, or paste error?
|
|
297
|
-
note([
|
|
298
|
-
...created.map((k) => `+ ${k} (${fingerprint(stored.get(k))})`),
|
|
299
|
-
...updated.map((k) => `~ ${k} ${fingerprint(index.get(k).value)} → ${fingerprint(stored.get(k))}`),
|
|
300
|
-
...orphaned.map((k) => `? ${k} only in the project — left alone`),
|
|
301
|
-
].join("\n"), `${spec.project} will change`);
|
|
302
|
-
// Overwrites are the destructive half and get their own question, so that
|
|
303
|
-
// answering yes to "create three new secrets" is not also answering yes to
|
|
304
|
-
// "replace a live credential".
|
|
305
|
-
if (updated.length > 0 && !options.yes) {
|
|
306
|
-
const ok = unwrap(await confirm({
|
|
307
|
-
message: `Overwrite ${updated.length} existing value(s) in ${spec.project}?`,
|
|
308
|
-
initialValue: false,
|
|
309
|
-
}));
|
|
310
|
-
if (!ok)
|
|
311
|
-
bail("Nothing written.");
|
|
312
|
-
}
|
|
313
|
-
if (spec.guarded) {
|
|
314
|
-
if (options.yes) {
|
|
315
|
-
explain("Refusing to push production non-interactively.", "", [
|
|
316
|
-
"Drop --yes and confirm at the prompt.",
|
|
317
|
-
]);
|
|
318
|
-
process.exitCode = 1;
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const sure = unwrap(await confirm({
|
|
322
|
-
message: `This writes to PRODUCTION (${spec.project}). Continue?`,
|
|
323
|
-
initialValue: false,
|
|
324
|
-
}));
|
|
325
|
-
if (!sure)
|
|
326
|
-
bail("Nothing written.");
|
|
327
|
-
}
|
|
328
|
-
// Writes are paced ~1.1s apart, under Bitwarden's published 60-POSTs-a-
|
|
329
|
-
// minute limit (see bws/pace.ts). Said out loud for a big push, because a
|
|
330
|
-
// minute of silence reads as a hang and gets Ctrl-C'd.
|
|
331
|
-
const writes = created.length + updated.length;
|
|
332
|
-
if (writes > 5) {
|
|
333
|
-
log.info(`Writing ${writes} secrets, paced to stay under Bitwarden's rate ` +
|
|
334
|
-
`limit — about ${Math.ceil((writes * 1.1) / 10) * 10} seconds.`);
|
|
335
|
-
}
|
|
336
|
-
for (const key of created) {
|
|
337
|
-
await createSecret(projectId, key, stored.get(key), MANAGED);
|
|
338
|
-
}
|
|
339
|
-
for (const key of updated) {
|
|
340
|
-
await updateSecret(index.get(key), stored.get(key), MANAGED);
|
|
341
|
-
}
|
|
342
|
-
log.success(`${spec.project}: ${created.length} created, ${updated.length} updated.`);
|
|
343
|
-
}
|
|
344
|
-
await pushToGithub(target, secrets, publicValues, options.yes);
|
|
345
|
-
// Record what went where, in the file itself. Values are untouched, since
|
|
346
|
-
// this rewrites the trailing comment only, so it needs no confirmation. It is
|
|
347
|
-
// what makes a stale file say so rather than look freshly synced.
|
|
348
|
-
const stamp = stampFor(target, "pushed");
|
|
349
|
-
for (const [key, value] of stored)
|
|
350
|
-
doc.set(key, value, stamp);
|
|
351
|
-
await save(path, doc);
|
|
352
|
-
}
|
|
353
|
-
const MANAGED = "Managed by `pnpm devtools env push`.";
|
|
354
|
-
/**
|
|
355
|
-
* The second half of every push.
|
|
356
|
-
*
|
|
357
|
-
* One Bitwarden project can feed more than one GitHub environment: `production`
|
|
358
|
-
* feeds both `production` and `production-apply`, and which key goes where IS
|
|
359
|
-
* the reviewer gate. So this loops over the routed targets rather than assuming
|
|
360
|
-
* one, and confirms each separately. Agreeing to update production's ordinary
|
|
361
|
-
* secrets is not agreeing to touch the two write-capable credentials sitting
|
|
362
|
-
* behind the reviewers.
|
|
363
|
-
*
|
|
364
|
-
* The two maps stay two maps all the way down to the two `gh` calls. Merging
|
|
365
|
-
* them and branching at the bottom would put "which store does this go to?" one
|
|
366
|
-
* boolean away from being wrong, and getting it wrong in the variable direction
|
|
367
|
-
* publishes a credential's plaintext to everyone who can read the repository's
|
|
368
|
-
* Actions config.
|
|
369
|
-
*
|
|
370
|
-
* Exported ONLY so dispatch can be tested against a mocked `gh` client. The two
|
|
371
|
-
* `setSecret`/`setVariable` lines below are the one place where a swap is
|
|
372
|
-
* silent, irreversible, and invisible to `selection.ts`'s tests.
|
|
373
|
-
*/
|
|
374
|
-
export async function pushToGithub(target, secrets, publicValues, yes) {
|
|
375
|
-
const project = environmentSpecs()[target].project;
|
|
376
|
-
// `ghEnvironment`, not `target`: a GitHub environment is a THIRD vocabulary
|
|
377
|
-
// (there are four of them, and `production-apply` is not an env target at
|
|
378
|
-
// all), so it keeps a name of its own. Reusing `target` here is how the two
|
|
379
|
-
// vocabularies got confused in the first place.
|
|
380
|
-
for (const ghEnvironment of githubTargets(project)) {
|
|
381
|
-
// Routing applies to both stores, and the two loops below are the same
|
|
382
|
-
// filter twice rather than one filter and a branch. See the header.
|
|
383
|
-
//
|
|
384
|
-
// `production-apply` accepts everything `production` does plus the
|
|
385
|
-
// apply-tier credential, so a production push writes MOST keys twice: once
|
|
386
|
-
// to the unreviewed environment the deploy reads, once to the reviewed one
|
|
387
|
-
// whose jobs (`production-config`, `prune-orphans`) were previously
|
|
388
|
-
// starved of them. That is not the gate leaking. The gate is
|
|
389
|
-
// `production.excludeKeys`, which keeps the apply-tier credential out of
|
|
390
|
-
// the FIRST environment; it has never had anything to say about the
|
|
391
|
-
// second.
|
|
392
|
-
const chosenSecrets = new Map([...secrets].filter(([key]) => accepts(ghEnvironment, key)));
|
|
393
|
-
const chosenVariables = new Map([...publicValues].filter(([key]) => accepts(ghEnvironment, key)));
|
|
394
|
-
const total = chosenSecrets.size + chosenVariables.size;
|
|
395
|
-
if (total === 0)
|
|
396
|
-
continue;
|
|
397
|
-
const knownSecrets = new Set((await listGhSecrets(ghEnvironment)).map((s) => s.name));
|
|
398
|
-
const knownVariables = new Set((await listGhVariables(ghEnvironment)).map((v) => v.name));
|
|
399
|
-
const fresh = [...chosenSecrets.keys()].filter((k) => !knownSecrets.has(k)).length +
|
|
400
|
-
[...chosenVariables.keys()].filter((k) => !knownVariables.has(k)).length;
|
|
401
|
-
if (!yes) {
|
|
402
|
-
const ok = unwrap(await confirm({
|
|
403
|
-
message: `Sync ${chosenSecrets.size} secret(s) and ` +
|
|
404
|
-
`${chosenVariables.size} variable(s) to the \`${ghEnvironment}\` ` +
|
|
405
|
-
`GitHub environment (${fresh} new)?`,
|
|
406
|
-
// The gated environments hold what a reviewer is meant to see before
|
|
407
|
-
// it can be used, so the default answer there is no.
|
|
408
|
-
initialValue: !GITHUB_ENVIRONMENT_SPECS[ghEnvironment].guarded,
|
|
409
|
-
}));
|
|
410
|
-
if (!ok) {
|
|
411
|
-
log.warn(`Skipped \`${ghEnvironment}\`. ⚠️ Bitwarden is now ahead of GitHub ` +
|
|
412
|
-
`— the deploy still uses the previous values. Run \`pnpm devtools ` +
|
|
413
|
-
`env audit --target ${target}\` when you fix it.`);
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
// Sequential. `gh` is one process per secret, and a burst of them against
|
|
418
|
-
// the same environment is a good way to meet a secondary rate limit. That
|
|
419
|
-
// leaves the set half-applied, the one outcome worse than not having
|
|
420
|
-
// started.
|
|
421
|
-
for (const [key, value] of chosenSecrets)
|
|
422
|
-
await setSecret(ghEnvironment, key, value);
|
|
423
|
-
for (const [key, value] of chosenVariables) {
|
|
424
|
-
await setVariable(ghEnvironment, key, value);
|
|
425
|
-
}
|
|
426
|
-
log.success(`Synced ${chosenSecrets.size} secret(s) and ${chosenVariables.size} ` +
|
|
427
|
-
`variable(s) to \`${ghEnvironment}\`.`);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
// ── audit ────────────────────────────────────────────────────────────────────
|
|
431
|
-
/** Read-only, and safe to run against anything. */
|
|
432
|
-
export async function runEnvAudit(options) {
|
|
433
|
-
assertVaultTarget(options.target);
|
|
434
|
-
const target = options.target;
|
|
435
|
-
const spec = environmentSpecs()[target];
|
|
436
|
-
// The target's own file, like pull and push. Auditing `--target staging`
|
|
437
|
-
// against the development `.env` reported drift on every key that legitimately
|
|
438
|
-
// differs between the two, which is most of them.
|
|
439
|
-
const path = pathFor(target, options.file);
|
|
440
|
-
const doc = await readDocument(path);
|
|
441
|
-
const projectId = await projectIdFor(spec.project);
|
|
442
|
-
const bwsSecrets = await listBwsSecrets(projectId);
|
|
443
|
-
// Every GitHub environment this project feeds, so that a key sitting in the
|
|
444
|
-
// WRONG one is visible rather than merely absent from the right one. Both
|
|
445
|
-
// stores are read for each: a key in the wrong STORE is as invisible to a
|
|
446
|
-
// presence check as one in the wrong environment, and more dangerous.
|
|
447
|
-
const github = [];
|
|
448
|
-
const githubVariables = [];
|
|
449
|
-
const unreachable = [];
|
|
450
|
-
for (const ghEnvironment of githubTargets(spec.project)) {
|
|
451
|
-
try {
|
|
452
|
-
// Both reads complete before either is recorded. Half an environment is
|
|
453
|
-
// worse than none of it: the secrets alone, with the environment then
|
|
454
|
-
// marked unreachable, would leave every variable it holds looking like a
|
|
455
|
-
// GitHub orphan that somebody should delete.
|
|
456
|
-
const secrets = await listGhSecrets(ghEnvironment);
|
|
457
|
-
const variables = await listGhVariables(ghEnvironment);
|
|
458
|
-
for (const secret of secrets) {
|
|
459
|
-
github.push({
|
|
460
|
-
environment: ghEnvironment,
|
|
461
|
-
name: secret.name,
|
|
462
|
-
updatedAt: secret.updatedAt,
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
for (const variable of variables) {
|
|
466
|
-
githubVariables.push({
|
|
467
|
-
environment: ghEnvironment,
|
|
468
|
-
name: variable.name,
|
|
469
|
-
value: variable.value,
|
|
470
|
-
updatedAt: variable.updatedAt,
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
catch {
|
|
475
|
-
// Usually an environment nobody has created yet. Reported, then routed
|
|
476
|
-
// around: inventing "missing from GitHub" for every key in an
|
|
477
|
-
// environment that could not be read would bury everything else.
|
|
478
|
-
unreachable.push(ghEnvironment);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
// The repository's own variables, which no environment read can see and push
|
|
482
|
-
// never writes. Failure is CARRIED rather than thrown or flattened to `[]`:
|
|
483
|
-
// this list can be unreadable where the environment ones are readable, and an
|
|
484
|
-
// audit that quietly downgraded "could not look" to "nothing there" would
|
|
485
|
-
// report the exact hazard it was added to catch as health.
|
|
486
|
-
let repositoryVariables;
|
|
487
|
-
try {
|
|
488
|
-
repositoryVariables = {
|
|
489
|
-
readable: true,
|
|
490
|
-
names: (await listRepositoryVariables()).map((v) => v.name),
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
catch (err) {
|
|
494
|
-
const firstLine = errorMessage(err).split("\n")[0]?.trim() ?? "";
|
|
495
|
-
repositoryVariables = {
|
|
496
|
-
readable: false,
|
|
497
|
-
// The FIRST line only. `describe()` in the gh client returns a paragraph
|
|
498
|
-
// of guidance, and a finding is one line. The rest is reproducible by
|
|
499
|
-
// running the command the finding names.
|
|
500
|
-
reason: firstLine || "`gh` failed",
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
const { secrets: cloudflare, unreadable } = await listWorkerSecrets(target);
|
|
504
|
-
const commented = new Set(bwsSecrets.map((s) => s.key).filter((k) => doc.isCommented(k)));
|
|
505
|
-
const findings = audit({
|
|
506
|
-
local: new Map(doc.entries()),
|
|
507
|
-
localCommented: commented,
|
|
508
|
-
bws: new Map(bwsSecrets.map((s) => [
|
|
509
|
-
s.key,
|
|
510
|
-
{ value: s.value, revisionDate: s.revisionDate },
|
|
511
|
-
])),
|
|
512
|
-
github,
|
|
513
|
-
githubVariables,
|
|
514
|
-
// Which store each key belongs in. Read from the registry rather than
|
|
515
|
-
// inferred from where a copy turned up: otherwise a misplaced key would
|
|
516
|
-
// define its own correctness and never be reported.
|
|
517
|
-
variables: pushableVariables(),
|
|
518
|
-
route: (key) => {
|
|
519
|
-
const routed = routeTo(spec.project, key);
|
|
520
|
-
return routed && unreachable.includes(routed) ? null : routed;
|
|
521
|
-
},
|
|
522
|
-
// Whether a SECOND copy is legitimate, which stopped being "the one place
|
|
523
|
-
// `route` names" when `production-apply` became a superset of `production`.
|
|
524
|
-
// A push now writes most production keys to both, and comparing against
|
|
525
|
-
// `route` alone would report every one as a stray to delete, burying the one
|
|
526
|
-
// stray that matters: an apply-tier key in the unreviewed environment.
|
|
527
|
-
// `acceptsKey()` still says no to that one, and is a name rather than a
|
|
528
|
-
// lambda so it has tests of its own.
|
|
529
|
-
accepted: acceptsKey,
|
|
530
|
-
cloudflare,
|
|
531
|
-
ignore: ignoredFor(target),
|
|
532
|
-
neverStore: neverStore(),
|
|
533
|
-
// Keeps the Worker's minted credential from being reported as a Cloudflare
|
|
534
|
-
// orphan, and so from being pruned, while still flagging a stored copy.
|
|
535
|
-
minted: minted(),
|
|
536
|
-
// Lets the audit tell "undeclared" apart from drift: the fix for one is a
|
|
537
|
-
// define() in a manifest, for the other a push or a pull.
|
|
538
|
-
declared: new Set(getEnvSync().variables().keys()),
|
|
539
|
-
// The scope nothing else here addresses. Passed as the scan rather than as
|
|
540
|
-
// a list so that `audit` can tell "checked, clean" from "could not check".
|
|
541
|
-
repositoryVariables,
|
|
542
|
-
});
|
|
543
|
-
note(renderFindings(findings), `${target} (${path}) — drift`);
|
|
544
|
-
if (unreachable.length > 0) {
|
|
545
|
-
log.warn(`Could not read the ${unreachable.join(", ")} GitHub environment(s), so ` +
|
|
546
|
-
`nothing was checked there. Usually means it does not exist yet.`);
|
|
547
|
-
}
|
|
548
|
-
if (unreadable.length > 0) {
|
|
549
|
-
log.warn(`Could not read Worker secrets for: ${unreadable.join(", ")}. ` +
|
|
550
|
-
`Usually means the Worker has not been deployed yet.`);
|
|
551
|
-
}
|
|
552
|
-
// Said explicitly, because "no drift" reads as a stronger claim than it is,
|
|
553
|
-
// and is now a stronger claim for some keys than for others. That is the sort
|
|
554
|
-
// of difference a summary line loses.
|
|
555
|
-
log.info("GitHub *secrets* and Cloudflare secrets are write-only: those were " +
|
|
556
|
-
"checked for presence, for routing, and for whether GitHub's copy " +
|
|
557
|
-
"predates the Bitwarden revision — a changed value is undetectable. " +
|
|
558
|
-
"GitHub *variables* are readable, so the public per-environment keys " +
|
|
559
|
-
"were compared by VALUE, as your env file and Bitwarden were. " +
|
|
560
|
-
// Stated only when it is true. The claim is the coverage itself, so
|
|
561
|
-
// printing it unconditionally would turn the one run that could not look
|
|
562
|
-
// into the one run that says it did.
|
|
563
|
-
(repositoryVariables.readable
|
|
564
|
-
? "The repository's own variables were listed too, and checked for " +
|
|
565
|
-
"names that an environment copy would shadow."
|
|
566
|
-
: "The repository's own variables could NOT be listed, so nothing " +
|
|
567
|
-
"above rules out a shadowed copy at that scope."));
|
|
568
|
-
if (hasErrors(findings))
|
|
569
|
-
process.exitCode = 1;
|
|
570
|
-
}
|
|
571
|
-
// ── reset ────────────────────────────────────────────────────────────────────
|
|
572
|
-
/**
|
|
573
|
-
* Empties every value in a local env file, losing none of them.
|
|
574
|
-
*
|
|
575
|
-
* The use is handing a filled-in file back to its blank state: after pulling
|
|
576
|
-
* production onto a laptop, before passing a machine on, or when a set of keys
|
|
577
|
-
* has to be re-entered from scratch. Deleting the values would do that too, and
|
|
578
|
-
* would also delete the only copy of anything that was never pushed. So each
|
|
579
|
-
* becomes a commented line holding what it was, plus an empty active line
|
|
580
|
-
* beneath. The file still declares every key it needs, which makes it a
|
|
581
|
-
* checklist rather than a blank page.
|
|
582
|
-
*
|
|
583
|
-
* Purely local. It touches no remote store, so it takes no `--target`: asking
|
|
584
|
-
* which target to clear a file against would imply it reaches one. It works on
|
|
585
|
-
* `.env` unless `--file` says otherwise, and that default is written out here
|
|
586
|
-
* rather than inherited, so the shared `pathFor` has no "no target" case for a
|
|
587
|
-
* remote command to fall into.
|
|
588
|
-
*/
|
|
589
|
-
export async function runEnvReset(options) {
|
|
590
|
-
const path = pathFor("development", options.file);
|
|
591
|
-
const doc = await readDocument(path);
|
|
592
|
-
const active = doc.entries().filter(([, value]) => value !== "");
|
|
593
|
-
if (active.length === 0) {
|
|
594
|
-
log.success(`Nothing to clear — every value in ${path} is already empty.`);
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
note(active
|
|
598
|
-
.map(([key, value]) => `~ ${key} ${fingerprint(value)} → empty`)
|
|
599
|
-
.join("\n"), `${path} will be cleared`);
|
|
600
|
-
if (!options.yes) {
|
|
601
|
-
const ok = unwrap(await confirm({
|
|
602
|
-
message: `Clear ${active.length} value(s)? Each is kept, commented out, on the line above.`,
|
|
603
|
-
initialValue: false,
|
|
604
|
-
}));
|
|
605
|
-
if (!ok)
|
|
606
|
-
bail("Nothing written.");
|
|
607
|
-
}
|
|
608
|
-
const cleared = doc.reset();
|
|
609
|
-
await save(path, doc);
|
|
610
|
-
log.success(`Cleared ${cleared.length} value(s) in ${path}.`);
|
|
611
|
-
log.info("Every previous value is still in the file, commented out. `env pull` " +
|
|
612
|
-
"will fill them back in from Bitwarden.");
|
|
613
|
-
}
|
package/dist/env/discovery.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Imports every manifest so the registry populates. Idempotent; concurrent
|
|
3
|
-
* callers share one pass. Await this before touching any derived selector.
|
|
4
|
-
*/
|
|
5
|
-
export declare function loadRegistry(): Promise<void>;
|
|
6
|
-
/**
|
|
7
|
-
* Refuses to proceed on an empty registry.
|
|
8
|
-
*
|
|
9
|
-
* For SYNC consumers of the derived selectors (`selectForPush`, `routeTo`),
|
|
10
|
-
* which cannot await the load themselves. An empty registry makes every
|
|
11
|
-
* selector return `[]`, and `[]` fails open in the worst direction: nothing is
|
|
12
|
-
* never-store, so `BWS_ACCESS_TOKEN` would upload. Better a crash naming the
|
|
13
|
-
* missing call than a push that quietly stores the unstorable.
|
|
14
|
-
*/
|
|
15
|
-
export declare function assertRegistryLoaded(): void;
|