@devdogsuga/backstage 0.1.0

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 ADDED
@@ -0,0 +1,147 @@
1
+ # @devdogsuga/backstage
2
+
3
+ The officer and production CLI: everything that needs production secrets and is
4
+ used only by the two people who hold them (and CI), plus the tools that need
5
+ only the user's own login. Run it anywhere:
6
+
7
+ ```bash
8
+ pnpm dlx --config.minimum-release-age=0 --config.dlx-cache-max-age=0 @devdogsuga/backstage
9
+ ```
10
+
11
+ It always runs the latest publish, CI included, so local runs and CI runs
12
+ match. The two flags matter outside a DevDogsUGA checkout, where the workspace's
13
+ `minimumReleaseAgeExclude` and `dlxCacheMaxAge: 0` do not apply (inside one,
14
+ `pnpm backstage` is the script).
15
+
16
+ It **starts without a checkout**. Help, `version`, `completions`, the tools
17
+ below that need no secrets (`graphics`, `qr`, `github`, `newsletter`) and
18
+ anything run with `--no-env` never look for one; a command that reads a checkout says
19
+ "run this from inside a DevDogsUGA clone" and exits 1. The DevDogsUGA libraries
20
+ it reads (`@devdogsuga/env`, `@devdogsuga/db`) are optional peers, resolved
21
+ through the checkout by the commands that need them.
22
+
23
+ ```bash
24
+ pnpm backstage # no arguments: a menu
25
+ pnpm backstage env pull --target production # Bitwarden → .env.production
26
+ pnpm backstage env audit --target production # every store, plus orphaned Worker secrets
27
+ pnpm backstage env audit --target production --prune
28
+ pnpm backstage deploy platform --tier staging # token check, secrets file, wrangler deploy
29
+ pnpm backstage --no-env deploy write-env # the CI steps that supply their own environment
30
+ pnpm backstage --no-env planner status # the preflight credential, from DB_URL
31
+ pnpm backstage graphics 'event/*' --out ~/images # club images, no checkout
32
+ pnpm backstage qr https://devdogsuga.org --format svg,png,webp --logo acm
33
+ pnpm backstage newsletter send 3.0.1 --to a@uga.edu # asks first; --yes with no terminal
34
+ ```
35
+
36
+ ## Commands
37
+
38
+ | Command | What it does |
39
+ | ---------------------------------------------- | -------------------------------------------------------------------------------------- |
40
+ | `deploy <app> --tier <t>` | Checks `CLOUDFLARE_API_TOKEN`, writes the Worker's secrets file, deploys, removes it. |
41
+ | `deploy write-env` | Composes `.env.<DEPLOY_ENV>` from the GitHub environment. |
42
+ | `deploy preflight` | Classifies the project: paused (skip) or broken (fail). |
43
+ | `deploy plan`, `deploy migrate` | Dry-run the migrations into the job summary; apply them to `DB_URL`. |
44
+ | `deploy smoke --tier <t> [--app]` | Public routes answer 200, the auth redirect works, this deploy's Sentry release shows. |
45
+ | `deploy reconcile --tier <t>` | The platform's config reconcile, after the deploy (`CRON_SECRET`). |
46
+ | `env pull\|push\|audit --target <t>` | One env file per target, synced to Bitwarden and GitHub. `audit` lists orphans. |
47
+ | `planner status\|create\|reset-password\|drop` | The `migration_planner` role the preflight tier holds. |
48
+ | `graphics [graphic…]` | Club images from `@devdogsuga/brand`: `brand/*`, `app/*`, `event/*`. |
49
+ | `qr <text>` | QR codes with every option of `/console/qr`. |
50
+ | `github rulesets\|settings` | Diff (and with `--apply` write) GitHub config, through `gh`. |
51
+ | `newsletter render\|draft\|send <issue…>` | Changelog issues as files, mailbox drafts, or a send. |
52
+
53
+ `smoke` and `reconcile` replace DevDogsUGA's `packages/deploy-checks`. The
54
+ per-app data (hosts, public paths, the protected path and its redirect) stays in
55
+ DevDogsUGA, as a `smoke` field on each app's entry in `workers.json`:
56
+
57
+ ```json
58
+ [
59
+ {
60
+ "path": "apps/platform",
61
+ "smoke": {
62
+ "hosts": {
63
+ "staging": "staging.devdogsuga.org",
64
+ "production": "devdogsuga.org"
65
+ },
66
+ "publicPaths": ["/", "/events"],
67
+ "protectedPath": "/console/permissions",
68
+ "protectedRedirectPrefix": "/auth"
69
+ }
70
+ },
71
+ "apps/sandbox"
72
+ ]
73
+ ```
74
+
75
+ Entries may be bare paths or objects. While the field is absent, the table
76
+ `deploy-checks` carried answers for `platform` and `schedule-builder`.
77
+
78
+ ## The tools that need no production secrets
79
+
80
+ None of these reads an env file or needs a checkout, so the launcher skips env
81
+ entry for them (`envFree` in the command tree).
82
+
83
+ - **`graphics`** renders the brand, app and event images with
84
+ `@devdogsuga/brand/render`, reading meetings from the published
85
+ `@devdogsuga/events`. Files go to `--out` or the current directory, flat, as
86
+ `<name>-<format>.png`. There is no `page/*` group (the platform renders page
87
+ cards per request) and no `--default-out`.
88
+ - **`qr`** takes its options from `qrRequestSchema` in `@devdogsuga/brand/qr`,
89
+ the same schema `/console/qr` parses, so a new option reaches both. Every
90
+ schema field has a flag (`qr/options.ts` is typed over the schema, and a test
91
+ checks it): content, `--size`, `--margin`, `--color`, `--background`,
92
+ `--gradient`, `--shape`, `--error-level`, `--qr-version`, `--logo` (a preset,
93
+ `none`, or an image file) with `--logo-size`, `--logo-padding` and
94
+ `--logo-crop x,y,w,h`, and `--format` with any of svg, png, jpg, webp, avif
95
+ and tiff at once. It prints the same scannability warning as the page.
96
+ `/console/qr` stays, deprecated; new options are CLI-only.
97
+ - **`github rulesets|settings`** keep GitHub's rulesets and repository settings
98
+ as code, through `gh` and your own login. They must keep matching what the
99
+ platform's `server/github/rulesets.ts` and `teamSync.ts` assume.
100
+ - **`newsletter render|draft|send`** are described in the
101
+ [newsletter package](../newsletter/README.md). Drafts and sends use the club
102
+ mailbox (`devdogs@uga.edu`) with the officer's own Microsoft sign-in; there
103
+ is no `--mailbox`. `send` needs `--to` and always asks first, naming the issue
104
+ and every recipient; with no terminal, `--yes` answers it. The sign-in
105
+ borrows Thunderbird's public client ID (see `src/newsletter/oauth.ts`); if
106
+ Microsoft or UGA's tenant blocks it, sending needs a club-owned app
107
+ registration.
108
+
109
+ ## Tiers, `--no-env`, CI
110
+
111
+ Nothing is ever asked at launch. `--tier <t>` (anywhere in argv) or
112
+ `DEPLOY_ENV` names the session's tier and so the env file loaded; with neither
113
+ the session is plain development (where `BWS_ACCESS_TOKEN` lives). `deploy`
114
+ refuses to run without one of the two.
115
+
116
+ `--no-env` loads no env files and does not look for a checkout: the caller's
117
+ environment is the environment. It is for `deploy write-env`, which creates the
118
+ file tier resolution would otherwise insist on reading, and for CI steps that
119
+ hold one narrow credential in the job's `env:` block.
120
+
121
+ With no terminal, or `CI=true`: no menu, no banner, plain lines, errors on
122
+ stderr, and every confirmation needs `--yes`. Against staging or production a
123
+ command that is not read-only asks once first (`--yes` answers it).
124
+
125
+ Each command checks for the secrets it uses up front: `deploy <app>` for
126
+ `CLOUDFLARE_API_TOKEN`, `deploy reconcile` for `CRON_SECRET`, `env` for the
127
+ Secrets Manager token (flag, environment, then the Bitwarden vault, which `env`
128
+ signs in to and unlocks itself, then asking).
129
+
130
+ ## Layout
131
+
132
+ Like devtools: `src/<domain>/catalog.ts` (inert data) and `commands.ts`;
133
+ `src/catalog.ts` composes the tree, `src/cli.ts` maps names to handlers,
134
+ `src/launch-core.ts` settles the tier before any command is imported. The
135
+ shared core is the private `@devdogsuga/cli-core`, which `tsdown` inlines; every
136
+ other import must be declared here, and the build fails when one is not.
137
+
138
+ `env.ts` is the operator manifest (`BWS_ACCESS_TOKEN`, `CLOUDFLARE_API_TOKEN`
139
+ and friends). It is a copy of devtools' own, kept identical by a test, because
140
+ each CLI loads the one beside it.
141
+
142
+ Contract tests (`pnpm test:contract`) pack the real tarball, install it with
143
+ install scripts off outside any repo (and check the Bitwarden native module
144
+ loads), and run it again inside the fixture repo. They gate publishing.
145
+
146
+ Crash reporting is the same as devtools' (Sentry, scrubbed, off with
147
+ `DEVTOOLS_TELEMETRY=0`); the release is named `backstage@<version>`.
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Entry point for the `backstage` bin.
4
+ *
5
+ * Ships built JS (tsdown bundles `src/` and the private
6
+ * `@devdogsuga/cli-core` into a flat `dist/`) and runs it with plain `node`.
7
+ * It starts anywhere, a DevDogsUGA checkout or not: the DevDogsUGA packages it
8
+ * reads (`@devdogsuga/env`, `@devdogsuga/db`) are optional peers, resolved
9
+ * through the checkout only by the commands that need one.
10
+ */
11
+ import { dirname, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const dist = join(dirname(fileURLToPath(import.meta.url)), "..", "dist");
15
+
16
+ const { launch } = await import(join(dist, "launch.js"));
17
+ try {
18
+ await launch(process.argv.slice(2));
19
+ } catch (err) {
20
+ // Whatever escapes before a command is dispatched (repo discovery, env
21
+ // entry); the dispatch reports its own.
22
+ process.stderr.write(
23
+ `backstage: ${err instanceof Error ? err.message : String(err)}\n`,
24
+ );
25
+ const { captureDevtoolsError } = await import(join(dist, "telemetry.js"));
26
+ await captureDevtoolsError(err);
27
+ process.exitCode = 1;
28
+ }
@@ -0,0 +1,66 @@
1
+ //#region ../cli-core/src/args.ts
2
+ /**
3
+ * Positional arguments, with flag values excluded.
4
+ *
5
+ * Its own module because of one bug it prevents. The first positional is the
6
+ * subcommand, and `pull`, `push` and `audit` do very different things to live
7
+ * credentials. A naive `filter((a) => !a.startsWith("--"))` reads a flag's
8
+ * VALUE as a positional, so `env --file notes.env audit` fails loudly:
9
+ * `notes.env` is not a subcommand and the command refuses. The quiet version
10
+ * is the one that matters:
11
+ *
12
+ * env --file push audit
13
+ *
14
+ * which runs `push`, writing to Bitwarden and GitHub, when the caller asked
15
+ * for `audit`, which writes nothing at all.
16
+ */
17
+ /**
18
+ * Flags that consume the token after them.
19
+ */
20
+ const VALUE_FLAGS = /* @__PURE__ */ new Set([
21
+ "--access-token",
22
+ "--format",
23
+ "--out",
24
+ "--version",
25
+ "--app",
26
+ "--apps",
27
+ "--base-url",
28
+ "--file",
29
+ "--source",
30
+ "--target",
31
+ "--tier",
32
+ "--cron",
33
+ "--workflow",
34
+ "--params",
35
+ "--port",
36
+ "--preview-url",
37
+ "--db-url",
38
+ "--user",
39
+ "--filter",
40
+ "--shell"
41
+ ]);
42
+ function positionals(argv) {
43
+ const found = [];
44
+ for (let i = 0; i < argv.length; i += 1) {
45
+ const arg = argv[i];
46
+ if (!arg.startsWith("-")) {
47
+ found.push(arg);
48
+ continue;
49
+ }
50
+ const next = argv[i + 1];
51
+ if (VALUE_FLAGS.has(arg) && next !== void 0 && !next.startsWith("-")) i += 1;
52
+ }
53
+ return found;
54
+ }
55
+ /**
56
+ * The value after `flag`, or `undefined` when the flag is absent or is
57
+ * followed by another flag instead of a value.
58
+ */
59
+ function flagValue(rest, flag) {
60
+ const index = rest.indexOf(flag);
61
+ if (index === -1) return void 0;
62
+ const value = rest[index + 1];
63
+ return value && !value.startsWith("--") ? value : void 0;
64
+ }
65
+ //#endregion
66
+ export { positionals as n, flagValue as t };
@@ -0,0 +1,3 @@
1
+ {
2
+ "sentryDsn": ""
3
+ }