@we8/cloudflare 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 +57 -0
- package/dist/cli-args.d.ts +22 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +76 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +122 -0
- package/dist/cli.js.map +1 -0
- package/dist/doctor.d.ts +87 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +236 -0
- package/dist/doctor.js.map +1 -0
- package/dist/email.d.ts +80 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +104 -0
- package/dist/email.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/jsonc.d.ts +15 -0
- package/dist/jsonc.d.ts.map +1 -0
- package/dist/jsonc.js +60 -0
- package/dist/jsonc.js.map +1 -0
- package/dist/project.d.ts +26 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +112 -0
- package/dist/project.js.map +1 -0
- package/dist/wrangler-config.d.ts +65 -0
- package/dist/wrangler-config.d.ts.map +1 -0
- package/dist/wrangler-config.js +179 -0
- package/dist/wrangler-config.js.map +1 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @we8/cloudflare
|
|
2
|
+
|
|
3
|
+
The Cloudflare pack for a we8 CMS project. Three things, and only three:
|
|
4
|
+
|
|
5
|
+
- **A real email sender.** `cloudflareEmailSender` plugs into the CMS email
|
|
6
|
+
seam and sends over Cloudflare Email Service. It is off until a site sets
|
|
7
|
+
`EMAIL_MODE=cloudflare`, binds `send_email` as `EMAIL`, and sets
|
|
8
|
+
`EMAIL_FROM`; short of all three it declines and the CMS keeps its own
|
|
9
|
+
behaviour, so installing the pack never changes how a site works. Sending
|
|
10
|
+
never throws: mail from a CMS is always a side effect of something that
|
|
11
|
+
already succeeded.
|
|
12
|
+
- **The wrangler config generator.** `generateWranglerConfig(answers)` returns
|
|
13
|
+
the `wrangler.jsonc` a project deploys with, shaped after the CMS package's
|
|
14
|
+
own config. `create-we8` writes a new project's config with it.
|
|
15
|
+
- **The `we8-cloudflare` CLI.** `doctor` says what is missing and the exact
|
|
16
|
+
command that fixes it; `migrate` applies the migrations that ship inside
|
|
17
|
+
`@we8/cms`; `seed` loads the development seed into the local database and
|
|
18
|
+
refuses, always, to touch a deployed one.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
npx @we8/cloudflare doctor # what is missing, and how to fix it
|
|
22
|
+
npx @we8/cloudflare doctor --remote # the same, including deployed secrets
|
|
23
|
+
npx @we8/cloudflare migrate # apply migrations locally
|
|
24
|
+
npx @we8/cloudflare migrate --remote # apply them to the deployed database
|
|
25
|
+
npx @we8/cloudflare seed # local development data, local only
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Peer dependencies, declared and auto-installed by npm: `@we8/cms` (the
|
|
31
|
+
worker this pack configures) and `wrangler` (the CLI it wraps). A scaffolded
|
|
32
|
+
project also declares both explicitly.
|
|
33
|
+
- Node 20 or newer.
|
|
34
|
+
- A Cloudflare account only when you deploy; local development runs entirely
|
|
35
|
+
offline.
|
|
36
|
+
## The guardrail
|
|
37
|
+
|
|
38
|
+
`seed/local.sql` contains plaintext API keys, because that is what a
|
|
39
|
+
development seed is for. The `seed` command has no remote mode, refuses
|
|
40
|
+
`--remote`, `--preview`, and `--env`, and the doctor fails a project whose
|
|
41
|
+
`db:seed` script points anywhere but the local database.
|
|
42
|
+
|
|
43
|
+
## Dependencies
|
|
44
|
+
|
|
45
|
+
None at runtime. `@we8/cms` and `wrangler` are peers rather than dependencies,
|
|
46
|
+
so the two packages typecheck and build in either order; the seam's types are
|
|
47
|
+
restated here and the generated project's own typecheck is what proves they
|
|
48
|
+
still line up.
|
|
49
|
+
|
|
50
|
+
Cloudflare is the only pack in 0.1, so the core may assume Workers runtime
|
|
51
|
+
conventions. The seam between core and pack stays thin (database, object
|
|
52
|
+
storage, analytics sink, email sender) so a second pack is a real possibility,
|
|
53
|
+
but 0.1 ships no driver framework.
|
|
54
|
+
|
|
55
|
+
See [docs/deploy.md](https://github.com/reveriext/we8-package/blob/main/docs/deploy.md) for the path from an empty directory
|
|
56
|
+
to a deployed CMS, and [docs/quick-start.md](https://github.com/reveriext/we8-package/blob/main/docs/quick-start.md) for the
|
|
57
|
+
three paths a project can take once it is deployed.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument parsing for `we8-cloudflare`, kept apart from the CLI that spawns
|
|
3
|
+
* wrangler so the shapes below can be tested without a subprocess.
|
|
4
|
+
*/
|
|
5
|
+
export type CommandName = 'doctor' | 'migrate' | 'seed' | 'help' | 'version';
|
|
6
|
+
export interface ParsedCommand {
|
|
7
|
+
command: CommandName;
|
|
8
|
+
/** Where the project lives. Defaults to the working directory. */
|
|
9
|
+
dir: string;
|
|
10
|
+
/** True when the command should act on the deployed database. */
|
|
11
|
+
remote: boolean;
|
|
12
|
+
/** Set when the arguments cannot be honoured. The CLI prints it and exits 1. */
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const USAGE = "we8-cloudflare - the Cloudflare pack for a we8 CMS project\n\nUsage:\n we8-cloudflare doctor [--remote] [--dir <path>]\n Check the project's configuration and print what is missing, with the\n command that fixes each thing. --remote also checks the deployed\n Worker's secrets.\n\n we8-cloudflare migrate [--remote] [--dir <path>]\n Apply the @we8/cms migrations in order. Local by default; --remote\n applies them to the deployed D1 database.\n\n we8-cloudflare seed [--dir <path>]\n Load the development seed into the LOCAL database. It contains plaintext\n API keys, so this command has no remote mode and refuses to be given one.\n\n we8-cloudflare help | --version\n";
|
|
16
|
+
/**
|
|
17
|
+
* Parses argv (without the node and script entries). Unknown flags are an
|
|
18
|
+
* error rather than a silent no-op, because a typo in `--remote` on a migrate
|
|
19
|
+
* would otherwise quietly do the wrong half of the job.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseCliArgs(argv: readonly string[], cwd: string): ParsedCommand;
|
|
22
|
+
//# sourceMappingURL=cli-args.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-args.d.ts","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAE7E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,CAAC;IACrB,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,MAAM,EAAE,OAAO,CAAC;IAChB,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,KAAK,2sBAiBjB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,CAoDhF"}
|
package/dist/cli-args.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument parsing for `we8-cloudflare`, kept apart from the CLI that spawns
|
|
3
|
+
* wrangler so the shapes below can be tested without a subprocess.
|
|
4
|
+
*/
|
|
5
|
+
import { seedTargetsRemote } from './doctor.js';
|
|
6
|
+
export const USAGE = `we8-cloudflare - the Cloudflare pack for a we8 CMS project
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
we8-cloudflare doctor [--remote] [--dir <path>]
|
|
10
|
+
Check the project's configuration and print what is missing, with the
|
|
11
|
+
command that fixes each thing. --remote also checks the deployed
|
|
12
|
+
Worker's secrets.
|
|
13
|
+
|
|
14
|
+
we8-cloudflare migrate [--remote] [--dir <path>]
|
|
15
|
+
Apply the @we8/cms migrations in order. Local by default; --remote
|
|
16
|
+
applies them to the deployed D1 database.
|
|
17
|
+
|
|
18
|
+
we8-cloudflare seed [--dir <path>]
|
|
19
|
+
Load the development seed into the LOCAL database. It contains plaintext
|
|
20
|
+
API keys, so this command has no remote mode and refuses to be given one.
|
|
21
|
+
|
|
22
|
+
we8-cloudflare help | --version
|
|
23
|
+
`;
|
|
24
|
+
/**
|
|
25
|
+
* Parses argv (without the node and script entries). Unknown flags are an
|
|
26
|
+
* error rather than a silent no-op, because a typo in `--remote` on a migrate
|
|
27
|
+
* would otherwise quietly do the wrong half of the job.
|
|
28
|
+
*/
|
|
29
|
+
export function parseCliArgs(argv, cwd) {
|
|
30
|
+
const base = { command: 'help', dir: cwd, remote: false };
|
|
31
|
+
const [first, ...rest] = argv;
|
|
32
|
+
if (first === undefined || first === 'help' || first === '--help' || first === '-h') {
|
|
33
|
+
return base;
|
|
34
|
+
}
|
|
35
|
+
if (first === '--version' || first === '-v' || first === 'version') {
|
|
36
|
+
return { ...base, command: 'version' };
|
|
37
|
+
}
|
|
38
|
+
if (first !== 'doctor' && first !== 'migrate' && first !== 'seed') {
|
|
39
|
+
return { ...base, error: `unknown command "${first}"` };
|
|
40
|
+
}
|
|
41
|
+
const parsed = { ...base, command: first };
|
|
42
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
43
|
+
const arg = rest[i];
|
|
44
|
+
if (arg === '--remote') {
|
|
45
|
+
parsed.remote = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (arg === '--local') {
|
|
49
|
+
parsed.remote = false;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (arg === '--dir') {
|
|
53
|
+
const value = rest[i + 1];
|
|
54
|
+
if (value === undefined)
|
|
55
|
+
return { ...parsed, error: '--dir needs a path' };
|
|
56
|
+
parsed.dir = value;
|
|
57
|
+
i += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (arg.startsWith('--dir=')) {
|
|
61
|
+
parsed.dir = arg.slice('--dir='.length);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
return { ...parsed, error: `unknown option "${arg}"` };
|
|
65
|
+
}
|
|
66
|
+
// The guardrail, stated once and enforced here: the development seed carries
|
|
67
|
+
// plaintext API keys, so it never goes anywhere but the local database.
|
|
68
|
+
if (parsed.command === 'seed' && (parsed.remote || seedTargetsRemote(rest))) {
|
|
69
|
+
return {
|
|
70
|
+
...parsed,
|
|
71
|
+
error: 'seed is local-only. seed/local.sql contains plaintext API keys, and loading it into a deployed database publishes them. To put real content into a deployed site, use the admin API.',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=cli-args.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-args.js","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAchD,MAAM,CAAC,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;CAiBpB,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,IAAuB,EAAE,GAAW;IAC/D,MAAM,IAAI,GAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAEzE,MAAM,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAE9B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACpF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACnE,OAAO,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IACzC,CAAC;IACD,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;QAClE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,oBAAoB,KAAK,GAAG,EAAE,CAAC;IAC1D,CAAC;IAED,MAAM,MAAM,GAAkB,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAE1D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAW,CAAC;QAC9B,IAAI,GAAG,KAAK,UAAU,EAAE,CAAC;YACvB,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC;YACrB,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC;YACtB,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1B,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;YAC3E,MAAM,CAAC,GAAG,GAAG,KAAK,CAAC;YACnB,CAAC,IAAI,CAAC,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7B,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACxC,SAAS;QACX,CAAC;QACD,OAAO,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,GAAG,EAAE,CAAC;IACzD,CAAC;IAED,6EAA6E;IAC7E,wEAAwE;IACxE,IAAI,MAAM,CAAC,OAAO,KAAK,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAC5E,OAAO;YACL,GAAG,MAAM;YACT,KAAK,EACH,sLAAsL;SACzL,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `we8-cloudflare`: the three commands a scaffolded project needs that are not
|
|
4
|
+
* just wrangler. Each one gathers facts, checks them, and only then shells out
|
|
5
|
+
* to wrangler, so the failure a stranger sees is a sentence about their
|
|
6
|
+
* project rather than a stack trace from a tool they have not met yet.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Pulls secret names out of `wrangler secret list`. It has printed a JSON
|
|
10
|
+
* array for a long time, but the fallback matters: a table instead would
|
|
11
|
+
* otherwise turn a real answer into an unverified one every time.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseSecretList(stdout: string): string[] | null;
|
|
14
|
+
export declare function main(argv?: readonly string[], cwd?: string): number;
|
|
15
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAwCH;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAe/D;AA0CD,wBAAgB,IAAI,CAAC,IAAI,GAAE,SAAS,MAAM,EAA0B,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CA0BzG"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `we8-cloudflare`: the three commands a scaffolded project needs that are not
|
|
4
|
+
* just wrangler. Each one gathers facts, checks them, and only then shells out
|
|
5
|
+
* to wrangler, so the failure a stranger sees is a sentence about their
|
|
6
|
+
* project rather than a stack trace from a tool they have not met yet.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
import { parseCliArgs, USAGE } from './cli-args.js';
|
|
11
|
+
import { formatDoctorReport, runDoctorChecks } from './doctor.js';
|
|
12
|
+
import { findSeedFile, gatherFacts, SEED_RELATIVE_PATH } from './project.js';
|
|
13
|
+
const VERSION = '0.1.0';
|
|
14
|
+
/** Runs wrangler in the project directory, inheriting stdio so its output is the user's. */
|
|
15
|
+
function runWrangler(dir, args) {
|
|
16
|
+
console.log(`> wrangler ${args.join(' ')}`);
|
|
17
|
+
const result = spawnSync('npx', ['--no-install', 'wrangler', ...args], {
|
|
18
|
+
cwd: dir,
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
shell: process.platform === 'win32',
|
|
21
|
+
});
|
|
22
|
+
if (result.error) {
|
|
23
|
+
console.error(`could not run wrangler: ${result.error.message}`);
|
|
24
|
+
console.error('fix: npm install');
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
return result.status ?? 1;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Reads the deployed Worker's secret names. Returns null when wrangler cannot
|
|
31
|
+
* answer, which the doctor reports as unverified rather than as missing.
|
|
32
|
+
*/
|
|
33
|
+
function readRemoteSecretNames(dir) {
|
|
34
|
+
const result = spawnSync('npx', ['--no-install', 'wrangler', 'secret', 'list'], {
|
|
35
|
+
cwd: dir,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
});
|
|
38
|
+
if (result.status !== 0 || typeof result.stdout !== 'string')
|
|
39
|
+
return null;
|
|
40
|
+
return parseSecretList(result.stdout);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Pulls secret names out of `wrangler secret list`. It has printed a JSON
|
|
44
|
+
* array for a long time, but the fallback matters: a table instead would
|
|
45
|
+
* otherwise turn a real answer into an unverified one every time.
|
|
46
|
+
*/
|
|
47
|
+
export function parseSecretList(stdout) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(stdout);
|
|
50
|
+
if (Array.isArray(parsed)) {
|
|
51
|
+
return parsed
|
|
52
|
+
.map((entry) => typeof entry === 'object' && entry !== null ? entry.name : null)
|
|
53
|
+
.filter((name) => typeof name === 'string');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Not JSON. Fall through to the text scan.
|
|
58
|
+
}
|
|
59
|
+
const names = stdout.match(/\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g);
|
|
60
|
+
return names ? [...new Set(names)] : null;
|
|
61
|
+
}
|
|
62
|
+
function doctor(dir, remote) {
|
|
63
|
+
const secrets = remote ? readRemoteSecretNames(dir) : null;
|
|
64
|
+
const report = runDoctorChecks(gatherFacts(dir, remote ? 'remote' : 'local', secrets));
|
|
65
|
+
process.stdout.write(formatDoctorReport(report));
|
|
66
|
+
return report.ok ? 0 : 1;
|
|
67
|
+
}
|
|
68
|
+
function migrate(dir, remote) {
|
|
69
|
+
// The same checks the doctor runs, narrowed to the ones a migration needs.
|
|
70
|
+
// Applying migrations against a placeholder database id fails inside
|
|
71
|
+
// wrangler with a message about an id, not about what to do next.
|
|
72
|
+
const facts = gatherFacts(dir, remote ? 'remote' : 'local');
|
|
73
|
+
const report = runDoctorChecks(facts);
|
|
74
|
+
const blockers = report.checks.filter((c) => c.status === 'fail' && (c.id === 'config' || c.id === 'migrations' || (remote && c.id === 'd1')));
|
|
75
|
+
if (blockers.length > 0) {
|
|
76
|
+
console.error('Cannot apply migrations yet:');
|
|
77
|
+
for (const blocker of blockers) {
|
|
78
|
+
console.error(` ${blocker.title}: ${blocker.detail}`);
|
|
79
|
+
if (blocker.fix)
|
|
80
|
+
console.error(` fix: ${blocker.fix}`);
|
|
81
|
+
}
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
return runWrangler(dir, ['d1', 'migrations', 'apply', 'DB', remote ? '--remote' : '--local']);
|
|
85
|
+
}
|
|
86
|
+
function seed(dir) {
|
|
87
|
+
const file = findSeedFile(dir);
|
|
88
|
+
if (!file) {
|
|
89
|
+
console.error(`no seed file at ${SEED_RELATIVE_PATH}`);
|
|
90
|
+
console.error('fix: npm install (the seed ships inside the @we8/cms package)');
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
console.log('Loading the development seed into the LOCAL database only.');
|
|
94
|
+
console.log('It contains plaintext API keys; never load it into a deployed database.');
|
|
95
|
+
return runWrangler(dir, ['d1', 'execute', 'DB', '--local', '--file', file]);
|
|
96
|
+
}
|
|
97
|
+
export function main(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
98
|
+
const parsed = parseCliArgs(argv, cwd);
|
|
99
|
+
if (parsed.error) {
|
|
100
|
+
console.error(parsed.error);
|
|
101
|
+
console.error('');
|
|
102
|
+
process.stdout.write(USAGE);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
const dir = resolve(parsed.dir);
|
|
106
|
+
switch (parsed.command) {
|
|
107
|
+
case 'help':
|
|
108
|
+
process.stdout.write(USAGE);
|
|
109
|
+
return 0;
|
|
110
|
+
case 'version':
|
|
111
|
+
console.log(VERSION);
|
|
112
|
+
return 0;
|
|
113
|
+
case 'doctor':
|
|
114
|
+
return doctor(dir, parsed.remote);
|
|
115
|
+
case 'migrate':
|
|
116
|
+
return migrate(dir, parsed.remote);
|
|
117
|
+
case 'seed':
|
|
118
|
+
return seed(dir);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
process.exitCode = main();
|
|
122
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The doctor: what is missing, and the exact command that fixes it.
|
|
3
|
+
*
|
|
4
|
+
* The checks are a pure function of gathered facts so they can be tested
|
|
5
|
+
* without a Cloudflare account, a filesystem, or a network. `src/cli.ts` does
|
|
6
|
+
* the gathering; everything below only reasons.
|
|
7
|
+
*
|
|
8
|
+
* Every failure carries a `fix` that can be pasted into a terminal. A first
|
|
9
|
+
* boot fails for about six reasons and all six of them have a one-line answer;
|
|
10
|
+
* printing the reason without the answer just moves the search to a browser.
|
|
11
|
+
*/
|
|
12
|
+
/** Where the project is headed. Some checks only matter for a real deployment. */
|
|
13
|
+
export type DoctorTarget = 'local' | 'remote';
|
|
14
|
+
export type CheckStatus = 'pass' | 'fail' | 'warn';
|
|
15
|
+
export interface DoctorCheck {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
status: CheckStatus;
|
|
19
|
+
detail: string;
|
|
20
|
+
/** A command or edit that resolves a fail or a warn. */
|
|
21
|
+
fix?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface DoctorReport {
|
|
24
|
+
target: DoctorTarget;
|
|
25
|
+
ok: boolean;
|
|
26
|
+
checks: DoctorCheck[];
|
|
27
|
+
}
|
|
28
|
+
/** The facts the CLI gathers from a project directory. */
|
|
29
|
+
export interface DoctorFacts {
|
|
30
|
+
target: DoctorTarget;
|
|
31
|
+
/** The parsed wrangler config, or null when the file is missing or unreadable. */
|
|
32
|
+
config: unknown;
|
|
33
|
+
/** Why the config could not be read, when it could not. */
|
|
34
|
+
configError?: string | undefined;
|
|
35
|
+
/** Variable names present in `.dev.vars`, or null when the file is absent. */
|
|
36
|
+
devVars: string[] | null;
|
|
37
|
+
/**
|
|
38
|
+
* Secret names the deployed Worker has, or null when they were not looked up
|
|
39
|
+
* (no account, or a local run). Absent knowledge is reported as unknown
|
|
40
|
+
* rather than guessed at.
|
|
41
|
+
*/
|
|
42
|
+
remoteSecrets: string[] | null;
|
|
43
|
+
/** Migration filenames found in the configured migrations_dir, or null when it is missing. */
|
|
44
|
+
migrations: string[] | null;
|
|
45
|
+
/** Scripts declared by the project's package.json. */
|
|
46
|
+
packageScripts: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
interface D1Binding {
|
|
49
|
+
binding?: unknown;
|
|
50
|
+
database_name?: unknown;
|
|
51
|
+
database_id?: unknown;
|
|
52
|
+
migrations_dir?: unknown;
|
|
53
|
+
}
|
|
54
|
+
interface R2Binding {
|
|
55
|
+
binding?: unknown;
|
|
56
|
+
bucket_name?: unknown;
|
|
57
|
+
}
|
|
58
|
+
/** Pulls the D1 binding named DB out of a config, or null when there is none. */
|
|
59
|
+
export declare function findD1Binding(config: unknown): D1Binding | null;
|
|
60
|
+
/** Pulls the R2 binding named MEDIA out of a config, or null when there is none. */
|
|
61
|
+
export declare function findR2Binding(config: unknown): R2Binding | null;
|
|
62
|
+
/** The migrations_dir the project configured, or the empty string when it has none. */
|
|
63
|
+
export declare function readMigrationsDir(config: unknown): string;
|
|
64
|
+
/**
|
|
65
|
+
* Migrations apply in filename order, so the order has to be a real sequence.
|
|
66
|
+
* A gap or a duplicate number is a merge that went wrong, and wrangler will
|
|
67
|
+
* happily apply it anyway.
|
|
68
|
+
*/
|
|
69
|
+
export declare function checkMigrationSequence(files: string[]): {
|
|
70
|
+
ok: boolean;
|
|
71
|
+
detail: string;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* The one guardrail that matters more than the rest: `seed/local.sql` carries
|
|
75
|
+
* plaintext API keys, and running it against a deployed database publishes
|
|
76
|
+
* them. Any seed invocation that is not explicitly local is refused.
|
|
77
|
+
*/
|
|
78
|
+
export declare function seedTargetsRemote(args: readonly string[]): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Runs every check against gathered facts. `ok` is false when anything failed;
|
|
81
|
+
* a warn is something to know about, not something to stop for.
|
|
82
|
+
*/
|
|
83
|
+
export declare function runDoctorChecks(facts: DoctorFacts): DoctorReport;
|
|
84
|
+
/** Renders a report for a terminal. Plain text, no colour, no symbols. */
|
|
85
|
+
export declare function formatDoctorReport(report: DoctorReport): string;
|
|
86
|
+
export {};
|
|
87
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,kFAAkF;AAClF,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE9C,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,YAAY,CAAC;IACrB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,0DAA0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,YAAY,CAAC;IACrB,kFAAkF;IAClF,MAAM,EAAE,OAAO,CAAC;IAChB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,8EAA8E;IAC9E,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,aAAa,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC/B,8FAA8F;IAC9F,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC5B,sDAAsD;IACtD,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,UAAU,SAAS;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,UAAU,SAAS;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAsBD,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAQ/D;AAED,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAQ/D;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAGzD;AAID;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAqBvF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAElE;AAYD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,YAAY,CAqQhE;AAID,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAuB/D"}
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The doctor: what is missing, and the exact command that fixes it.
|
|
3
|
+
*
|
|
4
|
+
* The checks are a pure function of gathered facts so they can be tested
|
|
5
|
+
* without a Cloudflare account, a filesystem, or a network. `src/cli.ts` does
|
|
6
|
+
* the gathering; everything below only reasons.
|
|
7
|
+
*
|
|
8
|
+
* Every failure carries a `fix` that can be pasted into a terminal. A first
|
|
9
|
+
* boot fails for about six reasons and all six of them have a one-line answer;
|
|
10
|
+
* printing the reason without the answer just moves the search to a browser.
|
|
11
|
+
*/
|
|
12
|
+
import { inspectEmailConfig } from './email.js';
|
|
13
|
+
import { D1_ID_PLACEHOLDER, isRealDatabaseId } from './wrangler-config.js';
|
|
14
|
+
function asRecord(value) {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
16
|
+
? value
|
|
17
|
+
: null;
|
|
18
|
+
}
|
|
19
|
+
function asArray(value) {
|
|
20
|
+
return Array.isArray(value) ? value : [];
|
|
21
|
+
}
|
|
22
|
+
/** Pulls the D1 binding named DB out of a config, or null when there is none. */
|
|
23
|
+
export function findD1Binding(config) {
|
|
24
|
+
const shape = asRecord(config);
|
|
25
|
+
if (!shape)
|
|
26
|
+
return null;
|
|
27
|
+
for (const entry of asArray(shape.d1_databases)) {
|
|
28
|
+
const record = asRecord(entry);
|
|
29
|
+
if (record && record['binding'] === 'DB')
|
|
30
|
+
return record;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** Pulls the R2 binding named MEDIA out of a config, or null when there is none. */
|
|
35
|
+
export function findR2Binding(config) {
|
|
36
|
+
const shape = asRecord(config);
|
|
37
|
+
if (!shape)
|
|
38
|
+
return null;
|
|
39
|
+
for (const entry of asArray(shape.r2_buckets)) {
|
|
40
|
+
const record = asRecord(entry);
|
|
41
|
+
if (record && record['binding'] === 'MEDIA')
|
|
42
|
+
return record;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** The migrations_dir the project configured, or the empty string when it has none. */
|
|
47
|
+
export function readMigrationsDir(config) {
|
|
48
|
+
const binding = findD1Binding(config);
|
|
49
|
+
return typeof binding?.migrations_dir === 'string' ? binding.migrations_dir : '';
|
|
50
|
+
}
|
|
51
|
+
const MIGRATION_PATTERN = /^(\d{4})_[a-z0-9_]+\.sql$/;
|
|
52
|
+
/**
|
|
53
|
+
* Migrations apply in filename order, so the order has to be a real sequence.
|
|
54
|
+
* A gap or a duplicate number is a merge that went wrong, and wrangler will
|
|
55
|
+
* happily apply it anyway.
|
|
56
|
+
*/
|
|
57
|
+
export function checkMigrationSequence(files) {
|
|
58
|
+
const sql = files.filter((name) => name.endsWith('.sql')).sort();
|
|
59
|
+
if (sql.length === 0)
|
|
60
|
+
return { ok: false, detail: 'no .sql migrations found' };
|
|
61
|
+
const badly = sql.filter((name) => !MIGRATION_PATTERN.test(name));
|
|
62
|
+
if (badly.length > 0) {
|
|
63
|
+
return { ok: false, detail: `not numbered NNNN_name.sql: ${badly.join(', ')}` };
|
|
64
|
+
}
|
|
65
|
+
const numbers = sql.map((name) => Number(MIGRATION_PATTERN.exec(name)[1]));
|
|
66
|
+
for (let i = 0; i < numbers.length; i += 1) {
|
|
67
|
+
const expected = i + 1;
|
|
68
|
+
if (numbers[i] !== expected) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
detail: `sequence breaks at ${sql[i]}: expected ${String(expected).padStart(4, '0')}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { ok: true, detail: `${String(sql.length)} migrations, 0001 to ${sql[sql.length - 1]}` };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The one guardrail that matters more than the rest: `seed/local.sql` carries
|
|
79
|
+
* plaintext API keys, and running it against a deployed database publishes
|
|
80
|
+
* them. Any seed invocation that is not explicitly local is refused.
|
|
81
|
+
*/
|
|
82
|
+
export function seedTargetsRemote(args) {
|
|
83
|
+
return args.some((arg) => arg === '--remote' || arg === '--preview' || arg.startsWith('--env'));
|
|
84
|
+
}
|
|
85
|
+
function check(id, title, status, detail, fix) {
|
|
86
|
+
return fix === undefined ? { id, title, status, detail } : { id, title, status, detail, fix };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Runs every check against gathered facts. `ok` is false when anything failed;
|
|
90
|
+
* a warn is something to know about, not something to stop for.
|
|
91
|
+
*/
|
|
92
|
+
export function runDoctorChecks(facts) {
|
|
93
|
+
const checks = [];
|
|
94
|
+
const remote = facts.target === 'remote';
|
|
95
|
+
// ---- the config file ---------------------------------------------------
|
|
96
|
+
if (facts.config === null || facts.config === undefined) {
|
|
97
|
+
checks.push(check('config', 'wrangler.jsonc', 'fail', facts.configError ?? 'not found in this directory', 'Run this from the project root, or scaffold one with: npm create we8'));
|
|
98
|
+
return { target: facts.target, ok: false, checks };
|
|
99
|
+
}
|
|
100
|
+
const config = asRecord(facts.config);
|
|
101
|
+
checks.push(check('config', 'wrangler.jsonc', 'pass', 'found and parsed'));
|
|
102
|
+
// ---- the worker itself -------------------------------------------------
|
|
103
|
+
checks.push(typeof config.main === 'string' && config.main.length > 0
|
|
104
|
+
? check('main', 'worker entry point', 'pass', String(config.main))
|
|
105
|
+
: check('main', 'worker entry point', 'fail', 'no "main" in wrangler.jsonc', 'Set "main": "src/index.ts"'));
|
|
106
|
+
const flags = asArray(config.compatibility_flags).filter((f) => typeof f === 'string');
|
|
107
|
+
checks.push(flags.includes('nodejs_compat')
|
|
108
|
+
? check('nodejs_compat', 'nodejs_compat flag', 'pass', 'enabled')
|
|
109
|
+
: check('nodejs_compat', 'nodejs_compat flag', 'fail', 'the CMS and better-auth need the Node built-ins', 'Add "compatibility_flags": ["nodejs_compat"] to wrangler.jsonc'));
|
|
110
|
+
// ---- bindings ----------------------------------------------------------
|
|
111
|
+
const d1 = findD1Binding(facts.config);
|
|
112
|
+
const databaseName = typeof d1?.database_name === 'string' ? d1.database_name : 'we8-cms';
|
|
113
|
+
if (!d1) {
|
|
114
|
+
checks.push(check('d1', 'D1 binding DB', 'fail', 'no d1_databases entry bound as DB', `wrangler d1 create ${databaseName} then add the binding to wrangler.jsonc`));
|
|
115
|
+
}
|
|
116
|
+
else if (!isRealDatabaseId(typeof d1.database_id === 'string' ? d1.database_id : undefined)) {
|
|
117
|
+
const current = typeof d1.database_id === 'string' ? d1.database_id : '(missing)';
|
|
118
|
+
const what = current === D1_ID_PLACEHOLDER ? 'still the placeholder' : `not a D1 id: ${current}`;
|
|
119
|
+
checks.push(
|
|
120
|
+
// Local development keys its database off the binding name and never
|
|
121
|
+
// reads the id, so a placeholder is a warning here and a wall on the way
|
|
122
|
+
// to a deployment. Failing it locally would make the doctor cry wolf on
|
|
123
|
+
// every fresh project.
|
|
124
|
+
remote
|
|
125
|
+
? check('d1', 'D1 binding DB', 'fail', `database_id is ${what}`, `wrangler d1 create ${databaseName} then paste the uuid it prints into d1_databases[0].database_id`)
|
|
126
|
+
: check('d1', 'D1 binding DB', 'warn', `database_id is ${what}; local development works, a deploy will not`, `wrangler d1 create ${databaseName} then paste the uuid it prints into d1_databases[0].database_id`));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
checks.push(check('d1', 'D1 binding DB', 'pass', `${databaseName} (${String(d1.database_id)})`));
|
|
130
|
+
}
|
|
131
|
+
const r2 = findR2Binding(facts.config);
|
|
132
|
+
checks.push(r2 && typeof r2.bucket_name === 'string' && r2.bucket_name.length > 0
|
|
133
|
+
? check('r2', 'R2 binding MEDIA', 'pass', r2.bucket_name)
|
|
134
|
+
: check('r2', 'R2 binding MEDIA', 'fail', 'no r2_buckets entry bound as MEDIA with a bucket_name', 'wrangler r2 bucket create we8-cms-media then add the binding to wrangler.jsonc'));
|
|
135
|
+
// ---- vars --------------------------------------------------------------
|
|
136
|
+
const vars = asRecord(config.vars) ?? {};
|
|
137
|
+
const siteUrl = typeof vars['SITE_URL'] === 'string' ? vars['SITE_URL'] : '';
|
|
138
|
+
if (!siteUrl) {
|
|
139
|
+
checks.push(check('site_url', 'SITE_URL', 'fail', 'not set', 'Add "SITE_URL": "https://example.com" to vars in wrangler.jsonc'));
|
|
140
|
+
}
|
|
141
|
+
else if (remote && /^https?:\/\/(localhost|127\.0\.0\.1)/.test(siteUrl)) {
|
|
142
|
+
checks.push(check('site_url', 'SITE_URL', 'fail', `a deployed site cannot advertise ${siteUrl}: sitemap, llms.txt, and JSON-LD are all built from it`, 'Set vars.SITE_URL in wrangler.jsonc to the public origin'));
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
checks.push(check('site_url', 'SITE_URL', 'pass', siteUrl));
|
|
146
|
+
}
|
|
147
|
+
checks.push(typeof vars['CMS_VERSION'] === 'string' && vars['CMS_VERSION'].length > 0
|
|
148
|
+
? check('cms_version', 'CMS_VERSION', 'pass', vars['CMS_VERSION'])
|
|
149
|
+
: check('cms_version', 'CMS_VERSION', 'warn', 'not set, so /v1/health reports nothing useful', 'Add "CMS_VERSION": "0.1.0" to vars in wrangler.jsonc'));
|
|
150
|
+
// ---- the secret everything depends on ----------------------------------
|
|
151
|
+
if (remote) {
|
|
152
|
+
if (facts.remoteSecrets === null) {
|
|
153
|
+
checks.push(check('better_auth_secret', 'BETTER_AUTH_SECRET', 'warn', 'could not read the deployed secrets, so this is unverified', 'wrangler secret list to check, or set it with: wrangler secret put BETTER_AUTH_SECRET'));
|
|
154
|
+
}
|
|
155
|
+
else if (facts.remoteSecrets.includes('BETTER_AUTH_SECRET')) {
|
|
156
|
+
checks.push(check('better_auth_secret', 'BETTER_AUTH_SECRET', 'pass', 'set on the deployed Worker'));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
checks.push(check('better_auth_secret', 'BETTER_AUTH_SECRET', 'fail', 'not set: the admin API refuses every request without it', 'openssl rand -hex 32 then: wrangler secret put BETTER_AUTH_SECRET'));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (facts.devVars === null) {
|
|
163
|
+
checks.push(check('better_auth_secret', 'BETTER_AUTH_SECRET', 'fail', 'no .dev.vars file, so local sign-in cannot work', 'cp .dev.vars.example .dev.vars then put a value from: openssl rand -hex 32'));
|
|
164
|
+
}
|
|
165
|
+
else if (facts.devVars.includes('BETTER_AUTH_SECRET')) {
|
|
166
|
+
checks.push(check('better_auth_secret', 'BETTER_AUTH_SECRET', 'pass', 'set in .dev.vars'));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
checks.push(check('better_auth_secret', 'BETTER_AUTH_SECRET', 'fail', '.dev.vars exists but does not set it', 'openssl rand -hex 32 then add BETTER_AUTH_SECRET=<value> to .dev.vars'));
|
|
170
|
+
}
|
|
171
|
+
// ---- migrations --------------------------------------------------------
|
|
172
|
+
const migrationsDir = readMigrationsDir(facts.config);
|
|
173
|
+
if (!migrationsDir) {
|
|
174
|
+
checks.push(check('migrations', 'migrations', 'fail', 'the DB binding has no migrations_dir', 'Add "migrations_dir": "node_modules/@we8/cms/migrations" to the DB binding'));
|
|
175
|
+
}
|
|
176
|
+
else if (facts.migrations === null) {
|
|
177
|
+
checks.push(check('migrations', 'migrations', 'fail', `${migrationsDir} does not exist`, 'npm install (the migrations ship inside the @we8/cms package)'));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const sequence = checkMigrationSequence(facts.migrations);
|
|
181
|
+
checks.push(sequence.ok
|
|
182
|
+
? check('migrations', 'migrations', 'pass', `${migrationsDir}: ${sequence.detail}`)
|
|
183
|
+
: check('migrations', 'migrations', 'fail', `${migrationsDir}: ${sequence.detail}`, 'Restore the migrations shipped with @we8/cms'));
|
|
184
|
+
}
|
|
185
|
+
// ---- email -------------------------------------------------------------
|
|
186
|
+
const emailMode = typeof vars['EMAIL_MODE'] === 'string' ? vars['EMAIL_MODE'] : undefined;
|
|
187
|
+
const sendEmailNames = asArray(config.send_email)
|
|
188
|
+
.map((entry) => asRecord(entry)?.['name'])
|
|
189
|
+
.filter((name) => typeof name === 'string');
|
|
190
|
+
const emailVerdict = inspectEmailConfig({
|
|
191
|
+
EMAIL_MODE: emailMode,
|
|
192
|
+
EMAIL_FROM: typeof vars['EMAIL_FROM'] === 'string' ? vars['EMAIL_FROM'] : undefined,
|
|
193
|
+
// The binding is present in the config rather than at runtime here, so a
|
|
194
|
+
// stub stands in for it: the doctor is checking configuration, not sending.
|
|
195
|
+
EMAIL: sendEmailNames.includes('EMAIL') ? { send: () => Promise.resolve({ messageId: '' }) } : undefined,
|
|
196
|
+
});
|
|
197
|
+
if (emailMode === undefined || emailMode === 'console') {
|
|
198
|
+
checks.push(check('email', 'email', 'pass', emailMode === 'console' ? 'console mode: mail is logged, not sent' : 'off: mail is accepted and dropped'));
|
|
199
|
+
}
|
|
200
|
+
else if (emailVerdict.enabled) {
|
|
201
|
+
checks.push(check('email', 'email', 'pass', `Cloudflare Email Service, ${emailVerdict.reason}`));
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
checks.push(check('email', 'email', 'fail', `EMAIL_MODE is "${emailMode}" but ${emailVerdict.reason}`, 'Add "send_email": [{ "name": "EMAIL", "remote": true }] and vars.EMAIL_FROM, on a domain onboarded to Email Service'));
|
|
205
|
+
}
|
|
206
|
+
// ---- the seed guardrail ------------------------------------------------
|
|
207
|
+
const seedScript = facts.packageScripts['db:seed'];
|
|
208
|
+
if (seedScript && seedTargetsRemote(seedScript.split(/\s+/))) {
|
|
209
|
+
checks.push(check('seed_guardrail', 'seed guardrail', 'fail', 'db:seed targets something other than the local database, and the seed contains plaintext API keys', 'Change db:seed back to: we8-cloudflare seed'));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
checks.push(check('seed_guardrail', 'seed guardrail', 'pass', 'the development seed is local-only'));
|
|
213
|
+
}
|
|
214
|
+
return { target: facts.target, ok: !checks.some((c) => c.status === 'fail'), checks };
|
|
215
|
+
}
|
|
216
|
+
const LABEL = { pass: 'PASS', fail: 'FAIL', warn: 'WARN' };
|
|
217
|
+
/** Renders a report for a terminal. Plain text, no colour, no symbols. */
|
|
218
|
+
export function formatDoctorReport(report) {
|
|
219
|
+
const lines = [];
|
|
220
|
+
lines.push(`we8 doctor (${report.target})`);
|
|
221
|
+
lines.push('');
|
|
222
|
+
for (const item of report.checks) {
|
|
223
|
+
lines.push(` ${LABEL[item.status]} ${item.title}: ${item.detail}`);
|
|
224
|
+
if (item.fix && item.status !== 'pass') {
|
|
225
|
+
lines.push(` fix: ${item.fix}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const failures = report.checks.filter((c) => c.status === 'fail').length;
|
|
229
|
+
const warnings = report.checks.filter((c) => c.status === 'warn').length;
|
|
230
|
+
lines.push('');
|
|
231
|
+
lines.push(report.ok
|
|
232
|
+
? `Ready. ${String(report.checks.length)} checks, ${String(warnings)} warnings.`
|
|
233
|
+
: `Not ready. ${String(failures)} of ${String(report.checks.length)} checks failed.`);
|
|
234
|
+
return `${lines.join('\n')}\n`;
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=doctor.js.map
|