@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/discovery.js
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fills the `@devdogsuga/env` registry by importing every manifest.
|
|
3
|
-
*
|
|
4
|
-
* The registry populates as a SIDE EFFECT of importing each manifest module.
|
|
5
|
-
* `declare()` runs at import time, so a process that never imports them sees an
|
|
6
|
-
* empty registry and every derived selector returns nothing. That is the
|
|
7
|
-
* fail-open shape this module closes: anything that routes secrets calls
|
|
8
|
-
* `loadRegistry()` first, and `assertRegistryLoaded()` is the guard the
|
|
9
|
-
* selectors' consumers use to refuse an empty answer.
|
|
10
|
-
*
|
|
11
|
-
* Discovery is by filename, per the model doc: `src/env.ts`, then `env.ts`, per
|
|
12
|
-
* workspace package. Both present is a hard error, because two manifests in one
|
|
13
|
-
* package means two places for the next declaration to land and no rule saying
|
|
14
|
-
* which. No `package.json` pointer: every other tool in this repo is found by
|
|
15
|
-
* filename (`tsconfig.json`, `wrangler.jsonc`, `supadart.yaml`), and a pointer
|
|
16
|
-
* would be the only one of its kind.
|
|
17
|
-
*
|
|
18
|
-
* One special case: `supabase/env.ts` sits at the repo root, OUTSIDE the
|
|
19
|
-
* workspace globs. Its variables belong to `config.toml` and the Supabase
|
|
20
|
-
* CLI, not to any package, so it lives next to the config that reads them.
|
|
21
|
-
*
|
|
22
|
-
* `@devdogsuga/env` itself (the package that exports `define()`/`declare()`)
|
|
23
|
-
* no longer needs a special-case exclusion here: since the Backstage
|
|
24
|
-
* cutover it is an installed npm dependency, not a workspace package under
|
|
25
|
-
* `packages/`, so this scan never sees it at all.
|
|
26
|
-
*
|
|
27
|
-
* devtools' OWN operator manifest (this package's root `env.ts` — the keys
|
|
28
|
-
* no app reads at all: `BWS_ACCESS_TOKEN`, `CLOUDFLARE_API_TOKEN`, and
|
|
29
|
-
* friends) has the same problem in reverse: since the Backstage cutover
|
|
30
|
-
* devtools is no longer one of the TARGET repo's workspace packages either,
|
|
31
|
-
* so the scan below would never see it. It is loaded unconditionally,
|
|
32
|
-
* always, regardless of which repo devtools is running against — see
|
|
33
|
-
* `ownManifestPath()`'s own comment for how it stays resolvable both from a
|
|
34
|
-
* workspace checkout and from an installed `pnpm dlx`/`node_modules` copy,
|
|
35
|
-
* and for why loading it through the same `importRepoTs()` tsx path as
|
|
36
|
-
* every other manifest (rather than a plain static `import`) is what keeps
|
|
37
|
-
* its `declare()`/`define()` calls landing in the SAME `@devdogsuga/env`
|
|
38
|
-
* registry instance the target repo's manifests populate (module identity —
|
|
39
|
-
* see `repo/peers.ts`'s header and the devtools-dlx prototype's
|
|
40
|
-
* FINDINGS.md experiment 3). Its bare `@devdogsuga/env` import is
|
|
41
|
-
* redirected to the copy `loadEnv()` resolved through the repo, because
|
|
42
|
-
* from a `pnpm dlx` copy the specifier does not resolve at all: devtools
|
|
43
|
-
* ships no copy of its own (`@devdogsuga/env` is only ever an optional
|
|
44
|
-
* peer), and nothing installs one next to it. See `repo/peer-redirect.ts`.
|
|
45
|
-
*
|
|
46
|
-
* Most workspace packages declare nothing, so "no env.ts found" means
|
|
47
|
-
* not-a-manifest rather than an error.
|
|
48
|
-
*
|
|
49
|
-
* ⚠️ `apps/sandbox` was once listed here as having no manifest ON PURPOSE, on
|
|
50
|
-
* the grounds that a Worker reads bindings that arrive as a function argument
|
|
51
|
-
* rather than `process.env`. That premise is still true and the conclusion
|
|
52
|
-
* drawn from it was still wrong: a manifest is also the only thing that routes
|
|
53
|
-
* a credential to a deployed environment and the only thing that tells
|
|
54
|
-
* `env audit` a Worker secret is supposed to be there. Without one,
|
|
55
|
-
* `SANDBOX_PROXY_TOKEN`, minted at deploy time and therefore in no Bitwarden
|
|
56
|
-
* project by design, was reported as an orphan, i.e. as safe for the §3.6 prune
|
|
57
|
-
* path to delete. `apps/sandbox/env.ts` exists now, and states that reasoning
|
|
58
|
-
* at length.
|
|
59
|
-
*/
|
|
60
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
61
|
-
import { dirname, join } from "node:path";
|
|
62
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
63
|
-
import { redirectPeer } from "../repo/peer-redirect.js";
|
|
64
|
-
import { getEnvSync, loadEnv, repoPeerUrl } from "../repo/peers.js";
|
|
65
|
-
import { findRepoRoot } from "../repo/root.js";
|
|
66
|
-
import { importRepoTs } from "../repo/tsx-loader.js";
|
|
67
|
-
/**
|
|
68
|
-
* The single in-flight (or settled) load.
|
|
69
|
-
*
|
|
70
|
-
* Node's module cache already makes a second `import()` of the same manifest a
|
|
71
|
-
* no-op, but memoizing the whole pass is asserted here rather than inherited: a
|
|
72
|
-
* future bundler or test runner that re-evaluates modules would duplicate every
|
|
73
|
-
* declaration, and duplicated declarations are what the completeness test
|
|
74
|
-
* treats as a bug. A failed load stays failed. Every later call gets the same
|
|
75
|
-
* rejection, because retrying into a half-populated registry would hide the
|
|
76
|
-
* manifest that broke.
|
|
77
|
-
*/
|
|
78
|
-
let loaded;
|
|
79
|
-
/**
|
|
80
|
-
* Imports every manifest so the registry populates. Idempotent; concurrent
|
|
81
|
-
* callers share one pass. Await this before touching any derived selector.
|
|
82
|
-
*/
|
|
83
|
-
export function loadRegistry() {
|
|
84
|
-
loaded ??= importManifests();
|
|
85
|
-
return loaded;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Refuses to proceed on an empty registry.
|
|
89
|
-
*
|
|
90
|
-
* For SYNC consumers of the derived selectors (`selectForPush`, `routeTo`),
|
|
91
|
-
* which cannot await the load themselves. An empty registry makes every
|
|
92
|
-
* selector return `[]`, and `[]` fails open in the worst direction: nothing is
|
|
93
|
-
* never-store, so `BWS_ACCESS_TOKEN` would upload. Better a crash naming the
|
|
94
|
-
* missing call than a push that quietly stores the unstorable.
|
|
95
|
-
*/
|
|
96
|
-
export function assertRegistryLoaded() {
|
|
97
|
-
if (getEnvSync().variables().size === 0) {
|
|
98
|
-
throw new Error("The env registry is empty — no manifest has been imported, so the " +
|
|
99
|
-
"derived key sets would all be []. Await loadRegistry() (from " +
|
|
100
|
-
"devtools' env/discovery.ts) before routing or selecting secrets.");
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
async function importManifests() {
|
|
104
|
-
// Warms `repo/peers.ts`'s sync cache before any manifest imports: each
|
|
105
|
-
// manifest's own `declare()`/`define()` calls run against the SAME
|
|
106
|
-
// `@devdogsuga/env` module instance this line resolves (module identity —
|
|
107
|
-
// see `repo/peers.ts`'s header), and `assertRegistryLoaded()` /
|
|
108
|
-
// `gh/environments.ts`'s getters read it back synchronously afterward.
|
|
109
|
-
await loadEnv();
|
|
110
|
-
// devtools' own manifest imports `@devdogsuga/env` by bare specifier, which
|
|
111
|
-
// does not resolve at all from a `pnpm dlx` copy. Point it at the copy
|
|
112
|
-
// `loadEnv()` just resolved through the repo. See `repo/peer-redirect.ts`.
|
|
113
|
-
const envUrl = repoPeerUrl("@devdogsuga/env");
|
|
114
|
-
if (envUrl) {
|
|
115
|
-
redirectPeer({
|
|
116
|
-
parentURL: pathToFileURL(ownManifestPath()).href,
|
|
117
|
-
specifier: "@devdogsuga/env",
|
|
118
|
-
url: envUrl,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
// The Next apps' manifests run `createEnv` at import time. Without this flag
|
|
122
|
-
// they would validate the AMBIENT environment, a devtools process rather than
|
|
123
|
-
// an app build, and throw on whatever is missing. The manifests already
|
|
124
|
-
// short-circuit their `resolveEnvironment()` call under the flag, so setting
|
|
125
|
-
// it here is the supported "read the declarations, skip the values" mode.
|
|
126
|
-
// Restored afterward so a long-lived process (tests) does not keep validation
|
|
127
|
-
// off for whatever runs next.
|
|
128
|
-
const previous = process.env.SKIP_ENV_VALIDATION;
|
|
129
|
-
process.env.SKIP_ENV_VALIDATION = "1";
|
|
130
|
-
try {
|
|
131
|
-
for (const path of manifestPaths()) {
|
|
132
|
-
try {
|
|
133
|
-
await importRepoTs(path);
|
|
134
|
-
}
|
|
135
|
-
catch (cause) {
|
|
136
|
-
throw new Error(`The env manifest at ${path} failed to import.`, {
|
|
137
|
-
cause,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
finally {
|
|
143
|
-
if (previous === undefined)
|
|
144
|
-
delete process.env.SKIP_ENV_VALIDATION;
|
|
145
|
-
else
|
|
146
|
-
process.env.SKIP_ENV_VALIDATION = previous;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Every manifest file, in a stable order.
|
|
151
|
-
*
|
|
152
|
-
* The workspace members are enumerated by scanning `apps/*` and `packages/*`
|
|
153
|
-
* plus the literal `docs`, a deliberately boring mirror of the three globs in
|
|
154
|
-
* `pnpm-workspace.yaml` rather than a YAML parse: the globs have not changed
|
|
155
|
-
* since the workspace existed, devtools carries no YAML dependency, and a new
|
|
156
|
-
* top-level glob would already mean editing more interesting files than this
|
|
157
|
-
* one.
|
|
158
|
-
*/
|
|
159
|
-
function manifestPaths() {
|
|
160
|
-
const paths = [];
|
|
161
|
-
for (const dir of workspaceDirs()) {
|
|
162
|
-
const manifest = manifestIn(dir);
|
|
163
|
-
if (manifest)
|
|
164
|
-
paths.push(manifest);
|
|
165
|
-
}
|
|
166
|
-
// The repo-root special case: not a workspace package, see the header.
|
|
167
|
-
const supabase = manifestIn(join(findRepoRoot(), "supabase"));
|
|
168
|
-
if (supabase)
|
|
169
|
-
paths.push(supabase);
|
|
170
|
-
// devtools' own operator manifest — always included, regardless of the
|
|
171
|
-
// target repo. Last, matching where it sat back when devtools itself was
|
|
172
|
-
// `packages/devtools` (alphabetically last of the packages this repo
|
|
173
|
-
// ever had). `SECTION_ORDER` in `env/example.ts` fixes the RENDERED
|
|
174
|
-
// section order independently of this insertion order, so this position
|
|
175
|
-
// is cosmetic for `.env.example`; it still matters for which source
|
|
176
|
-
// "wins" a shared key's declaration-order tie-break.
|
|
177
|
-
paths.push(ownManifestPath());
|
|
178
|
-
return paths;
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* The absolute path to devtools' own `env.ts`, resolved relative to THIS
|
|
182
|
-
* module's own file rather than the target repo — it ships alongside
|
|
183
|
-
* `dist/` (see `package.json`'s `files`), not inside the target repo's
|
|
184
|
-
* workspace, so `findRepoRoot()`/`workspaceDirs()` can never find it.
|
|
185
|
-
*
|
|
186
|
-
* `import.meta.url` here is `dist/env/discovery.js` (this file's build
|
|
187
|
-
* output — `tsconfig.json`'s `rootDir: "src"` / `outDir: "dist"` mirrors
|
|
188
|
-
* `src/env/discovery.ts`'s own location one level down), so two `..` reach
|
|
189
|
-
* the package root: `dist/env/discovery.js` -> `dist/env` -> `dist` ->
|
|
190
|
-
* package root, where `env.ts` sits (deliberately outside `src/`, per that
|
|
191
|
-
* file's own header — `tsconfig.typecheck.json` includes it as a sibling
|
|
192
|
-
* of `src` for exactly that reason). Same file, same relative shape, in
|
|
193
|
-
* both a workspace checkout (`packages/devtools/env.ts`) and an installed
|
|
194
|
-
* copy (`node_modules/@devdogsuga/devtools/env.ts`).
|
|
195
|
-
*/
|
|
196
|
-
function ownManifestPath() {
|
|
197
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
198
|
-
return join(here, "..", "..", "env.ts");
|
|
199
|
-
}
|
|
200
|
-
function workspaceDirs() {
|
|
201
|
-
const dirs = [];
|
|
202
|
-
const repoRoot = findRepoRoot();
|
|
203
|
-
for (const parent of ["apps", "packages"]) {
|
|
204
|
-
const names = [];
|
|
205
|
-
for (const entry of readdirSync(join(repoRoot, parent), {
|
|
206
|
-
withFileTypes: true,
|
|
207
|
-
})) {
|
|
208
|
-
// node_modules and dotfiles are not packages.
|
|
209
|
-
if (!entry.isDirectory())
|
|
210
|
-
continue;
|
|
211
|
-
if (entry.name === "node_modules" || entry.name.startsWith("."))
|
|
212
|
-
continue;
|
|
213
|
-
names.push(entry.name);
|
|
214
|
-
}
|
|
215
|
-
// Sorted, because readdir order is whatever the filesystem feels like and
|
|
216
|
-
// registry insertion order is now OBSERVABLE: `env example` renders
|
|
217
|
-
// keys in declaration order and CI byte-compares the result, so two
|
|
218
|
-
// machines walking the same tree must import the same manifests in the
|
|
219
|
-
// same sequence.
|
|
220
|
-
names.sort();
|
|
221
|
-
for (const name of names)
|
|
222
|
-
dirs.push(join(repoRoot, parent, name));
|
|
223
|
-
}
|
|
224
|
-
dirs.push(join(repoRoot, "docs"));
|
|
225
|
-
return dirs;
|
|
226
|
-
}
|
|
227
|
-
/** `src/env.ts`, then `env.ts`; both present in one package is a hard error. */
|
|
228
|
-
function manifestIn(dir) {
|
|
229
|
-
const inSrc = join(dir, "src", "env.ts");
|
|
230
|
-
const atRoot = join(dir, "env.ts");
|
|
231
|
-
const hasSrc = existsSync(inSrc);
|
|
232
|
-
const hasRoot = existsSync(atRoot);
|
|
233
|
-
if (hasSrc && hasRoot) {
|
|
234
|
-
throw new Error(`${dir} has BOTH src/env.ts and env.ts. One package gets one manifest — ` +
|
|
235
|
-
"two of them means two places for the next declaration to land, and " +
|
|
236
|
-
"no rule saying which. Merge them.");
|
|
237
|
-
}
|
|
238
|
-
return hasSrc ? inSrc : hasRoot ? atRoot : null;
|
|
239
|
-
}
|
package/dist/env/document.d.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An editable `.env` that survives being edited.
|
|
3
|
-
*
|
|
4
|
-
* The root `.env` is not a data file. It is 150 lines of hard-won commentary
|
|
5
|
-
* about which value breaks the Supabase CLI when empty, which one must stay
|
|
6
|
-
* commented out, and why. A writer that parses to a Map and serializes back
|
|
7
|
-
* destroys all of it on the first save.
|
|
8
|
-
*
|
|
9
|
-
* So this keeps the file as **lines** and edits in place. An untouched line is
|
|
10
|
-
* returned byte-for-byte, including its spacing, quoting style and trailing
|
|
11
|
-
* comment. Only the lines actually being changed are rewritten.
|
|
12
|
-
*
|
|
13
|
-
* Nothing is ever deleted. Removing a key comments it out, so the value stays
|
|
14
|
-
* recoverable from the file itself, and re-adding it later uncomments that line
|
|
15
|
-
* rather than appending a duplicate.
|
|
16
|
-
*/
|
|
17
|
-
export interface EnvLine {
|
|
18
|
-
raw: string;
|
|
19
|
-
key?: string;
|
|
20
|
-
/** Parsed value, when this line is an assignment. */
|
|
21
|
-
value?: string;
|
|
22
|
-
commented?: boolean;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Reads the value half of an assignment.
|
|
26
|
-
*
|
|
27
|
-
* Deliberately NOT a general dotenv parser: it does not expand `$VAR`, because
|
|
28
|
-
* a stored value that means one thing in the file and another in the process
|
|
29
|
-
* cannot be rotated with confidence.
|
|
30
|
-
*/
|
|
31
|
-
export declare function parseValue(rest: string): string;
|
|
32
|
-
/**
|
|
33
|
-
* Splits everything after `=` into its value and its trailing comment.
|
|
34
|
-
*
|
|
35
|
-
* Quote-aware, because the naive "cut at the first `#`" truncates a generated
|
|
36
|
-
* password into something that still looks like one. A `#` inside quotes is
|
|
37
|
-
* part of the value; outside them it starts a comment only when whitespace
|
|
38
|
-
* precedes it.
|
|
39
|
-
*/
|
|
40
|
-
export declare function splitComment(rest: string): {
|
|
41
|
-
comment: string;
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* Where a value came from and when, written on the same line as the value.
|
|
45
|
-
*
|
|
46
|
-
* It answers "is this still what production has?" without running anything. A
|
|
47
|
-
* `.env` that has been sitting for three weeks says so rather than looking
|
|
48
|
-
* exactly like one pulled this morning.
|
|
49
|
-
*/
|
|
50
|
-
export interface Stamp {
|
|
51
|
-
environment: string;
|
|
52
|
-
action: "pushed" | "pulled";
|
|
53
|
-
/** ISO date. Passed in rather than read from the clock, so this is testable. */
|
|
54
|
-
date: string;
|
|
55
|
-
}
|
|
56
|
-
export declare function isStamp(comment: string): boolean;
|
|
57
|
-
/** Always double-quoted, so a multi-line value round-trips through one line. */
|
|
58
|
-
export declare function quote(value: string): string;
|
|
59
|
-
export declare class EnvDocument {
|
|
60
|
-
private lines;
|
|
61
|
-
/** Whether this session has already appended, so it separates only once. */
|
|
62
|
-
private appended;
|
|
63
|
-
private constructor();
|
|
64
|
-
static parse(text: string): EnvDocument;
|
|
65
|
-
static empty(): EnvDocument;
|
|
66
|
-
private find;
|
|
67
|
-
/** The active value, or undefined when absent or commented out. */
|
|
68
|
-
get(key: string): string | undefined;
|
|
69
|
-
/** Present and active. */
|
|
70
|
-
has(key: string): boolean;
|
|
71
|
-
/** Present, but commented out. */
|
|
72
|
-
isCommented(key: string): boolean;
|
|
73
|
-
/** Every active assignment, in file order. */
|
|
74
|
-
entries(): [string, string][];
|
|
75
|
-
keys(): string[];
|
|
76
|
-
/**
|
|
77
|
-
* Sets a value, preferring to revive a commented line over appending.
|
|
78
|
-
*
|
|
79
|
-
* Appending when a commented form already exists is how a file ends up with
|
|
80
|
-
* the same key twice: one stale, both plausible, and which one looks
|
|
81
|
-
* authoritative depends on where the reader scrolled to.
|
|
82
|
-
*/
|
|
83
|
-
set(key: string, value: string, stamp?: Stamp): void;
|
|
84
|
-
/** Index of the first line mentioning a key, active or commented. */
|
|
85
|
-
private first;
|
|
86
|
-
/**
|
|
87
|
-
* Moves every line for a key next to that key's first appearance.
|
|
88
|
-
*
|
|
89
|
-
* Files drift: a key gets commented out near the bottom, re-added at the top
|
|
90
|
-
* six weeks later, and now two lines claim the same name a hundred lines
|
|
91
|
-
* apart. Whichever one a reader scrolls to first looks authoritative.
|
|
92
|
-
*
|
|
93
|
-
* The FIRST occurrence keeps its position, so the standalone comment block
|
|
94
|
-
* documenting a key stays attached to it. Everything else moves up to join
|
|
95
|
-
* it, in the order it already had.
|
|
96
|
-
*/
|
|
97
|
-
group(): boolean;
|
|
98
|
-
/**
|
|
99
|
-
* Blanks every active value without losing one.
|
|
100
|
-
*
|
|
101
|
-
* Each becomes a commented line holding what it was, plus an empty active
|
|
102
|
-
* line under it. The file still declares every key it needs, which is what
|
|
103
|
-
* makes it a usable checklist, while holding nothing.
|
|
104
|
-
*
|
|
105
|
-
* Already-empty keys are skipped. Commenting out `FOO=""` to write `FOO=""`
|
|
106
|
-
* underneath is churn that makes the next diff harder to read.
|
|
107
|
-
*/
|
|
108
|
-
reset(): string[];
|
|
109
|
-
/**
|
|
110
|
-
* Comments a key out rather than deleting it.
|
|
111
|
-
*
|
|
112
|
-
* A no-op when it is already commented or absent, so running a sync twice
|
|
113
|
-
* cannot produce `## KEY=`.
|
|
114
|
-
*/
|
|
115
|
-
comment(key: string): boolean;
|
|
116
|
-
/** Restores a commented assignment. Returns its value, or undefined. */
|
|
117
|
-
uncomment(key: string): string | undefined;
|
|
118
|
-
toString(): string;
|
|
119
|
-
}
|
package/dist/env/document.js
DELETED
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An editable `.env` that survives being edited.
|
|
3
|
-
*
|
|
4
|
-
* The root `.env` is not a data file. It is 150 lines of hard-won commentary
|
|
5
|
-
* about which value breaks the Supabase CLI when empty, which one must stay
|
|
6
|
-
* commented out, and why. A writer that parses to a Map and serializes back
|
|
7
|
-
* destroys all of it on the first save.
|
|
8
|
-
*
|
|
9
|
-
* So this keeps the file as **lines** and edits in place. An untouched line is
|
|
10
|
-
* returned byte-for-byte, including its spacing, quoting style and trailing
|
|
11
|
-
* comment. Only the lines actually being changed are rewritten.
|
|
12
|
-
*
|
|
13
|
-
* Nothing is ever deleted. Removing a key comments it out, so the value stays
|
|
14
|
-
* recoverable from the file itself, and re-adding it later uncomments that line
|
|
15
|
-
* rather than appending a duplicate.
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* `KEY=value`, active. Groups: indent, `export ` prefix, key, value.
|
|
19
|
-
*
|
|
20
|
-
* The prefix is CAPTURED rather than skipped so an update can put it back.
|
|
21
|
-
* Dropping it turns `export FOO=` into `FOO=`, which still parses here and
|
|
22
|
-
* stops being exported to child processes. That shows up as a missing variable
|
|
23
|
-
* three tools downstream.
|
|
24
|
-
*/
|
|
25
|
-
const ACTIVE = /^(\s*)((?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
|
|
26
|
-
/** `# KEY=value`, the commented form this tool writes and reads back. */
|
|
27
|
-
const COMMENTED = /^(\s*)#\s?((?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
|
|
28
|
-
/**
|
|
29
|
-
* Reads the value half of an assignment.
|
|
30
|
-
*
|
|
31
|
-
* Deliberately NOT a general dotenv parser: it does not expand `$VAR`, because
|
|
32
|
-
* a stored value that means one thing in the file and another in the process
|
|
33
|
-
* cannot be rotated with confidence.
|
|
34
|
-
*/
|
|
35
|
-
export function parseValue(rest) {
|
|
36
|
-
const text = rest.trim();
|
|
37
|
-
if (text.startsWith('"')) {
|
|
38
|
-
const end = findClosing(text, '"');
|
|
39
|
-
if (end === -1)
|
|
40
|
-
return unescape(text.slice(1));
|
|
41
|
-
return unescape(text.slice(1, end));
|
|
42
|
-
}
|
|
43
|
-
if (text.startsWith("'")) {
|
|
44
|
-
const end = findClosing(text, "'");
|
|
45
|
-
if (end === -1)
|
|
46
|
-
return text.slice(1);
|
|
47
|
-
// Single quotes are literal, the only way to store a backslash sequence
|
|
48
|
-
// verbatim.
|
|
49
|
-
return text.slice(1, end);
|
|
50
|
-
}
|
|
51
|
-
// Unquoted. A `#` only starts a comment when whitespace precedes it: `#` is
|
|
52
|
-
// an ordinary character in a generated password, and stripping from the first
|
|
53
|
-
// one truncates the credential into something that still looks like one.
|
|
54
|
-
const hash = text.search(/\s#/);
|
|
55
|
-
return (hash === -1 ? text : text.slice(0, hash)).trim();
|
|
56
|
-
}
|
|
57
|
-
function findClosing(text, quote) {
|
|
58
|
-
for (let i = 1; i < text.length; i += 1) {
|
|
59
|
-
if (text[i] === "\\") {
|
|
60
|
-
i += 1;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (text[i] === quote)
|
|
64
|
-
return i;
|
|
65
|
-
}
|
|
66
|
-
return -1;
|
|
67
|
-
}
|
|
68
|
-
function unescape(text) {
|
|
69
|
-
return text.replace(/\\([nrt\\"'])/g, (_, c) => c === "n" ? "\n" : c === "r" ? "\r" : c === "t" ? "\t" : c);
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Splits everything after `=` into its value and its trailing comment.
|
|
73
|
-
*
|
|
74
|
-
* Quote-aware, because the naive "cut at the first `#`" truncates a generated
|
|
75
|
-
* password into something that still looks like one. A `#` inside quotes is
|
|
76
|
-
* part of the value; outside them it starts a comment only when whitespace
|
|
77
|
-
* precedes it.
|
|
78
|
-
*/
|
|
79
|
-
export function splitComment(rest) {
|
|
80
|
-
const text = rest.trimStart();
|
|
81
|
-
let after;
|
|
82
|
-
if (text.startsWith('"') || text.startsWith("'")) {
|
|
83
|
-
const end = findClosing(text, text[0]);
|
|
84
|
-
after = end === -1 ? text.length : end + 1;
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
const hash = text.search(/\s#/);
|
|
88
|
-
after = hash === -1 ? text.length : hash;
|
|
89
|
-
}
|
|
90
|
-
const tail = text.slice(after);
|
|
91
|
-
const hash = tail.indexOf("#");
|
|
92
|
-
return { comment: hash === -1 ? "" : tail.slice(hash).trimEnd() };
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Recognises a stamp this tool wrote, so it is replaced rather than duplicated.
|
|
96
|
-
*
|
|
97
|
-
* Deliberately narrow: it matches the exact bracketed shape below and nothing
|
|
98
|
-
* else, because the cost of a false positive is deleting somebody's own note.
|
|
99
|
-
*/
|
|
100
|
-
const STAMP = /\s*#\s*\[[a-z][a-z0-9-]* (?:pushed|pulled) \d{4}-\d{2}-\d{2}\]\s*$/;
|
|
101
|
-
function stampText(stamp) {
|
|
102
|
-
return `# [${stamp.environment} ${stamp.action} ${stamp.date}]`;
|
|
103
|
-
}
|
|
104
|
-
export function isStamp(comment) {
|
|
105
|
-
return STAMP.test(comment);
|
|
106
|
-
}
|
|
107
|
-
/** Always double-quoted, so a multi-line value round-trips through one line. */
|
|
108
|
-
export function quote(value) {
|
|
109
|
-
return `"${value
|
|
110
|
-
.replace(/\\/g, "\\\\")
|
|
111
|
-
.replace(/"/g, '\\"')
|
|
112
|
-
.replace(/\n/g, "\\n")
|
|
113
|
-
.replace(/\r/g, "\\r")
|
|
114
|
-
.replace(/\t/g, "\\t")}"`;
|
|
115
|
-
}
|
|
116
|
-
export class EnvDocument {
|
|
117
|
-
lines;
|
|
118
|
-
/** Whether this session has already appended, so it separates only once. */
|
|
119
|
-
appended = false;
|
|
120
|
-
constructor(lines) {
|
|
121
|
-
this.lines = lines;
|
|
122
|
-
}
|
|
123
|
-
static parse(text) {
|
|
124
|
-
const lines = text.split("\n").map((raw) => {
|
|
125
|
-
const active = ACTIVE.exec(raw);
|
|
126
|
-
if (active) {
|
|
127
|
-
return { raw, key: active[3], value: parseValue(active[4]) };
|
|
128
|
-
}
|
|
129
|
-
const commented = COMMENTED.exec(raw);
|
|
130
|
-
if (commented) {
|
|
131
|
-
return {
|
|
132
|
-
raw,
|
|
133
|
-
key: commented[3],
|
|
134
|
-
value: parseValue(commented[4]),
|
|
135
|
-
commented: true,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
return { raw };
|
|
139
|
-
});
|
|
140
|
-
return new EnvDocument(lines);
|
|
141
|
-
}
|
|
142
|
-
static empty() {
|
|
143
|
-
return new EnvDocument([]);
|
|
144
|
-
}
|
|
145
|
-
find(key, commented) {
|
|
146
|
-
return this.lines.findIndex((l) => l.key === key && Boolean(l.commented) === commented);
|
|
147
|
-
}
|
|
148
|
-
/** The active value, or undefined when absent or commented out. */
|
|
149
|
-
get(key) {
|
|
150
|
-
const i = this.find(key, false);
|
|
151
|
-
return i === -1 ? undefined : this.lines[i].value;
|
|
152
|
-
}
|
|
153
|
-
/** Present and active. */
|
|
154
|
-
has(key) {
|
|
155
|
-
return this.find(key, false) !== -1;
|
|
156
|
-
}
|
|
157
|
-
/** Present, but commented out. */
|
|
158
|
-
isCommented(key) {
|
|
159
|
-
return this.find(key, false) === -1 && this.find(key, true) !== -1;
|
|
160
|
-
}
|
|
161
|
-
/** Every active assignment, in file order. */
|
|
162
|
-
entries() {
|
|
163
|
-
return this.lines
|
|
164
|
-
.filter((l) => l.key !== undefined && !l.commented)
|
|
165
|
-
.map((l) => [l.key, l.value]);
|
|
166
|
-
}
|
|
167
|
-
keys() {
|
|
168
|
-
return this.entries().map(([k]) => k);
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Sets a value, preferring to revive a commented line over appending.
|
|
172
|
-
*
|
|
173
|
-
* Appending when a commented form already exists is how a file ends up with
|
|
174
|
-
* the same key twice: one stale, both plausible, and which one looks
|
|
175
|
-
* authoritative depends on where the reader scrolled to.
|
|
176
|
-
*/
|
|
177
|
-
set(key, value, stamp) {
|
|
178
|
-
const active = this.find(key, false);
|
|
179
|
-
if (active !== -1) {
|
|
180
|
-
const line = this.lines[active];
|
|
181
|
-
const match = ACTIVE.exec(line.raw);
|
|
182
|
-
const existing = splitComment(match[4]).comment;
|
|
183
|
-
// Somebody's own note gets moved out of the way rather than overwritten.
|
|
184
|
-
// It was written about this key, so it belongs above the whole group:
|
|
185
|
-
// the commented-out history sits between the note and the key.
|
|
186
|
-
if (stamp && existing !== "" && !isStamp(existing)) {
|
|
187
|
-
this.lines.splice(this.first(key), 0, { raw: existing });
|
|
188
|
-
}
|
|
189
|
-
const trailing = stamp
|
|
190
|
-
? ` ${stampText(stamp)}`
|
|
191
|
-
: existing === ""
|
|
192
|
-
? ""
|
|
193
|
-
: ` ${existing}`;
|
|
194
|
-
// Re-find: the splice above may have shifted this line down.
|
|
195
|
-
const at = this.find(key, false);
|
|
196
|
-
const target = this.lines[at];
|
|
197
|
-
target.raw = `${match[1]}${match[2]}${key}=${quote(value)}${trailing}`;
|
|
198
|
-
target.value = value;
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
const commented = this.find(key, true);
|
|
202
|
-
if (commented !== -1) {
|
|
203
|
-
const line = this.lines[commented];
|
|
204
|
-
line.raw = `${key}=${quote(value)}${stamp ? ` ${stampText(stamp)}` : ""}`;
|
|
205
|
-
line.value = value;
|
|
206
|
-
line.commented = false;
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
// One blank line between the file's own contents and anything appended,
|
|
210
|
-
// and only one: a separator per appended key would grow a gap every time a
|
|
211
|
-
// sync adds a batch.
|
|
212
|
-
if (!this.appended &&
|
|
213
|
-
this.lines.length > 0 &&
|
|
214
|
-
this.lines.at(-1).raw !== "") {
|
|
215
|
-
this.lines.push({ raw: "" });
|
|
216
|
-
}
|
|
217
|
-
this.appended = true;
|
|
218
|
-
this.lines.push({
|
|
219
|
-
raw: `${key}=${quote(value)}${stamp ? ` ${stampText(stamp)}` : ""}`,
|
|
220
|
-
key,
|
|
221
|
-
value,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
/** Index of the first line mentioning a key, active or commented. */
|
|
225
|
-
first(key) {
|
|
226
|
-
return this.lines.findIndex((l) => l.key === key);
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Moves every line for a key next to that key's first appearance.
|
|
230
|
-
*
|
|
231
|
-
* Files drift: a key gets commented out near the bottom, re-added at the top
|
|
232
|
-
* six weeks later, and now two lines claim the same name a hundred lines
|
|
233
|
-
* apart. Whichever one a reader scrolls to first looks authoritative.
|
|
234
|
-
*
|
|
235
|
-
* The FIRST occurrence keeps its position, so the standalone comment block
|
|
236
|
-
* documenting a key stays attached to it. Everything else moves up to join
|
|
237
|
-
* it, in the order it already had.
|
|
238
|
-
*/
|
|
239
|
-
group() {
|
|
240
|
-
const out = [];
|
|
241
|
-
const taken = new Set();
|
|
242
|
-
let moved = false;
|
|
243
|
-
for (let i = 0; i < this.lines.length; i += 1) {
|
|
244
|
-
if (taken.has(i))
|
|
245
|
-
continue;
|
|
246
|
-
const line = this.lines[i];
|
|
247
|
-
out.push(line);
|
|
248
|
-
taken.add(i);
|
|
249
|
-
if (line.key === undefined)
|
|
250
|
-
continue;
|
|
251
|
-
for (let j = i + 1; j < this.lines.length; j += 1) {
|
|
252
|
-
if (taken.has(j) || this.lines[j].key !== line.key)
|
|
253
|
-
continue;
|
|
254
|
-
if (j !== i + 1 || taken.has(i + 1))
|
|
255
|
-
moved = true;
|
|
256
|
-
out.push(this.lines[j]);
|
|
257
|
-
taken.add(j);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
this.lines = out;
|
|
261
|
-
return moved;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Blanks every active value without losing one.
|
|
265
|
-
*
|
|
266
|
-
* Each becomes a commented line holding what it was, plus an empty active
|
|
267
|
-
* line under it. The file still declares every key it needs, which is what
|
|
268
|
-
* makes it a usable checklist, while holding nothing.
|
|
269
|
-
*
|
|
270
|
-
* Already-empty keys are skipped. Commenting out `FOO=""` to write `FOO=""`
|
|
271
|
-
* underneath is churn that makes the next diff harder to read.
|
|
272
|
-
*/
|
|
273
|
-
reset() {
|
|
274
|
-
const out = [];
|
|
275
|
-
const cleared = [];
|
|
276
|
-
for (const line of this.lines) {
|
|
277
|
-
if (line.key === undefined || line.commented || line.value === "") {
|
|
278
|
-
out.push(line);
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
const exported = ACTIVE.exec(line.raw)?.[2] ?? "";
|
|
282
|
-
line.raw = `# ${line.raw.trimStart()}`;
|
|
283
|
-
line.commented = true;
|
|
284
|
-
out.push(line);
|
|
285
|
-
out.push({
|
|
286
|
-
raw: `${exported}${line.key}=""`,
|
|
287
|
-
key: line.key,
|
|
288
|
-
value: "",
|
|
289
|
-
});
|
|
290
|
-
cleared.push(line.key);
|
|
291
|
-
}
|
|
292
|
-
this.lines = out;
|
|
293
|
-
return cleared;
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Comments a key out rather than deleting it.
|
|
297
|
-
*
|
|
298
|
-
* A no-op when it is already commented or absent, so running a sync twice
|
|
299
|
-
* cannot produce `## KEY=`.
|
|
300
|
-
*/
|
|
301
|
-
comment(key) {
|
|
302
|
-
const i = this.find(key, false);
|
|
303
|
-
if (i === -1)
|
|
304
|
-
return false;
|
|
305
|
-
const line = this.lines[i];
|
|
306
|
-
line.raw = `# ${line.raw.trimStart()}`;
|
|
307
|
-
line.commented = true;
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
/** Restores a commented assignment. Returns its value, or undefined. */
|
|
311
|
-
uncomment(key) {
|
|
312
|
-
const i = this.find(key, true);
|
|
313
|
-
if (i === -1)
|
|
314
|
-
return undefined;
|
|
315
|
-
const line = this.lines[i];
|
|
316
|
-
line.raw = line.raw.replace(/^(\s*)#\s?/, "$1");
|
|
317
|
-
line.commented = false;
|
|
318
|
-
return line.value;
|
|
319
|
-
}
|
|
320
|
-
toString() {
|
|
321
|
-
return this.lines.map((l) => l.raw).join("\n");
|
|
322
|
-
}
|
|
323
|
-
}
|