@devdogsuga/backstage 0.1.5 → 0.1.6

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 CHANGED
@@ -14,7 +14,8 @@ match. The two flags matter outside a DevDogsUGA checkout, where the workspace's
14
14
  `pnpm backstage` is the script).
15
15
 
16
16
  It **starts without a checkout**. Help, `version`, `completions`, the tools
17
- below that need no secrets (`graphics`, `qr`, `github`, `newsletter`, `creds`) and
17
+ below that need no secrets (`graphics`, `qr`, `github`, `newsletter`, `creds`),
18
+ `import`, `export` and
18
19
  anything run with `--no-env` never look for one; a command that reads a checkout says
19
20
  "run this from inside a DevDogsUGA clone" and exits 1. The DevDogsUGA libraries
20
21
  it reads (`@devdogsuga/env`, `@devdogsuga/db`) are optional peers, resolved
@@ -33,25 +34,31 @@ pnpm backstage qr https://devdogsuga.org --format svg,png,webp --logo acm
33
34
  pnpm backstage newsletter send 3.0.1 --to a@uga.edu # asks first; --yes with no terminal
34
35
  pnpm backstage creds # share a club login with officers, as a Bitwarden Send
35
36
  pnpm backstage creds renew # extend every Send 30 days
37
+ pnpm backstage import involvement --file OrganizationRoster.csv # verify members; previews, then asks
38
+ pnpm backstage import attendance --meeting 2026-09-09 --file sign-in.csv
39
+ pnpm backstage export stars --from 2026-08-17 # audited under your gh login
36
40
  ```
37
41
 
38
42
  ## Commands
39
43
 
40
- | Command | What it does |
41
- | ---------------------------------------------- | -------------------------------------------------------------------------------------- |
42
- | `deploy <app> --tier <t>` | Checks `CLOUDFLARE_API_TOKEN`, writes the Worker's secrets file, deploys, removes it. |
43
- | `deploy write-env` | Composes `.env.<DEPLOY_ENV>` from the GitHub environment. |
44
- | `deploy preflight` | Classifies the project: paused (skip) or broken (fail). |
45
- | `deploy plan`, `deploy migrate` | Dry-run the migrations into the job summary; apply them to `DB_URL`. |
46
- | `deploy smoke --tier <t> [--app]` | Public routes answer 200, the auth redirect works, this deploy's Sentry release shows. |
47
- | `deploy reconcile --tier <t>` | The platform's config reconcile, after the deploy (`CRON_SECRET`). |
48
- | `env pull\|push\|audit --target <t>` | One env file per target, synced to Bitwarden and GitHub. `audit` lists orphans. |
49
- | `planner status\|create\|reset-password\|drop` | The `migration_planner` role the preflight tier holds. |
50
- | `graphics [graphic…]` | Club images from `@devdogsuga/brand`: `brand/*`, `app/*`, `event/*`. |
51
- | `qr <text>` | QR codes with every option of `/console/qr`. |
52
- | `github rulesets\|settings` | Diff (and with `--apply` write) GitHub config, through `gh`. |
53
- | `newsletter render\|draft\|send <issue…>` | Changelog issues as files, mailbox drafts, or a send. |
54
- | `creds send\|add\|renew\|list\|report` | Club logins from Bitwarden as email-verified Sends, and the Linear report. |
44
+ | Command | What it does |
45
+ | ------------------------------------------------ | -------------------------------------------------------------------------------------- |
46
+ | `deploy <app> --tier <t>` | Checks `CLOUDFLARE_API_TOKEN`, writes the Worker's secrets file, deploys, removes it. |
47
+ | `deploy write-env` | Composes `.env.<DEPLOY_ENV>` from the GitHub environment. |
48
+ | `deploy preflight` | Classifies the project: paused (skip) or broken (fail). |
49
+ | `deploy plan`, `deploy migrate` | Dry-run the migrations into the job summary; apply them to `DB_URL`. |
50
+ | `deploy smoke --tier <t> [--app]` | Public routes answer 200, the auth redirect works, this deploy's Sentry release shows. |
51
+ | `deploy reconcile --tier <t>` | The platform's config reconcile, after the deploy (`CRON_SECRET`). |
52
+ | `env pull\|push\|audit --target <t>` | One env file per target, synced to Bitwarden and GitHub. `audit` lists orphans. |
53
+ | `planner status\|create\|reset-password\|drop` | The `migration_planner` role the preflight tier holds. |
54
+ | `graphics [graphic…]` | Club images from `@devdogsuga/brand`: `brand/*`, `app/*`, `event/*`. |
55
+ | `qr <text>` | QR codes with every option of `/console/qr`. |
56
+ | `github rulesets\|settings` | Diff (and with `--apply` write) GitHub config, through `gh`. |
57
+ | `newsletter render\|draft\|send <issue…>` | Changelog issues as files, mailbox drafts, or a send. |
58
+ | `creds send\|add\|renew\|list\|report` | Club logins from Bitwarden as email-verified Sends, and the Linear report. |
59
+ | `import involvement --file <csv>` | Verifies the members on the Involvement Network roster, unverifies everyone else. |
60
+ | `import attendance --meeting <day> --file <csv>` | Records a meeting's attendance from a sign-in sheet (method `import`). |
61
+ | `export stars\|attendance\|reflections` | Member data as CSV, each export audited under the officer's gh login. |
55
62
 
56
63
  `smoke` and `reconcile` replace DevDogsUGA's `packages/deploy-checks`. The
57
64
  per-app data (hosts, public paths, the protected path and its redirect) stays in
@@ -148,6 +155,62 @@ checkout, else Secrets Manager. The Linear key: `--linear-token`, then
148
155
  `LINEAR_API_KEY`, then the vault item "DevDogs Linear API key (backstage)", then
149
156
  a prompt; it is never saved.
150
157
 
158
+ ## Member data: `import` and `export`
159
+
160
+ These replace the platform's `/console/verification` upload and
161
+ `/console/exports` downloads. They read and write production only:
162
+ `DB_URL`, `API_URL` and `SECRET_KEY` come from `.env.production` in a checkout,
163
+ else Secrets Manager. There is no `--db-url`, since a database override would
164
+ leave account creation pointed at production. Days (`--meeting 2026-09-09`,
165
+ `--from`, `--to`) are Eastern, and `--to` includes the whole day.
166
+
167
+ ### `import involvement`
168
+
169
+ Replaces the platform's `/console/verification` upload. Export the roster from
170
+ the UGA Involvement Network (Roster → Export → Organization Roster) and pass the
171
+ file as it downloads: the title lines above the header, the `(Hidden)` columns
172
+ and the one-row-per-position layout are all expected. A roster for another
173
+ organization is refused.
174
+
175
+ The roster is the whole truth: everyone on it is verified, and everyone
176
+ verified who is not on it loses that status. A roster email finds its account
177
+ by the profile's UGA email first, then the sign-in address; with no account,
178
+ one is created (confirmed, no invite email) so the member is verified the first
179
+ time they sign in. Every run previews the counts, who is newly verified, who
180
+ loses verification, and whose preferred name differs from the roster, then
181
+ asks; `--dry-run` stops there, `--yes` answers with no terminal. Preferred
182
+ names are never changed. Rerunning the same export changes nothing.
183
+
184
+ ### `import attendance`
185
+
186
+ Records a meeting's attendance from a sign-in sheet, for members who could not
187
+ check in on the platform. The sheet is a Google or Microsoft Forms response
188
+ export or a hand-made spreadsheet, saved as CSV; one sheet is one meeting,
189
+ named with `--meeting` (its day, slug, config id or id; a day with two meetings
190
+ asks for the slug). The header and columns are found, not assumed: the email
191
+ column is the one holding UGA addresses, the name is "First/Last Name" or a
192
+ column mentioning "name", and `--email-column`/`--name-column` override
193
+ either. A sheet with no header is read as every address in it. Only UGA
194
+ addresses count; anything else is listed and skipped.
195
+
196
+ Check-ins are written with method `import`, stamped with the meeting's start,
197
+ and count for stars like any other. A member's own check-in is never touched.
198
+ Someone with no account gets one (confirmed, no invite), named from the sheet;
199
+ with no name on the sheet they are listed and skipped. Cancelled and future
200
+ meetings are refused. Rerunning a sheet records nothing new; `--replace`
201
+ makes the sheet the meeting's whole imported set, removing earlier imported
202
+ rows it no longer lists. Every run previews, then asks (`--dry-run`, `--yes`).
203
+
204
+ ### `export stars|attendance|reflections`
205
+
206
+ The platform's three CSVs, columns unchanged: `stars` (one row per earned
207
+ star), `attendance` (one row per check-in; `--meeting` for one night) and
208
+ `reflections` (each member's current text). Each export writes a
209
+ `platform.exportAudit` row before reading anything, attributed to the
210
+ platform account linked to the GitHub login `gh` is signed in as, and refuses
211
+ to run when there is none. The file goes to `./<kind>….csv` (owner-readable
212
+ only, never over an existing file without `--yes`) or stdout with `--out -`.
213
+
151
214
  No secret value reaches stdout, stderr, the failure log, Sentry, an error
152
215
  message or argv: values go to `bw` as base64 JSON on stdin, every error is
153
216
  scrubbed of every value read so far (`src/creds/secrets.ts`), and the clipboard
@@ -1,7 +1,9 @@
1
1
  import { A as YES, D as DRY_RUN, O as JSON_FLAG, j as createCatalog, k as SCOPES, v as noteRan, w as findRepoRoot } from "./telemetry-Bjoz29Hl.js";
2
- import { o as isDryRun, r as qrCatalogOptions } from "./options-BTjOf5KP.js";
2
+ import { n as isDryRun } from "./dry-run-3IxPmCtW.js";
3
3
  import { o as unwrap } from "./ui-CdKo8mLw.js";
4
4
  import { n as positionals } from "./args-Cjr_Iqts.js";
5
+ import { t as EXPORTS } from "./queries-DeEwfe2c.js";
6
+ import { r as qrCatalogOptions } from "./options-DCaqm8BI.js";
5
7
  import { existsSync, readFileSync } from "node:fs";
6
8
  import { join } from "node:path";
7
9
  import { confirm, note, select, text } from "@clack/prompts";
@@ -930,7 +932,7 @@ const ITEM = {
930
932
  value: "<name>",
931
933
  summary: "A DevDogs organization item, by name or id. Repeat for several."
932
934
  };
933
- const TO = {
935
+ const TO$1 = {
934
936
  flag: "--to",
935
937
  value: "<a@uga.edu,…>",
936
938
  summary: "Recipients, comma-separated. Replaces the item's current list."
@@ -970,7 +972,7 @@ const credsCommand = {
970
972
  hint: "pick accounts, then officers",
971
973
  options: [
972
974
  ITEM,
973
- TO,
975
+ TO$1,
974
976
  ROLE,
975
977
  ALLOW_EMAIL,
976
978
  DB_URL$1,
@@ -1013,7 +1015,7 @@ const credsCommand = {
1013
1015
  flag: "--password-stdin",
1014
1016
  summary: "Read the password from stdin, for scripts."
1015
1017
  },
1016
- TO,
1018
+ TO$1,
1017
1019
  ROLE,
1018
1020
  ALLOW_EMAIL,
1019
1021
  DB_URL$1,
@@ -1210,6 +1212,61 @@ const envCommand = {
1210
1212
  ]
1211
1213
  };
1212
1214
  //#endregion
1215
+ //#region src/export/catalog.ts
1216
+ /**
1217
+ * `export`'s place in the command tree: declaration only, nothing here runs.
1218
+ * The handler lives in `commands.ts` beside it.
1219
+ *
1220
+ * `envFree` like `involvement`: production's `DB_URL` comes from the
1221
+ * checkout's `.env.production` or Secrets Manager, and the audit identity from
1222
+ * `gh`, so it runs under `pnpm dlx`.
1223
+ */
1224
+ const FROM = {
1225
+ flag: "--from",
1226
+ value: "<day>",
1227
+ summary: "From this Eastern day (or ISO timestamp), inclusive."
1228
+ };
1229
+ const TO = {
1230
+ flag: "--to",
1231
+ value: "<day>",
1232
+ summary: "Through this Eastern day (or before this ISO timestamp)."
1233
+ };
1234
+ const OUT = {
1235
+ flag: "--out",
1236
+ value: "<path|->",
1237
+ summary: "Where to write. Defaults to ./<kind>….csv; - for stdout."
1238
+ };
1239
+ function exportOf(kind, extra = []) {
1240
+ return {
1241
+ name: kind,
1242
+ envFree: true,
1243
+ dryRun: "handled",
1244
+ summary: EXPORTS[kind].summary,
1245
+ options: [
1246
+ FROM,
1247
+ TO,
1248
+ ...extra,
1249
+ OUT,
1250
+ DRY_RUN,
1251
+ YES
1252
+ ]
1253
+ };
1254
+ }
1255
+ const exportCommand = {
1256
+ name: "export",
1257
+ summary: "Member data as CSV: stars, attendance, reflections. Audited.",
1258
+ hint: "production database; your gh login is recorded",
1259
+ subcommands: [
1260
+ exportOf("stars"),
1261
+ exportOf("attendance", [{
1262
+ flag: "--meeting",
1263
+ value: "<meeting>",
1264
+ summary: "One meeting: its day (2026-09-09), slug, config id or id."
1265
+ }]),
1266
+ exportOf("reflections")
1267
+ ]
1268
+ };
1269
+ //#endregion
1213
1270
  //#region src/github/catalog.ts
1214
1271
  /**
1215
1272
  * `github`'s place in the command tree: declaration only, nothing here runs.
@@ -1303,6 +1360,63 @@ const graphicsCommand = {
1303
1360
  ]
1304
1361
  };
1305
1362
  //#endregion
1363
+ //#region src/import/catalog.ts
1364
+ const importCommand = {
1365
+ name: "import",
1366
+ summary: "Member data in: the Involvement roster, a meeting's sign-in sheet.",
1367
+ hint: "production database; previews, then asks",
1368
+ subcommands: [{
1369
+ name: "involvement",
1370
+ envFree: true,
1371
+ dryRun: "handled",
1372
+ summary: "Import an Organization Roster export: verify everyone on it, unverify everyone else.",
1373
+ hint: "previews, then asks",
1374
+ options: [
1375
+ {
1376
+ flag: "--file",
1377
+ value: "<csv>",
1378
+ summary: "The roster export (Involvement Network → Roster → Export → Organization Roster)."
1379
+ },
1380
+ DRY_RUN,
1381
+ YES
1382
+ ]
1383
+ }, {
1384
+ name: "attendance",
1385
+ envFree: true,
1386
+ dryRun: "handled",
1387
+ summary: "Import a sign-in sheet (form responses or a spreadsheet, as CSV) for one meeting.",
1388
+ hint: "previews, then asks",
1389
+ options: [
1390
+ {
1391
+ flag: "--meeting",
1392
+ value: "<meeting>",
1393
+ summary: "The meeting: its day (2026-09-09), slug, config id or id."
1394
+ },
1395
+ {
1396
+ flag: "--file",
1397
+ value: "<csv>",
1398
+ summary: "The sign-in sheet, saved as CSV."
1399
+ },
1400
+ {
1401
+ flag: "--email-column",
1402
+ value: "<header>",
1403
+ summary: "The column of UGA addresses, when there are several."
1404
+ },
1405
+ {
1406
+ flag: "--name-column",
1407
+ value: "<header>",
1408
+ summary: "The column of names, used for new accounts."
1409
+ },
1410
+ {
1411
+ flag: "--replace",
1412
+ summary: "Make this sheet the meeting's whole imported set: remove earlier imported rows it lacks."
1413
+ },
1414
+ DRY_RUN,
1415
+ YES
1416
+ ]
1417
+ }]
1418
+ };
1419
+ //#endregion
1306
1420
  //#region src/newsletter/catalog.ts
1307
1421
  /**
1308
1422
  * `newsletter`'s place in the command tree: declaration only, nothing here
@@ -1458,8 +1572,12 @@ const catalog = createCatalog({
1458
1572
  commands: [envCommand]
1459
1573
  },
1460
1574
  {
1461
- title: "Database roles",
1462
- commands: [plannerCommand]
1575
+ title: "Production database",
1576
+ commands: [
1577
+ plannerCommand,
1578
+ importCommand,
1579
+ exportCommand
1580
+ ]
1463
1581
  },
1464
1582
  {
1465
1583
  title: "Graphics & QR codes (no credentials)",
@@ -1,13 +1,13 @@
1
1
  import { C as discoverRepoRoot, T as cliName, b as ownPackageDir, c as reportDevtoolsError, f as isNoEnv, l as reportDevtoolsFailure, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as isNonInteractive, w as findRepoRoot, x as ownVersion } from "./telemetry-Bjoz29Hl.js";
2
- import { a as recordEnteredTier, f as helpPath, i as beginInvocation, l as dbPush, m as renderHelp, n as bareGroupStartPath, o as recordResolved, p as renderCommandList, r as runMenu, s as reproducibleCommand, t as catalog, u as dbPushDryRun } from "./catalog-BUOwCd2Q.js";
2
+ import { a as recordEnteredTier, f as helpPath, i as beginInvocation, l as dbPush, m as renderHelp, n as bareGroupStartPath, o as recordResolved, p as renderCommandList, r as runMenu, s as reproducibleCommand, t as catalog, u as dbPushDryRun } from "./catalog-Dke7bQVz.js";
3
3
  import { a as repoPeerUrl, n as loadEnv, r as loadEnvLoad, t as getEnvSync } from "./peers-B0KDkI6a.js";
4
4
  import { t as nonEmpty } from "./connection-D3XsOE3S.js";
5
- import { a as dryRunKind, l as wouldRunLine, o as isDryRun } from "./options-BTjOf5KP.js";
5
+ import { a as wouldRunLine, n as isDryRun, t as dryRunKind } from "./dry-run-3IxPmCtW.js";
6
6
  import { a as explainError, i as explain, n as bail, o as unwrap, r as errorMessage } from "./ui-CdKo8mLw.js";
7
7
  import { n as positionals, t as flagValue } from "./args-Cjr_Iqts.js";
8
- import { a as listSecrets$1, c as updateSecret, i as createSecret, o as projectIdFor, r as byKey, s as setExplicitAccessToken } from "./client-B7HhuTaA.js";
9
- import { c as EnvDocument } from "./vault-C8LYJFOQ.js";
10
8
  import { t as DONE } from "./dispatch-D048O65I.js";
9
+ import { t as EnvDocument } from "./document-DumoZXoO.js";
10
+ import { a as listSecrets$1, c as updateSecret, i as createSecret, o as projectIdFor, r as byKey, s as setExplicitAccessToken } from "./client-B_MY9kVI.js";
11
11
  import { register } from "node:module";
12
12
  import { pathToFileURL } from "node:url";
13
13
  import { appendFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
@@ -17,9 +17,9 @@ import { chmod, readFile, writeFile } from "node:fs/promises";
17
17
  import { execFile, execFileSync, spawn } from "node:child_process";
18
18
  import { promisify } from "node:util";
19
19
  import { tmpdir } from "node:os";
20
+ import postgres from "postgres";
20
21
  import { z } from "zod";
21
22
  import { randomBytes } from "node:crypto";
22
- import postgres from "postgres";
23
23
  //#region ../cli-core/src/completions.ts
24
24
  /**
25
25
  * Groups all paths by their parent prefix to build per-parent completion lists.
@@ -5268,9 +5268,11 @@ const HANDLERS = {
5268
5268
  completions: handleCompletions,
5269
5269
  github: handleGithub,
5270
5270
  graphics: async (rest) => (await import("./commands-Dv5TXjvb.js")).handleGraphics(rest),
5271
- qr: async (rest) => (await import("./commands-Bk7IbhPD.js")).handleQr(rest),
5271
+ qr: async (rest) => (await import("./commands-Sx1fDNNn.js")).handleQr(rest),
5272
5272
  newsletter: async (rest) => (await import("./commands-D345F-Od.js")).handleNewsletter(rest),
5273
- creds: async (rest) => (await import("./commands-BMQ1OZIr.js")).handleCreds(rest)
5273
+ creds: async (rest) => (await import("./commands-zPSnOGzH.js")).handleCreds(rest),
5274
+ import: async (rest) => (await import("./commands-DzALVX5g.js")).handleImport(rest),
5275
+ export: async (rest) => (await import("./commands-5Ou8xJw5.js")).handleExport(rest)
5274
5276
  };
5275
5277
  /**
5276
5278
  * Names that were commands of the contributor CLI, refused with where they
@@ -0,0 +1,136 @@
1
+ import { C as discoverRepoRoot, p as isNonInteractive } from "./telemetry-Bjoz29Hl.js";
2
+ import { t as nonEmpty } from "./connection-D3XsOE3S.js";
3
+ import { a as explainError, i as explain, o as unwrap, t as UsageError } from "./ui-CdKo8mLw.js";
4
+ import { t as EnvDocument } from "./document-DumoZXoO.js";
5
+ import { resolve } from "node:path";
6
+ import { confirm, log, text } from "@clack/prompts";
7
+ import { readFile } from "node:fs/promises";
8
+ import postgres from "postgres";
9
+ //#region src/production/access.ts
10
+ /**
11
+ * Production access for the commands that read or write member data
12
+ * (`involvement`, `attendance`, `export`).
13
+ *
14
+ * Every key comes from one place, so a run never splits across two projects:
15
+ * the checkout's `.env.production`, else the production Secrets Manager
16
+ * project. There is deliberately no `--db-url`; a database override would
17
+ * leave account creation (which goes through Auth with `API_URL`) pointed at
18
+ * production.
19
+ */
20
+ /** A failure already worded for the officer. Safe to print. */
21
+ var ProductionError = class extends Error {
22
+ name = "ProductionError";
23
+ };
24
+ /** The production project in Secrets Manager (see `creds/roster.ts`). */
25
+ const PRODUCTION_PROJECT = "production";
26
+ /** `.env.production` in a checkout, else the production Secrets Manager project. */
27
+ async function readProductionKey(key) {
28
+ const root = discoverRepoRoot();
29
+ if (root) try {
30
+ const text = await readFile(resolve(root, ".env.production"), "utf8");
31
+ const value = nonEmpty(EnvDocument.parse(text).get(key));
32
+ if (value) return value;
33
+ } catch {}
34
+ const { listSecrets, projectIdFor } = await import("./client-DwDGjalU.js");
35
+ const secrets = await listSecrets(await projectIdFor(PRODUCTION_PROJECT));
36
+ return nonEmpty(secrets.find((s) => s.key === key)?.value);
37
+ }
38
+ async function requireProductionKey(key) {
39
+ const value = await readProductionKey(key);
40
+ if (!value) throw new ProductionError(`Could not find production's ${key}. Run \`backstage env pull --target production\` in a checkout, or sign in to Secrets Manager.`);
41
+ return value;
42
+ }
43
+ /**
44
+ * A database failure, described without the connection string: postgres.js
45
+ * can quote the host, and the URL carries the password. A server error
46
+ * (a five-character SQLSTATE) is the server's own message, which never does.
47
+ */
48
+ function dbError(what, err) {
49
+ const e = err;
50
+ return new ProductionError(`${what}: ${(e.code && /^[0-9A-Z]{5}$/.test(e.code) ? e.message : e.code) ?? "connection failed"}.`);
51
+ }
52
+ /** One short-lived connection; callers `end()` it. */
53
+ function connect(url) {
54
+ return postgres(url, {
55
+ max: 1,
56
+ prepare: false,
57
+ connect_timeout: 15
58
+ });
59
+ }
60
+ /** Runs `fn` on a fresh connection and closes it, rewording failures. */
61
+ async function withConnection(url, what, fn) {
62
+ const sql = connect(url);
63
+ try {
64
+ return await fn(sql);
65
+ } catch (err) {
66
+ if (err instanceof ProductionError) throw err;
67
+ throw dbError(what, err);
68
+ } finally {
69
+ await sql.end({ timeout: 5 });
70
+ }
71
+ }
72
+ //#endregion
73
+ //#region src/production/cli.ts
74
+ /**
75
+ * The terminal half shared by the member-data commands: progress lines, the
76
+ * input file, and the one confirmation before production is written.
77
+ */
78
+ /** Progress for a person; plain stderr lines when nobody is watching a terminal. */
79
+ function say(message, level = "info") {
80
+ if (isNonInteractive()) process.stderr.write(`${message}\n`);
81
+ else log[level](message);
82
+ }
83
+ function plural(n, one, many = `${one}s`) {
84
+ return `${n} ${n === 1 ? one : many}`;
85
+ }
86
+ /** Whether a person can answer a prompt. */
87
+ function canPrompt() {
88
+ return !isNonInteractive() && process.stdin.isTTY === true;
89
+ }
90
+ function expandHome(path) {
91
+ return path.startsWith("~/") ? `${process.env.HOME ?? "~"}${path.slice(1)}` : path;
92
+ }
93
+ /** `--file`, else asked for at a terminal; then read. */
94
+ async function readInputFile(file, options) {
95
+ let path = file;
96
+ if (!path) {
97
+ if (!options.interactive) throw new UsageError(`Pass the file with --file <${options.placeholder}>.`);
98
+ path = unwrap(await text({
99
+ message: options.message,
100
+ placeholder: options.placeholder,
101
+ validate: (v) => v?.trim() ? void 0 : "A path, please."
102
+ })).trim();
103
+ }
104
+ const full = expandHome(path);
105
+ try {
106
+ return await (options.read ?? ((p) => readFile(p, "utf8")))(full);
107
+ } catch {
108
+ throw new UsageError(`Could not read ${full}.`);
109
+ }
110
+ }
111
+ /**
112
+ * The one question before production is written. `--yes` answers it; with no
113
+ * terminal and no `--yes`, the command stops rather than guess. Defaults to no.
114
+ */
115
+ async function confirmWrite(question, options) {
116
+ if (options.yes) return true;
117
+ if (!options.interactive) throw new UsageError(`${question} Pass --yes to answer yes without a terminal.`);
118
+ return unwrap(await confirm({
119
+ message: question,
120
+ initialValue: false
121
+ }));
122
+ }
123
+ /**
124
+ * The shared `catch`: a worded failure is explained and the run exits 1; any
125
+ * other error is unexpected and goes to Sentry with the command's name.
126
+ */
127
+ function reportFailure(command, err, worded = []) {
128
+ process.exitCode = 1;
129
+ if (err instanceof UsageError || err instanceof ProductionError || worded.some((kind) => err instanceof kind)) {
130
+ explain(err.message, "");
131
+ return;
132
+ }
133
+ explainError(`${command} failed.`, err);
134
+ }
135
+ //#endregion
136
+ export { reportFailure as a, connect as c, withConnection as d, readInputFile as i, dbError as l, confirmWrite as n, say as o, plural as r, ProductionError as s, canPrompt as t, requireProductionKey as u };
@@ -1,7 +1,8 @@
1
1
  import { w as findRepoRoot } from "./telemetry-Bjoz29Hl.js";
2
2
  import { n as loadEnv } from "./peers-B0KDkI6a.js";
3
3
  import { o as unwrap } from "./ui-CdKo8mLw.js";
4
- import { a as readTokenFromVault, c as EnvDocument, o as saveTokenToVault, t as VAULT_ITEM_NAME } from "./vault-C8LYJFOQ.js";
4
+ import { t as EnvDocument } from "./document-DumoZXoO.js";
5
+ import { a as readTokenFromVault, o as saveTokenToVault, t as VAULT_ITEM_NAME } from "./vault-CsBC-1Mq.js";
5
6
  import { dirname, join, resolve } from "node:path";
6
7
  import { log, password, select, text } from "@clack/prompts";
7
8
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -0,0 +1,2 @@
1
+ import { a as listSecrets, o as projectIdFor } from "./client-B_MY9kVI.js";
2
+ export { listSecrets, projectIdFor };