@devdogsuga/devtools 0.1.21 → 0.1.23

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.
Files changed (53) hide show
  1. package/dist/bws/pace.d.ts +1 -3
  2. package/dist/bws/pace.js +1 -1
  3. package/dist/bws/token.js +1 -1
  4. package/dist/bws/vault.js +2 -3
  5. package/dist/cli.js +4 -2
  6. package/dist/commands.js +1 -1
  7. package/dist/cron/discovery.js +3 -3
  8. package/dist/db/new-migration.js +4 -1
  9. package/dist/db/seed-roles.js +3 -1
  10. package/dist/deploy/secrets-file.js +8 -9
  11. package/dist/deploy/write-env.js +3 -1
  12. package/dist/env/audit.js +3 -3
  13. package/dist/env/commands.js +2 -1
  14. package/dist/env/example.js +3 -1
  15. package/dist/env/selection.js +3 -1
  16. package/dist/environment-doctor.d.ts +3 -3
  17. package/dist/environment-doctor.js +37 -16
  18. package/dist/environment.js +3 -1
  19. package/dist/gen/hypno.js +1 -1
  20. package/dist/gh/client.js +2 -1
  21. package/dist/gh/rulesets/actors.js +2 -1
  22. package/dist/gh/rulesets/api.js +5 -2
  23. package/dist/gh/rulesets/desired.js +30 -6
  24. package/dist/gh/rulesets/diff.js +3 -1
  25. package/dist/gh/settings/api.js +11 -3
  26. package/dist/gh/settings/commands.js +15 -6
  27. package/dist/gh/settings/desired.js +5 -1
  28. package/dist/gh/settings/diff.js +10 -5
  29. package/dist/gh/settings/workflows.js +5 -1
  30. package/dist/images/commands.js +8 -6
  31. package/dist/images/render.js +5 -1
  32. package/dist/launch-ci.js +3 -1
  33. package/dist/launch.js +8 -6
  34. package/dist/moderation.js +5 -3
  35. package/dist/oauth/db.js +1 -1
  36. package/dist/oauth/device.js +12 -4
  37. package/dist/oauth/exchange.js +9 -3
  38. package/dist/oauth/loopback.js +11 -5
  39. package/dist/oauth/wizard.js +16 -5
  40. package/dist/persona.js +1 -1
  41. package/dist/planner/checks.js +1 -1
  42. package/dist/planner/commands.js +1 -1
  43. package/dist/planner/db.js +1 -1
  44. package/dist/repo/source.d.ts +1 -1
  45. package/dist/run/pick.js +1 -1
  46. package/dist/setup.js +1 -1
  47. package/dist/telemetry.js +3 -1
  48. package/dist/tier.js +5 -2
  49. package/dist/version.js +1 -3
  50. package/dist/workers.js +1 -3
  51. package/dist/workflows/commands.js +3 -2
  52. package/env.ts +5 -5
  53. package/package.json +11 -8
@@ -21,7 +21,5 @@
21
21
  export type CallKind = "read" | "write";
22
22
  export declare const WRITE_GAP_MS = 1100;
23
23
  export declare const READ_GAP_MS = 350;
24
- export interface Pacer {
25
- (kind: CallKind): Promise<void>;
26
- }
24
+ export type Pacer = (kind: CallKind) => Promise<void>;
27
25
  export declare function makePacer(now?: () => number, sleep?: (ms: number) => Promise<void>): Pacer;
package/dist/bws/pace.js CHANGED
@@ -37,7 +37,7 @@ export function makePacer(now = Date.now, sleep = wait) {
37
37
  });
38
38
  // The chain never rejects: a pacer that fails closed would wedge every
39
39
  // later call behind a rejection that had nothing to do with them.
40
- chain = turn.catch(() => { });
40
+ chain = turn.catch(() => undefined);
41
41
  return turn;
42
42
  };
43
43
  }
package/dist/bws/token.js CHANGED
@@ -18,7 +18,7 @@ import { unwrap } from "../ui.js";
18
18
  export class NoAccessTokenError extends Error {
19
19
  }
20
20
  export async function resolveToken(sources) {
21
- const announce = sources.onSource ?? (() => { });
21
+ const announce = sources.onSource ?? (() => undefined);
22
22
  // Explicit beats ambient. Someone who passes the flag while an environment
23
23
  // variable is set is overriding on purpose, and silently ignoring them would
24
24
  // point the command at the account they were trying to avoid.
package/dist/bws/vault.js CHANGED
@@ -40,9 +40,8 @@ export async function vaultStatus() {
40
40
  // `--response` wraps the payload; the bare form is also accepted, so read
41
41
  // whichever shape came back rather than depending on one.
42
42
  const parsed = JSON.parse(stdout);
43
- const status = ("status" in parsed && parsed.status) ||
44
- ("data" in parsed && parsed.data?.template?.status) ||
45
- undefined;
43
+ const status = ("status" in parsed ? parsed.status : undefined) ??
44
+ ("data" in parsed ? parsed.data?.template?.status : undefined);
46
45
  if (status === "unlocked")
47
46
  return "unlocked";
48
47
  if (status === "locked")
package/dist/cli.js CHANGED
@@ -45,7 +45,7 @@ import { loadEnv } from "./repo/peers.js";
45
45
  import { setExplicitAccessToken } from "./bws/client.js";
46
46
  import { positionals } from "./args.js";
47
47
  import { resolveVaultTarget } from "./pick.js";
48
- import { bail, errorMessage, explain, explainError, renderChecks, unwrap } from "./ui.js";
48
+ import { bail, errorMessage, explain, explainError, renderChecks, unwrap, } from "./ui.js";
49
49
  import { helpPath, renderHelp } from "./help.js";
50
50
  import { subcommandList, subcommandNames } from "./commands.js";
51
51
  import { bareGroupStartPath, runMenu } from "./menu.js";
@@ -597,7 +597,9 @@ async function runDbCommand(rest) {
597
597
  return;
598
598
  }
599
599
  if (sub === "types") {
600
- const connection = await resolveDbConnection({ label: "devtools db types" });
600
+ const connection = await resolveDbConnection({
601
+ label: "devtools db types",
602
+ });
601
603
  if (!connection) {
602
604
  process.exitCode = 1;
603
605
  return;
package/dist/commands.js CHANGED
@@ -677,7 +677,7 @@ const DECLARED_GROUPS = [
677
677
  {
678
678
  flag: "--base-url",
679
679
  value: "<url>",
680
- summary: "DevDogs API URL for \"Paste credentials instead\". Defaults to https://api.devdogsuga.org.",
680
+ summary: 'DevDogs API URL for "Paste credentials instead". Defaults to https://api.devdogsuga.org.',
681
681
  },
682
682
  {
683
683
  flag: "--platform-url",
@@ -8,7 +8,7 @@
8
8
  import { existsSync, readFileSync, readdirSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { pathToFileURL } from "node:url";
11
- import { parseConfigFileTextToJson } from "typescript";
11
+ import { flattenDiagnosticMessageText, parseConfigFileTextToJson, } from "typescript";
12
12
  import { findRepoRoot } from "../repo/root.js";
13
13
  import { CronRoutes, WorkflowCrons, } from "./schema.js";
14
14
  export const CRON_TIERS = ["development", "staging", "production"];
@@ -19,7 +19,7 @@ export function isCronTier(value) {
19
19
  export function parseWrangler(path) {
20
20
  const result = parseConfigFileTextToJson(path, readFileSync(path, "utf8"));
21
21
  if (result.error) {
22
- throw new Error(`${path}: ${result.error.messageText}`);
22
+ throw new Error(`${path}: ${flattenDiagnosticMessageText(result.error.messageText, "\n")}`);
23
23
  }
24
24
  return result.config;
25
25
  }
@@ -57,7 +57,7 @@ export async function discoverCronMaps() {
57
57
  const scheduledPath = join(findRepoRoot(), "apps", app, "cloudflare", "scheduled.ts");
58
58
  if (!existsSync(scheduledPath))
59
59
  continue;
60
- const mod = await import(pathToFileURL(scheduledPath).href);
60
+ const mod = (await import(pathToFileURL(scheduledPath).href));
61
61
  const parsed = CronRoutes.safeParse(mod.CRON_ROUTES);
62
62
  if (!parsed.success) {
63
63
  throw new Error(`${scheduledPath}: CRON_ROUTES does not match the required shape.\n` +
@@ -42,7 +42,10 @@ export function migrationName(appSlug, description) {
42
42
  async function pickApp() {
43
43
  return unwrap(await select({
44
44
  message: "Whose schema is this migration for?",
45
- options: Object.keys(APP_SCHEMAS).map((value) => ({ value, label: value })),
45
+ options: Object.keys(APP_SCHEMAS).map((value) => ({
46
+ value,
47
+ label: value,
48
+ })),
46
49
  }));
47
50
  }
48
51
  export async function runNewMigration(appArg, descriptionArg) {
@@ -13,7 +13,9 @@ import { supabase } from "./run.js";
13
13
  // top-level `const`, which broke `--help`/`setup` outside a repo — see
14
14
  // `workers.ts`'s header for the general pattern this follows).
15
15
  function seedFiles() {
16
- return [join(findRepoRoot(), "supabase", "seed", "production", "01_roles.sql")];
16
+ return [
17
+ join(findRepoRoot(), "supabase", "seed", "production", "01_roles.sql"),
18
+ ];
17
19
  }
18
20
  /**
19
21
  * Always `--db-url` — the session's own connection string, never the
@@ -83,15 +83,14 @@ export async function runDeploySecretsFile(options) {
83
83
  if (!declaredBy(entries, app))
84
84
  continue;
85
85
  const meta = entries[0].meta;
86
- // Public server keys ship alongside the secrets, because nothing else
87
- // reaches the Worker's runtime: OpenNext copies the Worker env into
88
- // process.env per request, and a value that was only in the composed
89
- // .env file at build time is gone by then. The first staging deploy
90
- // proved it: the schedule-builder Worker booted without API_URL,
91
- // REST_URL, PUBLISHABLE_KEY, STORAGE_S3_URL or S3_PROTOCOL_REGION and
92
- // answered 500 on every route. Client (NEXT_PUBLIC) keys are excluded
93
- // by `declaredBy` (inlined at build); never-store keys stay excluded
94
- // because a Worker secret is a remote copy, the thing they forbid.
86
+ // Public server keys ship alongside the secrets because a value that was
87
+ // only in the composed .env file at build time is absent from the deployed
88
+ // Worker's runtime environment. The first staging deploy proved it: the
89
+ // schedule-builder Worker booted without API_URL, REST_URL,
90
+ // PUBLISHABLE_KEY, STORAGE_S3_URL or S3_PROTOCOL_REGION and answered 500
91
+ // on every route. Client (NEXT_PUBLIC) keys are excluded by `declaredBy`
92
+ // (inlined at build); never-store keys stay excluded because a Worker
93
+ // secret is a remote copy, the thing they forbid.
95
94
  if (meta.secrecy === "secret") {
96
95
  if (!storable.has(key)) {
97
96
  minted.push(key);
@@ -237,7 +237,9 @@ function compose(options, env) {
237
237
  }
238
238
  const ghSecrets = readContext("DEPLOY_GITHUB_SECRETS", env);
239
239
  const ghVars = readContext("DEPLOY_GITHUB_VARS", env);
240
- const smuggled = getEnvSync().neverStoreKeys().filter((k) => k in ghSecrets || k in ghVars);
240
+ const smuggled = getEnvSync()
241
+ .neverStoreKeys()
242
+ .filter((k) => k in ghSecrets || k in ghVars);
241
243
  if (smuggled.length > 0) {
242
244
  throw new DeployError(`${smuggled.join(", ")} must never be a GitHub secret or variable.`, [
243
245
  "Delete it from the repository and the environment, then rotate it.",
package/dist/env/audit.js CHANGED
@@ -136,7 +136,7 @@ export function audit(input) {
136
136
  // variable store is an orphan from a rename, reported as one below.
137
137
  // Calling it a leaked secret would be a guess, and a loud wrong one. With
138
138
  // no declared set there is no way to tell the two apart, so neither fires.
139
- if (input.declared === undefined || !input.declared.has(copy.name))
139
+ if (!input.declared?.has(copy.name))
140
140
  continue;
141
141
  findings.push({
142
142
  key: copy.name,
@@ -174,7 +174,7 @@ export function audit(input) {
174
174
  "exactly like this report. Run `gh variable list` by hand",
175
175
  });
176
176
  }
177
- const repositoryNames = repository !== undefined && repository.readable ? repository.names : [];
177
+ const repositoryNames = repository?.readable ? repository.names : [];
178
178
  for (const name of repositoryNames) {
179
179
  // Membership in `variables` IS the declaration test for this branch, since
180
180
  // the set comes from the registry, which is why it does not also consult
@@ -213,7 +213,7 @@ export function audit(input) {
213
213
  // variable, and calling that a leaked secret would be a loud guess.
214
214
  if (ignore.has(name))
215
215
  continue;
216
- if (input.declared === undefined || !input.declared.has(name))
216
+ if (!input.declared?.has(name))
217
217
  continue;
218
218
  findings.push({
219
219
  key: name,
@@ -491,12 +491,13 @@ export async function runEnvAudit(options) {
491
491
  };
492
492
  }
493
493
  catch (err) {
494
+ const firstLine = errorMessage(err).split("\n")[0]?.trim() ?? "";
494
495
  repositoryVariables = {
495
496
  readable: false,
496
497
  // The FIRST line only. `describe()` in the gh client returns a paragraph
497
498
  // of guidance, and a finding is one line. The rest is reproducible by
498
499
  // running the command the finding names.
499
- reason: errorMessage(err).split("\n")[0]?.trim() || "`gh` failed",
500
+ reason: firstLine || "`gh` failed",
500
501
  };
501
502
  }
502
503
  const { secrets: cloudflare, unreadable } = await listWorkerSecrets(target);
@@ -190,7 +190,9 @@ function derivedValue(meta, routed) {
190
190
  const derivation = getEnvSync().derivationOf(meta);
191
191
  if (derivation === null)
192
192
  return "";
193
- return getEnvSync().envReferences(derivation).every((ref) => routed.has(ref))
193
+ return getEnvSync()
194
+ .envReferences(derivation)
195
+ .every((ref) => routed.has(ref))
194
196
  ? derivation
195
197
  : "";
196
198
  }
@@ -53,7 +53,9 @@ export function pushableVariables() {
53
53
  export function ignoredFor(target) {
54
54
  assertRegistryLoaded();
55
55
  const pushable = pushableVariables();
56
- const skip = new Set(getEnvSync().neverSecretKeys().filter((key) => !pushable.has(key)));
56
+ const skip = new Set(getEnvSync()
57
+ .neverSecretKeys()
58
+ .filter((key) => !pushable.has(key)));
57
59
  for (const key of getEnvSync().mintedKeys())
58
60
  skip.add(key);
59
61
  if (target !== "production") {
@@ -12,9 +12,9 @@ export interface DoctorCheck {
12
12
  * pins 24, which is what a `fnm use` picks up; a machine below the floor is
13
13
  * a WARN, not an error — plenty of contributors' shells still resolve an
14
14
  * old global `node` first. */
15
- export declare function checkNodeVersion(nodeVersion: string, app: string): DoctorCheck;
16
- export declare function checkFnmHookPresent(shellProfile: string | null, app: string): DoctorCheck;
17
- export declare function checkPnpmVersion(pnpmVersion: string | null, pinned: string, app: string): DoctorCheck;
15
+ export declare function checkNodeVersion(nodeVersion: string): DoctorCheck;
16
+ export declare function checkFnmHookPresent(shellProfile: string | null): DoctorCheck;
17
+ export declare function checkPnpmVersion(pnpmVersion: string | null, pinned: string): DoctorCheck;
18
18
  /** `git config core.autocrlf` — `input` or `false` is correct on every
19
19
  * platform this repo documents (WSL2, native Windows, macOS, Linux); `true`
20
20
  * rewrites LF to CRLF on checkout and breaks anything that shells out to a
@@ -26,7 +26,11 @@ import { log, note } from "@clack/prompts";
26
26
  import { parse as parseDotenv } from "dotenv";
27
27
  import { discoverRepoRoot } from "./repo/root.js";
28
28
  import { validateSessionPoolerUrl } from "./db/pooler.js";
29
- const TROUBLESHOOTING_APPS = ["schedule-builder", "study-group-finder", "platform"];
29
+ const TROUBLESHOOTING_APPS = [
30
+ "schedule-builder",
31
+ "study-group-finder",
32
+ "platform",
33
+ ];
30
34
  function faqUrl(app, id) {
31
35
  return `https://devdogsuga.org/docs/${app}/getting-started/troubleshooting#${id}`;
32
36
  }
@@ -35,7 +39,7 @@ function faqUrl(app, id) {
35
39
  * pins 24, which is what a `fnm use` picks up; a machine below the floor is
36
40
  * a WARN, not an error — plenty of contributors' shells still resolve an
37
41
  * old global `node` first. */
38
- export function checkNodeVersion(nodeVersion, app) {
42
+ export function checkNodeVersion(nodeVersion) {
39
43
  const [major = 0, minor = 0] = nodeVersion.split(".").map(Number);
40
44
  const ok = major > 22 || (major === 22 && minor >= 12);
41
45
  return {
@@ -50,8 +54,9 @@ export function checkNodeVersion(nodeVersion, app) {
50
54
  faqId: ok ? undefined : "node-version",
51
55
  };
52
56
  }
53
- export function checkFnmHookPresent(shellProfile, app) {
54
- const present = Boolean(shellProfile?.includes("fnm env") || shellProfile?.includes("fnm_env"));
57
+ export function checkFnmHookPresent(shellProfile) {
58
+ const present = shellProfile !== null &&
59
+ (shellProfile.includes("fnm env") || shellProfile.includes("fnm_env"));
55
60
  return {
56
61
  id: "fnm-not-found",
57
62
  status: present ? "ok" : "warn",
@@ -64,13 +69,13 @@ export function checkFnmHookPresent(shellProfile, app) {
64
69
  faqId: present ? undefined : "fnm-not-found",
65
70
  };
66
71
  }
67
- export function checkPnpmVersion(pnpmVersion, pinned, app) {
72
+ export function checkPnpmVersion(pnpmVersion, pinned) {
68
73
  if (!pnpmVersion) {
69
74
  return {
70
75
  id: "pnpm-missing",
71
76
  status: "warn",
72
77
  summary: "pnpm is not on PATH",
73
- fix: '`npm install -g pnpm` (never `corepack enable` — this repo does not use corepack).',
78
+ fix: "`npm install -g pnpm` (never `corepack enable` — this repo does not use corepack).",
74
79
  faqId: "pnpm-missing",
75
80
  };
76
81
  }
@@ -206,7 +211,12 @@ function readShellProfile() {
206
211
  const home = process.env.HOME;
207
212
  if (!home)
208
213
  return null;
209
- for (const file of [".bashrc", ".zshrc", ".config/fish/config.fish", ".bash_profile"]) {
214
+ for (const file of [
215
+ ".bashrc",
216
+ ".zshrc",
217
+ ".config/fish/config.fish",
218
+ ".bash_profile",
219
+ ]) {
210
220
  const path = join(home, file);
211
221
  if (existsSync(path)) {
212
222
  try {
@@ -234,13 +244,13 @@ export async function runEnvironmentDoctor(opts) {
234
244
  const app = opts.app && TROUBLESHOOTING_APPS.includes(opts.app) ? opts.app : "platform";
235
245
  const repoRoot = discoverRepoRoot();
236
246
  const checks = [];
237
- const nodeCheck = checkNodeVersion(process.versions.node, app);
247
+ const nodeCheck = checkNodeVersion(process.versions.node);
238
248
  checks.push(nodeCheck);
239
249
  // Only worth raising when Node is wrong: nvm, Volta or a system Node that
240
250
  // already satisfies the floor is fine, and flagging a missing fnm hook there
241
251
  // is a false alarm.
242
252
  if (nodeCheck.status !== "ok") {
243
- checks.push(checkFnmHookPresent(readShellProfile(), app));
253
+ checks.push(checkFnmHookPresent(readShellProfile()));
244
254
  }
245
255
  else {
246
256
  const pinnedMajor = repoRoot ? readNvmrc(repoRoot)?.split(".")[0] : null;
@@ -258,14 +268,14 @@ export async function runEnvironmentDoctor(opts) {
258
268
  const pnpmVersion = has("pnpm", ["--version"]);
259
269
  const pin = repoRoot ? readPackageManagerPin(repoRoot) : null;
260
270
  checks.push(pin
261
- ? checkPnpmVersion(pnpmVersion, pin, app)
271
+ ? checkPnpmVersion(pnpmVersion, pin)
262
272
  : {
263
273
  id: "pnpm-version",
264
274
  status: pnpmVersion ? "ok" : "warn",
265
275
  summary: pnpmVersion ? `pnpm ${pnpmVersion}` : "pnpm is not on PATH",
266
276
  fix: pnpmVersion
267
277
  ? undefined
268
- : '`npm install -g pnpm` (never `corepack enable`).',
278
+ : "`npm install -g pnpm` (never `corepack enable`).",
269
279
  faqId: pnpmVersion ? undefined : "pnpm-missing",
270
280
  });
271
281
  checks.push(checkAutocrlf(gitConfig("core.autocrlf")));
@@ -281,7 +291,9 @@ export async function runEnvironmentDoctor(opts) {
281
291
  checks.push({
282
292
  id: "docker-not-running",
283
293
  status: dockerUp ? "ok" : "warn",
284
- summary: dockerUp ? "Docker is running" : "Docker daemon is not answering",
294
+ summary: dockerUp
295
+ ? "Docker is running"
296
+ : "Docker daemon is not answering",
285
297
  fix: dockerUp
286
298
  ? undefined
287
299
  : "Start Docker Desktop, or Docker Engine in WSL2. Never Colima.",
@@ -294,7 +306,9 @@ export async function runEnvironmentDoctor(opts) {
294
306
  id: "flutter-missing",
295
307
  status: flutter ? "ok" : "skip",
296
308
  summary: flutter ? "Flutter is installed" : "Flutter is not installed",
297
- fix: flutter ? undefined : "Install the Flutter SDK for study-group-finder.",
309
+ fix: flutter
310
+ ? undefined
311
+ : "Install the Flutter SDK for study-group-finder.",
298
312
  faqId: flutter ? undefined : "flutter-missing",
299
313
  });
300
314
  if (flutter) {
@@ -322,7 +336,9 @@ export async function runEnvironmentDoctor(opts) {
322
336
  checks.push(checkEnvKeysPresent(env, ["API_URL", "PUBLISHABLE_KEY"]));
323
337
  }
324
338
  // ── Hosted checks — only when .env looks like a hosted project ───────────
325
- const isHosted = Boolean(env.API_URL && !env.API_URL.includes("127.0.0.1") && !env.API_URL.includes("localhost"));
339
+ const isHosted = Boolean(env.API_URL &&
340
+ !env.API_URL.includes("127.0.0.1") &&
341
+ !env.API_URL.includes("localhost"));
326
342
  if (isHosted && env.API_URL) {
327
343
  let reachable = false;
328
344
  let probeStatus = null;
@@ -360,7 +376,10 @@ export async function runEnvironmentDoctor(opts) {
360
376
  let keysValid = false;
361
377
  try {
362
378
  const res = await fetch(`${env.API_URL}/auth/v1/admin/users?page=1&per_page=1`, {
363
- headers: { Authorization: `Bearer ${env.SECRET_KEY}`, apikey: env.SECRET_KEY },
379
+ headers: {
380
+ Authorization: `Bearer ${env.SECRET_KEY}`,
381
+ apikey: env.SECRET_KEY,
382
+ },
364
383
  signal: AbortSignal.timeout(5000),
365
384
  });
366
385
  keysValid = res.status !== 401 && res.status !== 403;
@@ -371,7 +390,9 @@ export async function runEnvironmentDoctor(opts) {
371
390
  checks.push({
372
391
  id: "supabase-keys-invalid",
373
392
  status: keysValid ? "ok" : "warn",
374
- summary: keysValid ? "SECRET_KEY is accepted" : "SECRET_KEY was rejected",
393
+ summary: keysValid
394
+ ? "SECRET_KEY is accepted"
395
+ : "SECRET_KEY was rejected",
375
396
  fix: keysValid
376
397
  ? undefined
377
398
  : "Copy the service_role secret key again from Project Settings -> API.",
@@ -86,7 +86,9 @@ function run(file, args) {
86
86
  * which reads the session's already-entered env at the moment it matters.
87
87
  */
88
88
  export function probeEnvironment(execute = run) {
89
- const envFile = existsSync(join(findRepoRoot(), ".env")) ? "yes" : "no";
89
+ const envFile = existsSync(join(findRepoRoot(), ".env"))
90
+ ? "yes"
91
+ : "no";
90
92
  // `docker info` rather than `docker version`: version answers from the
91
93
  // client alone, so it reports success against a daemon that is not running.
92
94
  const docker = execute("docker", ["info"]) === null ? "no" : "yes";
package/dist/gen/hypno.js CHANGED
@@ -55,7 +55,7 @@ async function loadChromium() {
55
55
  join(findRepoRoot(), "node_modules/.pnpm/playwright@1.62.1/node_modules/playwright"),
56
56
  ]) {
57
57
  try {
58
- const mod = await import(require_.resolve(spec));
58
+ const mod = (await import(require_.resolve(spec)));
59
59
  const chromium = mod.chromium ?? mod.default?.chromium;
60
60
  if (chromium)
61
61
  return chromium;
package/dist/gh/client.js CHANGED
@@ -107,7 +107,8 @@ function describe(err) {
107
107
  "burst of pushes can trip the secondary limit — wait a minute and " +
108
108
  "re-run; already-written values are simply overwritten with themselves.");
109
109
  }
110
- return stderr || e.message || "gh failed with no output.";
110
+ const message = e.message ?? "";
111
+ return stderr || message || "gh failed with no output.";
111
112
  }
112
113
  /** Environment secret names and when each last changed. Never values. */
113
114
  export async function listSecrets(environment) {
@@ -17,7 +17,8 @@ const run = promisify(execFile);
17
17
  function describe(err) {
18
18
  const e = err;
19
19
  const stderr = (e.stderr ?? "").trim();
20
- return stderr || e.message || "gh failed with no output.";
20
+ const message = e.message ?? "";
21
+ return stderr || message || "gh failed with no output.";
21
22
  }
22
23
  /** `GET /orgs/{org}/teams/{slug}` -> the team's numeric id. */
23
24
  export async function resolveTeamId(org, slug) {
@@ -24,7 +24,8 @@ export class GhRulesetsError extends Error {
24
24
  function describe(err) {
25
25
  const e = err;
26
26
  const stderr = (e.stderr ?? "").trim();
27
- return stderr || e.message || "gh failed with no output.";
27
+ const message = e.message ?? "";
28
+ return stderr || message || "gh failed with no output.";
28
29
  }
29
30
  function path(r, suffix = "") {
30
31
  return `repos/${r.owner}/${r.repo}/rulesets${suffix}`;
@@ -36,7 +37,9 @@ export async function listRulesets(r) {
36
37
  maxBuffer: MAX_BUFFER,
37
38
  shell: false,
38
39
  });
39
- return stdout.trim() === "" ? [] : JSON.parse(stdout);
40
+ return stdout.trim() === ""
41
+ ? []
42
+ : JSON.parse(stdout);
40
43
  }
41
44
  catch (err) {
42
45
  throw new GhRulesetsError(describe(err));
@@ -48,7 +48,11 @@ export function buildDesiredRulesets(actors) {
48
48
  // side of a `main` push, so `always` (not `pull_request`-only) is the
49
49
  // right bypass mode here.
50
50
  bypass_actors: [
51
- { actor_id: actors.devopsTeamId, actor_type: "Team", bypass_mode: "always" },
51
+ {
52
+ actor_id: actors.devopsTeamId,
53
+ actor_type: "Team",
54
+ bypass_mode: "always",
55
+ },
52
56
  ],
53
57
  rules: [
54
58
  { type: "deletion" },
@@ -161,7 +165,11 @@ export function buildDesiredRulesets(actors) {
161
165
  },
162
166
  },
163
167
  bypass_actors: [
164
- { actor_id: actors.devopsTeamId, actor_type: "Team", bypass_mode: "always" },
168
+ {
169
+ actor_id: actors.devopsTeamId,
170
+ actor_type: "Team",
171
+ bypass_mode: "always",
172
+ },
165
173
  // Renovate (TASK-299) creates and updates `renovate/*` branches for
166
174
  // its dependency-update PRs — an ordinary bot workflow, not a hole,
167
175
  // so it bypasses the same way the platform App bypasses `team/**`.
@@ -215,8 +223,16 @@ export function buildDesiredRulesets(actors) {
215
223
  ref_name: { include: ["refs/heads/team/**"], exclude: [] },
216
224
  },
217
225
  bypass_actors: [
218
- { actor_id: actors.appId, actor_type: "Integration", bypass_mode: "always" },
219
- { actor_id: actors.devopsTeamId, actor_type: "Team", bypass_mode: "always" },
226
+ {
227
+ actor_id: actors.appId,
228
+ actor_type: "Integration",
229
+ bypass_mode: "always",
230
+ },
231
+ {
232
+ actor_id: actors.devopsTeamId,
233
+ actor_type: "Team",
234
+ bypass_mode: "always",
235
+ },
220
236
  ],
221
237
  rules: [
222
238
  { type: "creation" },
@@ -238,8 +254,16 @@ export function buildDesiredRulesets(actors) {
238
254
  ref_name: { include: ["refs/tags/**"], exclude: [] },
239
255
  },
240
256
  bypass_actors: [
241
- { actor_id: actors.adminsTeamId, actor_type: "Team", bypass_mode: "always" },
242
- { actor_id: actors.devopsTeamId, actor_type: "Team", bypass_mode: "always" },
257
+ {
258
+ actor_id: actors.adminsTeamId,
259
+ actor_type: "Team",
260
+ bypass_mode: "always",
261
+ },
262
+ {
263
+ actor_id: actors.devopsTeamId,
264
+ actor_type: "Team",
265
+ bypass_mode: "always",
266
+ },
243
267
  ],
244
268
  rules: [{ type: "creation" }, { type: "update" }, { type: "deletion" }],
245
269
  },
@@ -164,5 +164,7 @@ export function planRulesets(liveSummaries, liveDetails, actors, desiredRulesets
164
164
  }
165
165
  /** Whether a plan would write anything at all. */
166
166
  export function planHasChanges(plan) {
167
- return (plan.creates.length > 0 || plan.updates.length > 0 || plan.deletes.length > 0);
167
+ return (plan.creates.length > 0 ||
168
+ plan.updates.length > 0 ||
169
+ plan.deletes.length > 0);
168
170
  }
@@ -22,7 +22,8 @@ export class GhSettingsError extends Error {
22
22
  function describe(err) {
23
23
  const e = err;
24
24
  const stderr = (e.stderr ?? "").trim();
25
- return stderr || e.message || "gh failed with no output.";
25
+ const message = e.message ?? "";
26
+ return stderr || message || "gh failed with no output.";
26
27
  }
27
28
  function repoPath(r, suffix = "") {
28
29
  return `repos/${r.owner}/${r.repo}${suffix}`;
@@ -50,7 +51,10 @@ async function writeJson(method, apiPath, body) {
50
51
  const args = body === undefined
51
52
  ? ["api", apiPath, "-X", method]
52
53
  : ["api", apiPath, "-X", method, "--input", "-"];
53
- const child = spawn("gh", args, { stdio: ["pipe", "pipe", "pipe"], shell: false });
54
+ const child = spawn("gh", args, {
55
+ stdio: ["pipe", "pipe", "pipe"],
56
+ shell: false,
57
+ });
54
58
  let stdout = "";
55
59
  let stderr = "";
56
60
  child.stdout.on("data", (c) => (stdout += c.toString()));
@@ -129,7 +133,11 @@ export async function getSelectedActions(r) {
129
133
  catch (err) {
130
134
  if (err instanceof GhSettingsError &&
131
135
  (/\b409\b/.test(err.message) || /\(conflict\)/i.test(err.message))) {
132
- return { github_owned_allowed: false, verified_allowed: false, patterns_allowed: [] };
136
+ return {
137
+ github_owned_allowed: false,
138
+ verified_allowed: false,
139
+ patterns_allowed: [],
140
+ };
133
141
  }
134
142
  throw err;
135
143
  }
@@ -16,7 +16,7 @@ import { confirm } from "@clack/prompts";
16
16
  import { addDeploymentBranchPolicy, deleteDeploymentBranchPolicy, getActionsPermissions, getAutomatedSecurityFixes, getDeploymentBranchPolicies, getEnvironment, getRepo, getSelectedActions, getVulnerabilityAlertsEnabled, getWorkflowPermissions, patchSecurityAndAnalysis, setActionsPermissions, setAutomatedSecurityFixesEnabled, setDeploymentBranchPolicyMode, setSelectedActions, setVulnerabilityAlertsEnabled, setWorkflowPermissions, } from "./api.js";
17
17
  import { buildDesiredSettings } from "./desired.js";
18
18
  import { planHasFixableChanges, planSettings, } from "./diff.js";
19
- import { collectActionUses, computeActionPatterns, unpinnedActionUses } from "./workflows.js";
19
+ import { collectActionUses, computeActionPatterns, unpinnedActionUses, } from "./workflows.js";
20
20
  import { findRepoRoot } from "../../repo/root.js";
21
21
  import { unwrap } from "../../ui.js";
22
22
  const DEFAULT_ORG = "DevDogsUGA";
@@ -113,16 +113,22 @@ async function confirmApply(yes) {
113
113
  * `PUT .../actions/permissions`) are written together, once.
114
114
  */
115
115
  async function applyPlan(r, desired, live, plan) {
116
- const drift = new Set(plan.checks.filter((c) => c.status === "drift" && c.fixable).map((c) => c.key));
116
+ const drift = new Set(plan.checks
117
+ .filter((c) => c.status === "drift" && c.fixable)
118
+ .map((c) => c.key));
117
119
  const secretScanningFields = {};
118
120
  if (drift.has("security_and_analysis.secret_scanning")) {
119
121
  secretScanningFields.secret_scanning = {
120
- status: desired.securityAndAnalysis.secretScanning ? "enabled" : "disabled",
122
+ status: desired.securityAndAnalysis.secretScanning
123
+ ? "enabled"
124
+ : "disabled",
121
125
  };
122
126
  }
123
127
  if (drift.has("security_and_analysis.secret_scanning_push_protection")) {
124
128
  secretScanningFields.secret_scanning_push_protection = {
125
- status: desired.securityAndAnalysis.secretScanningPushProtection ? "enabled" : "disabled",
129
+ status: desired.securityAndAnalysis.secretScanningPushProtection
130
+ ? "enabled"
131
+ : "disabled",
126
132
  };
127
133
  }
128
134
  if (drift.has("security_and_analysis.secret_scanning_non_provider_patterns")) {
@@ -134,7 +140,9 @@ async function applyPlan(r, desired, live, plan) {
134
140
  }
135
141
  if (drift.has("security_and_analysis.secret_scanning_validity_checks")) {
136
142
  secretScanningFields.secret_scanning_validity_checks = {
137
- status: desired.securityAndAnalysis.secretScanningValidityChecks ? "enabled" : "disabled",
143
+ status: desired.securityAndAnalysis.secretScanningValidityChecks
144
+ ? "enabled"
145
+ : "disabled",
138
146
  };
139
147
  }
140
148
  if (Object.keys(secretScanningFields).length > 0) {
@@ -146,7 +154,8 @@ async function applyPlan(r, desired, live, plan) {
146
154
  if (drift.has("dependabot_security_updates")) {
147
155
  await setAutomatedSecurityFixesEnabled(r, desired.dependabotSecurityUpdates);
148
156
  }
149
- if (drift.has("actions.sha_pinning_required") || drift.has("actions.allowed_actions")) {
157
+ if (drift.has("actions.sha_pinning_required") ||
158
+ drift.has("actions.allowed_actions")) {
150
159
  await setActionsPermissions(r, {
151
160
  enabled: live.actionsPermissions.enabled,
152
161
  allowed_actions: drift.has("actions.allowed_actions")
@@ -28,7 +28,11 @@ export function buildDesiredSettings(actionPatterns) {
28
28
  },
29
29
  environments: [
30
30
  { name: "staging", allowedBranches: ["main"], requireReviewers: false },
31
- { name: "production", allowedBranches: ["production"], requireReviewers: false },
31
+ {
32
+ name: "production",
33
+ allowedBranches: ["production"],
34
+ requireReviewers: false,
35
+ },
32
36
  {
33
37
  name: "production-apply",
34
38
  allowedBranches: ["production"],
@@ -81,7 +81,8 @@ export function planSettings(desired, live, unpinnedActions) {
81
81
  : "drift",
82
82
  fixable: true,
83
83
  });
84
- const patternsMatch = live.selectedActions.github_owned_allowed === desired.actions.githubOwnedAllowed &&
84
+ const patternsMatch = live.selectedActions.github_owned_allowed ===
85
+ desired.actions.githubOwnedAllowed &&
85
86
  live.selectedActions.verified_allowed === desired.actions.verifiedAllowed &&
86
87
  sameSet(live.selectedActions.patterns_allowed, desired.actions.patterns);
87
88
  checks.push({
@@ -118,21 +119,25 @@ export function planSettings(desired, live, unpinnedActions) {
118
119
  continue;
119
120
  }
120
121
  const liveBranches = snapshot.branchPolicies.map((p) => p.name);
121
- const branchesMatch = snapshot.environment.deployment_branch_policy?.custom_branch_policies === true &&
122
- snapshot.environment.deployment_branch_policy?.protected_branches === false &&
122
+ const branchesMatch = snapshot.environment.deployment_branch_policy?.custom_branch_policies ===
123
+ true &&
124
+ snapshot.environment.deployment_branch_policy?.protected_branches ===
125
+ false &&
123
126
  sameSet(liveBranches, env.allowedBranches);
124
127
  checks.push({
125
128
  key: `environments.${env.name}.branch_policy`,
126
129
  description: `Environment "${env.name}" deployment branch policy`,
127
130
  desired: [...env.allowedBranches].sort().join(", "),
128
131
  live: snapshot.environment.deployment_branch_policy
129
- ? [...liveBranches].sort().join(", ") || "(no branch restriction — all branches allowed)"
132
+ ? [...liveBranches].sort().join(", ") ||
133
+ "(no branch restriction — all branches allowed)"
130
134
  : "(no deployment branch policy configured)",
131
135
  status: branchesMatch ? "ok" : "drift",
132
136
  fixable: true,
133
137
  });
134
138
  if (env.requireReviewers) {
135
- const hasReviewers = (snapshot.environment.protection_rules ?? []).some((rule) => rule.type === "required_reviewers" && (rule.reviewers?.length ?? 0) > 0);
139
+ const hasReviewers = (snapshot.environment.protection_rules ?? []).some((rule) => rule.type === "required_reviewers" &&
140
+ (rule.reviewers?.length ?? 0) > 0);
136
141
  checks.push({
137
142
  key: `environments.${env.name}.required_reviewers`,
138
143
  description: `Environment "${env.name}" required reviewers`,
@@ -82,7 +82,11 @@ export function parseUsesLines(file, contents) {
82
82
  }
83
83
  out.push({
84
84
  raw: value,
85
- ref: { owner: spec.slice(0, slash), repo: spec.slice(slash + 1), version },
85
+ ref: {
86
+ owner: spec.slice(0, slash),
87
+ repo: spec.slice(slash + 1),
88
+ version,
89
+ },
86
90
  file,
87
91
  line: i + 1,
88
92
  });
@@ -104,7 +104,10 @@ async function loadEvents(patterns, deps) {
104
104
  throw err;
105
105
  return { graphics: [], skipped: errorMessage(err) };
106
106
  }
107
- return { graphics: eventGraphics(await loadOpenGraph(), meetings), skipped: null };
107
+ return {
108
+ graphics: eventGraphics(await loadOpenGraph(), meetings),
109
+ skipped: null,
110
+ };
108
111
  }
109
112
  /** Where one selection is written. */
110
113
  export function destinationOf({ graphic, format }, out) {
@@ -183,13 +186,12 @@ async function images(options, deps) {
183
186
  if (selections.length === 0)
184
187
  throw new UsageError("Nothing to render.");
185
188
  // ── Where ─────────────────────────────────────────────────────────────────
189
+ // An empty `--out` counts as absent, exactly as the old truthiness test did.
190
+ const explicitOut = options.out === "" ? undefined : options.out;
186
191
  const out = options.defaultOut
187
192
  ? undefined
188
- : options.out
189
- ? options.out
190
- : options.noOutput
191
- ? undefined
192
- : await pickOutput(graphics);
193
+ : (explicitOut ??
194
+ (options.noOutput ? undefined : await pickOutput(graphics)));
193
195
  if (skipped)
194
196
  log.warn(`event graphics skipped — ${skipped}`);
195
197
  if (options.noOutput) {
@@ -37,7 +37,11 @@ async function satoriFonts() {
37
37
  })));
38
38
  }
39
39
  export async function render(element, { width, height, scale = 1 }) {
40
- const svg = await satori(element, { width, height, fonts: await satoriFonts() });
40
+ const svg = await satori(element, {
41
+ width,
42
+ height,
43
+ fonts: await satoriFonts(),
44
+ });
41
45
  const png = new Resvg(svg, {
42
46
  fitTo: { mode: "width", value: Math.round(width * scale) },
43
47
  // Satori has already turned every glyph into a path, so resvg never needs
package/dist/launch-ci.js CHANGED
@@ -54,7 +54,9 @@ export async function launchCi(argv) {
54
54
  }
55
55
  const tier = resolution.tier;
56
56
  try {
57
- const entered = await envSession.enterEnvironment(tier, { override: false });
57
+ const entered = await envSession.enterEnvironment(tier, {
58
+ override: false,
59
+ });
58
60
  for (const warning of entered.warnings) {
59
61
  process.stderr.write(`devtools-ci: ${warning}\n`);
60
62
  }
package/dist/launch.js CHANGED
@@ -52,7 +52,7 @@ import { select } from "@clack/prompts";
52
52
  import { findCommand } from "./commands.js";
53
53
  import { enterSessionEnvironment, realEnvEntryDeps, setMenuEnvHook, } from "./env-entry.js";
54
54
  import { bareGroupStartPath } from "./menu.js";
55
- import { discoverRepoRoot, findRepoRoot, RepoNotFoundError } from "./repo/root.js";
55
+ import { discoverRepoRoot, findRepoRoot, RepoNotFoundError, } from "./repo/root.js";
56
56
  import { loadEnvLoad, loadEnvSession } from "./repo/peers.js";
57
57
  import { captureDevtoolsError, initDevtoolsTelemetry } from "./telemetry.js";
58
58
  import { ignoreClosedPipes } from "./pipes.js";
@@ -188,12 +188,12 @@ export async function launch(argv) {
188
188
  // holds this session's entered values, not the outer shell's, and
189
189
  // recomputing from it would misclassify every one of them as a genuine
190
190
  // shell export — the exact bug this marker exists to prevent, one layer in.
191
- if (process.env[envLoad.SHELL_KEYS_ENV] === undefined) {
192
- process.env[envLoad.SHELL_KEYS_ENV] = Object.keys(process.env).join(",");
193
- }
191
+ process.env[envLoad.SHELL_KEYS_ENV] ??= Object.keys(process.env).join(",");
194
192
  let tier;
195
193
  let devDatabase;
196
- if (rest[0] === "setup" || rest[0] === "completions" || isEnvFreeCommand(rest)) {
194
+ if (rest[0] === "setup" ||
195
+ rest[0] === "completions" ||
196
+ isEnvFreeCommand(rest)) {
197
197
  // Two commands run BEFORE there is a tier to resolve, and forcing the
198
198
  // mandate on them breaks each in its own way:
199
199
  //
@@ -246,7 +246,9 @@ export async function launch(argv) {
246
246
  const remoteCandidate = couldBeBareDevelopment
247
247
  ? await envSession.developmentRemoteCandidate(findRepoRoot())
248
248
  : undefined;
249
- const localStackOnline = typeof remoteCandidate === "string" ? await envLoad.probeLocalStack() : undefined;
249
+ const localStackOnline = typeof remoteCandidate === "string"
250
+ ? await envLoad.probeLocalStack()
251
+ : undefined;
250
252
  const resolution = await envSession.resolveSessionTier({
251
253
  explicit,
252
254
  deployEnv: process.env.DEPLOY_ENV,
@@ -25,11 +25,13 @@
25
25
  import { adminClient, signedInClient, } from "./instance.js";
26
26
  /** Runs `platform.conformance_check()` as the given moderator client. */
27
27
  export async function conformance(client, appSlug) {
28
- const { data, error } = await client.rpc("conformance_check", {
28
+ const response = await client.rpc("conformance_check", {
29
29
  app_slug: appSlug,
30
30
  });
31
- if (error)
32
- throw new Error(error.message);
31
+ if (response.error)
32
+ throw new Error(response.error.message);
33
+ // The generated types do not describe this function's return shape.
34
+ const data = response.data;
33
35
  return (data ?? []);
34
36
  }
35
37
  /**
package/dist/oauth/db.js CHANGED
@@ -35,7 +35,7 @@ export function detectLocalSupabase(cwd) {
35
35
  if (first.code === "ENOENT") {
36
36
  throw new Error("supabase CLI not found — install it from https://supabase.com/docs/guides/cli");
37
37
  }
38
- const stderr = first.stderr?.trim();
38
+ const stderr = (first.stderr ?? "").trim();
39
39
  throw new Error(stderr || (first instanceof Error ? first.message : String(first)));
40
40
  }
41
41
  }
@@ -50,7 +50,9 @@ export async function requestDeviceCode({ platformUrl, label, callbackUri, fetch
50
50
  if (response.status === 429) {
51
51
  const retryAfterSeconds = retryAfterSecondsFrom(response);
52
52
  throw new DeviceError("rate_limited", `${url} is rate-limited` +
53
- (retryAfterSeconds !== undefined ? ` — retry after ${retryAfterSeconds}s.` : "."), retryAfterSeconds);
53
+ (retryAfterSeconds !== undefined
54
+ ? ` — retry after ${retryAfterSeconds}s.`
55
+ : "."), retryAfterSeconds);
54
56
  }
55
57
  let body;
56
58
  try {
@@ -61,8 +63,12 @@ export async function requestDeviceCode({ platformUrl, label, callbackUri, fetch
61
63
  }
62
64
  if (!response.ok) {
63
65
  const errBody = (body ?? {});
64
- const description = typeof errBody.error_description === "string" ? errBody.error_description : undefined;
65
- const code = typeof errBody.error === "string" ? errBody.error : `HTTP ${response.status}`;
66
+ const description = typeof errBody.error_description === "string"
67
+ ? errBody.error_description
68
+ : undefined;
69
+ const code = typeof errBody.error === "string"
70
+ ? errBody.error
71
+ : `HTTP ${response.status}`;
66
72
  throw new DeviceError("invalid_request", description ? `${code}: ${description}` : code);
67
73
  }
68
74
  const result = (body ?? {});
@@ -167,7 +173,9 @@ export async function pollForToken({ platformUrl, deviceCode, intervalSeconds, e
167
173
  }
168
174
  const errBody = (body ?? {});
169
175
  const errorCode = typeof errBody.error === "string" ? errBody.error : undefined;
170
- const description = typeof errBody.error_description === "string" ? errBody.error_description : undefined;
176
+ const description = typeof errBody.error_description === "string"
177
+ ? errBody.error_description
178
+ : undefined;
171
179
  switch (errorCode) {
172
180
  case "authorization_pending":
173
181
  continue;
@@ -53,7 +53,9 @@ export async function exchangeCode({ platformUrl, code, codeVerifier, }) {
53
53
  }
54
54
  if (response.status === 429) {
55
55
  const retryAfterHeader = response.headers.get("retry-after");
56
- const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : undefined;
56
+ const retryAfterSeconds = retryAfterHeader
57
+ ? Number(retryAfterHeader)
58
+ : undefined;
57
59
  throw new ExchangeError("rate_limited", `${url} is rate-limited` +
58
60
  (Number.isFinite(retryAfterSeconds)
59
61
  ? ` — retry after ${retryAfterSeconds}s.`
@@ -69,8 +71,12 @@ export async function exchangeCode({ platformUrl, code, codeVerifier, }) {
69
71
  if (!response.ok) {
70
72
  const errBody = (body ?? {});
71
73
  const kind = errBody.error === "invalid_grant" ? "invalid_grant" : "invalid_request";
72
- const description = typeof errBody.error_description === "string" ? errBody.error_description : undefined;
73
- const code = typeof errBody.error === "string" ? errBody.error : `HTTP ${response.status}`;
74
+ const description = typeof errBody.error_description === "string"
75
+ ? errBody.error_description
76
+ : undefined;
77
+ const code = typeof errBody.error === "string"
78
+ ? errBody.error
79
+ : `HTTP ${response.status}`;
74
80
  throw new ExchangeError(kind, description ? `${code}: ${description}` : code);
75
81
  }
76
82
  const result = (body ?? {});
@@ -66,7 +66,7 @@ export function verifyState(expected, callback) {
66
66
  return callback;
67
67
  }
68
68
  function renderCallbackPage(heading, body) {
69
- return ("<!doctype html><html><head><meta charset=\"utf-8\"><title>DevDogs devtools</title></head>" +
69
+ return ('<!doctype html><html><head><meta charset="utf-8"><title>DevDogs devtools</title></head>' +
70
70
  `<body style="font-family: system-ui, sans-serif; max-width: 32rem; margin: 4rem auto; text-align: center;">` +
71
71
  `<h1>${heading}</h1><p>${body}</p></body></html>`);
72
72
  }
@@ -96,7 +96,7 @@ export function start(opts = {}) {
96
96
  // moments later — this dummy handler exists purely to be that first
97
97
  // listener; it does not consume the rejection for real callers, who
98
98
  // still get their own `await`/`.catch` on the same promise.
99
- result.catch(() => { });
99
+ result.catch(() => undefined);
100
100
  const server = createServer((req, res) => {
101
101
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
102
102
  if (url.pathname !== "/callback") {
@@ -107,16 +107,22 @@ export function start(opts = {}) {
107
107
  const state = url.searchParams.get("state") ?? undefined;
108
108
  const error = url.searchParams.get("error");
109
109
  if (error) {
110
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(DENIED_PAGE);
110
+ res
111
+ .writeHead(200, { "content-type": "text/html; charset=utf-8" })
112
+ .end(DENIED_PAGE);
111
113
  settle(() => rejectResult(new CallbackDeniedError(error, state, url.searchParams.get("error_description") ?? undefined)));
112
114
  return;
113
115
  }
114
116
  if (!code || !state) {
115
- res.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(DENIED_PAGE);
117
+ res
118
+ .writeHead(400, { "content-type": "text/html; charset=utf-8" })
119
+ .end(DENIED_PAGE);
116
120
  settle(() => rejectResult(new CallbackMalformedError()));
117
121
  return;
118
122
  }
119
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(SUCCESS_PAGE);
123
+ res
124
+ .writeHead(200, { "content-type": "text/html; charset=utf-8" })
125
+ .end(SUCCESS_PAGE);
120
126
  settle(() => resolveResult({ code, state }));
121
127
  });
122
128
  const timer = setTimeout(() => {
@@ -28,7 +28,7 @@ import { openBrowser } from "./browser.js";
28
28
  import { codeChallengeFor, generateCodeVerifier, generateState, } from "./pkce.js";
29
29
  import { CallbackDeniedError, CallbackMalformedError, CallbackTimeoutError, StateMismatchError, start as startLoopback, verifyState, } from "./loopback.js";
30
30
  import { buildConnectUrl } from "./connect-url.js";
31
- import { ExchangeError, exchangeCode } from "./exchange.js";
31
+ import { ExchangeError, exchangeCode, } from "./exchange.js";
32
32
  import { DeviceError, PollError, pollForToken, requestDeviceCode, } from "./device.js";
33
33
  import { decideTransport } from "./transport.js";
34
34
  import { defaultLabel } from "./label.js";
@@ -70,6 +70,11 @@ function requireUrl(v) {
70
70
  return "Enter a valid URL";
71
71
  }
72
72
  }
73
+ /** An environment variable's value, with an empty one treated as unset. */
74
+ function nonEmptyEnv(key) {
75
+ const value = process.env[key];
76
+ return value === "" ? undefined : value;
77
+ }
73
78
  function requireNonEmpty(v) {
74
79
  return v?.trim() ? undefined : "Required";
75
80
  }
@@ -101,7 +106,11 @@ async function chooseHostedTargetFromEnvOrPrompt(cwd) {
101
106
  const fromFiles = readHostedTargetFromEnvFiles(cwd);
102
107
  if (fromFiles.apiUrl && fromFiles.serviceRoleKey) {
103
108
  log.info("Using the Supabase URL and service-role key from .env.local/.env.");
104
- return { apiUrl: fromFiles.apiUrl, serviceRoleKey: fromFiles.serviceRoleKey, kind: "hosted" };
109
+ return {
110
+ apiUrl: fromFiles.apiUrl,
111
+ serviceRoleKey: fromFiles.serviceRoleKey,
112
+ kind: "hosted",
113
+ };
105
114
  }
106
115
  const apiUrl = unwrap(await text({ message: "Supabase project URL", validate: requireUrl })).replace(/\/+$/, "");
107
116
  const serviceRoleKey = unwrap(await password({
@@ -343,9 +352,11 @@ export async function connectViaOneClick(target, platformUrl, cwd, transportOver
343
352
  async function connectByPasting(baseUrlOverride) {
344
353
  // Not prompted: contributors always want the canonical API. `--base-url` /
345
354
  // `OAUTH_BASE_URL` stay as overrides for testing against another project.
346
- const baseUrl = (baseUrlOverride ?? process.env[ENV_KEYS.baseUrl] ?? DEFAULT_API_URL).replace(/\/+$/, "");
347
- let clientId = process.env[ENV_KEYS.clientId] || undefined;
348
- let clientSecret = process.env[ENV_KEYS.clientSecret] || undefined;
355
+ const baseUrl = (baseUrlOverride ??
356
+ process.env[ENV_KEYS.baseUrl] ??
357
+ DEFAULT_API_URL).replace(/\/+$/, "");
358
+ let clientId = nonEmptyEnv(ENV_KEYS.clientId);
359
+ let clientSecret = nonEmptyEnv(ENV_KEYS.clientSecret);
349
360
  if (clientId && clientSecret) {
350
361
  const useSaved = unwrap(await confirm({
351
362
  message: `Use saved credentials? (client ID: ${clientId})`,
package/dist/persona.js CHANGED
@@ -28,7 +28,7 @@
28
28
  */
29
29
  import { randomUUID } from "node:crypto";
30
30
  import { confirm, log, note, select } from "@clack/prompts";
31
- import { adminClient, resolveInstance, } from "./instance.js";
31
+ import { adminClient, resolveInstance } from "./instance.js";
32
32
  import { grantModerator } from "./moderation.js";
33
33
  import { explain, unwrap } from "./ui.js";
34
34
  export const PERSONA_KINDS = ["member", "moderator"];
@@ -37,7 +37,7 @@ export const CHECK_MIGRATIONS = "select count(*)::int as n from supabase_migrati
37
37
  export async function checkPlanner(db) {
38
38
  const lines = [];
39
39
  const [identity] = await db.run(CHECK_IDENTITY);
40
- const who = String(identity?.who ?? "");
40
+ const who = typeof identity?.who === "string" ? identity.who : "";
41
41
  if (who !== PLANNER_ROLE) {
42
42
  return {
43
43
  ok: false,
@@ -337,7 +337,7 @@ export async function runPlannerDrop(options = {}) {
337
337
  return;
338
338
  }
339
339
  const stored = doc.get("DB_URL");
340
- if (stored && stored.includes(PLANNER_ROLE)) {
340
+ if (stored?.includes(PLANNER_ROLE)) {
341
341
  doc.set("DB_URL", "");
342
342
  await writeFile(path, doc.toString());
343
343
  log.info(`Blanked the dead DB_URL in ${fileFor("preflight")}. After the next ` +
@@ -24,7 +24,7 @@ export function connectDb(url) {
24
24
  });
25
25
  return {
26
26
  async run(query) {
27
- return (await sql.unsafe(query));
27
+ return await sql.unsafe(query);
28
28
  },
29
29
  async end() {
30
30
  await sql.end({ timeout: 5 });
@@ -4,7 +4,7 @@ import type { OpenGraphModule } from "../images/open-graph-types.js";
4
4
  export declare function loadOpenGraph(): Promise<OpenGraphModule>;
5
5
  import type { EmailTemplates } from "../emails/email-types.js";
6
6
  export interface EmailModule {
7
- render<K extends keyof EmailTemplates>(name: K, props: EmailTemplates[K]): {
7
+ render: <K extends keyof EmailTemplates>(name: K, props: EmailTemplates[K]) => {
8
8
  subject: string;
9
9
  html: string;
10
10
  text: string;
package/dist/run/pick.js CHANGED
@@ -417,7 +417,7 @@ async function runDev(plan, filters, rest, tierEnv) {
417
417
  }));
418
418
  // Ctrl-C reaches every child through the terminal's process group; this
419
419
  // process waits for them to finish rather than dying first.
420
- const ignore = () => { };
420
+ const ignore = () => undefined;
421
421
  process.on("SIGINT", ignore);
422
422
  process.on("SIGTERM", ignore);
423
423
  const first = await Promise.race(children.map((child) => new Promise((resolve) => {
package/dist/setup.js CHANGED
@@ -16,7 +16,7 @@
16
16
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
17
17
  import { execFileSync } from "node:child_process";
18
18
  import { join } from "node:path";
19
- import { confirm, log, note, password, select, spinner, text } from "@clack/prompts";
19
+ import { confirm, log, note, password, select, spinner, text, } from "@clack/prompts";
20
20
  import { loadRegistry } from "./env/discovery.js";
21
21
  import { renderInit, resolveSections } from "./env/example.js";
22
22
  import { discoverRepoRoot } from "./repo/root.js";
package/dist/telemetry.js CHANGED
@@ -44,7 +44,9 @@ function bakedSentryDsn() {
44
44
  }
45
45
  /** The run-time `DEVTOOLS_SENTRY_DSN` if set, else the baked one. */
46
46
  export function resolveDevtoolsDsn(env, baked) {
47
- return env.DEVTOOLS_SENTRY_DSN || baked;
47
+ // An empty override (`DEVTOOLS_SENTRY_DSN=`) must fall back to the baked DSN.
48
+ const override = env.DEVTOOLS_SENTRY_DSN ?? "";
49
+ return override || baked;
48
50
  }
49
51
  let initialized = false;
50
52
  /**
package/dist/tier.js CHANGED
@@ -50,10 +50,13 @@ export async function resolveTier(given, message, opts = {}) {
50
50
  // file is missing, which `loadEnvironment` then reports — so it is validated
51
51
  // for shape only, not existence, and does NOT fall through to the picker.
52
52
  const entered = opts.deployEnv ?? process.env.DEPLOY_ENV;
53
- if (entered !== undefined && entered !== "" && env.isDeployEnvironment(entered)) {
53
+ if (entered !== undefined &&
54
+ entered !== "" &&
55
+ env.isDeployEnvironment(entered)) {
54
56
  return entered;
55
57
  }
56
- const available = opts.available ?? (await (await loadEnvSession()).availableTiers(findRepoRoot()));
58
+ const available = opts.available ??
59
+ (await (await loadEnvSession()).availableTiers(findRepoRoot()));
57
60
  // Zero or one tier file present: there is nothing to choose between, so
58
61
  // asking would be a question with one possible answer. Fall through to
59
62
  // whichever tier is present, or development when nothing is (a machine
package/dist/version.js CHANGED
@@ -6,9 +6,7 @@ let cachedOwnDir;
6
6
  * i.e. the devtools package's own root (`dist/version.js` → up one).
7
7
  * Exported for tests; memoized since it never changes within a process. */
8
8
  export function ownPackageDir() {
9
- if (cachedOwnDir === undefined) {
10
- cachedOwnDir = join(dirname(fileURLToPath(import.meta.url)), "..");
11
- }
9
+ cachedOwnDir ??= join(dirname(fileURLToPath(import.meta.url)), "..");
12
10
  return cachedOwnDir;
13
11
  }
14
12
  /** Reads this build's own `package.json` version — used by `telemetry.ts`
package/dist/workers.js CHANGED
@@ -21,9 +21,7 @@ import { findRepoRoot } from "./repo/root.js";
21
21
  let cachedWorkerPaths;
22
22
  /** Workspace-relative paths, exactly as listed in root `workers.json`. */
23
23
  export function workerPaths() {
24
- if (cachedWorkerPaths === undefined) {
25
- cachedWorkerPaths = JSON.parse(readFileSync(join(findRepoRoot(), "workers.json"), "utf8"));
26
- }
24
+ cachedWorkerPaths ??= JSON.parse(readFileSync(join(findRepoRoot(), "workers.json"), "utf8"));
27
25
  return cachedWorkerPaths;
28
26
  }
29
27
  /** Basenames, e.g. `"schedule-builder"` — the `pnpm --filter` / CLI slug. */
@@ -7,7 +7,7 @@ import { basename, join } from "node:path";
7
7
  import { setTimeout as delay } from "node:timers/promises";
8
8
  import { confirm, select, text } from "@clack/prompts";
9
9
  import { loadEnvLoad } from "../repo/peers.js";
10
- import { createTemporaryWranglerEnv, renderWranglerEnvFile, scopedProcessEnv, } from "../cf/local-env.js";
10
+ import { createTemporaryWranglerEnv, scopedProcessEnv, } from "../cf/local-env.js";
11
11
  import { buildWorkspaceDeps, needsFrameworkBuild } from "../cf/build.js";
12
12
  import { runWithStderr } from "../db/run.js";
13
13
  import { findRepoRoot } from "../repo/root.js";
@@ -614,7 +614,8 @@ export async function runWorkflowsRun(argv) {
614
614
  let triggerEnv;
615
615
  if (tier !== "development") {
616
616
  try {
617
- triggerEnv = (await envLoad.loadEnvironment(tier, { override: true })).env;
617
+ triggerEnv = (await envLoad.loadEnvironment(tier, { override: true }))
618
+ .env;
618
619
  }
619
620
  catch (err) {
620
621
  if (err instanceof envLoad.MissingEnvFileError) {
package/env.ts CHANGED
@@ -78,11 +78,11 @@ declare({
78
78
  .optional(),
79
79
  {
80
80
  doc:
81
- "The Cloudflare account the Workers live in. OpenNext's R2 cache " +
82
- "provisioning reads it from the environment and cannot infer it " +
83
- "from the scoped CLOUDFLARE_API_TOKEN (the first staging deploy " +
84
- "hung on exactly that); wrangler honors it too. Identifies, does " +
85
- "not authorize — every capability is the token's.",
81
+ "The Cloudflare account the Workers live in. Wrangler honors this " +
82
+ "public identifier in the environment; each app's wrangler config " +
83
+ "pins the same value so scoped deploy tokens never need account " +
84
+ "discovery. Identifies, does not authorize — every capability is " +
85
+ "the token's.",
86
86
  scope: "default",
87
87
  secrecy: "public",
88
88
  example: "61d185ff419ef7bd5bd4b3d314081a49",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devdogsuga/devtools",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Contributor CLI for a DevDogsUGA checkout: database, env, images, emails, cron, deploy. Ships built JS; run via `pnpm dlx @devdogsuga/devtools` (or the `devtools`/`devtools-ci` bins once installed) from inside a DevDogsUGA clone.",
@@ -38,7 +38,7 @@
38
38
  "tsx": "^4.23.12",
39
39
  "typescript": "^6.0.3",
40
40
  "zod": "4.4.3",
41
- "@devdogsuga/telemetry": "0.1.1"
41
+ "@devdogsuga/telemetry": "0.1.2"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@devdogsuga/brand": "*",
@@ -64,13 +64,14 @@
64
64
  "@types/node": "^26.2.0",
65
65
  "@types/react": "^19.2.18",
66
66
  "@vitest/coverage-v8": "^4.1.11",
67
+ "eslint": "^9.39.5",
67
68
  "react": "^19.2.8",
68
69
  "vitest": "^4.1.11",
69
- "@devdogsuga/brand": "0.1.2",
70
- "@devdogsuga/config": "0.1.1",
71
- "@devdogsuga/db": "0.1.1",
72
- "@devdogsuga/events": "0.1.3",
73
- "@devdogsuga/env": "0.1.2"
70
+ "@devdogsuga/brand": "0.1.3",
71
+ "@devdogsuga/config": "0.1.2",
72
+ "@devdogsuga/env": "0.1.3",
73
+ "@devdogsuga/db": "0.1.2",
74
+ "@devdogsuga/events": "0.1.4"
74
75
  },
75
76
  "scripts": {
76
77
  "build": "tsc && node scripts/write-build-info.mjs",
@@ -80,6 +81,8 @@
80
81
  "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
81
82
  "test": "vitest run",
82
83
  "test:watch": "vitest",
83
- "test:contract": "vitest run --config vitest.contract.config.ts"
84
+ "test:contract": "vitest run --config vitest.contract.config.ts",
85
+ "lint": "eslint .",
86
+ "lint:fix": "eslint . --fix"
84
87
  }
85
88
  }