@devdogsuga/devtools 0.1.0 → 0.1.2
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/dist/cli.js +50 -23
- package/dist/commands.js +63 -23
- package/dist/db/new-migration.d.ts +8 -1
- package/dist/db/new-migration.js +60 -2
- package/dist/db/pooler.d.ts +23 -0
- package/dist/db/pooler.js +55 -0
- package/dist/environment-doctor.d.ts +36 -0
- package/dist/environment-doctor.js +395 -0
- package/dist/oauth/wizard.d.ts +8 -1
- package/dist/oauth/wizard.js +20 -4
- package/dist/setup.js +178 -50
- package/package.json +6 -6
- package/dist/newsletter/commands.d.ts +0 -16
- package/dist/newsletter/commands.js +0 -311
- package/dist/newsletter/imap.d.ts +0 -17
- package/dist/newsletter/imap.js +0 -127
- package/dist/newsletter/loopback.d.ts +0 -24
- package/dist/newsletter/loopback.js +0 -100
- package/dist/newsletter/oauth.d.ts +0 -26
- package/dist/newsletter/oauth.js +0 -130
- package/dist/newsletter/smtp.d.ts +0 -23
- package/dist/newsletter/smtp.js +0 -133
- package/dist/tsconfig.typecheck.tsbuildinfo +0 -1
- /package/dist/{doctor.d.ts → moderation.d.ts} +0 -0
- /package/dist/{doctor.js → moderation.js} +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pnpm devtools doctor [--app <slug>] [--report]`
|
|
3
|
+
*
|
|
4
|
+
* The new environment checker. Entirely read-only: it inspects this machine
|
|
5
|
+
* and this checkout's `.env`, and for a hosted Supabase project, makes a
|
|
6
|
+
* couple of network calls to say whether it is reachable — never writes
|
|
7
|
+
* anything. `setup.ts`'s hosted wizard runs the same checks at the end of
|
|
8
|
+
* its own run (see its last step), so a contributor who set up through the
|
|
9
|
+
* wizard and one who ran `doctor` cold see the same report.
|
|
10
|
+
*
|
|
11
|
+
* Every check carries a stable FAQ id from the docs contract's list (see
|
|
12
|
+
* `docs/_shared/getting-started/troubleshooting.md` once written) so a
|
|
13
|
+
* failure message can point at
|
|
14
|
+
* `https://devdogsuga.org/docs/<app>/getting-started/troubleshooting#<id>`
|
|
15
|
+
* rather than explaining itself twice.
|
|
16
|
+
*
|
|
17
|
+
* Pure classification lives in small exported functions so this file's own
|
|
18
|
+
* tests do not have to fake `child_process`/`fs` to prove the logic; the
|
|
19
|
+
* orchestrator (`runEnvironmentDoctor`) is the only part that actually
|
|
20
|
+
* touches the machine.
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { log, note } from "@clack/prompts";
|
|
26
|
+
import { parse as parseDotenv } from "dotenv";
|
|
27
|
+
import { discoverRepoRoot } from "./repo/root.js";
|
|
28
|
+
import { validateSessionPoolerUrl } from "./db/pooler.js";
|
|
29
|
+
const TROUBLESHOOTING_APPS = ["schedule-builder", "study-group-finder", "platform"];
|
|
30
|
+
function faqUrl(app, id) {
|
|
31
|
+
return `https://devdogsuga.org/docs/${app}/getting-started/troubleshooting#${id}`;
|
|
32
|
+
}
|
|
33
|
+
// ── Pure checks ──────────────────────────────────────────────────────────────
|
|
34
|
+
/** The floor is 22.12: pnpm 11 needs `node:sqlite`, stable there. `.nvmrc`
|
|
35
|
+
* pins 24, which is what a `fnm use` picks up; a machine below the floor is
|
|
36
|
+
* a WARN, not an error — plenty of contributors' shells still resolve an
|
|
37
|
+
* old global `node` first. */
|
|
38
|
+
export function checkNodeVersion(nodeVersion, app) {
|
|
39
|
+
const [major = 0, minor = 0] = nodeVersion.split(".").map(Number);
|
|
40
|
+
const ok = major > 22 || (major === 22 && minor >= 12);
|
|
41
|
+
return {
|
|
42
|
+
id: "node-version",
|
|
43
|
+
status: ok ? "ok" : "warn",
|
|
44
|
+
summary: ok
|
|
45
|
+
? `Node ${nodeVersion}`
|
|
46
|
+
: `Node ${nodeVersion} is below the floor (>= 22.12)`,
|
|
47
|
+
fix: ok
|
|
48
|
+
? undefined
|
|
49
|
+
: "Install fnm, then `fnm use` in the repo (reads .nvmrc).",
|
|
50
|
+
faqId: ok ? undefined : "node-version",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function checkFnmHookPresent(shellProfile, app) {
|
|
54
|
+
const present = Boolean(shellProfile?.includes("fnm env") || shellProfile?.includes("fnm_env"));
|
|
55
|
+
return {
|
|
56
|
+
id: "fnm-not-found",
|
|
57
|
+
status: present ? "ok" : "warn",
|
|
58
|
+
summary: present
|
|
59
|
+
? "fnm hook found in shell profile"
|
|
60
|
+
: "No fnm hook found in your shell profile",
|
|
61
|
+
fix: present
|
|
62
|
+
? undefined
|
|
63
|
+
: "Add fnm's shell hook (`fnm env --use-on-cd --shell <bash|zsh|powershell> | ...`) and restart your terminal.",
|
|
64
|
+
faqId: present ? undefined : "fnm-not-found",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function checkPnpmVersion(pnpmVersion, pinned, app) {
|
|
68
|
+
if (!pnpmVersion) {
|
|
69
|
+
return {
|
|
70
|
+
id: "pnpm-missing",
|
|
71
|
+
status: "warn",
|
|
72
|
+
summary: "pnpm is not on PATH",
|
|
73
|
+
fix: '`npm install -g pnpm` (never `corepack enable` — this repo does not use corepack).',
|
|
74
|
+
faqId: "pnpm-missing",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const pinnedVersion = pinned.replace(/^pnpm@/, "").split("+")[0];
|
|
78
|
+
const ok = pnpmVersion === pinnedVersion;
|
|
79
|
+
return {
|
|
80
|
+
id: "pnpm-version",
|
|
81
|
+
status: ok ? "ok" : "warn",
|
|
82
|
+
summary: ok
|
|
83
|
+
? `pnpm ${pnpmVersion}`
|
|
84
|
+
: `pnpm ${pnpmVersion} does not match the pinned ${pinnedVersion}`,
|
|
85
|
+
fix: ok
|
|
86
|
+
? undefined
|
|
87
|
+
: "pnpm self-switches to the pinned version on its first run in the repo (pmOnFail=download). Run any pnpm command here again.",
|
|
88
|
+
faqId: ok ? undefined : "pnpm-version",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** `git config core.autocrlf` — `input` or `false` is correct on every
|
|
92
|
+
* platform this repo documents (WSL2, native Windows, macOS, Linux); `true`
|
|
93
|
+
* rewrites LF to CRLF on checkout and breaks anything that shells out to a
|
|
94
|
+
* script with a shebang. */
|
|
95
|
+
export function checkAutocrlf(value) {
|
|
96
|
+
const ok = value === "input" || value === "false" || value === null;
|
|
97
|
+
return {
|
|
98
|
+
id: "crlf-line-endings",
|
|
99
|
+
status: ok ? "ok" : "warn",
|
|
100
|
+
summary: ok
|
|
101
|
+
? "core.autocrlf is not rewriting line endings"
|
|
102
|
+
: `core.autocrlf=${value} rewrites LF to CRLF on checkout`,
|
|
103
|
+
fix: ok
|
|
104
|
+
? undefined
|
|
105
|
+
: "`git config --global core.autocrlf input` (or `false`), then re-clone or `git rm --cached -r . && git reset --hard`.",
|
|
106
|
+
faqId: ok ? undefined : "crlf-line-endings",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** WSL2's own filesystem, never `/mnt/c/...` — a Windows-drive checkout
|
|
110
|
+
* makes every file operation cross the 9p boundary and Docker bind mounts
|
|
111
|
+
* silently fail to see edits. */
|
|
112
|
+
export function checkRepoUnderMntC(repoRoot) {
|
|
113
|
+
const ok = !repoRoot.startsWith("/mnt/");
|
|
114
|
+
return {
|
|
115
|
+
id: "repo-under-mnt-c",
|
|
116
|
+
status: ok ? "ok" : "warn",
|
|
117
|
+
summary: ok
|
|
118
|
+
? "Repo is on the Linux filesystem"
|
|
119
|
+
: `Repo is under ${repoRoot} — a Windows drive mounted into WSL2`,
|
|
120
|
+
fix: ok
|
|
121
|
+
? undefined
|
|
122
|
+
: "Clone into your Linux home (~), never /mnt/c/..., and reopen with the VS Code WSL extension.",
|
|
123
|
+
faqId: ok ? undefined : "repo-under-mnt-c",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Windows only. `RemoteSigned` (or looser) is what lets fnm's PowerShell
|
|
127
|
+
* hook and pnpm's shims actually run. */
|
|
128
|
+
export function checkPowerShellExecutionPolicy(policy) {
|
|
129
|
+
const restricted = policy === "Restricted" || policy === "AllSigned";
|
|
130
|
+
return {
|
|
131
|
+
id: "powershell-execution-policy",
|
|
132
|
+
status: restricted ? "warn" : "ok",
|
|
133
|
+
summary: restricted
|
|
134
|
+
? `Execution policy is ${policy}, which blocks locally-created scripts`
|
|
135
|
+
: `Execution policy is ${policy ?? "unrestricted"}`,
|
|
136
|
+
fix: restricted
|
|
137
|
+
? "`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`, then restart your terminal."
|
|
138
|
+
: undefined,
|
|
139
|
+
faqId: restricted ? "powershell-execution-policy" : undefined,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function checkEnvKeysPresent(env, keys) {
|
|
143
|
+
const missing = keys.filter((k) => !env[k]);
|
|
144
|
+
return {
|
|
145
|
+
id: "env-incomplete",
|
|
146
|
+
status: missing.length === 0 ? "ok" : "warn",
|
|
147
|
+
summary: missing.length === 0
|
|
148
|
+
? ".env has every key this run needs"
|
|
149
|
+
: `.env is missing: ${missing.join(", ")}`,
|
|
150
|
+
fix: missing.length === 0
|
|
151
|
+
? undefined
|
|
152
|
+
: "`pnpm devtools setup` (or `pnpm devtools env init`) fills in the blanks.",
|
|
153
|
+
faqId: missing.length === 0 ? undefined : "env-incomplete",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export { validateSessionPoolerUrl };
|
|
157
|
+
// ── Orchestrator ─────────────────────────────────────────────────────────────
|
|
158
|
+
function has(cmd, args) {
|
|
159
|
+
try {
|
|
160
|
+
return execFileSync(cmd, args, {
|
|
161
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
162
|
+
encoding: "utf8",
|
|
163
|
+
}).trim();
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function gitConfig(key) {
|
|
170
|
+
return has("git", ["config", "--get", key]);
|
|
171
|
+
}
|
|
172
|
+
function readNvmrc(repoRoot) {
|
|
173
|
+
try {
|
|
174
|
+
return readFileSync(join(repoRoot, ".nvmrc"), "utf8").trim();
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function readPackageManagerPin(repoRoot) {
|
|
181
|
+
try {
|
|
182
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
|
|
183
|
+
return pkg.packageManager ?? null;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function readShellProfile() {
|
|
190
|
+
const home = process.env.HOME;
|
|
191
|
+
if (!home)
|
|
192
|
+
return null;
|
|
193
|
+
for (const file of [".bashrc", ".zshrc", ".config/fish/config.fish", ".bash_profile"]) {
|
|
194
|
+
const path = join(home, file);
|
|
195
|
+
if (existsSync(path)) {
|
|
196
|
+
try {
|
|
197
|
+
return readFileSync(path, "utf8");
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// try the next one
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
function renderCheck(app, check) {
|
|
207
|
+
const icon = check.status === "ok" ? "OK " : check.status === "skip" ? "SKIP" : "WARN";
|
|
208
|
+
const lines = [`${icon} ${check.summary}`];
|
|
209
|
+
if (check.status === "warn") {
|
|
210
|
+
if (check.fix)
|
|
211
|
+
lines.push(` fix: ${check.fix}`);
|
|
212
|
+
if (check.faqId)
|
|
213
|
+
lines.push(` ${faqUrl(app, check.faqId)}`);
|
|
214
|
+
}
|
|
215
|
+
return lines.join("\n");
|
|
216
|
+
}
|
|
217
|
+
export async function runEnvironmentDoctor(opts) {
|
|
218
|
+
const app = opts.app && TROUBLESHOOTING_APPS.includes(opts.app) ? opts.app : "platform";
|
|
219
|
+
const repoRoot = discoverRepoRoot();
|
|
220
|
+
const checks = [];
|
|
221
|
+
const nodeCheck = checkNodeVersion(process.versions.node, app);
|
|
222
|
+
checks.push(nodeCheck);
|
|
223
|
+
// Only worth raising when Node is wrong: nvm, Volta or a system Node that
|
|
224
|
+
// already satisfies the floor is fine, and flagging a missing fnm hook there
|
|
225
|
+
// is a false alarm.
|
|
226
|
+
if (nodeCheck.status !== "ok") {
|
|
227
|
+
checks.push(checkFnmHookPresent(readShellProfile(), app));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const pinnedMajor = repoRoot ? readNvmrc(repoRoot)?.split(".")[0] : null;
|
|
231
|
+
const major = process.versions.node.split(".")[0];
|
|
232
|
+
if (pinnedMajor && /^\d+$/.test(pinnedMajor) && pinnedMajor !== major) {
|
|
233
|
+
checks.push({
|
|
234
|
+
id: "node-version",
|
|
235
|
+
status: "warn",
|
|
236
|
+
summary: `Node ${process.versions.node} works, but the repo pins Node ${pinnedMajor} (.nvmrc)`,
|
|
237
|
+
fix: "Run `fnm install && fnm use` in the repo.",
|
|
238
|
+
faqId: "node-version",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const pnpmVersion = has("pnpm", ["--version"]);
|
|
243
|
+
const pin = repoRoot ? readPackageManagerPin(repoRoot) : null;
|
|
244
|
+
checks.push(pin
|
|
245
|
+
? checkPnpmVersion(pnpmVersion, pin, app)
|
|
246
|
+
: {
|
|
247
|
+
id: "pnpm-version",
|
|
248
|
+
status: pnpmVersion ? "ok" : "warn",
|
|
249
|
+
summary: pnpmVersion ? `pnpm ${pnpmVersion}` : "pnpm is not on PATH",
|
|
250
|
+
fix: pnpmVersion
|
|
251
|
+
? undefined
|
|
252
|
+
: '`npm install -g pnpm` (never `corepack enable`).',
|
|
253
|
+
faqId: pnpmVersion ? undefined : "pnpm-missing",
|
|
254
|
+
});
|
|
255
|
+
checks.push(checkAutocrlf(gitConfig("core.autocrlf")));
|
|
256
|
+
if (repoRoot)
|
|
257
|
+
checks.push(checkRepoUnderMntC(repoRoot));
|
|
258
|
+
if (process.platform === "win32") {
|
|
259
|
+
checks.push(checkPowerShellExecutionPolicy(has("powershell", ["-NoProfile", "-Command", "Get-ExecutionPolicy"])));
|
|
260
|
+
}
|
|
261
|
+
// Local Docker stack — only relevant when the checkout has a local
|
|
262
|
+
// `.env.generated`, the signature of `db start` having run at least once.
|
|
263
|
+
if (repoRoot && existsSync(join(repoRoot, ".env.generated"))) {
|
|
264
|
+
const dockerUp = has("docker", ["info"]) !== null;
|
|
265
|
+
checks.push({
|
|
266
|
+
id: "docker-not-running",
|
|
267
|
+
status: dockerUp ? "ok" : "warn",
|
|
268
|
+
summary: dockerUp ? "Docker is running" : "Docker daemon is not answering",
|
|
269
|
+
fix: dockerUp
|
|
270
|
+
? undefined
|
|
271
|
+
: "Start Docker Desktop or OrbStack (macOS: set DOCKER_HOST=unix://$HOME/.orbstack/run/docker.sock for OrbStack). Never Colima.",
|
|
272
|
+
faqId: dockerUp ? undefined : "docker-not-running",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (!opts.app || opts.app === "study-group-finder") {
|
|
276
|
+
const flutter = has("flutter", ["--version"]) !== null;
|
|
277
|
+
checks.push({
|
|
278
|
+
id: "flutter-missing",
|
|
279
|
+
status: flutter ? "ok" : "skip",
|
|
280
|
+
summary: flutter ? "Flutter is installed" : "Flutter is not installed",
|
|
281
|
+
fix: flutter ? undefined : "Install the Flutter SDK for study-group-finder.",
|
|
282
|
+
faqId: flutter ? undefined : "flutter-missing",
|
|
283
|
+
});
|
|
284
|
+
if (flutter) {
|
|
285
|
+
const devices = has("adb", ["devices"]);
|
|
286
|
+
const hasDevice = Boolean(devices
|
|
287
|
+
?.split("\n")
|
|
288
|
+
.slice(1)
|
|
289
|
+
.some((line) => line.trim().endsWith("device")));
|
|
290
|
+
checks.push({
|
|
291
|
+
id: "adb-no-devices",
|
|
292
|
+
status: hasDevice ? "ok" : "skip",
|
|
293
|
+
summary: hasDevice
|
|
294
|
+
? "adb sees a device or emulator"
|
|
295
|
+
: "adb sees no device or emulator",
|
|
296
|
+
fix: hasDevice
|
|
297
|
+
? undefined
|
|
298
|
+
: "Start an Android Studio emulator (or plug in a device). On WSL2 with a Windows emulator, run `adb kill-server` on Windows first.",
|
|
299
|
+
faqId: hasDevice ? undefined : "adb-no-devices",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
let env = {};
|
|
304
|
+
if (repoRoot) {
|
|
305
|
+
const envPath = join(repoRoot, ".env");
|
|
306
|
+
if (existsSync(envPath)) {
|
|
307
|
+
env = parseDotenv(readFileSync(envPath, "utf8"));
|
|
308
|
+
}
|
|
309
|
+
checks.push(checkEnvKeysPresent(env, ["API_URL", "PUBLISHABLE_KEY"]));
|
|
310
|
+
}
|
|
311
|
+
// ── Hosted checks — only when .env looks like a hosted project ───────────
|
|
312
|
+
const isHosted = Boolean(env.API_URL && !env.API_URL.includes("127.0.0.1") && !env.API_URL.includes("localhost"));
|
|
313
|
+
if (isHosted && env.API_URL) {
|
|
314
|
+
let reachable = false;
|
|
315
|
+
let probeStatus = null;
|
|
316
|
+
try {
|
|
317
|
+
const res = await fetch(`${env.API_URL}/auth/v1/settings`, {
|
|
318
|
+
signal: AbortSignal.timeout(5000),
|
|
319
|
+
});
|
|
320
|
+
probeStatus = res.status;
|
|
321
|
+
reachable = res.status < 500;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
reachable = false;
|
|
325
|
+
}
|
|
326
|
+
checks.push({
|
|
327
|
+
id: "supabase-unreachable",
|
|
328
|
+
status: reachable ? "ok" : "warn",
|
|
329
|
+
summary: reachable
|
|
330
|
+
? "The hosted project answers"
|
|
331
|
+
: "The hosted project's API URL did not respond",
|
|
332
|
+
fix: reachable
|
|
333
|
+
? undefined
|
|
334
|
+
: "Check the API URL in .env, and that the project is not paused.",
|
|
335
|
+
faqId: reachable ? undefined : "supabase-unreachable",
|
|
336
|
+
});
|
|
337
|
+
if (probeStatus === 503 || probeStatus === 522 || probeStatus === 540) {
|
|
338
|
+
checks.push({
|
|
339
|
+
id: "supabase-paused",
|
|
340
|
+
status: "warn",
|
|
341
|
+
summary: "The project looks paused",
|
|
342
|
+
fix: "Unpause it from the Supabase dashboard (free tier pauses after ~1 week idle).",
|
|
343
|
+
faqId: "supabase-paused",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (reachable && env.SECRET_KEY) {
|
|
347
|
+
let keysValid = false;
|
|
348
|
+
try {
|
|
349
|
+
const res = await fetch(`${env.API_URL}/auth/v1/admin/users?page=1&per_page=1`, {
|
|
350
|
+
headers: { Authorization: `Bearer ${env.SECRET_KEY}`, apikey: env.SECRET_KEY },
|
|
351
|
+
signal: AbortSignal.timeout(5000),
|
|
352
|
+
});
|
|
353
|
+
keysValid = res.status !== 401 && res.status !== 403;
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
keysValid = false;
|
|
357
|
+
}
|
|
358
|
+
checks.push({
|
|
359
|
+
id: "supabase-keys-invalid",
|
|
360
|
+
status: keysValid ? "ok" : "warn",
|
|
361
|
+
summary: keysValid ? "SECRET_KEY is accepted" : "SECRET_KEY was rejected",
|
|
362
|
+
fix: keysValid
|
|
363
|
+
? undefined
|
|
364
|
+
: "Copy the service_role secret key again from Project Settings -> API.",
|
|
365
|
+
faqId: keysValid ? undefined : "supabase-keys-invalid",
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (env.DB_URL) {
|
|
370
|
+
const validation = validateSessionPoolerUrl(env.DB_URL);
|
|
371
|
+
checks.push({
|
|
372
|
+
id: validation.faqId ?? "db-url-direct-connection",
|
|
373
|
+
status: validation.ok ? "ok" : "warn",
|
|
374
|
+
summary: validation.ok
|
|
375
|
+
? "DB_URL is a Session pooler string"
|
|
376
|
+
: (validation.message ?? "DB_URL is not a Session pooler string"),
|
|
377
|
+
fix: validation.ok ? undefined : validation.message,
|
|
378
|
+
faqId: validation.faqId,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
note(checks.map((c) => renderCheck(app, c)).join("\n\n"), opts.app ? `Doctor — ${opts.app}` : "Doctor");
|
|
382
|
+
const warnings = checks.filter((c) => c.status === "warn").length;
|
|
383
|
+
if (warnings === 0)
|
|
384
|
+
log.success("Everything checked out.");
|
|
385
|
+
else
|
|
386
|
+
log.warn(`${warnings} check${warnings === 1 ? "" : "s"} need attention.`);
|
|
387
|
+
if (opts.report) {
|
|
388
|
+
const redacted = [
|
|
389
|
+
`OS: ${process.platform} ${process.arch}`,
|
|
390
|
+
`Node: ${process.versions.node}`,
|
|
391
|
+
...checks.map((c) => `${c.status.toUpperCase().padEnd(4)} ${c.id}: ${c.summary}`),
|
|
392
|
+
].join("\n");
|
|
393
|
+
note(redacted, "Paste this in Discord");
|
|
394
|
+
}
|
|
395
|
+
}
|
package/dist/oauth/wizard.d.ts
CHANGED
|
@@ -30,4 +30,11 @@ export declare function connectViaOneClick(target: ConnectTarget, platformUrl: s
|
|
|
30
30
|
* Intro and outro are the caller's, so this composes into the devtools menu
|
|
31
31
|
* without drawing a second box inside the first.
|
|
32
32
|
*/
|
|
33
|
-
export declare function runOAuthSetup(baseUrlOverride?: string, platformUrlOverride?: string, transportOverride?: ConnectTransport
|
|
33
|
+
export declare function runOAuthSetup(baseUrlOverride?: string, platformUrlOverride?: string, transportOverride?: ConnectTransport,
|
|
34
|
+
/**
|
|
35
|
+
* A target already resolved by a caller — `setup.ts`'s hosted wizard,
|
|
36
|
+
* chaining in with the same project/service-role key a contributor just
|
|
37
|
+
* typed, so they are not asked to pick "hosted" and paste the key a
|
|
38
|
+
* second time. Skips Step 1 entirely when given.
|
|
39
|
+
*/
|
|
40
|
+
presetTarget?: ConnectTarget): Promise<void>;
|
package/dist/oauth/wizard.js
CHANGED
|
@@ -424,11 +424,18 @@ async function connectByPasting(baseUrlOverride) {
|
|
|
424
424
|
* Intro and outro are the caller's, so this composes into the devtools menu
|
|
425
425
|
* without drawing a second box inside the first.
|
|
426
426
|
*/
|
|
427
|
-
export async function runOAuthSetup(baseUrlOverride, platformUrlOverride, transportOverride
|
|
427
|
+
export async function runOAuthSetup(baseUrlOverride, platformUrlOverride, transportOverride,
|
|
428
|
+
/**
|
|
429
|
+
* A target already resolved by a caller — `setup.ts`'s hosted wizard,
|
|
430
|
+
* chaining in with the same project/service-role key a contributor just
|
|
431
|
+
* typed, so they are not asked to pick "hosted" and paste the key a
|
|
432
|
+
* second time. Skips Step 1 entirely when given.
|
|
433
|
+
*/
|
|
434
|
+
presetTarget) {
|
|
428
435
|
const cwd = process.cwd();
|
|
429
436
|
log.info('Configures a Supabase project to support "Sign in with DevDogs".');
|
|
430
437
|
// ── Step 1: Choose target ────────────────────────────────────────────────
|
|
431
|
-
const target = await chooseTarget(cwd);
|
|
438
|
+
const target = presetTarget ?? (await chooseTarget(cwd));
|
|
432
439
|
// ── Step 2: Connect ──────────────────────────────────────────────────────
|
|
433
440
|
const providerName = unwrap(await text({
|
|
434
441
|
message: "Provider display name",
|
|
@@ -548,9 +555,18 @@ export async function runOAuthSetup(baseUrlOverride, platformUrlOverride, transp
|
|
|
548
555
|
` ${callbackUri}`, "One more step");
|
|
549
556
|
}
|
|
550
557
|
}
|
|
558
|
+
const redirectStep = target.kind === "hosted"
|
|
559
|
+
? [
|
|
560
|
+
`Add your app's localhost redirect URL in the dashboard:`,
|
|
561
|
+
` Auth -> URL Configuration -> Redirect URLs -> add http://localhost:<port>/**`,
|
|
562
|
+
` (schedule-builder is 3001, platform is 3000)`,
|
|
563
|
+
]
|
|
564
|
+
: [
|
|
565
|
+
`Make sure your project's supabase/config.toml allows your app callback:`,
|
|
566
|
+
` additional_redirect_urls = ["http://localhost:<port>/auth/callback"]`,
|
|
567
|
+
];
|
|
551
568
|
const nextSteps = [
|
|
552
|
-
|
|
553
|
-
` additional_redirect_urls = ["http://localhost:<port>/auth/callback"]`,
|
|
569
|
+
...redirectStep,
|
|
554
570
|
``,
|
|
555
571
|
`Trigger sign-in from your app:`,
|
|
556
572
|
``,
|