@devdogsuga/devtools 0.1.12 → 0.1.14
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/bin/devtools-ci.mjs +15 -1
- package/bin/devtools.mjs +15 -1
- package/dist/catalog.d.ts +7 -5
- package/dist/catalog.js +1 -25
- package/dist/cf/build.d.ts +13 -0
- package/dist/cf/build.js +65 -0
- package/dist/cf/commands.js +6 -16
- package/dist/cf/local-env.d.ts +14 -0
- package/dist/cf/local-env.js +29 -0
- package/dist/ci.js +8 -1
- package/dist/cli.js +134 -138
- package/dist/commands.d.ts +10 -4
- package/dist/commands.js +40 -30
- package/dist/db/generated-env.d.ts +69 -0
- package/dist/db/generated-env.js +113 -0
- package/dist/db/seed-production.js +12 -6
- package/dist/docs/index-pages.js +2 -2
- package/dist/emails/commands.js +3 -3
- package/dist/env-entry.d.ts +49 -0
- package/dist/env-entry.js +204 -0
- package/dist/environment-doctor.d.ts +9 -0
- package/dist/environment-doctor.js +18 -5
- package/dist/environment.d.ts +2 -2
- package/dist/environment.js +5 -28
- package/dist/images/commands.d.ts +3 -6
- package/dist/images/commands.js +26 -30
- package/dist/images/events.d.ts +27 -16
- package/dist/images/events.js +32 -68
- package/dist/images/graphics.js +2 -1
- package/dist/images/prompts.js +2 -2
- package/dist/instance.d.ts +29 -27
- package/dist/instance.js +49 -76
- package/dist/launch-ci.js +5 -5
- package/dist/launch.d.ts +2 -1
- package/dist/launch.js +74 -111
- package/dist/menu.d.ts +17 -5
- package/dist/menu.js +34 -11
- package/dist/moderation.d.ts +48 -40
- package/dist/moderation.js +95 -194
- package/dist/oauth/wizard.d.ts +2 -2
- package/dist/oauth/wizard.js +6 -26
- package/dist/persona.d.ts +18 -0
- package/dist/persona.js +221 -0
- package/dist/repo/peers.d.ts +2 -0
- package/dist/repo/peers.js +3 -0
- package/dist/repo/supabase-project.d.ts +60 -0
- package/dist/repo/supabase-project.js +124 -0
- package/dist/setup.js +2 -2
- package/dist/stack.js +142 -29
- package/dist/telemetry.d.ts +18 -5
- package/dist/telemetry.js +26 -5
- package/dist/ui.d.ts +15 -0
- package/dist/ui.js +20 -0
- package/dist/workflows/commands.d.ts +10 -0
- package/dist/workflows/commands.js +119 -25
- package/package.json +10 -5
package/dist/cli.js
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
* anywhere, so there is nothing to fall out of step.
|
|
19
19
|
*/
|
|
20
20
|
import { confirm, intro, log, note, outro, select, spinner, } from "@clack/prompts";
|
|
21
|
-
import {
|
|
22
|
-
import { conformance,
|
|
21
|
+
import { resolveInstance } from "./instance.js";
|
|
22
|
+
import { conformance, withTemporaryModerator } from "./moderation.js";
|
|
23
|
+
import { runPersona, refuseUnlessDevelopment } from "./persona.js";
|
|
23
24
|
import { runEnvironmentDoctor } from "./environment-doctor.js";
|
|
24
25
|
import { currentRootHolder, grantRoot, listCandidates, transferRoot, } from "./grantRoot.js";
|
|
25
26
|
import { connectRemoteProject, runStackCommand, } from "./stack.js";
|
|
@@ -44,7 +45,7 @@ import { loadEnv } from "./repo/peers.js";
|
|
|
44
45
|
import { setExplicitAccessToken } from "./bws/client.js";
|
|
45
46
|
import { positionals } from "./args.js";
|
|
46
47
|
import { resolveVaultTarget } from "./pick.js";
|
|
47
|
-
import { bail, errorMessage, explain, renderChecks, unwrap } from "./ui.js";
|
|
48
|
+
import { bail, errorMessage, explain, explainError, renderChecks, unwrap } from "./ui.js";
|
|
48
49
|
import { helpPath, renderHelp } from "./help.js";
|
|
49
50
|
import { subcommandList, subcommandNames } from "./commands.js";
|
|
50
51
|
import { bareGroupStartPath, runMenu } from "./menu.js";
|
|
@@ -62,10 +63,6 @@ import { runWorkflows } from "./workflows/commands.js";
|
|
|
62
63
|
import { runCf } from "./cf/commands.js";
|
|
63
64
|
import { captureDevtoolsError, initDevtoolsTelemetry } from "./telemetry.js";
|
|
64
65
|
import { ownVersion } from "./version.js";
|
|
65
|
-
const MODERATION_COMMANDS = ["moderation", "grant-root"];
|
|
66
|
-
function isModerationCommand(value) {
|
|
67
|
-
return MODERATION_COMMANDS.includes(value);
|
|
68
|
-
}
|
|
69
66
|
function flagValue(rest, flag) {
|
|
70
67
|
const index = rest.indexOf(flag);
|
|
71
68
|
if (index === -1)
|
|
@@ -93,46 +90,6 @@ function refuseRetiredDbFlags(rest) {
|
|
|
93
90
|
}
|
|
94
91
|
return false;
|
|
95
92
|
}
|
|
96
|
-
// ── Connecting ───────────────────────────────────────────────────────────────
|
|
97
|
-
/**
|
|
98
|
-
* Finds the local stack and checks it has been migrated.
|
|
99
|
-
*
|
|
100
|
-
* These commands only ever run against a local stack, and that is structural
|
|
101
|
-
* rather than a rule they follow: `detectLocalInstance` reads `supabase
|
|
102
|
-
* status`, which describes the Docker stack on this machine and nothing else.
|
|
103
|
-
* There is no remote project for it to return, so the tier check that used to
|
|
104
|
-
* sit here, reading a `production` flag out of the database, bought nothing and
|
|
105
|
-
* has been removed along with the table it read.
|
|
106
|
-
*/
|
|
107
|
-
async function connect() {
|
|
108
|
-
const s = spinner();
|
|
109
|
-
s.start("Looking for your database");
|
|
110
|
-
let instance;
|
|
111
|
-
try {
|
|
112
|
-
instance = detectLocalInstance();
|
|
113
|
-
}
|
|
114
|
-
catch (err) {
|
|
115
|
-
s.stop("Could not find a running database");
|
|
116
|
-
explain("The local Supabase stack is not reachable.", errorMessage(err), [
|
|
117
|
-
"1. Make sure Docker is running",
|
|
118
|
-
"2. Run `pnpm devtools db start` (or choose Database → start in the menu)",
|
|
119
|
-
"3. Confirm with `supabase status`",
|
|
120
|
-
]);
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
try {
|
|
124
|
-
await assertMigrated(instance);
|
|
125
|
-
s.stop(`Connected to your database at ${instance.apiUrl}`);
|
|
126
|
-
}
|
|
127
|
-
catch (err) {
|
|
128
|
-
s.stop("Connected, but the schema is not there");
|
|
129
|
-
explain("That instance cannot be used here.", errorMessage(err), [
|
|
130
|
-
"Run `pnpm devtools db reset` to rebuild your own database from migrations and seeds.",
|
|
131
|
-
]);
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
return instance;
|
|
135
|
-
}
|
|
136
93
|
// ── Commands ─────────────────────────────────────────────────────────────────
|
|
137
94
|
async function runStack(command, rest) {
|
|
138
95
|
// The data commands need the session's connection before anything else —
|
|
@@ -227,43 +184,56 @@ async function runStack(command, rest) {
|
|
|
227
184
|
}
|
|
228
185
|
}
|
|
229
186
|
catch (err) {
|
|
230
|
-
|
|
187
|
+
explainError(`\`${command}\` failed.`, err);
|
|
231
188
|
process.exitCode = 1;
|
|
232
189
|
}
|
|
233
190
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
191
|
+
/**
|
|
192
|
+
* `moderation check [--app <slug>]`.
|
|
193
|
+
*
|
|
194
|
+
* With no app, this is what `moderation catalog` used to be on its own: the
|
|
195
|
+
* report reasons and every app's moderatable content types, the two halves
|
|
196
|
+
* of "what can be reported here" that used to have no answer anywhere but
|
|
197
|
+
* the database. With one, it runs `platform.conformance_check()` for that
|
|
198
|
+
* app, as it always did. Folded into one command because they were always
|
|
199
|
+
* the same question at two different zoom levels, asked through two
|
|
200
|
+
* differently-named commands for no reason better than history.
|
|
201
|
+
*
|
|
202
|
+
* Both halves run inside ONE `withTemporaryModerator` — one throwaway
|
|
203
|
+
* account, created and torn down around whichever half ran, rather than a
|
|
204
|
+
* separate one per RPC call.
|
|
205
|
+
*/
|
|
238
206
|
async function runModerationCheck(instance, appSlug) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
207
|
+
if (!appSlug) {
|
|
208
|
+
const s = spinner();
|
|
209
|
+
s.start("Signing in as a temporary moderator");
|
|
210
|
+
try {
|
|
211
|
+
const catalog = await withTemporaryModerator(instance, readCatalog);
|
|
212
|
+
s.stop("Read the catalog");
|
|
213
|
+
note(renderCatalog(catalog), "Moderation catalog");
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
s.stop("Could not read the catalog");
|
|
217
|
+
explainError("Reading the catalog failed.", err);
|
|
218
|
+
process.exitCode = 1;
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
250
221
|
}
|
|
251
222
|
const s = spinner();
|
|
252
|
-
s.start(`Checking ${
|
|
223
|
+
s.start(`Checking ${appSlug}`);
|
|
253
224
|
let types;
|
|
254
225
|
try {
|
|
255
|
-
types = await
|
|
256
|
-
s.stop(`Checked ${
|
|
226
|
+
types = await withTemporaryModerator(instance, (client) => conformance(client, appSlug));
|
|
227
|
+
s.stop(`Checked ${appSlug}`);
|
|
257
228
|
}
|
|
258
229
|
catch (err) {
|
|
259
230
|
s.stop("The check could not run");
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
]);
|
|
231
|
+
explainError("conformance_check() failed.", err);
|
|
232
|
+
process.exitCode = 1;
|
|
263
233
|
return;
|
|
264
234
|
}
|
|
265
235
|
if (types.length === 0) {
|
|
266
|
-
note(`${
|
|
236
|
+
note(`${appSlug} has no moderatable content types.\n\n` +
|
|
267
237
|
"A table becomes one by carrying a foreign key to\n" +
|
|
268
238
|
'platform."reportResolutions" -- adding that column is the whole\n' +
|
|
269
239
|
"registration. See docs/platform/reporting-and-feedback.md.", "Nothing to check");
|
|
@@ -276,7 +246,7 @@ async function runModerationCheck(instance, appSlug) {
|
|
|
276
246
|
note(renderChecks(type.checks), `${type.tableName} → "${type.contentType}"`);
|
|
277
247
|
}
|
|
278
248
|
if (failures === 0) {
|
|
279
|
-
log.success(`${
|
|
249
|
+
log.success(`${appSlug} looks correctly integrated.`);
|
|
280
250
|
}
|
|
281
251
|
else {
|
|
282
252
|
log.warn(`${failures} check${failures === 1 ? "" : "s"} failed. The last two are ` +
|
|
@@ -284,29 +254,6 @@ async function runModerationCheck(instance, appSlug) {
|
|
|
284
254
|
"than trusting outright.");
|
|
285
255
|
}
|
|
286
256
|
}
|
|
287
|
-
async function runModerationRoundTrip(instance) {
|
|
288
|
-
const s = spinner();
|
|
289
|
-
s.start("Filing a report, quarantining it, and looking again");
|
|
290
|
-
try {
|
|
291
|
-
const steps = await quarantineRoundTrip(instance);
|
|
292
|
-
s.stop("Round-trip finished");
|
|
293
|
-
note(renderChecks(steps), "What happened");
|
|
294
|
-
const failed = steps.filter((step) => !step.ok);
|
|
295
|
-
if (failed.length === 0) {
|
|
296
|
-
log.success("Quarantine freezes a reported profile and resets the display name, and the fixtures were cleaned up.");
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
log.warn(`${failed.length} step${failed.length === 1 ? "" : "s"} did not hold. ` +
|
|
300
|
-
"The fixtures were still cleaned up.");
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
catch (err) {
|
|
304
|
-
s.stop("The round-trip could not run");
|
|
305
|
-
explain("Something went wrong before the checks could finish.", errorMessage(err), [
|
|
306
|
-
"`pnpm devtools db reset` rebuilds the database with the seeded personas and the open report they act on.",
|
|
307
|
-
]);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
257
|
/**
|
|
311
258
|
* Grants Root, asking who to if it was not told.
|
|
312
259
|
*
|
|
@@ -314,8 +261,14 @@ async function runModerationRoundTrip(instance) {
|
|
|
314
261
|
* different actions wearing the same name: one gives you a console you did not
|
|
315
262
|
* have, the other takes somebody else's away. `userRoles_root_singleton` means
|
|
316
263
|
* there is no state where both hold it, so the release cannot be skipped.
|
|
264
|
+
*
|
|
265
|
+
* Runs on any tier the session resolves to now — `resolveInstance` (see
|
|
266
|
+
* `instance.ts`) replaced the local-only `supabase status` probe this used
|
|
267
|
+
* to go through — so a PRODUCTION target gets the same `--yes`/stern-confirm
|
|
268
|
+
* treatment `db reset` does: this is a privilege escalation on whatever
|
|
269
|
+
* database it runs against, and on production that database is live.
|
|
317
270
|
*/
|
|
318
|
-
async function runGrantRoot(instance, userEmail) {
|
|
271
|
+
async function runGrantRoot(connection, instance, userEmail, rest) {
|
|
319
272
|
let holder;
|
|
320
273
|
let candidates;
|
|
321
274
|
try {
|
|
@@ -325,14 +278,16 @@ async function runGrantRoot(instance, userEmail) {
|
|
|
325
278
|
]);
|
|
326
279
|
}
|
|
327
280
|
catch (err) {
|
|
328
|
-
|
|
281
|
+
explainError("Could not read the current roles.", err);
|
|
329
282
|
process.exitCode = 1;
|
|
330
283
|
return;
|
|
331
284
|
}
|
|
332
285
|
if (candidates.length === 0) {
|
|
333
286
|
explain("There are no accounts on this database yet.", "", [
|
|
334
287
|
"Sign in once through the app, then run this again.",
|
|
335
|
-
|
|
288
|
+
...(connection.tier === "development"
|
|
289
|
+
? ["Or create one: `pnpm devtools persona member`."]
|
|
290
|
+
: []),
|
|
336
291
|
]);
|
|
337
292
|
return;
|
|
338
293
|
}
|
|
@@ -357,14 +312,40 @@ async function runGrantRoot(instance, userEmail) {
|
|
|
357
312
|
log.info(`${target.email} already holds Root.`);
|
|
358
313
|
return;
|
|
359
314
|
}
|
|
360
|
-
|
|
361
|
-
|
|
315
|
+
const action = holder
|
|
316
|
+
? `Take Root away from ${holder.email} and give it to ${target.email}`
|
|
317
|
+
: `Give ${target.email} Root, which confers every permission`;
|
|
318
|
+
if (connection.tier === "production") {
|
|
319
|
+
// ⚠️ SAFETY: same gate `runStack` uses for `db reset`/`migrate` — see
|
|
320
|
+
// that function's header for why `--yes` is checked before anything
|
|
321
|
+
// TTY-dependent runs.
|
|
322
|
+
if (!rest.includes("--yes")) {
|
|
323
|
+
if (!process.stdin.isTTY) {
|
|
324
|
+
process.stderr.write("devtools grant-root: --yes is required to run non-interactively.\n");
|
|
325
|
+
process.exitCode = 1;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
362
328
|
const confirmed = unwrap(await confirm({
|
|
363
|
-
message:
|
|
329
|
+
message: `${action} on the PRODUCTION database. Continue?`,
|
|
364
330
|
initialValue: false,
|
|
365
331
|
}));
|
|
366
332
|
if (!confirmed)
|
|
367
333
|
bail("Left Root where it was.");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else if (holder) {
|
|
337
|
+
// Transferring away from someone still asks, even off production —
|
|
338
|
+
// taking a console away from an existing holder is worth a question
|
|
339
|
+
// granting to nobody-yet-holding is not.
|
|
340
|
+
const confirmed = unwrap(await confirm({
|
|
341
|
+
message: `Root is held by ${holder.email}. Take it away and give it to ${target.email}?`,
|
|
342
|
+
initialValue: false,
|
|
343
|
+
}));
|
|
344
|
+
if (!confirmed)
|
|
345
|
+
bail("Left Root where it was.");
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
if (holder) {
|
|
368
349
|
await transferRoot(instance, holder.userId, target.userId);
|
|
369
350
|
}
|
|
370
351
|
else {
|
|
@@ -374,8 +355,9 @@ async function runGrantRoot(instance, userEmail) {
|
|
|
374
355
|
"Sign out and back in if the console was already open.");
|
|
375
356
|
}
|
|
376
357
|
catch (err) {
|
|
377
|
-
|
|
378
|
-
"Seeds create the Root role definition — try `pnpm devtools db reset`
|
|
358
|
+
explainError("Could not grant Root.", err, [
|
|
359
|
+
"Seeds create the Root role definition — try `pnpm devtools db reset` " +
|
|
360
|
+
"(development) or `pnpm devtools db seed production` (staging/production) first.",
|
|
379
361
|
]);
|
|
380
362
|
process.exitCode = 1;
|
|
381
363
|
}
|
|
@@ -437,7 +419,7 @@ async function runEnvCommand(rest) {
|
|
|
437
419
|
});
|
|
438
420
|
}
|
|
439
421
|
catch (err) {
|
|
440
|
-
|
|
422
|
+
explainError("The reset failed.", err);
|
|
441
423
|
process.exitCode = 1;
|
|
442
424
|
}
|
|
443
425
|
return;
|
|
@@ -459,7 +441,7 @@ async function runEnvCommand(rest) {
|
|
|
459
441
|
await runEnvExample({ check: rest.includes("--check") });
|
|
460
442
|
}
|
|
461
443
|
catch (err) {
|
|
462
|
-
|
|
444
|
+
explainError("Generating .env.example failed.", err);
|
|
463
445
|
process.exitCode = 1;
|
|
464
446
|
}
|
|
465
447
|
return;
|
|
@@ -486,7 +468,7 @@ async function runEnvCommand(rest) {
|
|
|
486
468
|
await runEnvInit(given, flagValue(rest, "--apps") ?? undefined);
|
|
487
469
|
}
|
|
488
470
|
catch (err) {
|
|
489
|
-
|
|
471
|
+
explainError("env init failed.", err);
|
|
490
472
|
process.exitCode = 1;
|
|
491
473
|
}
|
|
492
474
|
return;
|
|
@@ -523,7 +505,7 @@ async function runEnvCommand(rest) {
|
|
|
523
505
|
await runEnvAudit(options);
|
|
524
506
|
}
|
|
525
507
|
catch (err) {
|
|
526
|
-
|
|
508
|
+
explainError("The env command failed.", err, [
|
|
527
509
|
"The access token is read from --access-token, then BWS_ACCESS_TOKEN,",
|
|
528
510
|
"then your Bitwarden vault, and finally by asking.",
|
|
529
511
|
"`gh auth status` shows whether the GitHub CLI is signed in.",
|
|
@@ -836,10 +818,10 @@ async function dispatch(argv) {
|
|
|
836
818
|
return DONE;
|
|
837
819
|
}
|
|
838
820
|
if (first === "images") {
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
await runImages(rest
|
|
821
|
+
// No database dependency to pass through any more: event graphics read
|
|
822
|
+
// `@devdogsuga/events`'s committed config directly (see
|
|
823
|
+
// `images/events.ts`), so `images` never needs a running stack at all.
|
|
824
|
+
await runImages(rest);
|
|
843
825
|
return DONE;
|
|
844
826
|
}
|
|
845
827
|
if (first === "emails") {
|
|
@@ -917,15 +899,23 @@ async function dispatch(argv) {
|
|
|
917
899
|
}
|
|
918
900
|
// The old moderation names, refused with the new namespace rather than
|
|
919
901
|
// falling into "Unknown command" — same rationale as `secrets` above.
|
|
920
|
-
// `
|
|
921
|
-
//
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
902
|
+
// `catalog` still has an answer (`moderation check`, now folded together —
|
|
903
|
+
// see that function's header); `roundtrip` does not, because the command it
|
|
904
|
+
// named is gone outright, not renamed. `doctor` itself is NOT here: that
|
|
905
|
+
// name now belongs to the environment checker below, a deliberate reuse
|
|
906
|
+
// rather than a collision.
|
|
907
|
+
if (first === "catalog") {
|
|
908
|
+
explain("`catalog` is now `devtools moderation check`.", "", [
|
|
909
|
+
"pnpm devtools moderation check",
|
|
925
910
|
]);
|
|
926
911
|
process.exitCode = 1;
|
|
927
912
|
return null;
|
|
928
913
|
}
|
|
914
|
+
if (first === "roundtrip") {
|
|
915
|
+
explain("`roundtrip` is gone. The app repo's own CI covers the file/quarantine/check round trip more thoroughly than this command ever did.", "", ["pnpm devtools moderation check --app <slug>"]);
|
|
916
|
+
process.exitCode = 1;
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
929
919
|
if (first === "doctor") {
|
|
930
920
|
await runEnvironmentDoctor({
|
|
931
921
|
app: flagValue(rest, "--app"),
|
|
@@ -933,30 +923,23 @@ async function dispatch(argv) {
|
|
|
933
923
|
});
|
|
934
924
|
return DONE;
|
|
935
925
|
}
|
|
936
|
-
if (
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
// it to describe, and reprinting the whole tree here is what made the old
|
|
940
|
-
// help unreadable in the first place.
|
|
941
|
-
log.message(renderHelp());
|
|
942
|
-
process.exitCode = 1;
|
|
943
|
-
return null;
|
|
944
|
-
}
|
|
945
|
-
const instance = await connect();
|
|
946
|
-
if (!instance) {
|
|
947
|
-
process.exitCode = 1;
|
|
948
|
-
return null;
|
|
926
|
+
if (first === "persona") {
|
|
927
|
+
await runPersona(rest);
|
|
928
|
+
return DONE;
|
|
949
929
|
}
|
|
950
930
|
if (first === "moderation") {
|
|
951
|
-
const
|
|
952
|
-
if (
|
|
953
|
-
|
|
931
|
+
const resolved = await resolveInstance({ label: "devtools moderation" });
|
|
932
|
+
if (!resolved) {
|
|
933
|
+
process.exitCode = 1;
|
|
934
|
+
return null;
|
|
954
935
|
}
|
|
955
|
-
|
|
956
|
-
|
|
936
|
+
if (refuseUnlessDevelopment(resolved.connection, "devtools moderation check")) {
|
|
937
|
+
process.exitCode = 1;
|
|
938
|
+
return null;
|
|
957
939
|
}
|
|
958
|
-
|
|
959
|
-
|
|
940
|
+
const [msub, ...mrest] = rest;
|
|
941
|
+
if (msub === "check") {
|
|
942
|
+
await runModerationCheck(resolved.instance, flagValue(mrest, "--app"));
|
|
960
943
|
}
|
|
961
944
|
else {
|
|
962
945
|
log.error(msub
|
|
@@ -964,11 +947,24 @@ async function dispatch(argv) {
|
|
|
964
947
|
: `devtools moderation: which of ${subcommandList(["moderation"])}?`);
|
|
965
948
|
process.exitCode = 1;
|
|
966
949
|
}
|
|
950
|
+
return DONE;
|
|
967
951
|
}
|
|
968
|
-
|
|
969
|
-
await
|
|
952
|
+
if (first === "grant-root") {
|
|
953
|
+
const resolved = await resolveInstance({ label: "devtools grant-root" });
|
|
954
|
+
if (!resolved) {
|
|
955
|
+
process.exitCode = 1;
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
await runGrantRoot(resolved.connection, resolved.instance, flagValue(rest, "--user"), rest);
|
|
959
|
+
return DONE;
|
|
970
960
|
}
|
|
971
|
-
|
|
961
|
+
log.error(`Unknown command: ${first}`);
|
|
962
|
+
// The top level only. The command is unknown, so there is no level below
|
|
963
|
+
// it to describe, and reprinting the whole tree here is what made the old
|
|
964
|
+
// help unreadable in the first place.
|
|
965
|
+
log.message(renderHelp());
|
|
966
|
+
process.exitCode = 1;
|
|
967
|
+
return null;
|
|
972
968
|
}
|
|
973
969
|
// ── Entry ────────────────────────────────────────────────────────────────────
|
|
974
970
|
/**
|
package/dist/commands.d.ts
CHANGED
|
@@ -121,10 +121,16 @@ export interface CommandNode {
|
|
|
121
121
|
/**
|
|
122
122
|
* Offer this always, but say on the line why it will not work right now.
|
|
123
123
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
124
|
+
* Nothing in the tree carries it at the moment — the four moderation
|
|
125
|
+
* commands were the only ones that ever did, back when each opened a
|
|
126
|
+
* client against Supabase on this machine with no remote path at all, so
|
|
127
|
+
* with it down they were a spinner followed by a connection error. They
|
|
128
|
+
* moved onto the session system alongside `db migrate`/`db reset`, neither
|
|
129
|
+
* of which has ever gated on a local probe (a hosted session has no local
|
|
130
|
+
* stack to probe), so nothing here needs the hint any more. Left declared
|
|
131
|
+
* rather than deleted: a future local-only command is exactly the shape of
|
|
132
|
+
* thing that would want it back, and `blockedBecause`
|
|
133
|
+
* (`environment.test.ts`) still exercises the mechanism directly.
|
|
128
134
|
*/
|
|
129
135
|
needs?: Condition;
|
|
130
136
|
/**
|
package/dist/commands.js
CHANGED
|
@@ -339,47 +339,63 @@ const DECLARED_GROUPS = [
|
|
|
339
339
|
{
|
|
340
340
|
name: "moderation",
|
|
341
341
|
summary: "An app's moderation integration: the catalog and its wiring.",
|
|
342
|
-
hint: "check
|
|
342
|
+
hint: "check",
|
|
343
343
|
subcommands: [
|
|
344
344
|
{
|
|
345
345
|
name: "check",
|
|
346
|
-
summary: "
|
|
347
|
-
hint: "and
|
|
348
|
-
needs: "instance-running"
|
|
346
|
+
summary: "List the catalog, or check one app's moderation integration.",
|
|
347
|
+
hint: "reasons and content types with no --app; per-app wiring with one",
|
|
348
|
+
// No `needs: "instance-running"` any more — this resolves through
|
|
349
|
+
// the session system now (`resolveInstance`, `instance.ts`), the
|
|
350
|
+
// same as `db migrate`/`db reset`, so it works against a hosted
|
|
351
|
+
// development database with no local stack at all. Refuses
|
|
352
|
+
// staging/production itself, with its own message, rather than a
|
|
353
|
+
// menu-only hint.
|
|
349
354
|
options: [
|
|
350
355
|
{
|
|
351
356
|
flag: "--app",
|
|
352
357
|
value: "<slug>",
|
|
353
|
-
summary: "App to check.
|
|
358
|
+
summary: "App to check. Prints the catalog when absent.",
|
|
354
359
|
},
|
|
355
360
|
JSON_FLAG,
|
|
356
361
|
],
|
|
357
362
|
},
|
|
358
|
-
{
|
|
359
|
-
name: "catalog",
|
|
360
|
-
summary: "List the report reasons and content types in the database.",
|
|
361
|
-
hint: "what can be reported here",
|
|
362
|
-
needs: "instance-running",
|
|
363
|
-
options: [JSON_FLAG],
|
|
364
|
-
},
|
|
365
|
-
{
|
|
366
|
-
name: "roundtrip",
|
|
367
|
-
summary: "File a report, quarantine it, and check the freeze.",
|
|
368
|
-
hint: "end to end, then cleans up",
|
|
369
|
-
needs: "instance-running",
|
|
370
|
-
},
|
|
371
363
|
],
|
|
372
364
|
},
|
|
373
365
|
{
|
|
374
366
|
name: "grant-root",
|
|
375
367
|
summary: "Give an account every permission on your own database.",
|
|
376
|
-
|
|
368
|
+
// Also on the session system now — every tier, not just local. See
|
|
369
|
+
// `check`'s note above; production additionally gets `YES`'s stern
|
|
370
|
+
// confirmation, the same treatment `db reset` gives it.
|
|
377
371
|
options: [
|
|
378
372
|
{
|
|
379
373
|
flag: "--user",
|
|
380
374
|
value: "<email>",
|
|
381
375
|
summary: "Account to grant Root to. Asked for when absent.",
|
|
382
376
|
},
|
|
377
|
+
YES,
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
title: "Personas",
|
|
384
|
+
commands: [
|
|
385
|
+
{
|
|
386
|
+
name: "persona",
|
|
387
|
+
summary: "A throwaway member or moderator account for development.",
|
|
388
|
+
hint: "member, moderator, or --clean",
|
|
389
|
+
options: [
|
|
390
|
+
{
|
|
391
|
+
flag: "--clean",
|
|
392
|
+
summary: "Delete every account this command has created, instead of making one.",
|
|
393
|
+
prompt: {
|
|
394
|
+
kind: "confirm",
|
|
395
|
+
message: "Clean up every persona this command created, instead of making one?",
|
|
396
|
+
initial: false,
|
|
397
|
+
},
|
|
398
|
+
},
|
|
383
399
|
],
|
|
384
400
|
},
|
|
385
401
|
],
|
|
@@ -661,22 +677,12 @@ const DECLARED_GROUPS = [
|
|
|
661
677
|
{
|
|
662
678
|
flag: "--base-url",
|
|
663
679
|
value: "<url>",
|
|
664
|
-
summary: "DevDogs API URL
|
|
665
|
-
prompt: {
|
|
666
|
-
kind: "text",
|
|
667
|
-
message: "DevDogs API URL? (blank asks inside the wizard)",
|
|
668
|
-
optional: true,
|
|
669
|
-
},
|
|
680
|
+
summary: "DevDogs API URL for \"Paste credentials instead\". Defaults to https://api.devdogsuga.org.",
|
|
670
681
|
},
|
|
671
682
|
{
|
|
672
683
|
flag: "--platform-url",
|
|
673
684
|
value: "<url>",
|
|
674
685
|
summary: "Platform URL for one-click connect. Defaults to https://devdogsuga.org.",
|
|
675
|
-
prompt: {
|
|
676
|
-
kind: "text",
|
|
677
|
-
message: "Platform URL? (blank uses the default)",
|
|
678
|
-
optional: true,
|
|
679
|
-
},
|
|
680
686
|
},
|
|
681
687
|
{
|
|
682
688
|
flag: "--device",
|
|
@@ -1111,6 +1117,10 @@ export const GROUPS = [
|
|
|
1111
1117
|
title: "Moderation",
|
|
1112
1118
|
commands: commands("moderation", "grant-root"),
|
|
1113
1119
|
},
|
|
1120
|
+
{
|
|
1121
|
+
title: "Personas",
|
|
1122
|
+
commands: commands("persona"),
|
|
1123
|
+
},
|
|
1114
1124
|
{
|
|
1115
1125
|
title: "Environment",
|
|
1116
1126
|
commands: commands("doctor"),
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { DeployEnvironment } from "@devdogsuga/env";
|
|
2
|
+
import type { DevDatabase } from "@devdogsuga/env/load";
|
|
3
|
+
export interface EnsureGeneratedEnvDeps {
|
|
4
|
+
/** Is anything listening on the local stack port? Injected so tests never
|
|
5
|
+
* open a real socket. */
|
|
6
|
+
probeLocalStack: () => boolean | Promise<boolean>;
|
|
7
|
+
/** File existence, relative to `repoRoot`. Injected so tests never touch
|
|
8
|
+
* a real filesystem. */
|
|
9
|
+
exists: (file: string) => boolean;
|
|
10
|
+
/** `supabase status -o env`'s stdout, or a rejection when the CLI fails —
|
|
11
|
+
* most commonly because the stack on 54321 belongs to a different
|
|
12
|
+
* project. Injected so tests never spawn the real CLI. */
|
|
13
|
+
captureStatus: () => Promise<string>;
|
|
14
|
+
/** Writes `.env.generated`'s full path. Injected so tests never touch a
|
|
15
|
+
* real filesystem. */
|
|
16
|
+
write: (path: string, contents: string) => Promise<void>;
|
|
17
|
+
/** `docker ps --format '{{.Names}}'`, pre-split — or `null` when Docker
|
|
18
|
+
* could not be read. Injected so tests never spawn a real subprocess. */
|
|
19
|
+
listContainerNames: () => string[] | null;
|
|
20
|
+
/** This checkout's own `project_id`, or `null` if unreadable. Injected so
|
|
21
|
+
* tests never touch a real filesystem. */
|
|
22
|
+
projectId: () => string | null;
|
|
23
|
+
/** Used only to build the path `write` receives. */
|
|
24
|
+
repoRoot: string;
|
|
25
|
+
}
|
|
26
|
+
export type EnsureGeneratedEnvResult = {
|
|
27
|
+
outcome: "skipped";
|
|
28
|
+
} | {
|
|
29
|
+
outcome: "wrote";
|
|
30
|
+
line: string;
|
|
31
|
+
} | {
|
|
32
|
+
outcome: "foreign";
|
|
33
|
+
projectId: string;
|
|
34
|
+
line: string;
|
|
35
|
+
} | {
|
|
36
|
+
outcome: "unreachable";
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Regenerates `.env.generated` from a running local stack when THIS
|
|
40
|
+
* checkout's own copy is missing.
|
|
41
|
+
*
|
|
42
|
+
* A no-op (`"skipped"`) unless every one of these holds: `tier` is
|
|
43
|
+
* `"development"`, `devDatabase` is not `"remote"` (an explicit `"local"`
|
|
44
|
+
* or the probe-decides `undefined` both qualify), the file is genuinely
|
|
45
|
+
* absent, and something answers the local stack's port. That last check
|
|
46
|
+
* also covers "the file is missing because the stack is genuinely down" —
|
|
47
|
+
* nothing here runs `supabase status` against a stack that is not there;
|
|
48
|
+
* the existing `LocalStackOfflineError`/warning handling covers that case
|
|
49
|
+
* exactly as before.
|
|
50
|
+
*
|
|
51
|
+
* `"foreign"` and `"unreachable"` both mean `supabase status -o env`
|
|
52
|
+
* itself failed — the port answered, but the CLI could not produce a
|
|
53
|
+
* connection block. `"foreign"` is the case a running Docker container's
|
|
54
|
+
* name resolves to: another project's stack holds the port.
|
|
55
|
+
* `"unreachable"` is everything else (an unreadable `config.toml`, an
|
|
56
|
+
* unreadable `docker ps`, a container name that matches nobody) — the
|
|
57
|
+
* caller falls through to its existing offline handling either way, since
|
|
58
|
+
* `.env.generated` is still missing on disk.
|
|
59
|
+
*/
|
|
60
|
+
export declare function ensureGeneratedEnvFile(tier: DeployEnvironment, devDatabase: DevDatabase | undefined, deps: EnsureGeneratedEnvDeps): Promise<EnsureGeneratedEnvResult>;
|
|
61
|
+
/**
|
|
62
|
+
* The production wiring for `EnsureGeneratedEnvDeps`, shared by
|
|
63
|
+
* `../env-entry.ts` (via `realEnvEntryDeps`) and `../stack.ts`'s `db
|
|
64
|
+
* status`. `probeLocalStack` is passed in rather than resolved here because
|
|
65
|
+
* both callers already hold the loaded `@devdogsuga/env/load` peer their
|
|
66
|
+
* own dispatch needs anyway — resolving it again here would mean two
|
|
67
|
+
* different copies of the same optional peer in play.
|
|
68
|
+
*/
|
|
69
|
+
export declare function realEnsureGeneratedEnvDeps(probeLocalStack: () => boolean | Promise<boolean>): EnsureGeneratedEnvDeps;
|