@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 +79 -16
- package/dist/{catalog-BUOwCd2Q.js → catalog-Dke7bQVz.js} +124 -6
- package/dist/{cli-B2S6gw9o.js → cli-BhZ4je2D.js} +9 -7
- package/dist/cli-Do2nWD7W.js +136 -0
- package/dist/{client-B7HhuTaA.js → client-B_MY9kVI.js} +2 -1
- package/dist/client-DwDGjalU.js +2 -0
- package/dist/commands-5Ou8xJw5.js +225 -0
- package/dist/commands-BQ2NIqub.js +436 -0
- package/dist/commands-CLSxKz-l.js +352 -0
- package/dist/commands-DzALVX5g.js +12 -0
- package/dist/{commands-Bk7IbhPD.js → commands-Sx1fDNNn.js} +2 -1
- package/dist/{commands-BMQ1OZIr.js → commands-zPSnOGzH.js} +3 -2
- package/dist/document-DumoZXoO.js +287 -0
- package/dist/dry-run-3IxPmCtW.js +58 -0
- package/dist/launch.js +3 -3
- package/dist/meetings-DfU867eV.js +118 -0
- package/dist/{options-BTjOf5KP.js → options-DCaqm8BI.js} +1 -57
- package/dist/queries-DeEwfe2c.js +170 -0
- package/dist/read-B-aJJewb.js +141 -0
- package/dist/{vault-C8LYJFOQ.js → vault-CsBC-1Mq.js} +1 -287
- package/package.json +6 -6
- package/dist/client-DzXQHd58.js +0 -2
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`)
|
|
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
|
|
41
|
-
|
|
|
42
|
-
| `deploy <app> --tier <t>`
|
|
43
|
-
| `deploy write-env`
|
|
44
|
-
| `deploy preflight`
|
|
45
|
-
| `deploy plan`, `deploy migrate`
|
|
46
|
-
| `deploy smoke --tier <t> [--app]`
|
|
47
|
-
| `deploy reconcile --tier <t>`
|
|
48
|
-
| `env pull\|push\|audit --target <t>`
|
|
49
|
-
| `planner status\|create\|reset-password\|drop`
|
|
50
|
-
| `graphics [graphic…]`
|
|
51
|
-
| `qr <text>`
|
|
52
|
-
| `github rulesets\|settings`
|
|
53
|
-
| `newsletter render\|draft\|send <issue…>`
|
|
54
|
-
| `creds send\|add\|renew\|list\|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 {
|
|
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: "
|
|
1462
|
-
commands: [
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
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 {
|
|
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";
|