@devdogsuga/devtools 0.1.0
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 +44 -0
- package/bin/devtools-ci-bare.mjs +48 -0
- package/bin/devtools-ci.mjs +17 -0
- package/bin/devtools.mjs +27 -0
- package/dist/args.d.ts +26 -0
- package/dist/args.js +78 -0
- package/dist/bws/bw.d.ts +9 -0
- package/dist/bws/bw.js +49 -0
- package/dist/bws/client.d.ts +43 -0
- package/dist/bws/client.js +363 -0
- package/dist/bws/environments.d.ts +96 -0
- package/dist/bws/environments.js +111 -0
- package/dist/bws/pace.d.ts +27 -0
- package/dist/bws/pace.js +43 -0
- package/dist/bws/retry.d.ts +12 -0
- package/dist/bws/retry.js +39 -0
- package/dist/bws/store.d.ts +1 -0
- package/dist/bws/store.js +32 -0
- package/dist/bws/token.d.ts +31 -0
- package/dist/bws/token.js +88 -0
- package/dist/bws/vault.d.ts +29 -0
- package/dist/bws/vault.js +200 -0
- package/dist/catalog.d.ts +55 -0
- package/dist/catalog.js +81 -0
- package/dist/cf/commands.d.ts +1 -0
- package/dist/cf/commands.js +182 -0
- package/dist/cf/local-env.d.ts +44 -0
- package/dist/cf/local-env.js +148 -0
- package/dist/ci.d.ts +1 -0
- package/dist/ci.js +319 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +1077 -0
- package/dist/commands.d.ts +255 -0
- package/dist/commands.js +1279 -0
- package/dist/completions.d.ts +4 -0
- package/dist/completions.js +110 -0
- package/dist/cron/commands.d.ts +47 -0
- package/dist/cron/commands.js +372 -0
- package/dist/cron/discovery.d.ts +55 -0
- package/dist/cron/discovery.js +85 -0
- package/dist/cron/schema.d.ts +45 -0
- package/dist/cron/schema.js +35 -0
- package/dist/db/config-push.d.ts +8 -0
- package/dist/db/config-push.js +11 -0
- package/dist/db/connection.d.ts +79 -0
- package/dist/db/connection.js +114 -0
- package/dist/db/exec.d.ts +1 -0
- package/dist/db/exec.js +11 -0
- package/dist/db/generate-migration.d.ts +1 -0
- package/dist/db/generate-migration.js +67 -0
- package/dist/db/generate-types.d.ts +1 -0
- package/dist/db/generate-types.js +4 -0
- package/dist/db/introspect.d.ts +1 -0
- package/dist/db/introspect.js +113 -0
- package/dist/db/local-env.d.ts +65 -0
- package/dist/db/local-env.js +23 -0
- package/dist/db/new-migration.d.ts +1 -0
- package/dist/db/new-migration.js +8 -0
- package/dist/db/run.d.ts +70 -0
- package/dist/db/run.js +158 -0
- package/dist/db/seed-buckets.d.ts +2 -0
- package/dist/db/seed-buckets.js +4 -0
- package/dist/db/seed-production.d.ts +8 -0
- package/dist/db/seed-production.js +73 -0
- package/dist/db/seed-roles.d.ts +7 -0
- package/dist/db/seed-roles.js +31 -0
- package/dist/db/session-refresh.d.ts +43 -0
- package/dist/db/session-refresh.js +40 -0
- package/dist/deploy/migrations.d.ts +20 -0
- package/dist/deploy/migrations.js +68 -0
- package/dist/deploy/orphans.d.ts +20 -0
- package/dist/deploy/orphans.js +177 -0
- package/dist/deploy/preflight.d.ts +53 -0
- package/dist/deploy/preflight.js +189 -0
- package/dist/deploy/report.d.ts +26 -0
- package/dist/deploy/report.js +70 -0
- package/dist/deploy/require-planner.d.ts +2 -0
- package/dist/deploy/require-planner.js +60 -0
- package/dist/deploy/require-token.d.ts +15 -0
- package/dist/deploy/require-token.js +78 -0
- package/dist/deploy/secrets-file.d.ts +17 -0
- package/dist/deploy/secrets-file.js +167 -0
- package/dist/deploy/write-env.d.ts +40 -0
- package/dist/deploy/write-env.js +393 -0
- package/dist/docs/index-pages.d.ts +36 -0
- package/dist/docs/index-pages.js +184 -0
- package/dist/doctor.d.ts +62 -0
- package/dist/doctor.js +225 -0
- package/dist/emails/commands.d.ts +15 -0
- package/dist/emails/commands.js +130 -0
- package/dist/emails/email-types.d.ts +13 -0
- package/dist/emails/email-types.js +1 -0
- package/dist/emails/fixtures.d.ts +20 -0
- package/dist/emails/fixtures.js +20 -0
- package/dist/env/audit.d.ts +206 -0
- package/dist/env/audit.js +436 -0
- package/dist/env/cloudflare.d.ts +21 -0
- package/dist/env/cloudflare.js +61 -0
- package/dist/env/commands.d.ts +99 -0
- package/dist/env/commands.js +612 -0
- package/dist/env/discovery.d.ts +15 -0
- package/dist/env/discovery.js +226 -0
- package/dist/env/document.d.ts +119 -0
- package/dist/env/document.js +323 -0
- package/dist/env/example.d.ts +68 -0
- package/dist/env/example.js +603 -0
- package/dist/env/selection.d.ts +105 -0
- package/dist/env/selection.js +226 -0
- package/dist/environment.d.ts +92 -0
- package/dist/environment.js +206 -0
- package/dist/fingerprint.d.ts +10 -0
- package/dist/fingerprint.js +18 -0
- package/dist/gen/campus-map.d.ts +1 -0
- package/dist/gen/campus-map.js +473 -0
- package/dist/gen/commands.d.ts +1 -0
- package/dist/gen/commands.js +25 -0
- package/dist/gen/hypno.d.ts +1 -0
- package/dist/gen/hypno.js +134 -0
- package/dist/gh/client.d.ts +80 -0
- package/dist/gh/client.js +258 -0
- package/dist/gh/environments.d.ts +83 -0
- package/dist/gh/environments.js +178 -0
- package/dist/gh/rulesets/actors.d.ts +14 -0
- package/dist/gh/rulesets/actors.js +68 -0
- package/dist/gh/rulesets/api.d.ts +15 -0
- package/dist/gh/rulesets/api.js +99 -0
- package/dist/gh/rulesets/commands.d.ts +3 -0
- package/dist/gh/rulesets/commands.js +203 -0
- package/dist/gh/rulesets/desired.d.ts +112 -0
- package/dist/gh/rulesets/desired.js +247 -0
- package/dist/gh/rulesets/diff.d.ts +65 -0
- package/dist/gh/rulesets/diff.js +134 -0
- package/dist/gh/rulesets/types.d.ts +76 -0
- package/dist/gh/rulesets/types.js +13 -0
- package/dist/gh/settings/api.d.ts +40 -0
- package/dist/gh/settings/api.js +172 -0
- package/dist/gh/settings/commands.d.ts +1 -0
- package/dist/gh/settings/commands.js +245 -0
- package/dist/gh/settings/desired.d.ts +53 -0
- package/dist/gh/settings/desired.js +39 -0
- package/dist/gh/settings/diff.d.ts +57 -0
- package/dist/gh/settings/diff.js +156 -0
- package/dist/gh/settings/types.d.ts +92 -0
- package/dist/gh/settings/types.js +11 -0
- package/dist/gh/settings/workflows.d.ts +41 -0
- package/dist/gh/settings/workflows.js +124 -0
- package/dist/grantRoot.d.ts +42 -0
- package/dist/grantRoot.js +89 -0
- package/dist/help.d.ts +18 -0
- package/dist/help.js +169 -0
- package/dist/images/commands.d.ts +35 -0
- package/dist/images/commands.js +233 -0
- package/dist/images/events.d.ts +36 -0
- package/dist/images/events.js +152 -0
- package/dist/images/graphics.d.ts +70 -0
- package/dist/images/graphics.js +233 -0
- package/dist/images/open-graph-types.d.ts +75 -0
- package/dist/images/open-graph-types.js +1 -0
- package/dist/images/prompts.d.ts +34 -0
- package/dist/images/prompts.js +100 -0
- package/dist/images/render.d.ts +21 -0
- package/dist/images/render.js +57 -0
- package/dist/images/select.d.ts +64 -0
- package/dist/images/select.js +96 -0
- package/dist/instance.d.ts +67 -0
- package/dist/instance.js +129 -0
- package/dist/invocation.d.ts +49 -0
- package/dist/invocation.js +78 -0
- package/dist/launch-ci.d.ts +1 -0
- package/dist/launch-ci.js +86 -0
- package/dist/launch.d.ts +29 -0
- package/dist/launch.js +371 -0
- package/dist/menu.d.ts +31 -0
- package/dist/menu.js +277 -0
- package/dist/newsletter/commands.d.ts +16 -0
- package/dist/newsletter/commands.js +311 -0
- package/dist/newsletter/imap.d.ts +17 -0
- package/dist/newsletter/imap.js +127 -0
- package/dist/newsletter/loopback.d.ts +24 -0
- package/dist/newsletter/loopback.js +100 -0
- package/dist/newsletter/oauth.d.ts +26 -0
- package/dist/newsletter/oauth.js +130 -0
- package/dist/newsletter/smtp.d.ts +23 -0
- package/dist/newsletter/smtp.js +133 -0
- package/dist/oauth/browser.d.ts +1 -0
- package/dist/oauth/browser.js +13 -0
- package/dist/oauth/config.d.ts +24 -0
- package/dist/oauth/config.js +24 -0
- package/dist/oauth/connect-url.d.ts +17 -0
- package/dist/oauth/connect-url.js +23 -0
- package/dist/oauth/db.d.ts +37 -0
- package/dist/oauth/db.js +108 -0
- package/dist/oauth/device.d.ts +93 -0
- package/dist/oauth/device.js +190 -0
- package/dist/oauth/discovery.d.ts +28 -0
- package/dist/oauth/discovery.js +54 -0
- package/dist/oauth/env-file.d.ts +6 -0
- package/dist/oauth/env-file.js +35 -0
- package/dist/oauth/exchange.d.ts +43 -0
- package/dist/oauth/exchange.js +87 -0
- package/dist/oauth/label.d.ts +1 -0
- package/dist/oauth/label.js +25 -0
- package/dist/oauth/loopback.d.ts +52 -0
- package/dist/oauth/loopback.js +162 -0
- package/dist/oauth/pkce.d.ts +6 -0
- package/dist/oauth/pkce.js +34 -0
- package/dist/oauth/target.d.ts +54 -0
- package/dist/oauth/target.js +102 -0
- package/dist/oauth/transport.d.ts +41 -0
- package/dist/oauth/transport.js +32 -0
- package/dist/oauth/wizard.d.ts +33 -0
- package/dist/oauth/wizard.js +571 -0
- package/dist/pick.d.ts +6 -0
- package/dist/pick.js +64 -0
- package/dist/planner/checks.d.ts +26 -0
- package/dist/planner/checks.js +104 -0
- package/dist/planner/commands.d.ts +10 -0
- package/dist/planner/commands.js +367 -0
- package/dist/planner/db.d.ts +15 -0
- package/dist/planner/db.js +33 -0
- package/dist/planner/role.d.ts +33 -0
- package/dist/planner/role.js +68 -0
- package/dist/repo/peers.d.ts +24 -0
- package/dist/repo/peers.js +114 -0
- package/dist/repo/preflight.d.ts +109 -0
- package/dist/repo/preflight.js +266 -0
- package/dist/repo/resolve.d.ts +41 -0
- package/dist/repo/resolve.js +152 -0
- package/dist/repo/root.d.ts +23 -0
- package/dist/repo/root.js +91 -0
- package/dist/repo/source.d.ts +14 -0
- package/dist/repo/source.js +54 -0
- package/dist/repo/tsx-loader.d.ts +6 -0
- package/dist/repo/tsx-loader.js +41 -0
- package/dist/run/pick.d.ts +60 -0
- package/dist/run/pick.js +559 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +152 -0
- package/dist/stack.d.ts +48 -0
- package/dist/stack.js +290 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +93 -0
- package/dist/tier.d.ts +40 -0
- package/dist/tier.js +93 -0
- package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
- package/dist/ui.d.ts +25 -0
- package/dist/ui.js +51 -0
- package/dist/workers.d.ts +5 -0
- package/dist/workers.js +35 -0
- package/dist/workflows/commands.d.ts +44 -0
- package/dist/workflows/commands.js +668 -0
- package/env.ts +158 -0
- package/package.json +80 -0
package/dist/launch.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pnpm devtools`'s actual entry point, run by the `bin/devtools.mjs`
|
|
3
|
+
* bootstrap BEFORE `cli.ts` — and therefore every command it dispatches — is
|
|
4
|
+
* even imported.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this has to run first
|
|
7
|
+
*
|
|
8
|
+
* A deploy tier used to be resolved deep inside `menu.ts`, after the wizard
|
|
9
|
+
* had already opened, and a typed command's tier lived wherever that command
|
|
10
|
+
* happened to resolve one — `cf preview`, `cron run` and `db --target remote`
|
|
11
|
+
* each asking their own question. That meant `--tier` only worked for the
|
|
12
|
+
* commands that had been taught to parse it, and a contributor who wanted
|
|
13
|
+
* `pnpm devtools db status --target remote --tier staging` piped through a
|
|
14
|
+
* wizard question first anyway.
|
|
15
|
+
*
|
|
16
|
+
* This module settles the tier ONCE, before any command's imports — let alone
|
|
17
|
+
* its code — run, using the shared policy in `@devdogsuga/env/session`. By
|
|
18
|
+
* the time `cli.ts` is imported, `process.env.DEPLOY_ENV` already names the
|
|
19
|
+
* tier and its env files are already loaded, so `cli.ts`, `menu.ts` and every
|
|
20
|
+
* runner underneath them just read `process.env` like any other command would
|
|
21
|
+
* under `with-env` — this module IS the replacement for `with-env`'s job.
|
|
22
|
+
*
|
|
23
|
+
* ## `--tier`, a global flag
|
|
24
|
+
*
|
|
25
|
+
* `--tier <t>` is accepted at ANY position in argv and stripped here before
|
|
26
|
+
* `cli.ts` ever sees the rest, precisely so it cannot collide with a
|
|
27
|
+
* command's own flag of the same name (`db --target remote --tier staging`,
|
|
28
|
+
* `cron run --tier production`) — those still parse their OWN `--tier` out of
|
|
29
|
+
* the argv `cli.ts` receives, but by then the session's tier decision has
|
|
30
|
+
* already been made, and `resolveTier` (see `tier.ts`) falls back to reading
|
|
31
|
+
* it off `process.env.DEPLOY_ENV` rather than asking again.
|
|
32
|
+
*/
|
|
33
|
+
import { confirm, select } from "@clack/prompts";
|
|
34
|
+
import { findCommand } from "./commands.js";
|
|
35
|
+
import { discoverRepoRoot, findRepoRoot, RepoNotFoundError } from "./repo/root.js";
|
|
36
|
+
import { loadEnvLoad, loadEnvSession } from "./repo/peers.js";
|
|
37
|
+
import { isDevMode, ownVersion, runPreflight } from "./repo/preflight.js";
|
|
38
|
+
import { errorMessage, unwrap } from "./ui.js";
|
|
39
|
+
/**
|
|
40
|
+
* Pulls the two preflight-only boolean flags out of `argv`, wherever they
|
|
41
|
+
* sit, the same way `stripTierFlag` does for `--tier` below. Neither takes
|
|
42
|
+
* a value. `DEVTOOLS_SKIP_PREFLIGHT=1` is the env-var spelling of
|
|
43
|
+
* `--skip-preflight`, for shells/CI wrappers that would rather set an env
|
|
44
|
+
* var once than remember a flag on every invocation.
|
|
45
|
+
*/
|
|
46
|
+
export function stripPreflightFlags(argv) {
|
|
47
|
+
const rest = [];
|
|
48
|
+
let skipPreflight = process.env.DEVTOOLS_SKIP_PREFLIGHT === "1";
|
|
49
|
+
let refresh = false;
|
|
50
|
+
for (const arg of argv) {
|
|
51
|
+
if (arg === "--skip-preflight") {
|
|
52
|
+
skipPreflight = true;
|
|
53
|
+
}
|
|
54
|
+
else if (arg === "--refresh") {
|
|
55
|
+
refresh = true;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
rest.push(arg);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { skipPreflight, refresh, rest };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Pulls a global `--tier <t>` out of `argv`, wherever it sits, leaving every
|
|
65
|
+
* other argument untouched and in its original order. Exported for its own
|
|
66
|
+
* unit tests; `launch()` below is the only real caller.
|
|
67
|
+
*/
|
|
68
|
+
export function stripTierFlag(argv) {
|
|
69
|
+
const rest = [...argv];
|
|
70
|
+
const index = rest.indexOf("--tier");
|
|
71
|
+
if (index === -1)
|
|
72
|
+
return { explicit: undefined, rest };
|
|
73
|
+
const value = rest[index + 1];
|
|
74
|
+
// A trailing `--tier` with nothing after it removes just the flag; the
|
|
75
|
+
// missing value then reaches `resolveSessionTier` as `explicit: undefined`,
|
|
76
|
+
// which falls through to `DEPLOY_ENV`/the sole tier/the prompt exactly as
|
|
77
|
+
// if `--tier` had never been typed, rather than this function guessing.
|
|
78
|
+
//
|
|
79
|
+
// A following token that is itself a flag is treated the same way, NOT
|
|
80
|
+
// consumed as the value — the guard every other flag-value reader in this
|
|
81
|
+
// CLI keeps (`cli.ts`'s `flagValue`). Without it,
|
|
82
|
+
// `--tier --help` or `--tier -h` would swallow the flag as a bogus tier and
|
|
83
|
+
// refuse with "unknown tier" instead of reaching the help bypass below.
|
|
84
|
+
const missing = value === undefined || value.startsWith("-");
|
|
85
|
+
rest.splice(index, missing ? 1 : 2);
|
|
86
|
+
return { explicit: missing ? undefined : value, rest };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether the command `rest` dispatches to is declared `envFree` in
|
|
90
|
+
* `commands.ts`'s catalog — the leading run of non-flag tokens is the
|
|
91
|
+
* command path (`["github", "rulesets"]` out of `["github", "rulesets",
|
|
92
|
+
* "--apply"]`), the same convention `stripTierFlag`/`stripPreflightFlags`
|
|
93
|
+
* already use for pulling a flag out of argv wherever it sits.
|
|
94
|
+
*
|
|
95
|
+
* Catalog-driven rather than a second hardcoded name list: `setup` and
|
|
96
|
+
* `completions` below stay their own explicit branch (each skips tier
|
|
97
|
+
* resolution for a DIFFERENT reason worth spelling out at the call site —
|
|
98
|
+
* see the comment above), but a plain "this command touches no env at all"
|
|
99
|
+
* exemption reads once, from the same tree `--help` and the wizard already
|
|
100
|
+
* render, rather than as a name a future GitHub-only command has to
|
|
101
|
+
* remember to add here too.
|
|
102
|
+
*/
|
|
103
|
+
function isEnvFreeCommand(rest) {
|
|
104
|
+
const path = [];
|
|
105
|
+
for (const arg of rest) {
|
|
106
|
+
if (arg.startsWith("-"))
|
|
107
|
+
break;
|
|
108
|
+
path.push(arg);
|
|
109
|
+
}
|
|
110
|
+
return findCommand(path)?.envFree === true;
|
|
111
|
+
}
|
|
112
|
+
/** The real interactive picker: a clack `select`, unwrapped so Ctrl-C exits
|
|
113
|
+
* cleanly instead of leaking a cancel symbol into `resolveSessionTier`.
|
|
114
|
+
* Values are session selector words (`"development:local"`, `"staging"`…). */
|
|
115
|
+
async function promptTier(message, choices) {
|
|
116
|
+
return unwrap(await select({ message, options: choices }));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Imports `cli.ts` and runs it, reporting a thrown rejection the same way
|
|
120
|
+
* `cli.ts`'s own top level used to before `main` became an export instead of
|
|
121
|
+
* an auto-running IIFE (see `main`'s own header). Shared by both places
|
|
122
|
+
* `launch()` hands off to it: the `--help`/`-h` bypass below, and the normal
|
|
123
|
+
* post-tier-resolution path.
|
|
124
|
+
*/
|
|
125
|
+
async function dispatch(argv) {
|
|
126
|
+
const { main } = await import("./cli.js");
|
|
127
|
+
try {
|
|
128
|
+
await main(argv);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
process.stderr.write(`devtools: ${errorMessage(err)}\n`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolves the session's deploy tier, enters it, and hands off to `cli.ts`.
|
|
137
|
+
*
|
|
138
|
+
* Exits the process directly on a refusal from `resolveSessionTier` — there
|
|
139
|
+
* is no command dispatched yet for a caller to fall back to, so there is
|
|
140
|
+
* nothing this function could return that would mean anything.
|
|
141
|
+
*/
|
|
142
|
+
export async function launch(argv) {
|
|
143
|
+
// Preflight runs on EVERY command of this bin, including `--help` — see
|
|
144
|
+
// `repo/preflight.ts`'s header. It never requires a repo (the manifest
|
|
145
|
+
// fetch/cache/self-refresh logic is entirely repo-independent), so it
|
|
146
|
+
// runs before `findRepoRoot()` is anywhere near the picture, unlike tier
|
|
147
|
+
// resolution below. `devtools-ci` (`launch-ci.ts`) deliberately does not
|
|
148
|
+
// call this at all — CI pins an exact version on purpose.
|
|
149
|
+
const { skipPreflight, refresh, rest: afterPreflight } = stripPreflightFlags(argv);
|
|
150
|
+
await runPreflight({
|
|
151
|
+
currentVersion: ownVersion(),
|
|
152
|
+
argv: afterPreflight,
|
|
153
|
+
skipPreflight,
|
|
154
|
+
refresh,
|
|
155
|
+
devMode: isDevMode(),
|
|
156
|
+
// Set only for testing against a local Verdaccio (see the end-to-end
|
|
157
|
+
// self-refresh validation in CUTOVER.md); unset in production, where a
|
|
158
|
+
// bare `pnpm dlx @devdogsuga/devtools@<version>` resolves against the
|
|
159
|
+
// default npm registry with no flag needed at all.
|
|
160
|
+
registry: process.env.DEVTOOLS_REGISTRY,
|
|
161
|
+
});
|
|
162
|
+
const { explicit, rest } = stripTierFlag(afterPreflight);
|
|
163
|
+
// `--help`/`-h` bypasses tier resolution entirely, BEFORE it can refuse.
|
|
164
|
+
// `cli.ts`'s own `main()` already answers these with no env in play (see
|
|
165
|
+
// its header comment); resolving a tier first anyway meant `pnpm devtools
|
|
166
|
+
// --help` refused outright on a machine with two-or-more tier files
|
|
167
|
+
// present — the ordinary state for anyone who has ever run `env pull` for
|
|
168
|
+
// staging or production — even though the command that would have run
|
|
169
|
+
// needs no database, credential, or `DEPLOY_ENV` at all. `bw --help` is
|
|
170
|
+
// NOT special-cased here the way `cli.ts` special-cases it ahead of its own
|
|
171
|
+
// `--help` check: `main()` still sees `bw` first and hands off to Bitwarden
|
|
172
|
+
// before ever reaching its `--help` branch, so routing straight to `main()`
|
|
173
|
+
// below reproduces that passthrough correctly either way.
|
|
174
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
175
|
+
await dispatch(rest);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const envLoad = await loadEnvLoad();
|
|
179
|
+
const envSession = await loadEnvSession();
|
|
180
|
+
let tier;
|
|
181
|
+
let devDatabase;
|
|
182
|
+
if (rest[0] === "setup" || rest[0] === "completions" || isEnvFreeCommand(rest)) {
|
|
183
|
+
// Two commands run BEFORE there is a tier to resolve, and forcing the
|
|
184
|
+
// mandate on them breaks each in its own way:
|
|
185
|
+
//
|
|
186
|
+
// * `setup` exists to CREATE a missing env file. A stale
|
|
187
|
+
// `DEPLOY_ENV=staging` in the shell with no `.env.staging` on disk
|
|
188
|
+
// would resolve that tier, fail `enterEnvironment` fatally below, and
|
|
189
|
+
// lock a new contributor out of the one command that fixes their
|
|
190
|
+
// state — a bootstrap deadlock. It is development-only by nature
|
|
191
|
+
// (it writes `.env`), so the session tier question has one answer.
|
|
192
|
+
// * `completions` runs from shell rc files (`eval "$(pnpm devtools
|
|
193
|
+
// completions bash)"`), always non-TTY, and reads no env at all; the
|
|
194
|
+
// multi-tier refusal would exit 1 in every new shell on exactly the
|
|
195
|
+
// machines of the people working on the deploy workflow.
|
|
196
|
+
//
|
|
197
|
+
// A third, open-ended case joins them here via `isEnvFreeCommand`:
|
|
198
|
+
// `github rulesets` and `github settings` (TASK-322, TASK-342) touch no
|
|
199
|
+
// DevDogsUGA env file or database at all — every write either one makes
|
|
200
|
+
// is a `gh api` call resolved from its own `--org`/`--repo` flags — so
|
|
201
|
+
// demanding a `--tier` before either could run was never a real
|
|
202
|
+
// requirement, only every command sharing one dispatch gate. See
|
|
203
|
+
// `isEnvFreeCommand`'s own doc for why this is catalog-driven rather
|
|
204
|
+
// than a third name joining the `rest[0] ===` checks above.
|
|
205
|
+
//
|
|
206
|
+
// Development is still ENTERED below (missing-file tolerated), not
|
|
207
|
+
// skipped: `setup` under the old `with-env` wrapper saw whatever `.env`
|
|
208
|
+
// already existed, and keeping that means it can read current values
|
|
209
|
+
// when offering to rewrite them.
|
|
210
|
+
tier = "development";
|
|
211
|
+
}
|
|
212
|
+
else if (discoverRepoRoot() === null) {
|
|
213
|
+
// Every command past this point needs a real repo to resolve a tier
|
|
214
|
+
// against (`findRepoRoot()` below would throw `RepoNotFoundError`
|
|
215
|
+
// anyway) — surfaced here as the same clean, expected refusal every
|
|
216
|
+
// other repo-dependent command gives, rather than an uncaught throw
|
|
217
|
+
// with a stack trace pointing at internal module paths.
|
|
218
|
+
process.stderr.write(`devtools: ${new RepoNotFoundError().message}\n`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// The `.env`-names-a-remote-database lookup (and the port probe that
|
|
223
|
+
// phrases the local hint) are only paid when the resolution could
|
|
224
|
+
// actually land on BARE development — an explicit staging/production or
|
|
225
|
+
// qualified selector, an already-deployed DEPLOY_ENV, or a DEV_DB answer
|
|
226
|
+
// all settle the question without them, and the lookup imports dotenvx.
|
|
227
|
+
const deployEnv = process.env.DEPLOY_ENV ?? "";
|
|
228
|
+
const couldBeBareDevelopment = (process.env.DEV_DB ?? "") === "" &&
|
|
229
|
+
(explicit === "development" ||
|
|
230
|
+
(explicit === undefined &&
|
|
231
|
+
(deployEnv === "" || deployEnv === "development")));
|
|
232
|
+
const remoteCandidate = couldBeBareDevelopment
|
|
233
|
+
? await envSession.developmentRemoteCandidate(findRepoRoot())
|
|
234
|
+
: undefined;
|
|
235
|
+
const localStackOnline = typeof remoteCandidate === "string" ? await envLoad.probeLocalStack() : undefined;
|
|
236
|
+
const resolution = await envSession.resolveSessionTier({
|
|
237
|
+
explicit,
|
|
238
|
+
deployEnv: process.env.DEPLOY_ENV,
|
|
239
|
+
devDb: process.env.DEV_DB,
|
|
240
|
+
available: await envSession.availableTiers(findRepoRoot()),
|
|
241
|
+
isTTY: process.stdin.isTTY === true,
|
|
242
|
+
prompt: promptTier,
|
|
243
|
+
promptMessage: "Which environment should this session use?",
|
|
244
|
+
remoteCandidate,
|
|
245
|
+
localStackOnline,
|
|
246
|
+
});
|
|
247
|
+
if (!resolution.ok) {
|
|
248
|
+
process.stderr.write(`devtools: ${resolution.reason}\n`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
tier = resolution.tier;
|
|
252
|
+
devDatabase = resolution.devDatabase;
|
|
253
|
+
}
|
|
254
|
+
// Mandatory, not chattiness: which database a command is about to touch
|
|
255
|
+
// must never be a guess. Names the QUALIFIED session (`development:local`)
|
|
256
|
+
// when one was resolved. See `session.ts`'s and `load.ts`'s own headers.
|
|
257
|
+
const reportEntered = (files, label) => {
|
|
258
|
+
process.stderr.write(`devtools: loaded ${files.length > 0 ? files.join(", ") : "no env files"} (${label})\n`);
|
|
259
|
+
};
|
|
260
|
+
const sessionLabel = devDatabase === undefined ? tier : `${tier}:${devDatabase}`;
|
|
261
|
+
try {
|
|
262
|
+
// `override: false` — a fresh process, so an already-exported shell
|
|
263
|
+
// variable beats the file the same way `with-env` always let it, rather
|
|
264
|
+
// than a stale `.env.<tier>` value silently winning over what the caller
|
|
265
|
+
// just set for this one invocation.
|
|
266
|
+
const entered = await envSession.enterEnvironment(tier, {
|
|
267
|
+
override: false,
|
|
268
|
+
devDatabase,
|
|
269
|
+
});
|
|
270
|
+
for (const warning of entered.warnings) {
|
|
271
|
+
process.stderr.write(`devtools: ${warning}\n`);
|
|
272
|
+
}
|
|
273
|
+
reportEntered(entered.files, sessionLabel);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
if (err instanceof envLoad.LocalStackOfflineError) {
|
|
277
|
+
// The session explicitly means the local database and the stack is not
|
|
278
|
+
// reachable.
|
|
279
|
+
process.stderr.write(`devtools: ${err.message}\n`);
|
|
280
|
+
// Rather than making the contributor stop, run `db start`, and re-run
|
|
281
|
+
// whatever they meant, offer to bring the stack up right here and then
|
|
282
|
+
// carry on with their original command. Only on a TTY (there is no one
|
|
283
|
+
// to answer otherwise), and not when the command is ITSELF a stack
|
|
284
|
+
// lifecycle command — `db start`/`stop`/`restart` do this on their own,
|
|
285
|
+
// and `db stop` against an already-down stack must not be interrupted by
|
|
286
|
+
// an offer to start it. The bare top menu is likewise left to its own
|
|
287
|
+
// Database → start entry.
|
|
288
|
+
const lifecycle = rest[0] === "db" &&
|
|
289
|
+
(rest[1] === "start" || rest[1] === "stop" || rest[1] === "restart");
|
|
290
|
+
if (process.stdin.isTTY === true && rest.length !== 0 && !lifecycle) {
|
|
291
|
+
const start = unwrap(await confirm({
|
|
292
|
+
message: "Start the local Supabase stack now?",
|
|
293
|
+
}));
|
|
294
|
+
if (start) {
|
|
295
|
+
// Keep the session's local qualifier set so the env refresh that
|
|
296
|
+
// `runStackCommand("start")` performs on success re-applies the
|
|
297
|
+
// overlay under it, and the command we dispatch next resolves the
|
|
298
|
+
// freshly-started local database.
|
|
299
|
+
if (devDatabase !== undefined)
|
|
300
|
+
process.env.DEV_DB = devDatabase;
|
|
301
|
+
const { runStackCommand } = await import("./stack.js");
|
|
302
|
+
const { code, lines } = await runStackCommand("start", null);
|
|
303
|
+
for (const line of lines) {
|
|
304
|
+
process.stderr.write(`devtools: ${line}\n`);
|
|
305
|
+
}
|
|
306
|
+
if (code === 0) {
|
|
307
|
+
await dispatch(rest);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
process.stderr.write("devtools: the stack did not start — see the Supabase CLI output " +
|
|
311
|
+
"above.\n");
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Declined, or nobody to ask. `db` (whose `start` is the fix, and whose
|
|
316
|
+
// data commands re-check the connection themselves) and the bare menu
|
|
317
|
+
// (the road to `db start`) may continue in a degraded, unqualified
|
|
318
|
+
// entry; anything else stops here, with the error's own troubleshooting,
|
|
319
|
+
// instead of failing later against whatever `.env` happens to name.
|
|
320
|
+
if (rest.length !== 0 && rest[0] !== "db")
|
|
321
|
+
process.exit(1);
|
|
322
|
+
process.stderr.write("devtools: continuing without the overlay so `db start` can fix this.\n");
|
|
323
|
+
const inherited = process.env.DEV_DB;
|
|
324
|
+
delete process.env.DEV_DB;
|
|
325
|
+
const entered = await envSession.enterEnvironment(tier, { override: false });
|
|
326
|
+
if (devDatabase !== undefined) {
|
|
327
|
+
// Children spawned AFTER `db start` succeeds should still inherit
|
|
328
|
+
// the session's answer; the refresh that follows a successful start
|
|
329
|
+
// re-applies the overlay under this same variable.
|
|
330
|
+
process.env.DEV_DB = devDatabase;
|
|
331
|
+
}
|
|
332
|
+
else if (inherited !== undefined) {
|
|
333
|
+
process.env.DEV_DB = inherited;
|
|
334
|
+
}
|
|
335
|
+
for (const warning of entered.warnings) {
|
|
336
|
+
process.stderr.write(`devtools: ${warning}\n`);
|
|
337
|
+
}
|
|
338
|
+
reportEntered(entered.files, tier);
|
|
339
|
+
await dispatch(rest);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (!(err instanceof envLoad.MissingEnvFileError))
|
|
343
|
+
throw err;
|
|
344
|
+
if (tier === "development") {
|
|
345
|
+
// The clean-clone path: a fresh checkout has no `.env` at all, and
|
|
346
|
+
// `pnpm devtools setup` is how one gets created — `MissingEnvFileError`
|
|
347
|
+
// names it. Reporting and continuing here, rather than refusing, is
|
|
348
|
+
// what lets `setup` itself run through this same entry point. See
|
|
349
|
+
// `load.ts`'s `MissingEnvFileError` for the message this prints.
|
|
350
|
+
process.stderr.write(`devtools: ${err.message}\n`);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// An EXPLICITLY selected staging/production tier with no env file is
|
|
354
|
+
// not a first run — it is a real problem, and `env pull` is the fix
|
|
355
|
+
// `MissingEnvFileError`'s own message already names. Fatal.
|
|
356
|
+
process.stderr.write(`devtools: ${err.message}\n`);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Caught here rather than left to `bin/devtools.mjs`'s `child.on("error",
|
|
361
|
+
// …)`, which only ever sees a spawn failure, never a rejection thrown
|
|
362
|
+
// inside this process.
|
|
363
|
+
await dispatch(rest);
|
|
364
|
+
}
|
|
365
|
+
// `bin/devtools.mjs` runs this file directly through tsx — `tsx
|
|
366
|
+
// --conditions=devdogs-source src/launch.ts <argv…>` — so `process.argv`
|
|
367
|
+
// carries this file's own path where a bare `node` invocation would, and the
|
|
368
|
+
// real argv starts one slot later.
|
|
369
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
370
|
+
await launch(process.argv.slice(2));
|
|
371
|
+
}
|
package/dist/menu.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Environment } from "./environment.js";
|
|
2
|
+
/**
|
|
3
|
+
* The command path when argv names a group with subcommands but no subcommand
|
|
4
|
+
* token — what `devtools db` and `devtools db seed` are. Returns null for a
|
|
5
|
+
* leaf command, an unknown token, or a bare invocation (which the no-argument
|
|
6
|
+
* wizard already covers). The caller resumes the wizard at this node when
|
|
7
|
+
* stdin is a TTY; a non-TTY caller keeps the dispatcher's "which of …?" exit.
|
|
8
|
+
*/
|
|
9
|
+
export declare function bareGroupStartPath(argv: readonly string[]): string[] | null;
|
|
10
|
+
/**
|
|
11
|
+
* Runs the wizard, returning the `outro()` line its command earned.
|
|
12
|
+
*
|
|
13
|
+
* `dispatch` is the CLI's own argv handler, injected rather than imported so
|
|
14
|
+
* that `cli.ts` keeps a single definition of what each command does and the
|
|
15
|
+
* tests can watch what a walk produces without running it. Its return value
|
|
16
|
+
* passes straight through, the closing line or `null` for a failure already
|
|
17
|
+
* explained, so a command reached from the menu signs off exactly as it does
|
|
18
|
+
* from the command line.
|
|
19
|
+
*
|
|
20
|
+
* `options.startPath`, from `bareGroupStartPath`, skips straight to that
|
|
21
|
+
* node's subcommand screen — see `walk`'s `startPath` branch.
|
|
22
|
+
*
|
|
23
|
+
* The deploy tier is NOT asked here any more. `src/launch.ts` resolves and
|
|
24
|
+
* enters it before `cli.ts` — and therefore this module — is even imported,
|
|
25
|
+
* so by the time the wizard opens the whole session is already running under
|
|
26
|
+
* it, exactly as if `--tier` had been typed. Recording it for the "run it
|
|
27
|
+
* directly next time" line only needs to read `process.env.DEPLOY_ENV` back.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runMenu(dispatch: (argv: string[]) => Promise<string | null>, env?: Environment, options?: {
|
|
30
|
+
startPath?: string[];
|
|
31
|
+
}): Promise<string | null>;
|
package/dist/menu.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wizard: `pnpm devtools` with no arguments.
|
|
3
|
+
*
|
|
4
|
+
* ## The one property worth protecting
|
|
5
|
+
*
|
|
6
|
+
* **It builds an argv and hands it to the CLI's own dispatcher.** It does not
|
|
7
|
+
* call command functions directly. That is what makes "the menu covers every
|
|
8
|
+
* interactive command" structural instead of aspirational. The menu it replaced held a
|
|
9
|
+
* hand-written list of ten entries beside a CLI that had grown to sixteen
|
|
10
|
+
* top-level commands and thirty-one subcommands, so `env`, `planner` and
|
|
11
|
+
* `docs index` were reachable only by someone who already knew their names.
|
|
12
|
+
* A contributor who does not know a command name is the entire audience for
|
|
13
|
+
* this file.
|
|
14
|
+
*
|
|
15
|
+
* Walking `commands.ts` means an interactive command added there is in the
|
|
16
|
+
* menu the same day, with its options. Commands marked `cli-only` and deploy
|
|
17
|
+
* commands in the separate `devtools-ci` bin do not appear here.
|
|
18
|
+
*/
|
|
19
|
+
import { confirm, note, select, text } from "@clack/prompts";
|
|
20
|
+
import { positionals } from "./args.js";
|
|
21
|
+
import { findCommand, GROUPS, SCOPES, } from "./commands.js";
|
|
22
|
+
import { blockedBecause, describeEnvironment, isOffered, probeEnvironment, } from "./environment.js";
|
|
23
|
+
import { beginInvocation, recordEnteredTier } from "./invocation.js";
|
|
24
|
+
import { unwrap } from "./ui.js";
|
|
25
|
+
/** Chosen when a submenu should return to the screen above it. */
|
|
26
|
+
const BACK = Symbol("back");
|
|
27
|
+
const BACK_OPTION = { value: BACK, label: "← Back" };
|
|
28
|
+
// ── What this machine is offered ─────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* The commands at one level that are worth showing right now.
|
|
31
|
+
*
|
|
32
|
+
* `when` is the only thing that removes an entry, and it removes very few;
|
|
33
|
+
* see `isOffered`. Everything else stays on screen and explains itself
|
|
34
|
+
* through `hintFor` below, because the menu's job is to help someone who does
|
|
35
|
+
* not know a command's name, and it cannot do that for a command it declined
|
|
36
|
+
* to draw.
|
|
37
|
+
*/
|
|
38
|
+
function offered(nodes, env) {
|
|
39
|
+
return nodes.filter((node) => node.surface !== "cli-only" && isOffered(node, env));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The line beside a name, with whatever is standing in this command's way.
|
|
43
|
+
*
|
|
44
|
+
* The reader learns that `roundtrip` needs a stack that is not up *before*
|
|
45
|
+
* choosing it, instead of after a spinner and a connection error. The command
|
|
46
|
+
* stays selectable: the check is a probe, the probe can be wrong, and the
|
|
47
|
+
* command's own failure message is the authority on whether it can run.
|
|
48
|
+
*/
|
|
49
|
+
function hintFor(node, env) {
|
|
50
|
+
const base = node.hint ?? node.summary;
|
|
51
|
+
const blocked = blockedBecause(node, env);
|
|
52
|
+
const said = blocked ? `${base} — ${blocked}` : base;
|
|
53
|
+
// The scope leads, because in the one group that has scopes it is the thing
|
|
54
|
+
// the reader is actually choosing between: `restart` and `reset` sit four
|
|
55
|
+
// lines apart and act on different layers.
|
|
56
|
+
return node.scope ? `${SCOPES[node.scope].menu} · ${said}` : said;
|
|
57
|
+
}
|
|
58
|
+
// ── Screens ──────────────────────────────────────────────────────────────────
|
|
59
|
+
async function pickGroup(env) {
|
|
60
|
+
// A group whose every command is hidden has nothing behind its door, so the
|
|
61
|
+
// door is not drawn. No group in the tree can empty out today; this is here
|
|
62
|
+
// so that a later `when` cannot leave a dead entry on the first screen.
|
|
63
|
+
const groups = GROUPS.filter((group) => offered(group.commands, env).length > 0);
|
|
64
|
+
const choice = unwrap(await select({
|
|
65
|
+
message: "What would you like to do?",
|
|
66
|
+
options: [
|
|
67
|
+
...groups.map((group) => ({
|
|
68
|
+
value: group,
|
|
69
|
+
label: group.title,
|
|
70
|
+
// The group's own commands, so the first screen says what is behind
|
|
71
|
+
// each door rather than making the reader open every one to find out.
|
|
72
|
+
hint: offered(group.commands, env)
|
|
73
|
+
.map((command) => command.name)
|
|
74
|
+
.join(", "),
|
|
75
|
+
})),
|
|
76
|
+
{ value: null, label: "Quit" },
|
|
77
|
+
],
|
|
78
|
+
}));
|
|
79
|
+
return choice;
|
|
80
|
+
}
|
|
81
|
+
async function pickCommand(group, env) {
|
|
82
|
+
const commands = offered(group.commands, env);
|
|
83
|
+
// A group with one command has nothing to choose; asking would be a screen
|
|
84
|
+
// whose only real option is the one already implied by the group's title.
|
|
85
|
+
if (commands.length === 1)
|
|
86
|
+
return commands[0];
|
|
87
|
+
return unwrap(await select({
|
|
88
|
+
message: `${group.title}:`,
|
|
89
|
+
options: [
|
|
90
|
+
...commands.map((command) => ({
|
|
91
|
+
value: command,
|
|
92
|
+
label: command.name,
|
|
93
|
+
hint: hintFor(command, env),
|
|
94
|
+
})),
|
|
95
|
+
BACK_OPTION,
|
|
96
|
+
],
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
async function pickSubcommand(node, env) {
|
|
100
|
+
return unwrap(await select({
|
|
101
|
+
message: `${node.name}:`,
|
|
102
|
+
options: [
|
|
103
|
+
...offered(node.subcommands ?? [], env).map((child) => ({
|
|
104
|
+
value: child,
|
|
105
|
+
label: child.name,
|
|
106
|
+
hint: hintFor(child, env),
|
|
107
|
+
})),
|
|
108
|
+
BACK_OPTION,
|
|
109
|
+
],
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
// ── Options ──────────────────────────────────────────────────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* Asks for one option, returning the argv fragment it contributes.
|
|
115
|
+
*
|
|
116
|
+
* An empty array is a real answer, not a failure: a declined confirm adds no
|
|
117
|
+
* flag, and a blank optional text means "let the command decide", which is
|
|
118
|
+
* how `--db-url` falls back to `.env.production` and `--base-url` falls
|
|
119
|
+
* through to the OAuth wizard's own prompt.
|
|
120
|
+
*/
|
|
121
|
+
async function askOption(option) {
|
|
122
|
+
const prompt = option.prompt;
|
|
123
|
+
if (!prompt)
|
|
124
|
+
return [];
|
|
125
|
+
if (prompt.kind === "confirm") {
|
|
126
|
+
// Yes adds the flag, always. `commands.ts` phrases every message so that
|
|
127
|
+
// this needs no per-option inversion.
|
|
128
|
+
const yes = unwrap(await confirm({ message: prompt.message, initialValue: prompt.initial }));
|
|
129
|
+
return yes ? [option.flag] : [];
|
|
130
|
+
}
|
|
131
|
+
if (prompt.kind === "text") {
|
|
132
|
+
const answer = unwrap(await text({
|
|
133
|
+
message: prompt.message,
|
|
134
|
+
placeholder: prompt.placeholder,
|
|
135
|
+
defaultValue: "",
|
|
136
|
+
})).trim();
|
|
137
|
+
return answer ? [option.flag, answer] : [];
|
|
138
|
+
}
|
|
139
|
+
const choice = unwrap(await select({
|
|
140
|
+
message: prompt.message,
|
|
141
|
+
options: prompt.choices.map((c) => ({
|
|
142
|
+
value: c.value,
|
|
143
|
+
label: c.label ?? c.value,
|
|
144
|
+
hint: c.hint,
|
|
145
|
+
})),
|
|
146
|
+
}));
|
|
147
|
+
return [option.flag, choice];
|
|
148
|
+
}
|
|
149
|
+
async function askOptions(node) {
|
|
150
|
+
const argv = [];
|
|
151
|
+
for (const option of node.options ?? []) {
|
|
152
|
+
argv.push(...(await askOption(option)));
|
|
153
|
+
}
|
|
154
|
+
return argv;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Descends from `start` through its subcommand screens to a leaf, then asks
|
|
158
|
+
* that leaf's options.
|
|
159
|
+
*
|
|
160
|
+
* Shared by both entries into a walk: the top-of-tree loop below, which calls
|
|
161
|
+
* this once a group and its first command are chosen, and a resumed walk
|
|
162
|
+
* (`walk`'s `startPath` branch), which calls it directly on a node the caller
|
|
163
|
+
* already picked by argv. `while` rather than a single step because the tree
|
|
164
|
+
* is two deep today and this does not care. The condition counts the OFFERED
|
|
165
|
+
* children rather than all of them, so a node whose every subcommand is
|
|
166
|
+
* hidden is treated as the leaf it has become instead of opening a screen
|
|
167
|
+
* holding only "Back".
|
|
168
|
+
*/
|
|
169
|
+
async function descendFrom(start, pathNames, env) {
|
|
170
|
+
let node = start;
|
|
171
|
+
const path = [...pathNames];
|
|
172
|
+
while (offered(node.subcommands ?? [], env).length > 0) {
|
|
173
|
+
const child = await pickSubcommand(node, env);
|
|
174
|
+
if (child === BACK)
|
|
175
|
+
return BACK;
|
|
176
|
+
node = child;
|
|
177
|
+
path.push(node.name);
|
|
178
|
+
}
|
|
179
|
+
return { node, argv: [...path, ...(await askOptions(node))] };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Walks the tree to a leaf and its options, either from the top or resumed at
|
|
183
|
+
* a known group.
|
|
184
|
+
*
|
|
185
|
+
* `startPath`, when given, is a path to a group node from `bareGroupStartPath`
|
|
186
|
+
* — `["db"]` for `devtools db` — and lands here instead of at `pickGroup`.
|
|
187
|
+
* There is no group screen above a resumed node (the caller already named it
|
|
188
|
+
* by typing `db`), so BACK on its first subcommand screen has nowhere to
|
|
189
|
+
* return to but out, unlike BACK from the top of the tree, which returns to
|
|
190
|
+
* `pickGroup`.
|
|
191
|
+
*/
|
|
192
|
+
async function walk(env, startPath) {
|
|
193
|
+
if (startPath) {
|
|
194
|
+
const start = findCommand(startPath);
|
|
195
|
+
if (!start)
|
|
196
|
+
return null; // defensive: the caller guarantees a group node
|
|
197
|
+
const chosen = await descendFrom(start, [...startPath], env);
|
|
198
|
+
return chosen === BACK ? null : chosen;
|
|
199
|
+
}
|
|
200
|
+
for (;;) {
|
|
201
|
+
const group = await pickGroup(env);
|
|
202
|
+
if (!group)
|
|
203
|
+
return null;
|
|
204
|
+
const first = await pickCommand(group, env);
|
|
205
|
+
if (first === BACK)
|
|
206
|
+
continue;
|
|
207
|
+
const chosen = await descendFrom(first, [first.name], env);
|
|
208
|
+
if (chosen === BACK)
|
|
209
|
+
continue;
|
|
210
|
+
return chosen;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ── Resuming at a node ───────────────────────────────────────────────────────
|
|
214
|
+
/**
|
|
215
|
+
* The command path when argv names a group with subcommands but no subcommand
|
|
216
|
+
* token — what `devtools db` and `devtools db seed` are. Returns null for a
|
|
217
|
+
* leaf command, an unknown token, or a bare invocation (which the no-argument
|
|
218
|
+
* wizard already covers). The caller resumes the wizard at this node when
|
|
219
|
+
* stdin is a TTY; a non-TTY caller keeps the dispatcher's "which of …?" exit.
|
|
220
|
+
*/
|
|
221
|
+
export function bareGroupStartPath(argv) {
|
|
222
|
+
const path = positionals(argv);
|
|
223
|
+
if (path.length === 0)
|
|
224
|
+
return null;
|
|
225
|
+
const node = findCommand(path);
|
|
226
|
+
if (!node || (node.subcommands ?? []).length === 0)
|
|
227
|
+
return null;
|
|
228
|
+
return path;
|
|
229
|
+
}
|
|
230
|
+
// ── Entry ────────────────────────────────────────────────────────────────────
|
|
231
|
+
/**
|
|
232
|
+
* Runs the wizard, returning the `outro()` line its command earned.
|
|
233
|
+
*
|
|
234
|
+
* `dispatch` is the CLI's own argv handler, injected rather than imported so
|
|
235
|
+
* that `cli.ts` keeps a single definition of what each command does and the
|
|
236
|
+
* tests can watch what a walk produces without running it. Its return value
|
|
237
|
+
* passes straight through, the closing line or `null` for a failure already
|
|
238
|
+
* explained, so a command reached from the menu signs off exactly as it does
|
|
239
|
+
* from the command line.
|
|
240
|
+
*
|
|
241
|
+
* `options.startPath`, from `bareGroupStartPath`, skips straight to that
|
|
242
|
+
* node's subcommand screen — see `walk`'s `startPath` branch.
|
|
243
|
+
*
|
|
244
|
+
* The deploy tier is NOT asked here any more. `src/launch.ts` resolves and
|
|
245
|
+
* enters it before `cli.ts` — and therefore this module — is even imported,
|
|
246
|
+
* so by the time the wizard opens the whole session is already running under
|
|
247
|
+
* it, exactly as if `--tier` had been typed. Recording it for the "run it
|
|
248
|
+
* directly next time" line only needs to read `process.env.DEPLOY_ENV` back.
|
|
249
|
+
*/
|
|
250
|
+
export async function runMenu(dispatch, env = probeEnvironment(), options = {}) {
|
|
251
|
+
// Before the first question, not after a failure. Three lines saying what
|
|
252
|
+
// this machine currently is explain why the database commands below are
|
|
253
|
+
// flagged, and cost one glance on a healthy machine. Injected rather than
|
|
254
|
+
// probed inside `walk` so the tests can drive a machine they describe
|
|
255
|
+
// instead of the one they happen to run on.
|
|
256
|
+
note(describeEnvironment(env), "This machine");
|
|
257
|
+
const chosen = await walk(env, options.startPath);
|
|
258
|
+
// Quitting is not a failure, but it has nothing to announce either.
|
|
259
|
+
if (!chosen)
|
|
260
|
+
return null;
|
|
261
|
+
// Every step here was a prompt, so the built argv is the reproducible
|
|
262
|
+
// command — a runner that prompts further (a bare `workflows run`) appends
|
|
263
|
+
// the rest through `recordResolved`, and the entered tier rides along as the
|
|
264
|
+
// `--tier` flag `recordEnteredTier` adds.
|
|
265
|
+
beginInvocation(chosen.argv, true);
|
|
266
|
+
// A development session that answered the "which development database?"
|
|
267
|
+
// question records the QUALIFIED selector, so the "run it directly next
|
|
268
|
+
// time" line reproduces the whole session — a bare `development` would
|
|
269
|
+
// re-ask (or refuse, non-interactively) on a machine whose `.env` names a
|
|
270
|
+
// remote DB_URL.
|
|
271
|
+
const enteredTier = process.env.DEPLOY_ENV ?? "development";
|
|
272
|
+
const devDb = process.env.DEV_DB;
|
|
273
|
+
recordEnteredTier(enteredTier === "development" && (devDb === "local" || devDb === "remote")
|
|
274
|
+
? `development:${devDb}`
|
|
275
|
+
: enteredTier);
|
|
276
|
+
return dispatch(chosen.argv);
|
|
277
|
+
}
|