@devdogsuga/devtools 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 +44 -0
- package/bin/devtools-ci-bare.mjs +48 -0
- package/bin/devtools-ci.mjs +17 -0
- package/bin/devtools.mjs +27 -0
- package/dist/args.d.ts +26 -0
- package/dist/args.js +78 -0
- package/dist/bws/bw.d.ts +9 -0
- package/dist/bws/bw.js +49 -0
- package/dist/bws/client.d.ts +43 -0
- package/dist/bws/client.js +363 -0
- package/dist/bws/environments.d.ts +96 -0
- package/dist/bws/environments.js +111 -0
- package/dist/bws/pace.d.ts +27 -0
- package/dist/bws/pace.js +43 -0
- package/dist/bws/retry.d.ts +12 -0
- package/dist/bws/retry.js +39 -0
- package/dist/bws/store.d.ts +1 -0
- package/dist/bws/store.js +32 -0
- package/dist/bws/token.d.ts +31 -0
- package/dist/bws/token.js +88 -0
- package/dist/bws/vault.d.ts +29 -0
- package/dist/bws/vault.js +200 -0
- package/dist/catalog.d.ts +55 -0
- package/dist/catalog.js +81 -0
- package/dist/cf/commands.d.ts +1 -0
- package/dist/cf/commands.js +182 -0
- package/dist/cf/local-env.d.ts +44 -0
- package/dist/cf/local-env.js +148 -0
- package/dist/ci.d.ts +1 -0
- package/dist/ci.js +319 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +1077 -0
- package/dist/commands.d.ts +255 -0
- package/dist/commands.js +1279 -0
- package/dist/completions.d.ts +4 -0
- package/dist/completions.js +110 -0
- package/dist/cron/commands.d.ts +47 -0
- package/dist/cron/commands.js +372 -0
- package/dist/cron/discovery.d.ts +55 -0
- package/dist/cron/discovery.js +85 -0
- package/dist/cron/schema.d.ts +45 -0
- package/dist/cron/schema.js +35 -0
- package/dist/db/config-push.d.ts +8 -0
- package/dist/db/config-push.js +11 -0
- package/dist/db/connection.d.ts +79 -0
- package/dist/db/connection.js +114 -0
- package/dist/db/exec.d.ts +1 -0
- package/dist/db/exec.js +11 -0
- package/dist/db/generate-migration.d.ts +1 -0
- package/dist/db/generate-migration.js +67 -0
- package/dist/db/generate-types.d.ts +1 -0
- package/dist/db/generate-types.js +4 -0
- package/dist/db/introspect.d.ts +1 -0
- package/dist/db/introspect.js +113 -0
- package/dist/db/local-env.d.ts +65 -0
- package/dist/db/local-env.js +23 -0
- package/dist/db/new-migration.d.ts +1 -0
- package/dist/db/new-migration.js +8 -0
- package/dist/db/run.d.ts +70 -0
- package/dist/db/run.js +158 -0
- package/dist/db/seed-buckets.d.ts +2 -0
- package/dist/db/seed-buckets.js +4 -0
- package/dist/db/seed-production.d.ts +8 -0
- package/dist/db/seed-production.js +73 -0
- package/dist/db/seed-roles.d.ts +7 -0
- package/dist/db/seed-roles.js +31 -0
- package/dist/db/session-refresh.d.ts +43 -0
- package/dist/db/session-refresh.js +40 -0
- package/dist/deploy/migrations.d.ts +20 -0
- package/dist/deploy/migrations.js +68 -0
- package/dist/deploy/orphans.d.ts +20 -0
- package/dist/deploy/orphans.js +177 -0
- package/dist/deploy/preflight.d.ts +53 -0
- package/dist/deploy/preflight.js +189 -0
- package/dist/deploy/report.d.ts +26 -0
- package/dist/deploy/report.js +70 -0
- package/dist/deploy/require-planner.d.ts +2 -0
- package/dist/deploy/require-planner.js +60 -0
- package/dist/deploy/require-token.d.ts +15 -0
- package/dist/deploy/require-token.js +78 -0
- package/dist/deploy/secrets-file.d.ts +17 -0
- package/dist/deploy/secrets-file.js +167 -0
- package/dist/deploy/write-env.d.ts +40 -0
- package/dist/deploy/write-env.js +393 -0
- package/dist/docs/index-pages.d.ts +36 -0
- package/dist/docs/index-pages.js +184 -0
- package/dist/doctor.d.ts +62 -0
- package/dist/doctor.js +225 -0
- package/dist/emails/commands.d.ts +15 -0
- package/dist/emails/commands.js +130 -0
- package/dist/emails/email-types.d.ts +13 -0
- package/dist/emails/email-types.js +1 -0
- package/dist/emails/fixtures.d.ts +20 -0
- package/dist/emails/fixtures.js +20 -0
- package/dist/env/audit.d.ts +206 -0
- package/dist/env/audit.js +436 -0
- package/dist/env/cloudflare.d.ts +21 -0
- package/dist/env/cloudflare.js +61 -0
- package/dist/env/commands.d.ts +99 -0
- package/dist/env/commands.js +612 -0
- package/dist/env/discovery.d.ts +15 -0
- package/dist/env/discovery.js +226 -0
- package/dist/env/document.d.ts +119 -0
- package/dist/env/document.js +323 -0
- package/dist/env/example.d.ts +68 -0
- package/dist/env/example.js +603 -0
- package/dist/env/selection.d.ts +105 -0
- package/dist/env/selection.js +226 -0
- package/dist/environment.d.ts +92 -0
- package/dist/environment.js +206 -0
- package/dist/fingerprint.d.ts +10 -0
- package/dist/fingerprint.js +18 -0
- package/dist/gen/campus-map.d.ts +1 -0
- package/dist/gen/campus-map.js +473 -0
- package/dist/gen/commands.d.ts +1 -0
- package/dist/gen/commands.js +25 -0
- package/dist/gen/hypno.d.ts +1 -0
- package/dist/gen/hypno.js +134 -0
- package/dist/gh/client.d.ts +80 -0
- package/dist/gh/client.js +258 -0
- package/dist/gh/environments.d.ts +83 -0
- package/dist/gh/environments.js +178 -0
- package/dist/gh/rulesets/actors.d.ts +14 -0
- package/dist/gh/rulesets/actors.js +68 -0
- package/dist/gh/rulesets/api.d.ts +15 -0
- package/dist/gh/rulesets/api.js +99 -0
- package/dist/gh/rulesets/commands.d.ts +3 -0
- package/dist/gh/rulesets/commands.js +203 -0
- package/dist/gh/rulesets/desired.d.ts +112 -0
- package/dist/gh/rulesets/desired.js +247 -0
- package/dist/gh/rulesets/diff.d.ts +65 -0
- package/dist/gh/rulesets/diff.js +134 -0
- package/dist/gh/rulesets/types.d.ts +76 -0
- package/dist/gh/rulesets/types.js +13 -0
- package/dist/gh/settings/api.d.ts +40 -0
- package/dist/gh/settings/api.js +172 -0
- package/dist/gh/settings/commands.d.ts +1 -0
- package/dist/gh/settings/commands.js +245 -0
- package/dist/gh/settings/desired.d.ts +53 -0
- package/dist/gh/settings/desired.js +39 -0
- package/dist/gh/settings/diff.d.ts +57 -0
- package/dist/gh/settings/diff.js +156 -0
- package/dist/gh/settings/types.d.ts +92 -0
- package/dist/gh/settings/types.js +11 -0
- package/dist/gh/settings/workflows.d.ts +41 -0
- package/dist/gh/settings/workflows.js +124 -0
- package/dist/grantRoot.d.ts +42 -0
- package/dist/grantRoot.js +89 -0
- package/dist/help.d.ts +18 -0
- package/dist/help.js +169 -0
- package/dist/images/commands.d.ts +35 -0
- package/dist/images/commands.js +233 -0
- package/dist/images/events.d.ts +36 -0
- package/dist/images/events.js +152 -0
- package/dist/images/graphics.d.ts +70 -0
- package/dist/images/graphics.js +233 -0
- package/dist/images/open-graph-types.d.ts +75 -0
- package/dist/images/open-graph-types.js +1 -0
- package/dist/images/prompts.d.ts +34 -0
- package/dist/images/prompts.js +100 -0
- package/dist/images/render.d.ts +21 -0
- package/dist/images/render.js +57 -0
- package/dist/images/select.d.ts +64 -0
- package/dist/images/select.js +96 -0
- package/dist/instance.d.ts +67 -0
- package/dist/instance.js +129 -0
- package/dist/invocation.d.ts +49 -0
- package/dist/invocation.js +78 -0
- package/dist/launch-ci.d.ts +1 -0
- package/dist/launch-ci.js +86 -0
- package/dist/launch.d.ts +29 -0
- package/dist/launch.js +371 -0
- package/dist/menu.d.ts +31 -0
- package/dist/menu.js +277 -0
- package/dist/newsletter/commands.d.ts +16 -0
- package/dist/newsletter/commands.js +311 -0
- package/dist/newsletter/imap.d.ts +17 -0
- package/dist/newsletter/imap.js +127 -0
- package/dist/newsletter/loopback.d.ts +24 -0
- package/dist/newsletter/loopback.js +100 -0
- package/dist/newsletter/oauth.d.ts +26 -0
- package/dist/newsletter/oauth.js +130 -0
- package/dist/newsletter/smtp.d.ts +23 -0
- package/dist/newsletter/smtp.js +133 -0
- package/dist/oauth/browser.d.ts +1 -0
- package/dist/oauth/browser.js +13 -0
- package/dist/oauth/config.d.ts +24 -0
- package/dist/oauth/config.js +24 -0
- package/dist/oauth/connect-url.d.ts +17 -0
- package/dist/oauth/connect-url.js +23 -0
- package/dist/oauth/db.d.ts +37 -0
- package/dist/oauth/db.js +108 -0
- package/dist/oauth/device.d.ts +93 -0
- package/dist/oauth/device.js +190 -0
- package/dist/oauth/discovery.d.ts +28 -0
- package/dist/oauth/discovery.js +54 -0
- package/dist/oauth/env-file.d.ts +6 -0
- package/dist/oauth/env-file.js +35 -0
- package/dist/oauth/exchange.d.ts +43 -0
- package/dist/oauth/exchange.js +87 -0
- package/dist/oauth/label.d.ts +1 -0
- package/dist/oauth/label.js +25 -0
- package/dist/oauth/loopback.d.ts +52 -0
- package/dist/oauth/loopback.js +162 -0
- package/dist/oauth/pkce.d.ts +6 -0
- package/dist/oauth/pkce.js +34 -0
- package/dist/oauth/target.d.ts +54 -0
- package/dist/oauth/target.js +102 -0
- package/dist/oauth/transport.d.ts +41 -0
- package/dist/oauth/transport.js +32 -0
- package/dist/oauth/wizard.d.ts +33 -0
- package/dist/oauth/wizard.js +571 -0
- package/dist/pick.d.ts +6 -0
- package/dist/pick.js +64 -0
- package/dist/planner/checks.d.ts +26 -0
- package/dist/planner/checks.js +104 -0
- package/dist/planner/commands.d.ts +10 -0
- package/dist/planner/commands.js +367 -0
- package/dist/planner/db.d.ts +15 -0
- package/dist/planner/db.js +33 -0
- package/dist/planner/role.d.ts +33 -0
- package/dist/planner/role.js +68 -0
- package/dist/repo/peers.d.ts +24 -0
- package/dist/repo/peers.js +114 -0
- package/dist/repo/preflight.d.ts +109 -0
- package/dist/repo/preflight.js +266 -0
- package/dist/repo/resolve.d.ts +41 -0
- package/dist/repo/resolve.js +152 -0
- package/dist/repo/root.d.ts +23 -0
- package/dist/repo/root.js +91 -0
- package/dist/repo/source.d.ts +14 -0
- package/dist/repo/source.js +54 -0
- package/dist/repo/tsx-loader.d.ts +6 -0
- package/dist/repo/tsx-loader.js +41 -0
- package/dist/run/pick.d.ts +60 -0
- package/dist/run/pick.js +559 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +152 -0
- package/dist/stack.d.ts +48 -0
- package/dist/stack.js +290 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +93 -0
- package/dist/tier.d.ts +40 -0
- package/dist/tier.js +93 -0
- package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
- package/dist/ui.d.ts +25 -0
- package/dist/ui.js +51 -0
- package/dist/workers.d.ts +5 -0
- package/dist/workers.js +35 -0
- package/dist/workflows/commands.d.ts +44 -0
- package/dist/workflows/commands.js +668 -0
- package/env.ts +158 -0
- package/package.json +80 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { loadOpenGraph } from "../repo/source.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves graphic patterns.
|
|
4
|
+
*
|
|
5
|
+
* Every graphic is `group/name`, and three shapes of pattern read against
|
|
6
|
+
* that: a lone star (or a star over a star) is everything, `group/` plus a star
|
|
7
|
+
* is a whole group, and anything else is an exact name. Deliberately not a
|
|
8
|
+
* general glob — `event/2026-` plus a star looks like it should work and would
|
|
9
|
+
* be one more thing to specify, and the picker covers the case where somebody
|
|
10
|
+
* does not know the exact name.
|
|
11
|
+
*
|
|
12
|
+
* A bare group is accepted as shorthand for `group/*`, because `images icons`
|
|
13
|
+
* is what people type and refusing it teaches nothing.
|
|
14
|
+
*/
|
|
15
|
+
export function matchGraphics(patterns, graphics) {
|
|
16
|
+
const matched = new Map();
|
|
17
|
+
const unmatched = [];
|
|
18
|
+
const groups = new Set(graphics.map((graphic) => graphic.group));
|
|
19
|
+
for (const pattern of patterns) {
|
|
20
|
+
const hits = graphics.filter((graphic) => {
|
|
21
|
+
if (pattern === "*" || pattern === "*/*")
|
|
22
|
+
return true;
|
|
23
|
+
if (pattern.endsWith("/*"))
|
|
24
|
+
return graphic.group === pattern.slice(0, -2);
|
|
25
|
+
if (groups.has(pattern))
|
|
26
|
+
return graphic.group === pattern;
|
|
27
|
+
return graphic.name === pattern;
|
|
28
|
+
});
|
|
29
|
+
if (hits.length === 0) {
|
|
30
|
+
unmatched.push(pattern);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
for (const hit of hits)
|
|
34
|
+
matched.set(hit.name, hit);
|
|
35
|
+
}
|
|
36
|
+
return { matched: [...matched.values()], unmatched };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pairs each graphic with each requested format it actually supports.
|
|
40
|
+
*
|
|
41
|
+
* The matrix is sparse, and silently dropping a pair is the wrong answer in one
|
|
42
|
+
* direction only: asking for `*` at every format should quietly skip the
|
|
43
|
+
* combinations that do not exist, while asking for one graphic at one format it
|
|
44
|
+
* cannot do should say so. The caller decides which case it is; this reports
|
|
45
|
+
* both halves.
|
|
46
|
+
*/
|
|
47
|
+
export async function pair(graphics, formats) {
|
|
48
|
+
const { FORMATS } = await loadOpenGraph();
|
|
49
|
+
const selections = [];
|
|
50
|
+
const unsupported = [];
|
|
51
|
+
for (const graphic of graphics) {
|
|
52
|
+
for (const name of formats) {
|
|
53
|
+
const format = FORMATS[name];
|
|
54
|
+
if (!format)
|
|
55
|
+
continue;
|
|
56
|
+
if (graphic.formats.includes(name)) {
|
|
57
|
+
selections.push({ graphic, format });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
unsupported.push({ graphic: graphic.name, format: name });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { selections, unsupported };
|
|
65
|
+
}
|
|
66
|
+
/** Every format any of these graphics can be drawn at, in registry order. */
|
|
67
|
+
export async function formatsFor(graphics) {
|
|
68
|
+
const { FORMATS } = await loadOpenGraph();
|
|
69
|
+
const names = new Set(graphics.flatMap((graphic) => graphic.formats));
|
|
70
|
+
return Object.values(FORMATS).filter((format) => names.has(format.name));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Splits `--flag=value` into two tokens.
|
|
74
|
+
*
|
|
75
|
+
* The rest of this CLI reads flags as `--flag value`, and `positionals()`
|
|
76
|
+
* depends on that to tell a flag's value from a subcommand. `--format=a,b`
|
|
77
|
+
* arrives as one token, so it would be read as a positional — that is, as a
|
|
78
|
+
* graphic name — and `images --format=og` would try to render a graphic called
|
|
79
|
+
* `--format=og` and report nothing matched. Normalising here lets both spellings
|
|
80
|
+
* work without teaching the shared parser about either.
|
|
81
|
+
*/
|
|
82
|
+
export function normalizeArgv(argv) {
|
|
83
|
+
return argv.flatMap((token) => {
|
|
84
|
+
if (!token.startsWith("--"))
|
|
85
|
+
return [token];
|
|
86
|
+
const eq = token.indexOf("=");
|
|
87
|
+
return eq === -1 ? [token] : [token.slice(0, eq), token.slice(eq + 1)];
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** A comma-separated flag value, trimmed and emptied of blanks. */
|
|
91
|
+
export function commaList(value) {
|
|
92
|
+
return (value ?? "")
|
|
93
|
+
.split(",")
|
|
94
|
+
.map((part) => part.trim())
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { findRepoRoot } from "./repo/root.js";
|
|
2
|
+
/**
|
|
3
|
+
* The repo root, which is where `supabase/config.toml` lives.
|
|
4
|
+
*
|
|
5
|
+
* The Supabase CLI walks up from its working directory looking for that file,
|
|
6
|
+
* and falls back to `basename(cwd)` as the project name when it finds none, so
|
|
7
|
+
* running it from the wrong place fails with "No such container:
|
|
8
|
+
* supabase_db_<whatever directory you were in>". Resolved via `findRepoRoot()`
|
|
9
|
+
* (walking up from `process.cwd()`, not this source file — see `repo/root.ts`)
|
|
10
|
+
* so it does not matter where the contributor invoked the tool from.
|
|
11
|
+
*/
|
|
12
|
+
export { findRepoRoot };
|
|
13
|
+
/**
|
|
14
|
+
* Every client here goes through this one factory so they all share a type.
|
|
15
|
+
*
|
|
16
|
+
* supabase-js encodes the default schema in the client's type, so a `schema`
|
|
17
|
+
* parameter of type `string` produces a client that will not unify with a
|
|
18
|
+
* hand-written `SupabaseClient` annotation. Deriving `DevtoolsClient` from the
|
|
19
|
+
* factory sidesteps that.
|
|
20
|
+
*/
|
|
21
|
+
declare function makeClient(url: string, key: string, schema: string): import("@supabase/supabase-js").SupabaseClient<any, "public", string, any, any>;
|
|
22
|
+
export type DevtoolsClient = ReturnType<typeof makeClient>;
|
|
23
|
+
export interface Instance {
|
|
24
|
+
apiUrl: string;
|
|
25
|
+
publishableKey: string;
|
|
26
|
+
secretKey: string;
|
|
27
|
+
}
|
|
28
|
+
/** The seeded personas, from `supabase/seed/development/02_moderation.sql`. */
|
|
29
|
+
export declare const PERSONAS: {
|
|
30
|
+
readonly member: "member@devdogs.test";
|
|
31
|
+
readonly author: "author@devdogs.test";
|
|
32
|
+
readonly moderator: "moderator@devdogs.test";
|
|
33
|
+
};
|
|
34
|
+
/** The built-in Root role, from `supabase/seed/production/01_roles.sql`. */
|
|
35
|
+
export declare const ROOT_ROLE_ID = "00000000-0000-0000-0000-000000000002";
|
|
36
|
+
export declare const PERSONA_PASSWORD = "password";
|
|
37
|
+
/**
|
|
38
|
+
* Reads the local stack's credentials from the Supabase CLI.
|
|
39
|
+
*
|
|
40
|
+
* `-o env` rather than parsing the human-readable table: the table's labels
|
|
41
|
+
* have changed between CLI versions, the env output has not.
|
|
42
|
+
*/
|
|
43
|
+
export declare function detectLocalInstance(cwd?: string): Instance;
|
|
44
|
+
/**
|
|
45
|
+
* Confirms the instance has actually been migrated.
|
|
46
|
+
*
|
|
47
|
+
* This used to be `assertNotProduction()`, which read a tier out of a singleton
|
|
48
|
+
* `platform."instance"` table and refused anything reporting itself as
|
|
49
|
+
* production. That table is gone, and the tier column was guarding a door that
|
|
50
|
+
* was already walled up: `detectLocalInstance()` above reads `supabase status`,
|
|
51
|
+
* which only ever describes the Docker stack on this machine, so a remote
|
|
52
|
+
* project cannot reach these commands in the first place.
|
|
53
|
+
*
|
|
54
|
+
* What was useful was its error message, because "have you run migrations?" is
|
|
55
|
+
* the failure a contributor actually hits. That is all this does now.
|
|
56
|
+
*/
|
|
57
|
+
export declare function assertMigrated(instance: Instance): Promise<void>;
|
|
58
|
+
/** A service-role client. Bypasses RLS, so setup and teardown only. */
|
|
59
|
+
export declare function adminClient(instance: Instance, schema?: string): DevtoolsClient;
|
|
60
|
+
/**
|
|
61
|
+
* A client signed in as a seeded persona, subject to RLS.
|
|
62
|
+
*
|
|
63
|
+
* Signs in for real rather than hand-signing a JWT, so the token path exercised
|
|
64
|
+
* is the one production uses, with whatever claims Supabase Auth actually puts
|
|
65
|
+
* in a token rather than the ones we assume.
|
|
66
|
+
*/
|
|
67
|
+
export declare function personaClient(instance: Instance, email: string, schema?: string): Promise<DevtoolsClient>;
|
package/dist/instance.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding the instance to work against, and signing in to it.
|
|
3
|
+
*
|
|
4
|
+
* Detection is automatic where possible: `supabase status -o env` already
|
|
5
|
+
* prints everything needed, so nobody has to copy a URL and a key from one
|
|
6
|
+
* terminal into another.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { createClient } from "@supabase/supabase-js";
|
|
10
|
+
import { findRepoRoot } from "./repo/root.js";
|
|
11
|
+
/**
|
|
12
|
+
* The repo root, which is where `supabase/config.toml` lives.
|
|
13
|
+
*
|
|
14
|
+
* The Supabase CLI walks up from its working directory looking for that file,
|
|
15
|
+
* and falls back to `basename(cwd)` as the project name when it finds none, so
|
|
16
|
+
* running it from the wrong place fails with "No such container:
|
|
17
|
+
* supabase_db_<whatever directory you were in>". Resolved via `findRepoRoot()`
|
|
18
|
+
* (walking up from `process.cwd()`, not this source file — see `repo/root.ts`)
|
|
19
|
+
* so it does not matter where the contributor invoked the tool from.
|
|
20
|
+
*/
|
|
21
|
+
export { findRepoRoot };
|
|
22
|
+
/**
|
|
23
|
+
* Every client here goes through this one factory so they all share a type.
|
|
24
|
+
*
|
|
25
|
+
* supabase-js encodes the default schema in the client's type, so a `schema`
|
|
26
|
+
* parameter of type `string` produces a client that will not unify with a
|
|
27
|
+
* hand-written `SupabaseClient` annotation. Deriving `DevtoolsClient` from the
|
|
28
|
+
* factory sidesteps that.
|
|
29
|
+
*/
|
|
30
|
+
function makeClient(url, key, schema) {
|
|
31
|
+
return createClient(url, key, {
|
|
32
|
+
db: { schema },
|
|
33
|
+
auth: { autoRefreshToken: false, persistSession: false },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** The seeded personas, from `supabase/seed/development/02_moderation.sql`. */
|
|
37
|
+
export const PERSONAS = {
|
|
38
|
+
member: "member@devdogs.test",
|
|
39
|
+
author: "author@devdogs.test",
|
|
40
|
+
moderator: "moderator@devdogs.test",
|
|
41
|
+
};
|
|
42
|
+
/** The built-in Root role, from `supabase/seed/production/01_roles.sql`. */
|
|
43
|
+
export const ROOT_ROLE_ID = "00000000-0000-0000-0000-000000000002";
|
|
44
|
+
export const PERSONA_PASSWORD = "password";
|
|
45
|
+
/**
|
|
46
|
+
* Reads the local stack's credentials from the Supabase CLI.
|
|
47
|
+
*
|
|
48
|
+
* `-o env` rather than parsing the human-readable table: the table's labels
|
|
49
|
+
* have changed between CLI versions, the env output has not.
|
|
50
|
+
*/
|
|
51
|
+
export function detectLocalInstance(cwd = findRepoRoot()) {
|
|
52
|
+
let output;
|
|
53
|
+
try {
|
|
54
|
+
// Through `pnpm exec` because the Supabase CLI is a workspace devDependency
|
|
55
|
+
// rather than a global install. Invoking `supabase` directly works only on
|
|
56
|
+
// machines that happen to have it on PATH.
|
|
57
|
+
output = execFileSync("pnpm", ["exec", "supabase", "status", "-o", "env"], {
|
|
58
|
+
cwd,
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
throw new Error(`Could not read the local Supabase stack.\n${err instanceof Error ? err.message : String(err)}`);
|
|
65
|
+
}
|
|
66
|
+
// The CLI prefixes the block with a "Stopped services: [...]" line whenever
|
|
67
|
+
// any optional service is down, which is the normal state here. Lines that do
|
|
68
|
+
// not parse as KEY="value" are skipped rather than treated as an error.
|
|
69
|
+
const values = new Map();
|
|
70
|
+
for (const line of output.split("\n")) {
|
|
71
|
+
const match = /^([A-Z0-9_]+)="?([^"]*)"?$/.exec(line.trim());
|
|
72
|
+
if (match?.[1])
|
|
73
|
+
values.set(match[1], match[2] ?? "");
|
|
74
|
+
}
|
|
75
|
+
const apiUrl = values.get("API_URL");
|
|
76
|
+
// Newer CLI versions print PUBLISHABLE_KEY/SECRET_KEY; older ones print the
|
|
77
|
+
// ANON_KEY/SERVICE_ROLE_KEY names. Both are accepted so the tool does not
|
|
78
|
+
// break on whichever the contributor happens to have installed.
|
|
79
|
+
const publishableKey = values.get("PUBLISHABLE_KEY") ?? values.get("ANON_KEY");
|
|
80
|
+
const secretKey = values.get("SECRET_KEY") ?? values.get("SERVICE_ROLE_KEY");
|
|
81
|
+
if (!apiUrl || !publishableKey || !secretKey) {
|
|
82
|
+
throw new Error("`supabase status` did not report an API URL and keys. Is the stack running?");
|
|
83
|
+
}
|
|
84
|
+
return { apiUrl, publishableKey, secretKey };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Confirms the instance has actually been migrated.
|
|
88
|
+
*
|
|
89
|
+
* This used to be `assertNotProduction()`, which read a tier out of a singleton
|
|
90
|
+
* `platform."instance"` table and refused anything reporting itself as
|
|
91
|
+
* production. That table is gone, and the tier column was guarding a door that
|
|
92
|
+
* was already walled up: `detectLocalInstance()` above reads `supabase status`,
|
|
93
|
+
* which only ever describes the Docker stack on this machine, so a remote
|
|
94
|
+
* project cannot reach these commands in the first place.
|
|
95
|
+
*
|
|
96
|
+
* What was useful was its error message, because "have you run migrations?" is
|
|
97
|
+
* the failure a contributor actually hits. That is all this does now.
|
|
98
|
+
*/
|
|
99
|
+
export async function assertMigrated(instance) {
|
|
100
|
+
const client = makeClient(instance.apiUrl, instance.secretKey, "platform");
|
|
101
|
+
const { error } = await client.from("apps").select("slug").limit(1);
|
|
102
|
+
if (error) {
|
|
103
|
+
throw new Error(`Could not read platform."apps": ${error.message}. ` +
|
|
104
|
+
"Have migrations been applied? `pnpm devtools db reset` rebuilds from scratch.");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** A service-role client. Bypasses RLS, so setup and teardown only. */
|
|
108
|
+
export function adminClient(instance, schema = "platform") {
|
|
109
|
+
return makeClient(instance.apiUrl, instance.secretKey, schema);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A client signed in as a seeded persona, subject to RLS.
|
|
113
|
+
*
|
|
114
|
+
* Signs in for real rather than hand-signing a JWT, so the token path exercised
|
|
115
|
+
* is the one production uses, with whatever claims Supabase Auth actually puts
|
|
116
|
+
* in a token rather than the ones we assume.
|
|
117
|
+
*/
|
|
118
|
+
export async function personaClient(instance, email, schema = "platform") {
|
|
119
|
+
const client = makeClient(instance.apiUrl, instance.publishableKey, schema);
|
|
120
|
+
const { error } = await client.auth.signInWithPassword({
|
|
121
|
+
email,
|
|
122
|
+
password: PERSONA_PASSWORD,
|
|
123
|
+
});
|
|
124
|
+
if (error) {
|
|
125
|
+
throw new Error(`Could not sign in as ${email}: ${error.message}. ` +
|
|
126
|
+
"Seeded personas come from `pnpm devtools` → Reset database.");
|
|
127
|
+
}
|
|
128
|
+
return client;
|
|
129
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records the fully-resolved argv of the command being run, so the CLI can
|
|
3
|
+
* close by printing a copy-pasteable line — the same command with every prompt
|
|
4
|
+
* already answered — for anyone who reached it through the wizard or let a
|
|
5
|
+
* missing flag fall to a prompt.
|
|
6
|
+
*
|
|
7
|
+
* ## Why a module-level recorder rather than a return value
|
|
8
|
+
*
|
|
9
|
+
* A command's inputs arrive from two places that never meet: the wizard walks
|
|
10
|
+
* `commands.ts` and gathers the *declared* options into the argv it dispatches
|
|
11
|
+
* (see `menu.ts`), while a runner resolves anything it prompts for *itself* —
|
|
12
|
+
* `workflows run` picking a tier, `env init` picking projects — deep inside its
|
|
13
|
+
* own call stack, where the dispatched argv is long out of reach. Threading a
|
|
14
|
+
* return value back up through every runner would touch each one's signature;
|
|
15
|
+
* a recorder every layer can reach touches only the layers that actually
|
|
16
|
+
* prompt.
|
|
17
|
+
*
|
|
18
|
+
* The rule for a runner: whenever a prompt (not a flag the caller already
|
|
19
|
+
* passed) decides a value, `recordResolved` the flag form of that decision. A
|
|
20
|
+
* value that came in as a flag is already in the base argv, so recording it
|
|
21
|
+
* again would double it — only the prompt branch records.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Start recording for one invocation.
|
|
25
|
+
*
|
|
26
|
+
* `base` is what the dispatcher received: the wizard's built argv, or the argv
|
|
27
|
+
* the user typed. `fromMenu` marks the wizard path, where every step was a
|
|
28
|
+
* prompt, so the rerun line is worth printing even if no runner records a
|
|
29
|
+
* thing. A typed command starts non-interactive and only earns the line if a
|
|
30
|
+
* runner resolves a missing flag from a prompt.
|
|
31
|
+
*/
|
|
32
|
+
export declare function beginInvocation(base: readonly string[], fromMenu: boolean): void;
|
|
33
|
+
/**
|
|
34
|
+
* Record the deploy tier the wizard entered up front, so the rerun line carries
|
|
35
|
+
* it. development is the ambient default, so a prefix for it would be noise and
|
|
36
|
+
* is dropped.
|
|
37
|
+
*/
|
|
38
|
+
export declare function recordEnteredTier(tier: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* Append the flag form of a value a prompt just resolved, e.g.
|
|
41
|
+
* `recordResolved("--tier", "development")`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function recordResolved(...fragment: string[]): void;
|
|
44
|
+
/**
|
|
45
|
+
* The command to print, or `null` when there is nothing worth printing —
|
|
46
|
+
* either no invocation ran, or the user typed a complete command and answered
|
|
47
|
+
* no prompts, so echoing it back would be noise.
|
|
48
|
+
*/
|
|
49
|
+
export declare function reproducibleCommand(): string | null;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records the fully-resolved argv of the command being run, so the CLI can
|
|
3
|
+
* close by printing a copy-pasteable line — the same command with every prompt
|
|
4
|
+
* already answered — for anyone who reached it through the wizard or let a
|
|
5
|
+
* missing flag fall to a prompt.
|
|
6
|
+
*
|
|
7
|
+
* ## Why a module-level recorder rather than a return value
|
|
8
|
+
*
|
|
9
|
+
* A command's inputs arrive from two places that never meet: the wizard walks
|
|
10
|
+
* `commands.ts` and gathers the *declared* options into the argv it dispatches
|
|
11
|
+
* (see `menu.ts`), while a runner resolves anything it prompts for *itself* —
|
|
12
|
+
* `workflows run` picking a tier, `env init` picking projects — deep inside its
|
|
13
|
+
* own call stack, where the dispatched argv is long out of reach. Threading a
|
|
14
|
+
* return value back up through every runner would touch each one's signature;
|
|
15
|
+
* a recorder every layer can reach touches only the layers that actually
|
|
16
|
+
* prompt.
|
|
17
|
+
*
|
|
18
|
+
* The rule for a runner: whenever a prompt (not a flag the caller already
|
|
19
|
+
* passed) decides a value, `recordResolved` the flag form of that decision. A
|
|
20
|
+
* value that came in as a flag is already in the base argv, so recording it
|
|
21
|
+
* again would double it — only the prompt branch records.
|
|
22
|
+
*/
|
|
23
|
+
/** The base argv plus whatever prompts resolved; `null` until an invocation begins. */
|
|
24
|
+
let argv = null;
|
|
25
|
+
/** Did the wizard, or an in-command prompt, actually decide anything here? */
|
|
26
|
+
let interactive = false;
|
|
27
|
+
/**
|
|
28
|
+
* The deploy tier `src/launch.ts` resolved and entered for this session, or
|
|
29
|
+
* `null` for the development default. Reproduced as a leading `--tier <tier>`
|
|
30
|
+
* flag, the same global flag `launch.ts` strips off ANY invocation before
|
|
31
|
+
* `cli.ts` ever runs — see its header — so this reproduces the tier for
|
|
32
|
+
* EVERY command, `db` and `env` included, exactly as typing `--tier <tier>`
|
|
33
|
+
* up front would.
|
|
34
|
+
*/
|
|
35
|
+
let enteredTier = null;
|
|
36
|
+
/**
|
|
37
|
+
* Start recording for one invocation.
|
|
38
|
+
*
|
|
39
|
+
* `base` is what the dispatcher received: the wizard's built argv, or the argv
|
|
40
|
+
* the user typed. `fromMenu` marks the wizard path, where every step was a
|
|
41
|
+
* prompt, so the rerun line is worth printing even if no runner records a
|
|
42
|
+
* thing. A typed command starts non-interactive and only earns the line if a
|
|
43
|
+
* runner resolves a missing flag from a prompt.
|
|
44
|
+
*/
|
|
45
|
+
export function beginInvocation(base, fromMenu) {
|
|
46
|
+
argv = [...base];
|
|
47
|
+
interactive = fromMenu;
|
|
48
|
+
enteredTier = null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Record the deploy tier the wizard entered up front, so the rerun line carries
|
|
52
|
+
* it. development is the ambient default, so a prefix for it would be noise and
|
|
53
|
+
* is dropped.
|
|
54
|
+
*/
|
|
55
|
+
export function recordEnteredTier(tier) {
|
|
56
|
+
enteredTier = tier === "development" ? null : tier;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Append the flag form of a value a prompt just resolved, e.g.
|
|
60
|
+
* `recordResolved("--tier", "development")`.
|
|
61
|
+
*/
|
|
62
|
+
export function recordResolved(...fragment) {
|
|
63
|
+
if (argv === null || fragment.length === 0)
|
|
64
|
+
return;
|
|
65
|
+
argv.push(...fragment);
|
|
66
|
+
interactive = true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The command to print, or `null` when there is nothing worth printing —
|
|
70
|
+
* either no invocation ran, or the user typed a complete command and answered
|
|
71
|
+
* no prompts, so echoing it back would be noise.
|
|
72
|
+
*/
|
|
73
|
+
export function reproducibleCommand() {
|
|
74
|
+
if (argv === null || !interactive || argv.length === 0)
|
|
75
|
+
return null;
|
|
76
|
+
const tierFlag = enteredTier ? ["--tier", enteredTier] : [];
|
|
77
|
+
return `pnpm devtools ${[...tierFlag, ...argv].join(" ")}`;
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function launchCi(argv: readonly string[]): Promise<void>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `devtools-ci`'s entry point, in the same shape as `src/launch.ts` (read
|
|
3
|
+
* that file's header first) — resolve the session's deploy tier and enter its
|
|
4
|
+
* environment BEFORE `ci.ts` is imported — but with one deliberate
|
|
5
|
+
* difference: this NEVER prompts.
|
|
6
|
+
*
|
|
7
|
+
* CI is never interactive — `process.stdin.isTTY` is `undefined` in every job
|
|
8
|
+
* this runs in — so `resolveSessionTier` would already refuse to prompt on
|
|
9
|
+
* its own `isTTY` check. No `prompt` function is injected anyway, making that
|
|
10
|
+
* doubly true rather than relying on the ambient TTY guess: a CI job that
|
|
11
|
+
* somehow got attached to a terminal must still refuse ambiguity outright
|
|
12
|
+
* instead of hanging on a question nobody in a workflow can answer.
|
|
13
|
+
*
|
|
14
|
+
* ## Which `devtools-ci` invocations reach this file at all
|
|
15
|
+
*
|
|
16
|
+
* Only `run ci:env` does. The bare `run ci` package script — used for
|
|
17
|
+
* `deploy write-env`, which CREATES the env file this module would otherwise
|
|
18
|
+
* insist on reading, and for the handful of steps that hold one narrow
|
|
19
|
+
* credential and compose no env file at all — still runs `ci.ts` directly
|
|
20
|
+
* through `tsx`, deliberately bypassing tier resolution entirely. See
|
|
21
|
+
* `.github/workflows/deploy.yaml`'s own header for the two entry points and
|
|
22
|
+
* why each step picked the one it did.
|
|
23
|
+
*
|
|
24
|
+
* A `deploy <app> --tier <t>` invocation's `--tier` is a command flag `ci.ts`
|
|
25
|
+
* parses for itself (see its `runDeployCommand`), not this module's global
|
|
26
|
+
* `--tier` — but they resolve to the same tier here: `stripTierFlag` pulls it
|
|
27
|
+
* off before `ci.ts` sees argv, `resolveSessionTier` resolves the identical
|
|
28
|
+
* value as `explicit`, and `ci.ts`'s own parse then falls back to
|
|
29
|
+
* `process.env.DEPLOY_ENV`, which `enterEnvironment` just set to it.
|
|
30
|
+
*/
|
|
31
|
+
import { findRepoRoot } from "./repo/root.js";
|
|
32
|
+
import { loadEnvLoad, loadEnvSession } from "./repo/peers.js";
|
|
33
|
+
import { stripTierFlag } from "./launch.js";
|
|
34
|
+
export async function launchCi(argv) {
|
|
35
|
+
const { explicit, rest } = stripTierFlag(argv);
|
|
36
|
+
const envLoad = await loadEnvLoad();
|
|
37
|
+
const envSession = await loadEnvSession();
|
|
38
|
+
const resolution = await envSession.resolveSessionTier({
|
|
39
|
+
explicit,
|
|
40
|
+
deployEnv: process.env.DEPLOY_ENV,
|
|
41
|
+
available: await envSession.availableTiers(findRepoRoot()),
|
|
42
|
+
isTTY: false,
|
|
43
|
+
// No `prompt` — see this file's header. Ambiguity is always a refusal.
|
|
44
|
+
});
|
|
45
|
+
if (!resolution.ok) {
|
|
46
|
+
process.stderr.write(`devtools-ci: ${resolution.reason}\n`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const tier = resolution.tier;
|
|
50
|
+
try {
|
|
51
|
+
const entered = await envSession.enterEnvironment(tier, { override: false });
|
|
52
|
+
for (const warning of entered.warnings) {
|
|
53
|
+
process.stderr.write(`devtools-ci: ${warning}\n`);
|
|
54
|
+
}
|
|
55
|
+
process.stderr.write(`devtools-ci: loaded ${entered.files.length > 0 ? entered.files.join(", ") : "no env files"} (${tier})\n`);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (!(err instanceof envLoad.MissingEnvFileError))
|
|
59
|
+
throw err;
|
|
60
|
+
// A CI job that reaches `run ci:env` at all has already run `deploy
|
|
61
|
+
// write-env` (see this file's header), so a missing file here — for
|
|
62
|
+
// ANY tier, `development` included — is never a first-run story the way
|
|
63
|
+
// it is for a contributor's own machine. Always fatal.
|
|
64
|
+
process.stderr.write(`devtools-ci: ${err.message}\n`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const { main } = await import("./ci.js");
|
|
68
|
+
try {
|
|
69
|
+
await main(rest);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
process.stderr.write(`devtools-ci: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
73
|
+
// Report BEFORE exiting — see `captureDevtoolsError`'s header. `ci.ts`'s
|
|
74
|
+
// own `main()` already initialized telemetry (it does so at its own
|
|
75
|
+
// bootstrap, before this throw could happen), so this call only needs to
|
|
76
|
+
// capture and flush.
|
|
77
|
+
const { captureDevtoolsError } = await import("./telemetry.js");
|
|
78
|
+
await captureDevtoolsError(err);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// See `launch.ts`'s matching guard for why this checks `process.argv[1]`
|
|
83
|
+
// rather than self-invoking unconditionally.
|
|
84
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
85
|
+
await launchCi(process.argv.slice(2));
|
|
86
|
+
}
|
package/dist/launch.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulls the two preflight-only boolean flags out of `argv`, wherever they
|
|
3
|
+
* sit, the same way `stripTierFlag` does for `--tier` below. Neither takes
|
|
4
|
+
* a value. `DEVTOOLS_SKIP_PREFLIGHT=1` is the env-var spelling of
|
|
5
|
+
* `--skip-preflight`, for shells/CI wrappers that would rather set an env
|
|
6
|
+
* var once than remember a flag on every invocation.
|
|
7
|
+
*/
|
|
8
|
+
export declare function stripPreflightFlags(argv: readonly string[]): {
|
|
9
|
+
skipPreflight: boolean;
|
|
10
|
+
refresh: boolean;
|
|
11
|
+
rest: string[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Pulls a global `--tier <t>` out of `argv`, wherever it sits, leaving every
|
|
15
|
+
* other argument untouched and in its original order. Exported for its own
|
|
16
|
+
* unit tests; `launch()` below is the only real caller.
|
|
17
|
+
*/
|
|
18
|
+
export declare function stripTierFlag(argv: readonly string[]): {
|
|
19
|
+
explicit: string | undefined;
|
|
20
|
+
rest: string[];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the session's deploy tier, enters it, and hands off to `cli.ts`.
|
|
24
|
+
*
|
|
25
|
+
* Exits the process directly on a refusal from `resolveSessionTier` — there
|
|
26
|
+
* is no command dispatched yet for a caller to fall back to, so there is
|
|
27
|
+
* nothing this function could return that would mean anything.
|
|
28
|
+
*/
|
|
29
|
+
export declare function launch(argv: readonly string[]): Promise<void>;
|