@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.
- package/README.md +42 -25
- package/dist/{catalog-BiXI0wyi.js → catalog-COleRgQD.js} +22 -13
- package/dist/{cli-Cz8faASC.js → cli-CMThHZgV.js} +23 -29
- package/dist/{cli-Do2nWD7W.js → cli-DjIwbjnx.js} +4 -4
- package/dist/client-A6pJcxSE.js +2 -0
- package/dist/{client-B_MY9kVI.js → client-NuoKMQmV.js} +4 -4
- package/dist/{commands-D345F-Od.js → commands-35rBJ1R0.js} +2 -2
- package/dist/{commands-zPSnOGzH.js → commands-BF5eptEi.js} +4 -4
- package/dist/commands-CaSixDwT.js +699 -0
- package/dist/{commands-DzALVX5g.js → commands-CuYIkHeU.js} +3 -3
- package/dist/{commands-BQ2NIqub.js → commands-DL5fpABN.js} +5 -5
- package/dist/{commands-Sx1fDNNn.js → commands-DQYH_m6i.js} +3 -3
- package/dist/{commands-CLSxKz-l.js → commands-KY-9X1Rh.js} +4 -4
- package/dist/{commands-Dv5TXjvb.js → commands-jJcS7D2a.js} +6 -6
- package/dist/{dry-run-3IxPmCtW.js → dry-run-CFCZUpjb.js} +1 -1
- package/dist/launch.js +6 -6
- package/dist/{meetings-DfU867eV.js → meetings-CLZ6xQQB.js} +2 -2
- package/dist/{peers-B0KDkI6a.js → peers-u1zxe-Ub.js} +1 -1
- package/dist/{queries-DeEwfe2c.js → queries-CaAe3cve.js} +100 -8
- package/dist/{read-B-aJJewb.js → read-B8DSwYfB.js} +2 -2
- package/dist/{telemetry-Bjoz29Hl.js → telemetry-Be_lbB5c.js} +2 -2
- package/dist/telemetry.js +1 -1
- package/dist/{ui-CdKo8mLw.js → ui-Bb69ZfVv.js} +1 -1
- package/dist/{vault-CsBC-1Mq.js → vault-DFV0zjbT.js} +1 -1
- package/package.json +5 -5
- package/dist/client-DwDGjalU.js +0 -2
- package/dist/commands-5Ou8xJw5.js +0 -225
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { i as explain } from "./ui-
|
|
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-
|
|
6
|
-
if (what === "attendance") return (await import("./commands-
|
|
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-
|
|
2
|
-
import { o as unwrap, r as errorMessage, t as UsageError } from "./ui-
|
|
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
|
|
5
|
-
import { a as createAccounts, i as createAccountFor, n as accountFinder, r as accountsWith, t as parseCsv } from "./read-
|
|
6
|
-
import { n as findMeetingsWith, r as pickMeeting, t as describeMeeting } from "./meetings-
|
|
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-
|
|
2
|
-
import { n as isDryRun } from "./dry-run-
|
|
3
|
-
import { a as explainError, o as unwrap, t as UsageError } from "./ui-
|
|
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-
|
|
2
|
-
import { r as errorMessage, t as UsageError } from "./ui-
|
|
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
|
|
5
|
-
import { a as createAccounts, i as createAccountFor, n as accountFinder, o as readAccounts, t as parseCsv } from "./read-
|
|
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-
|
|
2
|
-
import { a as explainError, o as unwrap, r as errorMessage, t as UsageError } from "./ui-
|
|
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.
|
|
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.
|
|
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.
|
|
44
|
+
slug: row.slug,
|
|
45
45
|
hint: describeMeeting(meeting),
|
|
46
46
|
detail,
|
|
47
47
|
items: items.map((item) => {
|
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-
|
|
2
|
-
import { c as setMenuEnvHook, d as formatCommand, f as helpPath, n as bareGroupStartPath, t as catalog } from "./catalog-
|
|
3
|
-
import { i as loadEnvSession, r as loadEnvLoad } from "./peers-
|
|
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-
|
|
6
|
-
import { o as unwrap, r as errorMessage } from "./ui-
|
|
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-
|
|
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-
|
|
2
|
-
import "./cli-
|
|
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-
|
|
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
|
|
4
|
-
* columns unchanged: anything downstream that read
|
|
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
|
|
8
|
-
*
|
|
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-
|
|
2
|
-
import {
|
|
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
|
|
442
|
-
*
|
|
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-
|
|
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-
|
|
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
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devdogsuga/backstage",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
44
|
-
"@devdogsuga/events": "0.1.
|
|
45
|
-
"@devdogsuga/
|
|
46
|
-
"@devdogsuga/
|
|
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": "*",
|
package/dist/client-DwDGjalU.js
DELETED
|
@@ -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 };
|