@devdogsuga/backstage 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 +147 -0
- package/bin/backstage.mjs +28 -0
- package/dist/args-Cjr_Iqts.js +66 -0
- package/dist/build-info.json +3 -0
- package/dist/catalog-BOBE4ee7.js +1570 -0
- package/dist/cli-YLCe3Zrx.js +6300 -0
- package/dist/commands-Bk7IbhPD.js +220 -0
- package/dist/commands-D345F-Od.js +800 -0
- package/dist/commands-Dv5TXjvb.js +545 -0
- package/dist/dispatch-D048O65I.js +13 -0
- package/dist/launch.js +329 -0
- package/dist/options-BTjOf5KP.js +183 -0
- package/dist/peer-redirect-hooks.js +15 -0
- package/dist/telemetry-Bjoz29Hl.js +609 -0
- package/dist/telemetry.js +2 -0
- package/dist/ui-CdKo8mLw.js +66 -0
- package/env.ts +163 -0
- package/package.json +82 -0
package/dist/launch.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { C as discoverRepoRoot, E as setCliName, S as RepoNotFoundError, T as cliName, _ as installFailureLog, d as hasYes, g as stripTierFlag, h as stripNoEnvFlag, m as setNoEnv, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as isNonInteractive, s as lastSentryEventId } from "./telemetry-Bjoz29Hl.js";
|
|
2
|
+
import { c as setMenuEnvHook, d as formatCommand, f as nonEmpty, g as loadEnvSession, h as loadEnvLoad, n as bareGroupStartPath, t as catalog, v as helpPath } from "./catalog-BOBE4ee7.js";
|
|
3
|
+
import { c as setDryRun, o as isDryRun, s as resolveDryRun } from "./options-BTjOf5KP.js";
|
|
4
|
+
import { o as unwrap, r as errorMessage } from "./ui-CdKo8mLw.js";
|
|
5
|
+
import { confirm } from "@clack/prompts";
|
|
6
|
+
//#region ../cli-core/src/pipes.ts
|
|
7
|
+
/**
|
|
8
|
+
* Keeps a closed stdout/stderr from crashing devtools.
|
|
9
|
+
*
|
|
10
|
+
* When whatever reads devtools' output goes away first (`devtools … | head`,
|
|
11
|
+
* or a wrapper that stopped listening), the next write fails with EPIPE. The
|
|
12
|
+
* stream emits that as an `'error'` event nobody handles, so Node throws it as
|
|
13
|
+
* an uncaught exception and Sentry files it as a fatal. Nobody is left to read
|
|
14
|
+
* the output, so dropping it is the right outcome. Any other stream error is
|
|
15
|
+
* rethrown and crashes as it did before.
|
|
16
|
+
*
|
|
17
|
+
* Idempotent, because `launch`, `launchCi` and `ci.ts`'s `main` can all run in
|
|
18
|
+
* one process.
|
|
19
|
+
*/
|
|
20
|
+
let installed = false;
|
|
21
|
+
function ignoreClosedPipes() {
|
|
22
|
+
if (installed) return;
|
|
23
|
+
installed = true;
|
|
24
|
+
for (const stream of [process.stdout, process.stderr]) stream.on("error", (err) => {
|
|
25
|
+
if (err.code !== "EPIPE") throw err;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region ../cli-core/src/safety-gate.ts
|
|
30
|
+
/**
|
|
31
|
+
* The production and staging safety gate.
|
|
32
|
+
*
|
|
33
|
+
* devtools can reach any tier, and it cannot tell which `supabase`
|
|
34
|
+
* subcommands write, so the question is asked before ANY command runs against
|
|
35
|
+
* a hosted tier, passthroughs and presets included. This replaces the
|
|
36
|
+
* per-command confirmations `db reset`, `db migrate`, `seed production` and
|
|
37
|
+
* `cf preview` each used to carry.
|
|
38
|
+
*
|
|
39
|
+
* * production: shows the tier and project ref (never the `DB_URL`, which
|
|
40
|
+
* holds the password) and asks once, defaulting to no.
|
|
41
|
+
* * staging: the same, with milder wording.
|
|
42
|
+
* * non-interactive runs need `--yes`; without it the gate refuses instead
|
|
43
|
+
* of hanging on a prompt nobody can answer. clack's `confirm()` never
|
|
44
|
+
* resolves without a TTY, so `--yes` is checked before anything that
|
|
45
|
+
* could wait on one.
|
|
46
|
+
* * development (local or remote): no prompt.
|
|
47
|
+
*/
|
|
48
|
+
/** Set once the gate passed, so a devtools run spawned by devtools does not ask again. */
|
|
49
|
+
const GATE_PASSED_ENV = "DEVTOOLS_GATE_PASSED";
|
|
50
|
+
const HOSTED = /* @__PURE__ */ new Set(["staging", "production"]);
|
|
51
|
+
function isHostedTier(tier) {
|
|
52
|
+
return HOSTED.has(tier);
|
|
53
|
+
}
|
|
54
|
+
async function askClack(message) {
|
|
55
|
+
return unwrap(await confirm({
|
|
56
|
+
message,
|
|
57
|
+
initialValue: false
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Decides whether a command may run against `tier`. Writes its own refusal to
|
|
62
|
+
* stderr, so the caller only has to exit non-zero on `proceed: false`.
|
|
63
|
+
*/
|
|
64
|
+
async function gateHostedTier(input) {
|
|
65
|
+
const env = input.env ?? process.env;
|
|
66
|
+
if (!isHostedTier(input.tier)) return { proceed: true };
|
|
67
|
+
if (env["DEVTOOLS_GATE_PASSED"] === input.tier) return { proceed: true };
|
|
68
|
+
const production = input.tier === "production";
|
|
69
|
+
const target = `${production ? "PRODUCTION" : "staging"} (project ${input.projectRef ?? "unknown"})`;
|
|
70
|
+
const cli = cliName();
|
|
71
|
+
const command = formatCommand(cli, input.argv);
|
|
72
|
+
if (input.yes) {
|
|
73
|
+
if (input.nonInteractive) process.stderr.write(`${cli}: running \`${command}\` against ${target}.\n`);
|
|
74
|
+
return { proceed: true };
|
|
75
|
+
}
|
|
76
|
+
if (input.nonInteractive) {
|
|
77
|
+
process.stderr.write(`${cli}: refusing to run \`${command}\` against ${target} without --yes. Nobody is here to confirm it.
|
|
78
|
+
`);
|
|
79
|
+
return {
|
|
80
|
+
proceed: false,
|
|
81
|
+
reason: "refused"
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (!await (input.ask ?? askClack)(production ? `This runs \`${command}\` against PRODUCTION, project ${input.projectRef ?? "unknown"}. It is live data. Continue?` : `This runs \`${command}\` against staging, project ${input.projectRef ?? "unknown"}. Continue?`)) {
|
|
85
|
+
process.stderr.write(`${cli}: left ${input.tier} alone.\n`);
|
|
86
|
+
return {
|
|
87
|
+
proceed: false,
|
|
88
|
+
reason: "declined"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return { proceed: true };
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/launch-core.ts
|
|
95
|
+
/**
|
|
96
|
+
* What `backstage` does BEFORE `cli.ts` (and so every command it dispatches)
|
|
97
|
+
* is imported: settle the session's tier, enter it, hand off. `launch.ts` is
|
|
98
|
+
* the entry point the `bin/backstage.mjs` bootstrap runs.
|
|
99
|
+
*
|
|
100
|
+
* ## It starts anywhere
|
|
101
|
+
*
|
|
102
|
+
* `pnpm dlx @devdogsuga/backstage` works from any directory, a DevDogsUGA
|
|
103
|
+
* checkout or not. Nothing here needs one until a command does: help, version,
|
|
104
|
+
* completions and anything run with `--no-env` never look for a repository,
|
|
105
|
+
* and never import the optional-peer `@devdogsuga/*` libraries, which are only
|
|
106
|
+
* installed next to a checkout. A command that reads a checkout (`env`,
|
|
107
|
+
* `deploy <app>`, `planner`…) says "run this from inside a DevDogsUGA clone"
|
|
108
|
+
* and exits 1, instead of a stack trace.
|
|
109
|
+
*
|
|
110
|
+
* ## The session's tier
|
|
111
|
+
*
|
|
112
|
+
* Every command here always needs production secrets, but they name their
|
|
113
|
+
* own targets: `env` takes `--target`, `planner` works on the production
|
|
114
|
+
* database. So unlike devtools, nothing is ever asked at launch:
|
|
115
|
+
*
|
|
116
|
+
* * `--tier <t>` (anywhere in argv, stripped here), else `DEPLOY_ENV`, names
|
|
117
|
+
* the session's tier and so the env file loaded (`.env.<tier>`);
|
|
118
|
+
* * with neither, the session is plain `development` and `.env` is loaded,
|
|
119
|
+
* which is where `BWS_ACCESS_TOKEN` lives;
|
|
120
|
+
* * the `deploy` group refuses to run without one of the two. Applying
|
|
121
|
+
* migrations to whatever `DB_URL` the development `.env` happens to hold is
|
|
122
|
+
* not a default anybody wants.
|
|
123
|
+
*
|
|
124
|
+
* ## `--no-env`
|
|
125
|
+
*
|
|
126
|
+
* Skips tier resolution and env-file loading: the caller's environment is the
|
|
127
|
+
* environment. For `deploy write-env`, which CREATES the file tier resolution
|
|
128
|
+
* would otherwise insist on reading, and for the CI steps that hold one narrow
|
|
129
|
+
* credential in the job's own `env:` block (`preflight`, `plan`, `migrate`,
|
|
130
|
+
* `smoke`, `reconcile`, `planner status`).
|
|
131
|
+
*
|
|
132
|
+
* ## Staging and production ask first
|
|
133
|
+
*
|
|
134
|
+
* A command that is not read-only asks once before it runs against a hosted
|
|
135
|
+
* tier, `--yes` answers, and with no terminal (or `CI=true`) it refuses
|
|
136
|
+
* without `--yes` (`@devdogsuga/cli-core/safety-gate`).
|
|
137
|
+
*/
|
|
138
|
+
/** The tier words a `--tier` or `DEPLOY_ENV` may carry. */
|
|
139
|
+
const SESSION_TIERS = [
|
|
140
|
+
"development",
|
|
141
|
+
"development:local",
|
|
142
|
+
"development:remote",
|
|
143
|
+
"staging",
|
|
144
|
+
"production"
|
|
145
|
+
];
|
|
146
|
+
/** `--tier`, else `DEPLOY_ENV`, else plain development. Exported for the tests. */
|
|
147
|
+
function resolveSession(explicit, deployEnv = process.env.DEPLOY_ENV) {
|
|
148
|
+
const selector = explicit ?? nonEmpty(deployEnv);
|
|
149
|
+
if (selector === void 0) return {
|
|
150
|
+
ok: true,
|
|
151
|
+
session: {
|
|
152
|
+
tier: "development",
|
|
153
|
+
named: false
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
if (!SESSION_TIERS.includes(selector)) return {
|
|
157
|
+
ok: false,
|
|
158
|
+
reason: `unknown tier "${selector}". Expected: ${SESSION_TIERS.join(", ")}.`
|
|
159
|
+
};
|
|
160
|
+
const [tier, qualifier] = selector.split(":");
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
session: qualifier === void 0 ? {
|
|
164
|
+
tier,
|
|
165
|
+
named: true
|
|
166
|
+
} : {
|
|
167
|
+
tier,
|
|
168
|
+
devDatabase: qualifier,
|
|
169
|
+
named: true
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether the command `argv` names is declared `envFree`: it needs no env
|
|
175
|
+
* file and no tier (`graphics`, `qr`, `github`, `newsletter`), so the session
|
|
176
|
+
* behaves as if `--no-env` was typed. They run in a directory with no
|
|
177
|
+
* checkout and no `.env`, which is the point of `pnpm dlx`.
|
|
178
|
+
*/
|
|
179
|
+
function isEnvFree(argv) {
|
|
180
|
+
const path = helpPath(argv);
|
|
181
|
+
for (let end = path.length; end > 0; end -= 1) {
|
|
182
|
+
const node = catalog.findCommand(path.slice(0, end));
|
|
183
|
+
if (node) return node.envFree === true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
function fail(label, message) {
|
|
188
|
+
process.stderr.write(`${label}: ${message}\n`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Loads the session's env files onto `process.env`, or just names the tier
|
|
193
|
+
* under `--no-env`. Exits on a refusal: there is no command dispatched yet for
|
|
194
|
+
* a caller to fall back to.
|
|
195
|
+
*/
|
|
196
|
+
async function enterSession(session, noEnv, label) {
|
|
197
|
+
if (noEnv) {
|
|
198
|
+
process.env.DEPLOY_ENV = session.tier;
|
|
199
|
+
if (session.devDatabase !== void 0) process.env.DEV_DB = session.devDatabase;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (discoverRepoRoot() === null) fail(label, new RepoNotFoundError().message);
|
|
203
|
+
const envLoad = await loadEnvLoad();
|
|
204
|
+
const envSession = await loadEnvSession();
|
|
205
|
+
process.env[envLoad.SHELL_KEYS_ENV] ??= Object.keys(process.env).join(",");
|
|
206
|
+
try {
|
|
207
|
+
const entered = await envSession.enterEnvironment(session.tier, {
|
|
208
|
+
override: false,
|
|
209
|
+
devDatabase: session.devDatabase
|
|
210
|
+
});
|
|
211
|
+
for (const warning of entered.warnings) process.stderr.write(`${label}: ${warning}\n`);
|
|
212
|
+
process.stderr.write(`${label}: loaded ${entered.files.length > 0 ? entered.files.join(", ") : "no env files"} (${session.tier})\n`);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (!(err instanceof envLoad.MissingEnvFileError)) throw err;
|
|
215
|
+
if (session.tier !== "development") fail(label, err.message);
|
|
216
|
+
process.stderr.write(`${label}: ${err.message}\n`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Runs `proceed` only if the hosted-tier gate lets the command through. A
|
|
221
|
+
* command the gate stops exits non-zero and returns `blocked`.
|
|
222
|
+
*/
|
|
223
|
+
async function gated(session, commandArgv, proceed, blocked, enabled) {
|
|
224
|
+
if (!enabled || isDryRun() || catalog.findCommand(helpPath(commandArgv))?.dryRun === "read-only") return proceed();
|
|
225
|
+
if (!(await gateHostedTier({
|
|
226
|
+
tier: session.tier,
|
|
227
|
+
projectRef: nonEmpty(process.env.PROJECT_REF),
|
|
228
|
+
argv: commandArgv,
|
|
229
|
+
yes: hasYes(commandArgv),
|
|
230
|
+
nonInteractive: isNonInteractive()
|
|
231
|
+
})).proceed) {
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
return blocked;
|
|
234
|
+
}
|
|
235
|
+
process.env[GATE_PASSED_ENV] = session.tier;
|
|
236
|
+
return proceed();
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The refusal for a `deploy` command run with no tier named, if it applies.
|
|
240
|
+
* Under `--no-env` the caller supplied the environment, credentials and all,
|
|
241
|
+
* so there is no env file for the tier to choose and nothing to default.
|
|
242
|
+
*/
|
|
243
|
+
function missingTier(argv, session, noEnv) {
|
|
244
|
+
if (argv[0] !== "deploy" || session.named || noEnv) return null;
|
|
245
|
+
return "no tier named. A deploy command must be told which environment it is for: pass --tier <staging|production> or set DEPLOY_ENV.";
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Resolves the session's tier, enters it, and hands off to `options.dispatch`.
|
|
249
|
+
*/
|
|
250
|
+
async function launchWith(argv, options) {
|
|
251
|
+
setCliName("backstage");
|
|
252
|
+
const label = "backstage";
|
|
253
|
+
const gate = options.gate ?? true;
|
|
254
|
+
const { noEnv: typedNoEnv, rest: withoutNoEnv } = stripNoEnvFlag(argv);
|
|
255
|
+
const { explicit, rest: withoutTier } = stripTierFlag(withoutNoEnv);
|
|
256
|
+
const { dryRun, rest } = resolveDryRun(withoutTier);
|
|
257
|
+
const noEnv = typedNoEnv || isEnvFree(rest);
|
|
258
|
+
setNoEnv(noEnv);
|
|
259
|
+
setDryRun(dryRun);
|
|
260
|
+
installFailureLog({
|
|
261
|
+
argv,
|
|
262
|
+
eventId: lastSentryEventId
|
|
263
|
+
});
|
|
264
|
+
ignoreClosedPipes();
|
|
265
|
+
initDevtoolsTelemetry(rest.slice(0, 2).join(" ") || "menu");
|
|
266
|
+
const dispatch = async (args) => {
|
|
267
|
+
try {
|
|
268
|
+
await options.dispatch(args);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
process.stderr.write(`${label}: ${errorMessage(err)}\n`);
|
|
271
|
+
await captureDevtoolsError(err);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
if (rest.includes("--help") || rest.includes("-h") || rest[0] === "version" || rest[0] === "completions") {
|
|
276
|
+
await dispatch(rest);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const name = helpPath(rest)[0];
|
|
280
|
+
if (name !== void 0 && catalog.findCommand([name]) === null) {
|
|
281
|
+
await dispatch(rest);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const resolved = resolveSession(explicit);
|
|
285
|
+
if (!resolved.ok) fail(label, resolved.reason);
|
|
286
|
+
const { session } = resolved;
|
|
287
|
+
if (rest.length === 0 || process.stdin.isTTY === true && bareGroupStartPath(catalog, rest) !== null) {
|
|
288
|
+
setMenuEnvHook(async (commandArgv, dispatchCommand) => {
|
|
289
|
+
const chosen = resolveSession(stripTierFlag(commandArgv).explicit);
|
|
290
|
+
if (!chosen.ok) {
|
|
291
|
+
process.stderr.write(`${label}: ${chosen.reason}\n`);
|
|
292
|
+
process.exitCode = 1;
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const skipEnv = noEnv || isEnvFree(commandArgv);
|
|
296
|
+
setNoEnv(skipEnv);
|
|
297
|
+
const refusal = missingTier(commandArgv, chosen.session, skipEnv);
|
|
298
|
+
if (refusal) {
|
|
299
|
+
process.stderr.write(`${label}: ${refusal}\n`);
|
|
300
|
+
process.exitCode = 1;
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
await enterSession(chosen.session, skipEnv, label);
|
|
304
|
+
return gated(chosen.session, commandArgv, dispatchCommand, null, gate);
|
|
305
|
+
});
|
|
306
|
+
await dispatch(rest);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const refusal = missingTier(rest, session, noEnv);
|
|
310
|
+
if (refusal) fail(label, refusal);
|
|
311
|
+
await enterSession(session, noEnv, label);
|
|
312
|
+
await gated(session, rest, () => dispatch(rest), void 0, gate);
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/launch.ts
|
|
316
|
+
/**
|
|
317
|
+
* `backstage`'s entry point, run by the `bin/backstage.mjs` bootstrap. All of
|
|
318
|
+
* the launching is in `launch-core.ts`; this file only says which dispatcher
|
|
319
|
+
* runs the command.
|
|
320
|
+
*/
|
|
321
|
+
async function launch(argv) {
|
|
322
|
+
await launchWith(argv, { dispatch: async (args) => {
|
|
323
|
+
const { main } = await import("./cli-YLCe3Zrx.js");
|
|
324
|
+
await main(args);
|
|
325
|
+
} });
|
|
326
|
+
}
|
|
327
|
+
if (import.meta.url === `file://${process.argv[1]}`) await launch(process.argv.slice(2));
|
|
328
|
+
//#endregion
|
|
329
|
+
export { launch };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { T as cliName } from "./telemetry-Bjoz29Hl.js";
|
|
2
|
+
//#region ../cli-core/src/dry-run.ts
|
|
3
|
+
const DRY_RUN_FLAG = "--dry-run";
|
|
4
|
+
/** Commands that forward everything after their name to another tool. */
|
|
5
|
+
const FORWARDING = /* @__PURE__ */ new Set([
|
|
6
|
+
"supabase",
|
|
7
|
+
"wrangler",
|
|
8
|
+
"drizzle-kit",
|
|
9
|
+
"psql",
|
|
10
|
+
"bw",
|
|
11
|
+
"run"
|
|
12
|
+
]);
|
|
13
|
+
let active = false;
|
|
14
|
+
function setDryRun(on) {
|
|
15
|
+
active = on;
|
|
16
|
+
}
|
|
17
|
+
function isDryRun() {
|
|
18
|
+
return active;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decides whether `argv` asks for a dry run and returns the argv to dispatch.
|
|
22
|
+
*
|
|
23
|
+
* For a forwarding command a leading `--dry-run` is consumed (the tool never
|
|
24
|
+
* sees it) and a later one is the tool's. For any other command the argv is
|
|
25
|
+
* returned as it came, because several commands read `--dry-run` themselves.
|
|
26
|
+
*/
|
|
27
|
+
function resolveDryRun(argv) {
|
|
28
|
+
const firstPositional = argv.findIndex((arg) => !arg.startsWith("-"));
|
|
29
|
+
const lead = firstPositional === -1 ? argv.length : firstPositional;
|
|
30
|
+
const command = argv[firstPositional];
|
|
31
|
+
if (command !== void 0 && FORWARDING.has(command)) {
|
|
32
|
+
const rest = argv.filter((arg, i) => i >= lead || arg !== "--dry-run");
|
|
33
|
+
return {
|
|
34
|
+
dryRun: rest.length !== argv.length,
|
|
35
|
+
rest
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
dryRun: argv.includes(DRY_RUN_FLAG),
|
|
40
|
+
rest: [...argv]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* How a command treats `--dry-run`, from the nearest node on its path that
|
|
45
|
+
* says. `undefined` means it did not say, and the dispatcher stops it.
|
|
46
|
+
*/
|
|
47
|
+
function dryRunKind(catalog, path) {
|
|
48
|
+
for (let end = path.length; end > 0; end -= 1) {
|
|
49
|
+
const kind = catalog.findCommand(path.slice(0, end))?.dryRun;
|
|
50
|
+
if (kind !== void 0) return kind;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** The command line a stopped command would have run. */
|
|
54
|
+
function wouldRunLine(argv) {
|
|
55
|
+
return `Would run: ${cliName()} ${argv.join(" ")}`;
|
|
56
|
+
}
|
|
57
|
+
const QR_FLAGS = {
|
|
58
|
+
text: {
|
|
59
|
+
name: "text",
|
|
60
|
+
value: "<text>",
|
|
61
|
+
summary: "What to encode: a URL or any text. May be given as the argument."
|
|
62
|
+
},
|
|
63
|
+
theme: {
|
|
64
|
+
name: "theme",
|
|
65
|
+
value: `<${[
|
|
66
|
+
"devdogs-light",
|
|
67
|
+
"devdogs-dark",
|
|
68
|
+
"acm-light",
|
|
69
|
+
"acm-dark"
|
|
70
|
+
].join("|")}>`,
|
|
71
|
+
summary: "Logo, shape and ink colour in one go, as the console's theme buttons."
|
|
72
|
+
},
|
|
73
|
+
size: {
|
|
74
|
+
name: "size",
|
|
75
|
+
value: "<px>",
|
|
76
|
+
summary: "Width and height of the image in pixels. Defaults to 999."
|
|
77
|
+
},
|
|
78
|
+
margin: {
|
|
79
|
+
name: "margin",
|
|
80
|
+
value: "<modules>",
|
|
81
|
+
summary: "Quiet zone around the code, in modules. Defaults to 2."
|
|
82
|
+
},
|
|
83
|
+
color: {
|
|
84
|
+
name: "color",
|
|
85
|
+
value: "<css colour>",
|
|
86
|
+
summary: "Ink colour. Defaults to the theme's."
|
|
87
|
+
},
|
|
88
|
+
background: {
|
|
89
|
+
name: "background",
|
|
90
|
+
value: "<css colour|transparent>",
|
|
91
|
+
summary: "Background colour. Transparent by default."
|
|
92
|
+
},
|
|
93
|
+
gradient: {
|
|
94
|
+
name: "gradient",
|
|
95
|
+
value: "<from,to>",
|
|
96
|
+
summary: "Two-colour diagonal gradient for the ink, instead of --color."
|
|
97
|
+
},
|
|
98
|
+
shape: {
|
|
99
|
+
name: "shape",
|
|
100
|
+
value: "<rounded|square>",
|
|
101
|
+
summary: "Module shape. Defaults to the theme's."
|
|
102
|
+
},
|
|
103
|
+
logo: {
|
|
104
|
+
name: "logo",
|
|
105
|
+
value: "<preset|none|file>",
|
|
106
|
+
summary: `Centre logo: a preset (${[
|
|
107
|
+
"devdogs",
|
|
108
|
+
"acm",
|
|
109
|
+
"discord-white",
|
|
110
|
+
"discord-black",
|
|
111
|
+
"discord-blurple"
|
|
112
|
+
].join(", ")}), none, or an image file path.`
|
|
113
|
+
},
|
|
114
|
+
logoSize: {
|
|
115
|
+
name: "logo-size",
|
|
116
|
+
value: "<modules>",
|
|
117
|
+
summary: "Logo width in modules. Defaults to 9."
|
|
118
|
+
},
|
|
119
|
+
logoPadding: {
|
|
120
|
+
name: "logo-padding",
|
|
121
|
+
value: "<modules>",
|
|
122
|
+
summary: "Clear space around the logo, in modules. Defaults to 1."
|
|
123
|
+
},
|
|
124
|
+
errorLevel: {
|
|
125
|
+
name: "error-level",
|
|
126
|
+
value: `<${[
|
|
127
|
+
"L",
|
|
128
|
+
"M",
|
|
129
|
+
"Q",
|
|
130
|
+
"H"
|
|
131
|
+
].join("|")}>`,
|
|
132
|
+
summary: "Error-correction level. Defaults to H."
|
|
133
|
+
},
|
|
134
|
+
version: {
|
|
135
|
+
name: "qr-version",
|
|
136
|
+
value: "<1-40>",
|
|
137
|
+
summary: "Force a QR version (grid size). Defaults to the smallest that fits."
|
|
138
|
+
},
|
|
139
|
+
formats: {
|
|
140
|
+
name: "format",
|
|
141
|
+
value: `<${[
|
|
142
|
+
"svg",
|
|
143
|
+
"png",
|
|
144
|
+
"jpg",
|
|
145
|
+
"webp",
|
|
146
|
+
"avif",
|
|
147
|
+
"tiff"
|
|
148
|
+
].join(",")}>`,
|
|
149
|
+
summary: "Output formats, comma-separated, several at once. Defaults to svg,png."
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
/** CLI-only flags: the crop of a logo file, and where the files go. */
|
|
153
|
+
const QR_EXTRA_FLAGS = {
|
|
154
|
+
logoCrop: {
|
|
155
|
+
name: "logo-crop",
|
|
156
|
+
value: "<x,y,width,height>",
|
|
157
|
+
summary: "Crop a logo file to this region (source pixels) before it goes in the code."
|
|
158
|
+
},
|
|
159
|
+
out: {
|
|
160
|
+
name: "out",
|
|
161
|
+
value: "<dir>",
|
|
162
|
+
summary: "Directory for the files. Defaults to the current directory."
|
|
163
|
+
},
|
|
164
|
+
name: {
|
|
165
|
+
name: "name",
|
|
166
|
+
value: "<stem>",
|
|
167
|
+
summary: "File name without extension. Defaults to qr."
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
/** The flags as the command catalog declares them (help, completions, menu). */
|
|
171
|
+
function qrCatalogOptions() {
|
|
172
|
+
return [...Object.values(QR_FLAGS), ...Object.values(QR_EXTRA_FLAGS)].map((flag) => ({
|
|
173
|
+
flag: `--${flag.name}`,
|
|
174
|
+
value: flag.value,
|
|
175
|
+
summary: flag.summary
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
/** Every flag name `parseArgs` should accept, all string-valued. */
|
|
179
|
+
function qrParseOptions() {
|
|
180
|
+
return Object.fromEntries([...Object.values(QR_FLAGS), ...Object.values(QR_EXTRA_FLAGS)].map((flag) => [flag.name, { type: "string" }]));
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
export { dryRunKind as a, setDryRun as c, qrParseOptions as i, wouldRunLine as l, QR_FLAGS as n, isDryRun as o, qrCatalogOptions as r, resolveDryRun as s, QR_EXTRA_FLAGS as t };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region ../cli-core/src/repo/peer-redirect-hooks.ts
|
|
2
|
+
let redirects = [];
|
|
3
|
+
const initialize = (data) => {
|
|
4
|
+
redirects = data;
|
|
5
|
+
};
|
|
6
|
+
const resolve = (specifier, context, nextResolve) => {
|
|
7
|
+
const redirect = redirects.find((r) => r.specifier === specifier && r.parentURL === context.parentURL);
|
|
8
|
+
if (redirect) return {
|
|
9
|
+
url: redirect.url,
|
|
10
|
+
shortCircuit: true
|
|
11
|
+
};
|
|
12
|
+
return nextResolve(specifier, context);
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { initialize, resolve };
|