@devdogsuga/backstage 0.1.7 → 0.1.9

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 (27) hide show
  1. package/README.md +42 -25
  2. package/dist/{catalog-BiXI0wyi.js → catalog-COleRgQD.js} +22 -13
  3. package/dist/{cli-Cz8faASC.js → cli-CMThHZgV.js} +23 -29
  4. package/dist/{cli-Do2nWD7W.js → cli-DjIwbjnx.js} +4 -4
  5. package/dist/client-A6pJcxSE.js +2 -0
  6. package/dist/{client-B_MY9kVI.js → client-NuoKMQmV.js} +4 -4
  7. package/dist/{commands-D345F-Od.js → commands-35rBJ1R0.js} +2 -2
  8. package/dist/{commands-zPSnOGzH.js → commands-BF5eptEi.js} +4 -4
  9. package/dist/commands-CaSixDwT.js +699 -0
  10. package/dist/{commands-DzALVX5g.js → commands-CuYIkHeU.js} +3 -3
  11. package/dist/{commands-BQ2NIqub.js → commands-DL5fpABN.js} +5 -5
  12. package/dist/{commands-Sx1fDNNn.js → commands-DQYH_m6i.js} +3 -3
  13. package/dist/{commands-CLSxKz-l.js → commands-KY-9X1Rh.js} +4 -4
  14. package/dist/{commands-Dv5TXjvb.js → commands-jJcS7D2a.js} +6 -6
  15. package/dist/{dry-run-3IxPmCtW.js → dry-run-CFCZUpjb.js} +1 -1
  16. package/dist/launch.js +6 -6
  17. package/dist/{meetings-DfU867eV.js → meetings-CLZ6xQQB.js} +2 -2
  18. package/dist/{peers-B0KDkI6a.js → peers-u1zxe-Ub.js} +1 -1
  19. package/dist/{queries-DeEwfe2c.js → queries-CaAe3cve.js} +100 -8
  20. package/dist/{read-B-aJJewb.js → read-B8DSwYfB.js} +2 -2
  21. package/dist/{telemetry-Bjoz29Hl.js → telemetry-Be_lbB5c.js} +2 -2
  22. package/dist/telemetry.js +1 -1
  23. package/dist/{ui-CdKo8mLw.js → ui-Bb69ZfVv.js} +1 -1
  24. package/dist/{vault-CsBC-1Mq.js → vault-DFV0zjbT.js} +1 -1
  25. package/package.json +5 -5
  26. package/dist/client-DwDGjalU.js +0 -2
  27. package/dist/commands-5Ou8xJw5.js +0 -225
@@ -1,9 +1,9 @@
1
- import { i as explain } from "./ui-CdKo8mLw.js";
1
+ import { i as explain } from "./ui-Bb69ZfVv.js";
2
2
  //#region src/import/commands.ts
3
3
  const handleImport = async (rest) => {
4
4
  const [what, ...args] = rest;
5
- if (what === "involvement") return (await import("./commands-CLSxKz-l.js")).handleInvolvement(args);
6
- if (what === "attendance") return (await import("./commands-BQ2NIqub.js")).handleAttendance(args);
5
+ if (what === "involvement") return (await import("./commands-KY-9X1Rh.js")).handleInvolvement(args);
6
+ if (what === "attendance") return (await import("./commands-DL5fpABN.js")).handleAttendance(args);
7
7
  explain(what ? `Unknown import "${what}". Try involvement or attendance.` : "Name the import: involvement or attendance.", "");
8
8
  process.exitCode = 1;
9
9
  return null;
@@ -1,9 +1,9 @@
1
- import { n as isDryRun } from "./dry-run-3IxPmCtW.js";
2
- import { o as unwrap, r as errorMessage, t as UsageError } from "./ui-CdKo8mLw.js";
1
+ import { n as isDryRun } from "./dry-run-CFCZUpjb.js";
2
+ import { o as unwrap, r as errorMessage, t as UsageError } from "./ui-Bb69ZfVv.js";
3
3
  import { t as DONE } from "./dispatch-D048O65I.js";
4
- import { a as reportFailure, c as connect, d as withConnection, i as readInputFile, n as confirmWrite, o as say, r as plural, s as ProductionError, t as canPrompt, u as requireProductionKey } from "./cli-Do2nWD7W.js";
5
- import { a as createAccounts, i as createAccountFor, n as accountFinder, r as accountsWith, t as parseCsv } from "./read-B-aJJewb.js";
6
- import { n as findMeetingsWith, r as pickMeeting, t as describeMeeting } from "./meetings-DfU867eV.js";
4
+ import { a as readInputFile, c as ProductionError, d as requireProductionKey, f as withConnection, i as plural, l as connect, n as confirmWrite, o as reportFailure, s as say, t as canPrompt } from "./cli-DjIwbjnx.js";
5
+ import { a as createAccounts, i as createAccountFor, n as accountFinder, r as accountsWith, t as parseCsv } from "./read-B8DSwYfB.js";
6
+ import { n as findMeetingsWith, r as pickMeeting, t as describeMeeting } from "./meetings-CLZ6xQQB.js";
7
7
  import { text } from "@clack/prompts";
8
8
  import { parseArgs } from "node:util";
9
9
  //#region src/attendance/plan.ts
@@ -1,6 +1,6 @@
1
- import { p as isNonInteractive } from "./telemetry-Bjoz29Hl.js";
2
- import { n as isDryRun } from "./dry-run-3IxPmCtW.js";
3
- import { a as explainError, o as unwrap, t as UsageError } from "./ui-CdKo8mLw.js";
1
+ import { p as isNonInteractive } from "./telemetry-Be_lbB5c.js";
2
+ import { n as isDryRun } from "./dry-run-CFCZUpjb.js";
3
+ import { a as explainError, o as unwrap, t as UsageError } from "./ui-Bb69ZfVv.js";
4
4
  import { i as qrParseOptions, n as QR_FLAGS, t as QR_EXTRA_FLAGS } from "./options-DCaqm8BI.js";
5
5
  import { t as DONE } from "./dispatch-D048O65I.js";
6
6
  import { dirname, extname, relative, resolve } from "node:path";
@@ -1,8 +1,8 @@
1
- import { n as isDryRun } from "./dry-run-3IxPmCtW.js";
2
- import { r as errorMessage, t as UsageError } from "./ui-CdKo8mLw.js";
1
+ import { n as isDryRun } from "./dry-run-CFCZUpjb.js";
2
+ import { r as errorMessage, t as UsageError } from "./ui-Bb69ZfVv.js";
3
3
  import { t as DONE } from "./dispatch-D048O65I.js";
4
- import { a as reportFailure, d as withConnection, i as readInputFile, n as confirmWrite, o as say, r as plural, t as canPrompt, u as requireProductionKey } from "./cli-Do2nWD7W.js";
5
- import { a as createAccounts, i as createAccountFor, n as accountFinder, o as readAccounts, t as parseCsv } from "./read-B-aJJewb.js";
4
+ import { a as readInputFile, d as requireProductionKey, f as withConnection, i as plural, n as confirmWrite, o as reportFailure, s as say, t as canPrompt } from "./cli-DjIwbjnx.js";
5
+ import { a as createAccounts, i as createAccountFor, n as accountFinder, o as readAccounts, t as parseCsv } from "./read-B8DSwYfB.js";
6
6
  import { parseArgs } from "node:util";
7
7
  //#region src/involvement/csv.ts
8
8
  /**
@@ -1,27 +1,27 @@
1
- import { p as isNonInteractive } from "./telemetry-Bjoz29Hl.js";
2
- import { a as explainError, o as unwrap, r as errorMessage, t as UsageError } from "./ui-CdKo8mLw.js";
1
+ import { p as isNonInteractive } from "./telemetry-Be_lbB5c.js";
2
+ import { a as explainError, o as unwrap, r as errorMessage, t as UsageError } from "./ui-Bb69ZfVv.js";
3
3
  import { n as positionals } from "./args-Cjr_Iqts.js";
4
4
  import { t as DONE } from "./dispatch-D048O65I.js";
5
5
  import { dirname, relative, resolve } from "node:path";
6
6
  import { autocompleteMultiselect, log, multiselect, note } from "@clack/prompts";
7
7
  import { mkdir, writeFile } from "node:fs/promises";
8
8
  import { homedir } from "node:os";
9
+ import { getClubConfig } from "@devdogsuga/events";
9
10
  import { APPS, AppIcon, Banner, EmailSignature, EventCard, FORMATS, OPAQUE_FORMATS, PageCard, THEME } from "@devdogsuga/brand";
10
11
  import { EVENT_SEGMENT_VISUALS, meetingCardDetail, meetingLocation } from "@devdogsuga/brand/event";
11
- import { getClubConfig } from "@devdogsuga/events";
12
12
  import { render as render$1 } from "@devdogsuga/brand/render";
13
13
  //#region src/graphics/events.ts
14
14
  /**
15
15
  * Builds the reader from `@devdogsuga/events`'s committed data file.
16
16
  *
17
- * `Meeting.id` is the card's `slug` and `Meeting.title` is its authored name
17
+ * `Meeting.slug` is the card's `slug` (its platform URL) and `Meeting.title` is its authored name
18
18
  * (`nameOverride` on the platform's meetings table).
19
19
  */
20
20
  function configEvents() {
21
21
  return { async meetings() {
22
22
  return [...getClubConfig().meetings].sort((a, b) => Date.parse(b.startsAt) - Date.parse(a.startsAt)).map((row) => {
23
23
  const meeting = {
24
- slug: row.id,
24
+ slug: row.slug,
25
25
  startsAt: new Date(row.startsAt),
26
26
  endsAt: new Date(row.endsAt),
27
27
  building: row.building,
@@ -41,7 +41,7 @@ function configEvents() {
41
41
  location: meetingLocation(row.building, row.location)
42
42
  });
43
43
  return {
44
- slug: row.id,
44
+ slug: row.slug,
45
45
  hint: describeMeeting(meeting),
46
46
  detail,
47
47
  items: items.map((item) => {
@@ -1,4 +1,4 @@
1
- import { T as cliName } from "./telemetry-Bjoz29Hl.js";
1
+ import { T as cliName } from "./telemetry-Be_lbB5c.js";
2
2
  //#region ../cli-core/src/dry-run.ts
3
3
  const DRY_RUN_FLAG = "--dry-run";
4
4
  /** Commands that forward everything after their name to another tool. */
package/dist/launch.js CHANGED
@@ -1,9 +1,9 @@
1
- import { C as discoverRepoRoot, E as setCliName, S as RepoNotFoundError, T as cliName, _ as installFailureLog, d as hasYes, g as stripTierFlag, h as stripNoEnvFlag, m as setNoEnv, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as isNonInteractive, s as lastSentryEventId } from "./telemetry-Bjoz29Hl.js";
2
- import { c as setMenuEnvHook, d as formatCommand, f as helpPath, n as bareGroupStartPath, t as catalog } from "./catalog-BiXI0wyi.js";
3
- import { i as loadEnvSession, r as loadEnvLoad } from "./peers-B0KDkI6a.js";
1
+ import { C as discoverRepoRoot, E as setCliName, S as RepoNotFoundError, T as cliName, _ as installFailureLog, d as hasYes, g as stripTierFlag, h as stripNoEnvFlag, m as setNoEnv, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as isNonInteractive, s as lastSentryEventId } from "./telemetry-Be_lbB5c.js";
2
+ import { c as setMenuEnvHook, d as formatCommand, f as helpPath, n as bareGroupStartPath, t as catalog } from "./catalog-COleRgQD.js";
3
+ import { i as loadEnvSession, r as loadEnvLoad } from "./peers-u1zxe-Ub.js";
4
4
  import { t as nonEmpty } from "./connection-D3XsOE3S.js";
5
- import { i as setDryRun, n as isDryRun, r as resolveDryRun } from "./dry-run-3IxPmCtW.js";
6
- import { o as unwrap, r as errorMessage } from "./ui-CdKo8mLw.js";
5
+ import { i as setDryRun, n as isDryRun, r as resolveDryRun } from "./dry-run-CFCZUpjb.js";
6
+ import { o as unwrap, r as errorMessage } from "./ui-Bb69ZfVv.js";
7
7
  import { confirm } from "@clack/prompts";
8
8
  //#region ../cli-core/src/pipes.ts
9
9
  /**
@@ -322,7 +322,7 @@ async function launchWith(argv, options) {
322
322
  */
323
323
  async function launch(argv) {
324
324
  await launchWith(argv, { dispatch: async (args) => {
325
- const { main } = await import("./cli-Cz8faASC.js");
325
+ const { main } = await import("./cli-CMThHZgV.js");
326
326
  await main(args);
327
327
  } });
328
328
  }
@@ -1,5 +1,5 @@
1
- import { t as UsageError } from "./ui-CdKo8mLw.js";
2
- import "./cli-Do2nWD7W.js";
1
+ import { t as UsageError } from "./ui-Bb69ZfVv.js";
2
+ import "./cli-DjIwbjnx.js";
3
3
  //#region src/production/time.ts
4
4
  /**
5
5
  * Dates as officers type them. Meetings happen in Athens, so a bare
@@ -1,4 +1,4 @@
1
- import { S as RepoNotFoundError, w as findRepoRoot } from "./telemetry-Bjoz29Hl.js";
1
+ import { S as RepoNotFoundError, w as findRepoRoot } from "./telemetry-Be_lbB5c.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { existsSync, readFileSync, readdirSync } from "node:fs";
@@ -1,17 +1,21 @@
1
+ import { describeAnswer } from "@devdogsuga/events";
1
2
  //#region src/export/queries.ts
2
3
  /**
3
- * The three exports, moved from the platform's `server/export/` with their
4
- * columns unchanged: anything downstream that read `/export/stars` reads
5
- * `backstage export stars` the same way.
4
+ * The exports. The first three moved from the platform's `server/export/`
5
+ * with their columns unchanged: anything downstream that read
6
+ * `/export/stars` reads `backstage export stars` the same way. `responses`
7
+ * (the check-in survey's answers) was born here.
6
8
  *
7
- * Each query selects exactly its columns, aliased to the header names, in
8
- * header order. Parameters: `$1` from (inclusive), `$2` to (exclusive), `$3`
9
+ * Each query selects its columns, aliased to the header names, in header
10
+ * order; attendance also selects the names and UGA address the Bevy and
11
+ * Involvement Network formats need (see `formats.ts`), past its columns. Parameters: `$1` from (inclusive), `$2` to (exclusive), `$3`
9
12
  * a meeting id; each null when not given.
10
13
  */
11
14
  const EXPORT_KINDS = [
12
15
  "stars",
13
16
  "attendance",
14
- "reflections"
17
+ "reflections",
18
+ "responses"
15
19
  ];
16
20
  /** The member columns every export leads with. */
17
21
  const MEMBER = (userId) => `
@@ -33,6 +37,90 @@ const MEMBER_COLUMNS = [
33
37
  const MEMBER_JOINS = (userId) => `
34
38
  left join "platform"."profile" p on p."userId" = ${userId}
35
39
  left join "auth"."users" u on u."id" = ${userId}`;
40
+ /**
41
+ * The survey answers one export reads: every current answer, or for one
42
+ * meeting (`$3`), that meeting's answers plus each attendee's member answers
43
+ * AS THEY STOOD WHEN IT ENDED -- the latest revision at or before its end,
44
+ * else the first after it (a member who answered on the way out), from the
45
+ * append-only `surveyAnswerRevisions`. A member answer cleared by then is
46
+ * left out. `--from`/`--to` filter the unscoped case on when the answer was
47
+ * last changed.
48
+ */
49
+ const PICKED_ANSWERS = `
50
+ picked as (
51
+ select a."userId", a."questionId", a."meetingId", a.answer,
52
+ a."updatedAt" as "answeredAt"
53
+ from "platform"."surveyAnswers" a
54
+ where $3::uuid is null
55
+ and ($1::timestamptz is null or a."updatedAt" >= $1::timestamptz)
56
+ and ($2::timestamptz is null or a."updatedAt" < $2::timestamptz)
57
+
58
+ union all
59
+
60
+ select a."userId", a."questionId", a."meetingId", a.answer, a."updatedAt"
61
+ from "platform"."surveyAnswers" a
62
+ where a."meetingId" = $3::uuid
63
+
64
+ union all
65
+
66
+ select "userId", "questionId", null::uuid, answer, "recordedAt"
67
+ from (
68
+ select distinct on (r."userId", r."questionId")
69
+ r."userId", r."questionId", r.answer, r."recordedAt"
70
+ from "platform"."surveyAnswerRevisions" r
71
+ join "platform"."attendance" att
72
+ on att."userId" = r."userId" and att."meetingId" = $3::uuid
73
+ join "platform"."meetings" ended on ended.id = $3::uuid
74
+ where r."meetingId" is null
75
+ order by r."userId", r."questionId",
76
+ r."recordedAt" > ended."endsAt",
77
+ case when r."recordedAt" <= ended."endsAt" then r."recordedAt" end
78
+ desc nulls last,
79
+ r."recordedAt"
80
+ ) as stood
81
+ where answer is not null
82
+ )`;
83
+ /**
84
+ * One row per survey answer, worded with the question's current definition
85
+ * (so a relabelled option exports under its new label) beside the stored
86
+ * value, for anything that needs option ids.
87
+ */
88
+ const RESPONSES = {
89
+ summary: "One row per survey answer; --meeting for one meeting's.",
90
+ columns: [
91
+ ...MEMBER_COLUMNS,
92
+ "meeting_config_id",
93
+ "meeting_title",
94
+ "question_id",
95
+ "question_scope",
96
+ "question_prompt",
97
+ "answer",
98
+ "answer_value",
99
+ "answered_at"
100
+ ],
101
+ sql: `
102
+ with ${PICKED_ANSWERS}
103
+ select ${MEMBER("pa.\"userId\"")},
104
+ m."configId" as "meeting_config_id",
105
+ case when m.id is not null
106
+ then coalesce(m."nameOverride", m."kind", 'Meeting') end as "meeting_title",
107
+ q.id as "question_id",
108
+ q.scope as "question_scope",
109
+ q.definition ->> 'prompt' as "question_prompt",
110
+ q.definition as "definition",
111
+ pa.answer as "answer_json",
112
+ pa."answeredAt" as "answered_at"
113
+ from picked pa
114
+ join "platform"."surveyQuestions" q on q.id = pa."questionId"
115
+ left join "platform"."meetings" m on m.id = pa."meetingId"
116
+ ${MEMBER_JOINS("pa.\"userId\"")}
117
+ order by pa."userId", q.scope, q.id, m."startsAt" nulls first`,
118
+ transform: (row) => ({
119
+ ...row,
120
+ answer: describeAnswer(row.definition, row.answer_json),
121
+ answer_value: JSON.stringify(row.answer_json)
122
+ })
123
+ };
36
124
  const EXPORTS = {
37
125
  /**
38
126
  * One row per earned star: a meeting attended or a competition entered.
@@ -94,7 +182,10 @@ select ${MEMBER("a.\"userId\"")},
94
182
  m."startsAt" as "meeting_starts_at",
95
183
  a."recordedAt" as "checked_in_at",
96
184
  a."method"::text as "check_in_method",
97
- m."countsForCredit" as "counts_for_credit"
185
+ m."countsForCredit" as "counts_for_credit",
186
+ lower(p."ugaEmail") as "uga_email",
187
+ coalesce(p."involvementFirstName", p."legalFirstName") as "first_name",
188
+ coalesce(p."involvementLastName", p."legalLastName") as "last_name"
98
189
  from "platform"."attendance" a
99
190
  join "platform"."meetings" m on m."id" = a."meetingId"
100
191
  ${MEMBER_JOINS("a.\"userId\"")}
@@ -146,7 +237,8 @@ where ($1::timestamptz is null or r."createdAt" >= $1::timestamptz)
146
237
  and ($2::timestamptz is null or r."createdAt" < $2::timestamptz)
147
238
  and $3::uuid is null
148
239
  order by r."createdAt", r."userId", r."id"`
149
- }
240
+ },
241
+ responses: RESPONSES
150
242
  };
151
243
  function exportParams(filters) {
152
244
  return [
@@ -1,5 +1,5 @@
1
- import { r as errorMessage } from "./ui-CdKo8mLw.js";
2
- import { d as withConnection, s as ProductionError } from "./cli-Do2nWD7W.js";
1
+ import { r as errorMessage } from "./ui-Bb69ZfVv.js";
2
+ import { c as ProductionError, f as withConnection } from "./cli-DjIwbjnx.js";
3
3
  //#region src/production/accounts.ts
4
4
  /**
5
5
  * Production accounts: reading them, finding one by email, and creating the
@@ -438,8 +438,8 @@ function isNoEnv() {
438
438
  //#region ../cli-core/src/telemetry.ts
439
439
  /**
440
440
  * devtools' own Sentry wiring — the CLI is a consumer of
441
- * `@devdogsuga/telemetry` like `apps/platform`, `apps/schedule-builder`, and
442
- * `apps/sandbox`, but on `@sentry/node` rather than a framework SDK: a CLI
441
+ * `@devdogsuga/telemetry` like `apps/platform` and `apps/schedule-builder`,
442
+ * but on `@sentry/node` rather than a framework SDK: a CLI
443
443
  * process starts, runs one command, and exits, with none of a server's
444
444
  * request lifecycle for a framework integration to hook into.
445
445
  *
package/dist/telemetry.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as hasRealCaller, c as reportDevtoolsError, i as devtoolsTelemetryEnabled, l as reportDevtoolsFailure, n as captureDevtoolsError, o as initDevtoolsTelemetry, r as devtoolsEnvironment, s as lastSentryEventId, t as captureDevtoolsDeprecation, u as resolveDevtoolsDsn } from "./telemetry-Bjoz29Hl.js";
1
+ import { a as hasRealCaller, c as reportDevtoolsError, i as devtoolsTelemetryEnabled, l as reportDevtoolsFailure, n as captureDevtoolsError, o as initDevtoolsTelemetry, r as devtoolsEnvironment, s as lastSentryEventId, t as captureDevtoolsDeprecation, u as resolveDevtoolsDsn } from "./telemetry-Be_lbB5c.js";
2
2
  export { captureDevtoolsDeprecation, captureDevtoolsError, devtoolsEnvironment, devtoolsTelemetryEnabled, hasRealCaller, initDevtoolsTelemetry, lastSentryEventId, reportDevtoolsError, reportDevtoolsFailure, resolveDevtoolsDsn };
@@ -1,4 +1,4 @@
1
- import { c as reportDevtoolsError, p as isNonInteractive, y as suppressFailureLog } from "./telemetry-Bjoz29Hl.js";
1
+ import { c as reportDevtoolsError, p as isNonInteractive, y as suppressFailureLog } from "./telemetry-Be_lbB5c.js";
2
2
  import { cancel, isCancel, log, note } from "@clack/prompts";
3
3
  //#region ../cli-core/src/ui.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { o as unwrap } from "./ui-CdKo8mLw.js";
1
+ import { o as unwrap } from "./ui-Bb69ZfVv.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, join } from "node:path";
4
4
  import { confirm, log, spinner } from "@clack/prompts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devdogsuga/backstage",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Officer and production CLI for DevDogsUGA: deploys, env sync with Bitwarden and GitHub, the migration planner role, club graphics and QR codes, GitHub rulesets and settings, and the newsletter. Ships built JS; run it anywhere with `pnpm dlx @devdogsuga/backstage` (no checkout needed to start; commands that read a checkout say so).",
@@ -40,10 +40,10 @@
40
40
  "tsx": "^4.23.12",
41
41
  "typescript": "^6.0.3",
42
42
  "zod": "4.4.3",
43
- "@devdogsuga/brand": "0.1.5",
44
- "@devdogsuga/events": "0.1.5",
45
- "@devdogsuga/newsletter": "0.1.10",
46
- "@devdogsuga/telemetry": "0.1.3"
43
+ "@devdogsuga/brand": "0.1.6",
44
+ "@devdogsuga/events": "0.1.6",
45
+ "@devdogsuga/telemetry": "0.1.4",
46
+ "@devdogsuga/newsletter": "0.1.11"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@devdogsuga/db": "*",
@@ -1,2 +0,0 @@
1
- import { a as listSecrets, o as projectIdFor } from "./client-B_MY9kVI.js";
2
- export { listSecrets, projectIdFor };
@@ -1,225 +0,0 @@
1
- import { n as isDryRun } from "./dry-run-3IxPmCtW.js";
2
- import { r as errorMessage, t as UsageError } from "./ui-CdKo8mLw.js";
3
- import { i as exportParams, n as EXPORT_KINDS, r as auditFilters, t as EXPORTS } from "./queries-DeEwfe2c.js";
4
- import { t as DONE } from "./dispatch-D048O65I.js";
5
- import { a as reportFailure, c as connect, l as dbError, o as say, r as plural, s as ProductionError, u as requireProductionKey } from "./cli-Do2nWD7W.js";
6
- import { i as parseBound, n as findMeetingsWith, r as pickMeeting, t as describeMeeting } from "./meetings-DfU867eV.js";
7
- import { open, rm } from "node:fs/promises";
8
- import { execFile } from "node:child_process";
9
- import { parseArgs, promisify } from "node:util";
10
- /**
11
- * Quotes a field only when it has to be: a comma, a quote, a carriage return
12
- * or a line feed. Quoting only what needs it keeps the file readable in a
13
- * terminal.
14
- *
15
- * A leading `=`, `+`, `-`, `@`, tab or carriage return gets a tab prefix:
16
- * spreadsheets start a formula on the first four, and a member can name their
17
- * team `=cmd|'/c calc'!A1`. The tab is invisible in a cell and neutralises it.
18
- */
19
- function csvField(value) {
20
- if (value === null || value === void 0) return "";
21
- let text;
22
- if (typeof value === "string") text = value;
23
- else if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") text = String(value);
24
- else if (value instanceof Date) text = csvTimestamp(value);
25
- else text = JSON.stringify(value) ?? "";
26
- const guarded = /^[=+\-@\t\r]/.test(text) ? `\t${text}` : text;
27
- if (!/[",\r\n]/.test(guarded)) return guarded;
28
- return `"${guarded.replace(/"/g, "\"\"")}"`;
29
- }
30
- function csvRow(values) {
31
- return values.map(csvField).join(",") + "\r\n";
32
- }
33
- /**
34
- * ISO 8601 with an explicit offset, never a bare local time: a timestamp
35
- * without one is read in whatever zone the importer runs in.
36
- */
37
- function csvTimestamp(value) {
38
- if (value === null) return "";
39
- const date = value instanceof Date ? value : new Date(value);
40
- return Number.isNaN(date.getTime()) ? "" : date.toISOString();
41
- }
42
- //#endregion
43
- //#region src/export/commands.ts
44
- /**
45
- * `backstage export <stars|attendance|reflections>`: the platform's CSV
46
- * downloads, moved here from `/console/exports`.
47
- *
48
- * pnpm backstage export stars --from 2026-08-17 --to 2026-12-12
49
- * pnpm backstage export attendance --meeting 2026-09-09 --out -
50
- *
51
- * ## Audited, as the downloads were
52
- *
53
- * Each export writes a `platform.exportAudit` row BEFORE the first row is
54
- * read, as the web routes did: an export that fails halfway still put rows
55
- * in front of somebody. The row names the officer's platform account, found
56
- * through the GitHub login `gh` is signed in as (the identity they linked on
57
- * the platform), so an export nobody can attribute is refused rather than
58
- * recorded as nobody's. The row count is filled in when the file is done.
59
- *
60
- * ## The file
61
- *
62
- * Written to `./<kind>[-<meeting>][-<from>][-<to>].csv` by default, created
63
- * readable by its owner only (it is member PII), never over an existing file
64
- * without `--yes`. `--out -` streams to stdout without the byte-order mark,
65
- * for a pipe. A failure partway removes the partial file.
66
- *
67
- * `--from`/`--to` take Eastern days, `--to` inclusive (see `production/time.ts`).
68
- */
69
- function parse(argv) {
70
- const [kind, ...rest] = argv;
71
- if (!kind || !EXPORT_KINDS.includes(kind)) throw new UsageError(kind ? `Unknown export "${kind}". Try ${EXPORT_KINDS.join(", ")}.` : `Name the export: ${EXPORT_KINDS.join(", ")}.`);
72
- try {
73
- const { values } = parseArgs({
74
- args: rest,
75
- options: {
76
- from: { type: "string" },
77
- to: { type: "string" },
78
- meeting: { type: "string" },
79
- out: { type: "string" },
80
- yes: { type: "boolean" },
81
- "dry-run": { type: "boolean" }
82
- },
83
- allowPositionals: false,
84
- strict: true
85
- });
86
- if (values.meeting && kind !== "attendance") throw new UsageError("--meeting only applies to the attendance export.");
87
- return {
88
- kind,
89
- values
90
- };
91
- } catch (err) {
92
- if (err instanceof UsageError) throw err;
93
- throw new UsageError(errorMessage(err));
94
- }
95
- }
96
- const run = promisify(execFile);
97
- /** The GitHub login `gh` is signed in as. */
98
- async function ghLogin() {
99
- try {
100
- const { stdout } = await run("gh", [
101
- "api",
102
- "user",
103
- "--jq",
104
- ".login"
105
- ]);
106
- const login = stdout.trim();
107
- if (login) return login;
108
- } catch {}
109
- throw new ProductionError("Exports are audited under your GitHub login, and `gh` could not say who you are. Install it from https://cli.github.com and run `gh auth login`.");
110
- }
111
- const OFFICER_QUERY = `
112
- select distinct i."user_id"::text as "userId"
113
- from "auth"."identities" i
114
- where i."provider" = 'github'
115
- and lower(i."identity_data" ->> 'user_name') = lower($1)
116
- `;
117
- function sourceWith(sql) {
118
- return {
119
- officersFor: async (login) => (await sql.unsafe(OFFICER_QUERY, [login])).map((r) => String(r.userId)),
120
- findMeetings: findMeetingsWith(sql),
121
- audit: async (userId, kind, filters) => {
122
- const [row] = await sql.unsafe(`insert into "platform"."exportAudit" ("userId", "kind", "filters")
123
- values ($1, $2, $3::text::jsonb) returning "id"::text as "id"`, [
124
- userId,
125
- kind,
126
- JSON.stringify(filters)
127
- ]);
128
- return String(row.id);
129
- },
130
- rows: (kind, filters) => sql.unsafe(EXPORTS[kind].sql, exportParams(filters)).cursor(500),
131
- finish: async (auditId, rowCount) => {
132
- await sql.unsafe(`update "platform"."exportAudit" set "rowCount" = $1 where "id" = $2::uuid`, [rowCount, auditId]);
133
- },
134
- close: () => sql.end({ timeout: 5 })
135
- };
136
- }
137
- async function productionSource() {
138
- return sourceWith(connect(await requireProductionKey("DB_URL")));
139
- }
140
- async function fileSink(path, overwrite) {
141
- if (path === "-") return {
142
- write: (text) => new Promise((resolve, reject) => process.stdout.write(text, (err) => err ? reject(err) : resolve())),
143
- close: () => Promise.resolve(),
144
- discard: () => Promise.resolve()
145
- };
146
- let handle;
147
- try {
148
- handle = await open(path, overwrite ? "w" : "wx", 384);
149
- } catch (err) {
150
- if (err.code === "EEXIST") throw new UsageError(`${path} already exists. Pass --yes to replace it.`);
151
- throw new UsageError(`Could not write ${path}.`);
152
- }
153
- await handle.write("");
154
- return {
155
- write: async (text) => {
156
- await handle.write(text);
157
- },
158
- close: () => handle.close(),
159
- discard: async () => {
160
- await handle.close().catch(() => void 0);
161
- await rm(path, { force: true });
162
- }
163
- };
164
- }
165
- function defaultPath(kind, values, meetingSlug) {
166
- const parts = [kind];
167
- if (meetingSlug) parts.push(meetingSlug);
168
- if (values.from) parts.push(values.from.slice(0, 10));
169
- if (values.to) parts.push(values.to.slice(0, 10));
170
- return `${parts.join("-")}.csv`;
171
- }
172
- async function runExport(argv, deps = {}) {
173
- let source;
174
- let sink;
175
- let done = false;
176
- try {
177
- const { kind, values } = parse(argv);
178
- const filters = {
179
- from: parseBound("--from", values.from),
180
- to: parseBound("--to", values.to)
181
- };
182
- if (filters.from && filters.to && filters.from >= filters.to) throw new UsageError("--from must come before --to.");
183
- const login = await (deps.ghLogin ?? ghLogin)();
184
- source = await (deps.source ?? productionSource)();
185
- const officers = await source.officersFor(login);
186
- if (officers.length !== 1) throw new ProductionError(officers.length === 0 ? `No platform account has linked the GitHub login ${login}. Link GitHub on your platform profile, then export again.` : `${plural(officers.length, "platform account")} linked the GitHub login ${login}; an export must be attributable to one.`);
187
- let slug;
188
- if (values.meeting) {
189
- const meeting = pickMeeting(values.meeting, await source.findMeetings(values.meeting));
190
- filters.meetingId = meeting.id;
191
- slug = meeting.slug;
192
- say(`Meeting: ${describeMeeting(meeting)}.`);
193
- }
194
- const path = values.out ?? defaultPath(kind, values, slug);
195
- if (values["dry-run"] === true || isDryRun()) {
196
- say(`Would export ${kind} to ${path === "-" ? "stdout" : path}, audited as ${login}.`);
197
- return;
198
- }
199
- sink = await (deps.sink ?? fileSink)(path, values.yes === true);
200
- const auditId = await source.audit(officers[0], kind, auditFilters(filters));
201
- const { columns } = EXPORTS[kind];
202
- await sink.write(csvRow(columns));
203
- let count = 0;
204
- for await (const page of source.rows(kind, filters)) {
205
- await sink.write(page.map((row) => csvRow(columns.map((c) => row[c]))).join(""));
206
- count += page.length;
207
- }
208
- await sink.close();
209
- done = true;
210
- await source.finish(auditId, count);
211
- say(`Exported ${plural(count, "row")} to ${path === "-" ? "stdout" : path}.`, "success");
212
- } catch (err) {
213
- if (sink && !done) await sink.discard().catch(() => void 0);
214
- const worded = err instanceof UsageError || err instanceof ProductionError ? err : err.code ? dbError("The export failed", err) : err;
215
- reportFailure("export", worded);
216
- } finally {
217
- await source?.close().catch(() => void 0);
218
- }
219
- }
220
- const handleExport = async (rest) => {
221
- await runExport(rest);
222
- return process.exitCode ? null : DONE;
223
- };
224
- //#endregion
225
- export { handleExport };