@clubedge/create-clubedge-app 0.14.1 → 0.16.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/CHANGELOG.md +18 -0
- package/README.md +32 -14
- package/dist/cli/args.js +29 -11
- package/dist/cli/ui.js +19 -8
- package/dist/core/env.js +4 -0
- package/dist/core/health.js +161 -0
- package/dist/core/merge.js +8 -0
- package/dist/core/releases.js +59 -0
- package/dist/core/source.js +3 -1
- package/dist/index.js +9 -0
- package/dist/inspect.js +125 -0
- package/dist/modify.js +49 -28
- package/package.json +3 -3
- package/template/.env.example +6 -0
- package/template/README.md +10 -4
- package/template/SETUP.md +14 -3
- package/template/apps/start/.env.example +6 -0
- package/template/apps/start/AGENTS.md +1 -1
- package/template/apps/start/package.json +1 -1
- package/template/apps/start/src/components/auth/auth-form-controls.tsx +27 -8
- package/template/apps/start/src/components/dashboard/account-button.tsx +20 -14
- package/template/apps/start/src/components/error-reporting.tsx +49 -0
- package/template/apps/start/src/components/route-error.tsx +35 -0
- package/template/apps/start/src/components/variants/error-reporting.sentry.tsx +34 -0
- package/template/apps/start/src/env/auth.ts +9 -1
- package/template/apps/start/src/jobs/index.ts +24 -0
- package/template/apps/start/src/routes/__root.tsx +8 -0
- package/template/apps/start/src/routes/api/client-errors.ts +49 -0
- package/template/apps/start/src/routes/auth/change-password.ts +12 -0
- package/template/apps/start/src/routes/auth/delete-account.ts +12 -0
- package/template/apps/start/src/routes/auth/update-profile.ts +12 -0
- package/template/apps/start/src/routes/dashboard.tsx +25 -19
- package/template/apps/start/src/routes/index.tsx +29 -26
- package/template/apps/start/src/routes/login.tsx +1 -0
- package/template/apps/start/src/routes/settings.tsx +156 -0
- package/template/apps/start/src/server/auth-email.ts +13 -2
- package/template/apps/start/src/server/observability.ts +6 -1
- package/template/apps/start/src/server/payments.ts +6 -1
- package/template/apps/start/src/server/settings-forms.ts +110 -0
- package/template/apps/start/src/server/settings.functions.ts +24 -0
- package/template/apps/start/src/server/variants/observability.sentry.ts +17 -1
- package/template/apps/start/vite.config.ts +10 -2
- package/template/apps/web/next.config.ts +22 -0
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/app/api/client-errors/route.ts +48 -0
- package/template/apps/web/src/app/dashboard/_components/account-button.tsx +20 -14
- package/template/apps/web/src/app/dashboard/page.tsx +25 -25
- package/template/apps/web/src/app/error.tsx +30 -0
- package/template/apps/web/src/app/layout.tsx +6 -0
- package/template/apps/web/src/app/login/_components/auth-form-controls.tsx +27 -8
- package/template/apps/web/src/app/login/page.tsx +1 -0
- package/template/apps/web/src/app/page.tsx +29 -26
- package/template/apps/web/src/app/settings/actions.ts +102 -0
- package/template/apps/web/src/app/settings/page.tsx +160 -0
- package/template/apps/web/src/components/error-reporting.tsx +51 -0
- package/template/apps/web/src/components/variants/error-reporting.sentry.tsx +36 -0
- package/template/apps/web/src/env/auth.ts +9 -1
- package/template/apps/web/src/jobs/index.ts +24 -0
- package/template/apps/web/src/server/auth-email.ts +13 -2
- package/template/apps/web/src/server/observability.ts +6 -1
- package/template/apps/web/src/server/payments.ts +6 -1
- package/template/apps/web/src/server/variants/observability.sentry.ts +17 -1
- package/template/clubedge.template.json +32 -2
- package/template/e2e/auth.spec.ts +1 -1
- package/template/e2e/email-verification.spec.ts +3 -1
- package/template/e2e/observability.spec.ts +48 -22
- package/template/e2e/password-reset.spec.ts +1 -1
- package/template/e2e/settings.spec.ts +78 -0
- package/template/e2e/telemetry-fake.ts +7 -0
- package/template/package.json +1 -1
- package/template/packages/auth/package.json +1 -1
- package/template/packages/auth/src/types.ts +19 -0
- package/template/packages/auth-better-auth/package.json +1 -1
- package/template/packages/auth-better-auth/src/index.test.ts +80 -1
- package/template/packages/auth-better-auth/src/index.ts +38 -0
- package/template/packages/auth-supabase/package.json +1 -1
- package/template/packages/auth-supabase/src/index.test.ts +86 -2
- package/template/packages/auth-supabase/src/index.ts +42 -2
- package/template/packages/cache/package.json +1 -1
- package/template/packages/cache-redis/package.json +1 -1
- package/template/packages/core/package.json +1 -1
- package/template/packages/core/src/index.ts +1 -0
- package/template/packages/core/src/request-body.test.ts +36 -0
- package/template/packages/core/src/request-body.ts +29 -0
- package/template/packages/db/package.json +2 -1
- package/template/packages/db/src/index.ts +7 -1
- package/template/packages/db/src/users.test.ts +39 -0
- package/template/packages/db/src/users.ts +27 -0
- package/template/packages/email/package.json +1 -1
- package/template/packages/email-resend/package.json +1 -1
- package/template/packages/email-smtp/package.json +1 -1
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs-pg-boss/package.json +1 -1
- package/template/packages/local-db/package.json +1 -1
- package/template/packages/observability/package.json +1 -1
- package/template/packages/observability/src/index.test.ts +67 -0
- package/template/packages/observability/src/index.ts +56 -0
- package/template/packages/observability-otel/package.json +1 -1
- package/template/packages/observability-sentry/package.json +8 -3
- package/template/packages/observability-sentry/src/browser.ts +36 -0
- package/template/packages/observability-sentry/src/sourcemaps.cjs +52 -0
- package/template/packages/observability-sentry/src/sourcemaps.d.cts +10 -0
- package/template/packages/observability-sentry/src/sourcemaps.test.ts +56 -0
- package/template/packages/observability-sentry/src/vite.d.mts +3 -0
- package/template/packages/observability-sentry/src/vite.mjs +21 -0
- package/template/packages/payments/package.json +1 -1
- package/template/packages/payments/src/billing.test.ts +2 -1
- package/template/packages/payments/src/billing.ts +15 -10
- package/template/packages/payments/src/index.ts +1 -0
- package/template/packages/payments-stripe/package.json +1 -1
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage-s3/package.json +1 -1
- package/template/packages/storage-supabase/package.json +1 -1
- package/template/packages/ui/package.json +1 -1
- package/template/playwright.config.ts +15 -2
- package/template/pnpm-lock.yaml +172 -0
- package/template.lock.json +29 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.16.0
|
|
6
|
+
|
|
7
|
+
- Default to Starter `v0.14.0`, which adds:
|
|
8
|
+
- An account settings page at `/settings` when an auth provider is selected: display name, password change (signing out other sessions), and account deletion. Supabase projects set `SUPABASE_SECRET_KEY` to allow deletion.
|
|
9
|
+
- Browser error reporting with observability on: uncaught errors, unhandled rejections, and render errors caught by the new error pages, through `/api/client-errors` (`otel`) or Sentry's browser SDK (`sentry`). Sentry builds with `SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, and `SENTRY_PROJECT` upload source maps and then delete them.
|
|
10
|
+
- Auth emails sent through the background-jobs queue, with retries, when both email and jobs are selected.
|
|
11
|
+
- None of this needs CLI code: it all comes from the Starter's manifest.
|
|
12
|
+
|
|
13
|
+
## 0.15.0
|
|
14
|
+
|
|
15
|
+
- Add `status`: a project's Starter release, framework, modules, and Git state, whether a newer Starter release exists (from npm, still working offline), and the command to run next.
|
|
16
|
+
- Add `doctor`: checks Node.js and pnpm against the project's versions, installed dependencies, the lockfile, and the env file against the example, then runs `pnpm check:services` (`--no-services` skips it). It exits with an error when a check fails.
|
|
17
|
+
- `add` and `remove` take several modules (`add email resend payments stripe`, `remove storage payments`) and apply them as one change, checking requirements after every change.
|
|
18
|
+
- After a change with conflicts, list each file with its number of conflict blocks.
|
|
19
|
+
- `upgrade` lists the Starter releases it crosses, with the sections of their release notes.
|
|
20
|
+
- Fix a finished command exiting with an error when Windows briefly locked the downloaded Starter's temporary folder.
|
|
21
|
+
- CI: test generated projects on macOS too; the release workflow waits up to 15 minutes for npm.
|
|
22
|
+
|
|
5
23
|
## 0.14.1
|
|
6
24
|
|
|
7
25
|
- Default to Starter `v0.13.1`. In projects with background jobs or observability, a configuration error such as a missing `DATABASE_URL` no longer stops the server at startup; pages that need no database keep working, and the error is reported once.
|
package/README.md
CHANGED
|
@@ -95,8 +95,8 @@ Interactive runs ask whether to customize these after the framework question.
|
|
|
95
95
|
```text
|
|
96
96
|
Usage:
|
|
97
97
|
pnpm dlx @clubedge/create-clubedge-app [project-directory] [options]
|
|
98
|
-
pnpm dlx @clubedge/create-clubedge-app add [module
|
|
99
|
-
pnpm dlx @clubedge/create-clubedge-app remove [module]
|
|
98
|
+
pnpm dlx @clubedge/create-clubedge-app add [module option]... (inside a project)
|
|
99
|
+
pnpm dlx @clubedge/create-clubedge-app remove [module]... (inside a project)
|
|
100
100
|
pnpm dlx @clubedge/create-clubedge-app upgrade [--ref <tag>] (inside a project)
|
|
101
101
|
|
|
102
102
|
Options:
|
|
@@ -127,7 +127,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
|
|
|
127
127
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
128
128
|
|
|
129
129
|
# Reproduce a specific Starter release or commit
|
|
130
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.
|
|
130
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.14.0
|
|
131
131
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
|
|
132
132
|
|
|
133
133
|
# Test local Starter changes before releasing them
|
|
@@ -136,15 +136,17 @@ pnpm dlx @clubedge/create-clubedge-app my-app --template-dir ../clubedge-starter
|
|
|
136
136
|
|
|
137
137
|
## Change an existing project
|
|
138
138
|
|
|
139
|
-
Inside a project created by the CLI, `add` and `remove` change
|
|
139
|
+
Inside a project created by the CLI, `add` and `remove` change modules, and `upgrade` moves the project to a newer Starter release. Projects from Starter v0.8.0 on have a `clubedge` script, so `pnpm clubedge add cache redis` works too. Without a module or option, `add` and `remove` ask for them:
|
|
140
140
|
|
|
141
141
|
```sh
|
|
142
142
|
pnpm clubedge add # choose a module and option from a list
|
|
143
143
|
pnpm dlx @clubedge/create-clubedge-app add cache redis # or switch: add auth better-auth
|
|
144
144
|
pnpm dlx @clubedge/create-clubedge-app remove storage
|
|
145
|
+
pnpm clubedge add email resend payments stripe # several modules as one change
|
|
146
|
+
pnpm clubedge remove storage payments
|
|
145
147
|
pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
|
|
146
148
|
pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
|
|
147
|
-
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.
|
|
149
|
+
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.14.0 # or a specific tag, branch, or commit
|
|
148
150
|
```
|
|
149
151
|
|
|
150
152
|
The CLI renders the project twice, from the Starter commit and modules recorded in your `package.json` and from the new state, and merges the difference into your project. The new state has other modules for `add` and `remove`, and the newer Starter with the same modules for `upgrade`. Modules a newer Starter introduces start as `none` where possible, so an upgrade never adds a service on its own.
|
|
@@ -155,34 +157,47 @@ The CLI renders the project twice, from the Starter commit and modules recorded
|
|
|
155
157
|
- `apps/web/.env.local` keeps your values. Values still at their generated defaults follow the new selection, and new variables are added.
|
|
156
158
|
- `pnpm-lock.yaml` is replaced if you never changed your dependencies; otherwise `pnpm install` reconciles it.
|
|
157
159
|
|
|
160
|
+
Several modules in one command are applied as one change, and their requirements are checked together, so `add auth supabase storage supabase` works even though Supabase Storage needs Supabase Auth. When lines conflict, the CLI lists each file with its number of conflict blocks. Before an upgrade, it lists the Starter releases it crosses with the sections of their release notes, such as `v0.13.0 Observability, Tests, Also`.
|
|
161
|
+
|
|
158
162
|
The project must be a Git repository without uncommitted changes, so you can review the result with `git diff` and undo it. Pass `--force` to skip this check. Add `--no-install` to skip `pnpm install`. Afterward, run `pnpm db:migrate` and `pnpm check:services` if the change added tables or services.
|
|
159
163
|
|
|
164
|
+
## Check a project
|
|
165
|
+
|
|
166
|
+
Two read-only commands help you keep a project healthy:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
pnpm clubedge status # Starter release, modules, Git state, and whether a newer Starter exists
|
|
170
|
+
pnpm clubedge doctor # Node.js, pnpm, dependencies, lockfile, env file, and pnpm check:services
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`status` ends with the command to run next, such as `pnpm clubedge upgrade` when a newer Starter release is available; it asks npm for the newest CLI and still works offline. `doctor` checks that Node.js and pnpm meet the project's versions, that dependencies are installed, that `pnpm-lock.yaml` matches every `package.json`, and that `apps/web/.env.local` has every variable in `.env.example`, then runs `pnpm check:services` (skip it with `--no-services`). It exits with an error when a check fails, so it also works in CI.
|
|
174
|
+
|
|
160
175
|
## Version relationship
|
|
161
176
|
|
|
162
177
|
Each CLI release is tested against one specific Starter release:
|
|
163
178
|
|
|
164
179
|
```text
|
|
165
|
-
create-clubedge-app v0.
|
|
180
|
+
create-clubedge-app v0.16.0
|
|
166
181
|
|
|
|
167
182
|
v
|
|
168
|
-
Clubedge Starter v0.
|
|
169
|
-
commit
|
|
183
|
+
Clubedge Starter v0.14.0
|
|
184
|
+
commit 2219a21c2eb237b7d8ee004ea8bbeb098b70106e
|
|
170
185
|
```
|
|
171
186
|
|
|
172
187
|
The current relationship is:
|
|
173
188
|
|
|
174
189
|
| CLI release | Default Starter release | Meaning |
|
|
175
190
|
| --- | --- | --- |
|
|
176
|
-
| `create-clubedge-app@0.
|
|
191
|
+
| `create-clubedge-app@0.16.0` | `clubedge-starter@v0.14.0` | The CLI's required, tested default |
|
|
177
192
|
|
|
178
193
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
179
194
|
|
|
180
|
-
The `v0.
|
|
181
|
-
`
|
|
195
|
+
The `v0.14.0` tag is expected to resolve to commit
|
|
196
|
+
`2219a21c2eb237b7d8ee004ea8bbeb098b70106e`. The CLI verifies this before
|
|
182
197
|
scaffolding with the default reference and fails rather than silently using a
|
|
183
198
|
moved tag.
|
|
184
199
|
|
|
185
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.
|
|
200
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.14.0`. The CLI is then updated to use that release, all generated-project checks must pass, and a new CLI version is published. This keeps Starter and CLI releases independently versioned while preserving a tested compatibility relationship.
|
|
186
201
|
|
|
187
202
|
Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
|
|
188
203
|
|
|
@@ -208,6 +223,7 @@ src/
|
|
|
208
223
|
| --- | --- |
|
|
209
224
|
| Configure a generated project | Its `SETUP.md`, also in the [Starter repository](https://github.com/Clubedge/clubedge-starter/blob/main/SETUP.md) |
|
|
210
225
|
| Change modules or upgrade a project | [Change an existing project](#change-an-existing-project) |
|
|
226
|
+
| See where a project stands, or why it does not run | [Check a project](#check-a-project) |
|
|
211
227
|
| Reproduce an exact Starter version | [Version relationship](#version-relationship) |
|
|
212
228
|
| See what changed in each release | [CHANGELOG.md](CHANGELOG.md) |
|
|
213
229
|
| See what is planned next | [ROADMAP.md](ROADMAP.md) |
|
|
@@ -236,7 +252,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
|
|
|
236
252
|
1. Make and validate changes in `clubedge-starter`.
|
|
237
253
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
238
254
|
3. Update the Starter package versions.
|
|
239
|
-
4. Commit `main`, create a release tag such as `v0.
|
|
255
|
+
4. Commit `main`, create a release tag such as `v0.14.0`, and publish the Starter release.
|
|
240
256
|
|
|
241
257
|
### CLI maintainers
|
|
242
258
|
|
|
@@ -260,7 +276,9 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
260
276
|
| 0.12.0 | v0.11.0 | Payments |
|
|
261
277
|
| 0.13.0 | v0.12.0 | Background jobs |
|
|
262
278
|
| 0.14.0 | v0.13.0 | Observability |
|
|
263
|
-
|
|
|
279
|
+
| 0.15.0 | v0.13.1 | `status` and `doctor`, several modules in one change, conflict and upgrade summaries, macOS CI |
|
|
280
|
+
| 0.16.0 | v0.14.0 | Account settings page, browser error reporting and Sentry source maps, auth emails through the jobs queue |
|
|
281
|
+
| next | | Show the Starter's version of a conflicted file next to yours |
|
|
264
282
|
|
|
265
283
|
## Community
|
|
266
284
|
|
package/dist/cli/args.js
CHANGED
|
@@ -12,6 +12,7 @@ const OPTIONS = {
|
|
|
12
12
|
yes: { type: "boolean", short: "y", default: false },
|
|
13
13
|
"dry-run": { type: "boolean", default: false },
|
|
14
14
|
force: { type: "boolean", default: false },
|
|
15
|
+
services: { type: "boolean", default: true },
|
|
15
16
|
help: { type: "boolean", short: "h", default: false },
|
|
16
17
|
version: { type: "boolean", short: "v", default: false },
|
|
17
18
|
};
|
|
@@ -78,9 +79,9 @@ export function parseCliArgs(argv) {
|
|
|
78
79
|
throw new UsageError("Only one project directory can be provided.");
|
|
79
80
|
}
|
|
80
81
|
if (command.name !== "create" && (values.framework !== undefined || Object.keys(modules).length)) {
|
|
81
|
-
throw new UsageError("--framework and module flags only apply when creating a project; use add or remove to change
|
|
82
|
+
throw new UsageError("--framework and module flags only apply when creating a project; use add or remove to change modules.");
|
|
82
83
|
}
|
|
83
|
-
if (
|
|
84
|
+
if (command.name !== "create" && command.name !== "upgrade" && values.ref !== undefined) {
|
|
84
85
|
throw new UsageError("--ref only applies when creating or upgrading a project.");
|
|
85
86
|
}
|
|
86
87
|
if (values.framework !== undefined && !values.framework.trim()) {
|
|
@@ -104,25 +105,42 @@ export function parseCliArgs(argv) {
|
|
|
104
105
|
yes: values.yes,
|
|
105
106
|
dryRun: values["dry-run"],
|
|
106
107
|
force: values.force,
|
|
108
|
+
services: values.services,
|
|
107
109
|
help: values.help,
|
|
108
110
|
version: values.version,
|
|
109
111
|
};
|
|
110
112
|
}
|
|
113
|
+
function assertDistinct(modules) {
|
|
114
|
+
const repeated = modules.find((module, index) => modules.indexOf(module) !== index);
|
|
115
|
+
if (repeated)
|
|
116
|
+
throw new UsageError(`The module "${repeated}" is named more than once.`);
|
|
117
|
+
}
|
|
111
118
|
/**
|
|
112
|
-
* `add <module> <option
|
|
113
|
-
* else is the directory of a new project. Create a project
|
|
119
|
+
* `add <module> <option>...`, `remove <module>...`, and `upgrade` change an existing project, and `status`
|
|
120
|
+
* and `doctor` inspect one; anything else is the directory of a new project. Create a project
|
|
121
|
+
* literally named "add" with `./add`.
|
|
114
122
|
*/
|
|
115
123
|
function parseCommand(positionals) {
|
|
116
|
-
const [name,
|
|
124
|
+
const [name, ...rest] = positionals;
|
|
125
|
+
const module = rest[0];
|
|
117
126
|
if (name === "add") {
|
|
118
|
-
if (
|
|
119
|
-
throw new UsageError("Usage: add [module
|
|
120
|
-
|
|
127
|
+
if (rest.length > 1 && rest.length % 2 === 1) {
|
|
128
|
+
throw new UsageError("Usage: add [module option]..., for example: add cache redis, or add email resend payments stripe");
|
|
129
|
+
}
|
|
130
|
+
const changes = rest.length === 1
|
|
131
|
+
? [{ module: rest[0] }]
|
|
132
|
+
: Array.from({ length: rest.length / 2 }, (_, i) => ({ module: rest[2 * i], option: rest[2 * i + 1] }));
|
|
133
|
+
assertDistinct(changes.map((change) => change.module));
|
|
134
|
+
return { name, changes };
|
|
121
135
|
}
|
|
122
136
|
if (name === "remove") {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
assertDistinct(rest);
|
|
138
|
+
return { name, modules: rest };
|
|
139
|
+
}
|
|
140
|
+
if (name === "status" || name === "doctor") {
|
|
141
|
+
if (module !== undefined)
|
|
142
|
+
throw new UsageError(`Usage: ${name}, run inside a project.`);
|
|
143
|
+
return { name };
|
|
126
144
|
}
|
|
127
145
|
if (name === "upgrade") {
|
|
128
146
|
if (module !== undefined)
|
package/dist/cli/ui.js
CHANGED
|
@@ -86,17 +86,24 @@ This CLI release scaffolds Starter ${starterPin.starterRef} (commit ${starterPin
|
|
|
86
86
|
|
|
87
87
|
Usage:
|
|
88
88
|
pnpm dlx @clubedge/create-clubedge-app [project-directory] [options]
|
|
89
|
-
pnpm dlx @clubedge/create-clubedge-app add [module
|
|
90
|
-
pnpm dlx @clubedge/create-clubedge-app remove [module]
|
|
89
|
+
pnpm dlx @clubedge/create-clubedge-app add [module option]... (inside a project)
|
|
90
|
+
pnpm dlx @clubedge/create-clubedge-app remove [module]... (inside a project)
|
|
91
91
|
pnpm dlx @clubedge/create-clubedge-app upgrade [--ref <tag>] (inside a project)
|
|
92
|
+
pnpm dlx @clubedge/create-clubedge-app status (inside a project)
|
|
93
|
+
pnpm dlx @clubedge/create-clubedge-app doctor (inside a project)
|
|
92
94
|
|
|
93
|
-
Inside a project with the clubedge script, use pnpm clubedge add, remove,
|
|
94
|
-
upgrade. Without a module or option, add and remove ask for them.
|
|
95
|
+
Inside a project with the clubedge script, use pnpm clubedge add, remove,
|
|
96
|
+
upgrade, status, or doctor. Without a module or option, add and remove ask for them.
|
|
95
97
|
|
|
96
|
-
add and remove change
|
|
97
|
-
to a newer Starter (this CLI's release unless --ref
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
add and remove change modules of an existing project, several at once as one
|
|
99
|
+
change, and upgrade moves it to a newer Starter (this CLI's release unless --ref
|
|
100
|
+
is given) and lists the Starter releases it crosses. The change is merged into
|
|
101
|
+
your code; lines you changed that it also changes get conflict markers. Commit
|
|
102
|
+
first so you can review the result with git diff.
|
|
103
|
+
|
|
104
|
+
status shows the project's Starter release, modules, and Git state, and whether a
|
|
105
|
+
newer Starter is available. doctor checks Node.js, pnpm, the dependencies, the
|
|
106
|
+
lockfile, and the env file, then runs pnpm check:services; it changes nothing.
|
|
100
107
|
|
|
101
108
|
Options:
|
|
102
109
|
${selectionHelp(manifest)}
|
|
@@ -105,6 +112,7 @@ ${selectionHelp(manifest)}
|
|
|
105
112
|
--template-dir <path> Scaffold from a local Starter checkout (for Starter development)
|
|
106
113
|
--dry-run Show what would be created or changed without writing anything
|
|
107
114
|
--force add/remove: change a project with uncommitted changes or without Git
|
|
115
|
+
--no-services doctor: skip pnpm check:services
|
|
108
116
|
-y, --yes Accept defaults and never prompt (default directory: my-app)
|
|
109
117
|
--no-install Skip dependency installation
|
|
110
118
|
--no-git Skip Git repository initialization
|
|
@@ -119,8 +127,11 @@ Examples:
|
|
|
119
127
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
120
128
|
pnpm dlx @clubedge/create-clubedge-app my-app --template-dir ../clubedge-starter
|
|
121
129
|
pnpm dlx @clubedge/create-clubedge-app add auth better-auth
|
|
130
|
+
pnpm dlx @clubedge/create-clubedge-app add email resend payments stripe
|
|
122
131
|
pnpm dlx @clubedge/create-clubedge-app remove storage --dry-run
|
|
123
132
|
pnpm dlx @clubedge/create-clubedge-app@latest upgrade
|
|
133
|
+
pnpm clubedge status
|
|
134
|
+
pnpm clubedge doctor --no-services
|
|
124
135
|
|
|
125
136
|
Learn more:
|
|
126
137
|
CLI ${cliUrl}
|
package/dist/core/env.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
const LINE = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
|
|
5
5
|
const COMMENT = /^\s*#/;
|
|
6
6
|
const keyOf = (line) => LINE.exec(line)?.[1];
|
|
7
|
+
/** The variable names an env file sets, in order. */
|
|
8
|
+
export function envKeys(text) {
|
|
9
|
+
return text.split(/\r?\n/).flatMap((line) => keyOf(line) ?? []);
|
|
10
|
+
}
|
|
7
11
|
const valueOf = (line) => LINE.exec(line)[2].replace(/\s+#.*$/, "").trim();
|
|
8
12
|
const find = (lines, key) => lines.findIndex((line) => keyOf(line) === key);
|
|
9
13
|
function values(lines) {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Pure checks behind `status` and `doctor`: they take what the commands read from the project,
|
|
2
|
+
// the system, and npm, and return what to show, so each can be tested without a real project.
|
|
3
|
+
import { envKeys } from "./env.js";
|
|
4
|
+
/** Compares dotted versions such as "v0.13.1" and "0.12.0"; null when either is not one. */
|
|
5
|
+
export function compareVersions(a, b) {
|
|
6
|
+
const parse = (value) => {
|
|
7
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
|
|
8
|
+
return match ? match.slice(1).map(Number) : null;
|
|
9
|
+
};
|
|
10
|
+
const left = parse(a);
|
|
11
|
+
const right = parse(b);
|
|
12
|
+
if (!left || !right)
|
|
13
|
+
return null;
|
|
14
|
+
for (let i = 0; i < 3; i++) {
|
|
15
|
+
if (left[i] !== right[i])
|
|
16
|
+
return left[i] - right[i];
|
|
17
|
+
}
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
const REGISTRY_URL = "https://registry.npmjs.org/@clubedge%2fcreate-clubedge-app/latest";
|
|
21
|
+
/**
|
|
22
|
+
* The newest published CLI and the Starter release it is pinned to, from npm. Resolves null when
|
|
23
|
+
* npm cannot be reached in time, so status still works offline.
|
|
24
|
+
*/
|
|
25
|
+
export async function fetchLatestRelease(fetcher = fetch, timeoutMs = 4_000) {
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetcher(REGISTRY_URL, {
|
|
28
|
+
headers: { accept: "application/json" },
|
|
29
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok)
|
|
32
|
+
return null;
|
|
33
|
+
const latest = (await response.json());
|
|
34
|
+
if (typeof latest.version !== "string" || typeof latest.clubedge?.starterRef !== "string")
|
|
35
|
+
return null;
|
|
36
|
+
return { cliVersion: latest.version, starterRef: latest.clubedge.starterRef };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function updateState(projectRef, latest) {
|
|
43
|
+
if (!latest)
|
|
44
|
+
return { kind: "unknown" };
|
|
45
|
+
const difference = compareVersions(latest.starterRef, projectRef);
|
|
46
|
+
if (difference === null)
|
|
47
|
+
return { kind: "unknown" };
|
|
48
|
+
return difference > 0 ? { kind: "available", ...latest } : { kind: "current" };
|
|
49
|
+
}
|
|
50
|
+
/** The command that runs the newest CLI in this project. */
|
|
51
|
+
export function latestCliCommand(cli) {
|
|
52
|
+
return cli === "pnpm clubedge" ? cli : "pnpm dlx @clubedge/create-clubedge-app@latest";
|
|
53
|
+
}
|
|
54
|
+
/** What to do next, given the working tree and whether a newer Starter exists. */
|
|
55
|
+
export function nextStep(git, update, cli) {
|
|
56
|
+
if (update.kind === "available") {
|
|
57
|
+
const upgrade = `${latestCliCommand(cli)} upgrade`;
|
|
58
|
+
return git.kind === "dirty"
|
|
59
|
+
? `Commit or stash your changes, then run \`${upgrade}\` to move to Starter ${update.starterRef}.`
|
|
60
|
+
: `Run \`${upgrade}\` to move to Starter ${update.starterRef}.`;
|
|
61
|
+
}
|
|
62
|
+
if (update.kind === "current") {
|
|
63
|
+
return `The project is on the newest Starter release. Run \`${cli} doctor\` to check its setup.`;
|
|
64
|
+
}
|
|
65
|
+
return `Could not check for a newer Starter release. Run \`${cli} doctor\` to check the project's setup.`;
|
|
66
|
+
}
|
|
67
|
+
/** The lowest version a range such as ">=22.12.0" or "^22.12.0" accepts. */
|
|
68
|
+
function minimumOf(range) {
|
|
69
|
+
const match = /(\d+\.\d+\.\d+)/.exec(range ?? "");
|
|
70
|
+
return match ? match[1] : null;
|
|
71
|
+
}
|
|
72
|
+
export function checkNode(actual, engines) {
|
|
73
|
+
const minimum = minimumOf(engines) ?? "22.12.0";
|
|
74
|
+
const difference = compareVersions(actual, minimum);
|
|
75
|
+
return difference !== null && difference >= 0
|
|
76
|
+
? { name: "Node.js", status: "ok", detail: `${actual} (needs ${minimum} or newer)` }
|
|
77
|
+
: {
|
|
78
|
+
name: "Node.js",
|
|
79
|
+
status: "fail",
|
|
80
|
+
detail: `${actual}, but the project needs ${minimum} or newer`,
|
|
81
|
+
fix: `Install Node.js ${minimum} or newer, such as the current LTS release.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Compares pnpm with the version in package.json's packageManager, such as "pnpm@10.9.0". */
|
|
85
|
+
export function checkPnpm(actual, packageManager) {
|
|
86
|
+
const pinned = /^pnpm@(\d+\.\d+\.\d+)/.exec(packageManager ?? "")?.[1];
|
|
87
|
+
if (!actual) {
|
|
88
|
+
return {
|
|
89
|
+
name: "pnpm",
|
|
90
|
+
status: "fail",
|
|
91
|
+
detail: "not found",
|
|
92
|
+
fix: "Run `corepack enable`, which installs the pnpm version the project pins.",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (!pinned)
|
|
96
|
+
return { name: "pnpm", status: "ok", detail: actual };
|
|
97
|
+
const difference = compareVersions(actual, pinned);
|
|
98
|
+
return difference !== null && difference >= 0
|
|
99
|
+
? { name: "pnpm", status: "ok", detail: `${actual} (project pins ${pinned})` }
|
|
100
|
+
: {
|
|
101
|
+
name: "pnpm",
|
|
102
|
+
status: "warn",
|
|
103
|
+
detail: `${actual}, older than the ${pinned} the project pins`,
|
|
104
|
+
fix: "Run `corepack enable` so pnpm follows the project's packageManager field.",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Compares the local env file with the example: variables the example has and the local file
|
|
109
|
+
* lacks, and variables only the local file sets.
|
|
110
|
+
*/
|
|
111
|
+
export function checkEnvFile(example, local, paths) {
|
|
112
|
+
const name = "Environment file";
|
|
113
|
+
if (local === null) {
|
|
114
|
+
return {
|
|
115
|
+
name,
|
|
116
|
+
status: "fail",
|
|
117
|
+
detail: `${paths.local} does not exist`,
|
|
118
|
+
fix: `Copy ${paths.example} to ${paths.local} and fill in the values.`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (example === null)
|
|
122
|
+
return { name, status: "ok", detail: `${paths.local} exists (no ${paths.example} to compare)` };
|
|
123
|
+
const exampleKeys = new Set(envKeys(example));
|
|
124
|
+
const localKeys = new Set(envKeys(local));
|
|
125
|
+
const missing = [...exampleKeys].filter((key) => !localKeys.has(key));
|
|
126
|
+
const unused = [...localKeys].filter((key) => !exampleKeys.has(key));
|
|
127
|
+
if (!missing.length && !unused.length) {
|
|
128
|
+
return { name, status: "ok", detail: `${paths.local} has every variable in ${paths.example}` };
|
|
129
|
+
}
|
|
130
|
+
const parts = [
|
|
131
|
+
missing.length ? `missing ${missing.join(", ")}` : "",
|
|
132
|
+
unused.length ? `not in ${paths.example}: ${unused.join(", ")}` : "",
|
|
133
|
+
].filter(Boolean);
|
|
134
|
+
return {
|
|
135
|
+
name,
|
|
136
|
+
status: "warn",
|
|
137
|
+
detail: `${paths.local}: ${parts.join("; ")}`,
|
|
138
|
+
fix: missing.length
|
|
139
|
+
? `Add the missing variables from ${paths.example}; empty values are fine for services you do not use yet.`
|
|
140
|
+
: "Remove variables the project no longer uses, unless your own code reads them.",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Reads the result of `pnpm install --frozen-lockfile --lockfile-only`. */
|
|
144
|
+
export function checkLockfile(error) {
|
|
145
|
+
const name = "Lockfile";
|
|
146
|
+
if (error === null)
|
|
147
|
+
return { name, status: "ok", detail: "pnpm-lock.yaml matches every package.json" };
|
|
148
|
+
if (error.includes("ERR_PNPM_OUTDATED_LOCKFILE")) {
|
|
149
|
+
return {
|
|
150
|
+
name,
|
|
151
|
+
status: "fail",
|
|
152
|
+
detail: "pnpm-lock.yaml is out of date with a package.json",
|
|
153
|
+
fix: "Run `pnpm install` to update it, then commit pnpm-lock.yaml.",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
name,
|
|
158
|
+
status: "warn",
|
|
159
|
+
detail: `could not check: ${error.split("\n").find((line) => line.trim()) ?? error}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
package/dist/core/merge.js
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
// without losing the project's own edits. Pure: callers read and write the files.
|
|
4
4
|
import { diff3Merge } from "node-diff3";
|
|
5
5
|
export const CONFLICT_LABELS = { yours: "yours", theirs: "clubedge" };
|
|
6
|
+
/** How many conflict blocks a merged file has. */
|
|
7
|
+
export function countConflicts(content) {
|
|
8
|
+
const marker = `<<<<<<< ${CONFLICT_LABELS.yours}`;
|
|
9
|
+
return content
|
|
10
|
+
.toString("utf8")
|
|
11
|
+
.split(/\r?\n/)
|
|
12
|
+
.filter((line) => line === marker).length;
|
|
13
|
+
}
|
|
6
14
|
function isBinary(content) {
|
|
7
15
|
return content.subarray(0, 8000).includes(0);
|
|
8
16
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The Starter releases an upgrade crosses, from GitHub, so `upgrade` can say what changed before
|
|
2
|
+
// the developer reviews `git diff`.
|
|
3
|
+
import { compareVersions } from "./health.js";
|
|
4
|
+
/**
|
|
5
|
+
* The repository's published releases, newest first. Resolves null when GitHub cannot be reached
|
|
6
|
+
* in time or refuses, so an upgrade never depends on it. GITHUB_TOKEN raises the rate limit.
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchReleases(repository, fetcher = fetch, timeoutMs = 5_000) {
|
|
9
|
+
try {
|
|
10
|
+
const token = process.env.GITHUB_TOKEN;
|
|
11
|
+
const response = await fetcher(`https://api.github.com/repos/${repository}/releases?per_page=100`, {
|
|
12
|
+
headers: {
|
|
13
|
+
accept: "application/vnd.github+json",
|
|
14
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
15
|
+
},
|
|
16
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok)
|
|
19
|
+
return null;
|
|
20
|
+
const releases = (await response.json());
|
|
21
|
+
if (!Array.isArray(releases))
|
|
22
|
+
return null;
|
|
23
|
+
return releases
|
|
24
|
+
.filter((release) => typeof release.tag_name === "string" && !release.draft && !release.prerelease)
|
|
25
|
+
.map((release) => ({
|
|
26
|
+
tag: release.tag_name,
|
|
27
|
+
name: typeof release.name === "string" && release.name ? release.name : release.tag_name,
|
|
28
|
+
body: typeof release.body === "string" ? release.body : "",
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Releases after `from`, up to and including `to`, oldest first. */
|
|
36
|
+
export function releasesBetween(releases, from, to) {
|
|
37
|
+
return releases
|
|
38
|
+
.filter((release) => {
|
|
39
|
+
const afterFrom = compareVersions(release.tag, from);
|
|
40
|
+
const upToTo = compareVersions(release.tag, to);
|
|
41
|
+
return afterFrom !== null && upToTo !== null && afterFrom > 0 && upToTo <= 0;
|
|
42
|
+
})
|
|
43
|
+
.sort((a, b) => compareVersions(a.tag, b.tag));
|
|
44
|
+
}
|
|
45
|
+
/** The second-level headings of release notes, such as "Background jobs" and "Tests". */
|
|
46
|
+
export function releaseHeadings(body) {
|
|
47
|
+
return body
|
|
48
|
+
.split(/\r?\n/)
|
|
49
|
+
.flatMap((line) => /^##\s+(.+?)\s*#*\s*$/.exec(line)?.[1] ?? []);
|
|
50
|
+
}
|
|
51
|
+
/** One line per release: its tag and what its notes cover. */
|
|
52
|
+
export function describeReleases(releases) {
|
|
53
|
+
return releases
|
|
54
|
+
.map((release) => {
|
|
55
|
+
const headings = releaseHeadings(release.body);
|
|
56
|
+
return `${release.tag} ${headings.length ? headings.join(", ") : release.name}`;
|
|
57
|
+
})
|
|
58
|
+
.join("\n");
|
|
59
|
+
}
|
package/dist/core/source.js
CHANGED
|
@@ -83,7 +83,9 @@ export async function downloadGitHubTemplate(ref, { repository = starterPin.star
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
const temporary = await mkdtemp(join(tmpdir(), "create-clubedge-app-"));
|
|
86
|
-
|
|
86
|
+
// Windows can briefly lock a folder that was just read (antivirus, indexing). Retry, and never
|
|
87
|
+
// fail a command that already did its work over a leftover folder in the system temp directory.
|
|
88
|
+
const cleanup = () => rm(temporary, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }).catch(() => undefined);
|
|
87
89
|
try {
|
|
88
90
|
const root = join(temporary, "starter");
|
|
89
91
|
await downloadTemplate(`gh:${repository}#${ref}`, { dir: root, force: true });
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { readTemplateManifest } from "./core/manifest.js";
|
|
|
9
9
|
import { createPlan, describePlan, selectFramework } from "./core/plan.js";
|
|
10
10
|
import { readBundledTemplate, resolveTemplateSource } from "./core/source.js";
|
|
11
11
|
import { cliUrl, cliVersion, siteUrl, starterPin } from "./package-info.js";
|
|
12
|
+
import { runDoctor, showStatus } from "./inspect.js";
|
|
12
13
|
import { changeProject } from "./modify.js";
|
|
13
14
|
import { initializeGit } from "./steps/git.js";
|
|
14
15
|
import { detectLauncher, installDependencies, resolvePnpm } from "./steps/install.js";
|
|
@@ -225,6 +226,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
225
226
|
intro("Create a project with Clubedge Starter");
|
|
226
227
|
await createProject(args);
|
|
227
228
|
}
|
|
229
|
+
else if (args.command.name === "status") {
|
|
230
|
+
intro("Project status");
|
|
231
|
+
await showStatus(args);
|
|
232
|
+
}
|
|
233
|
+
else if (args.command.name === "doctor") {
|
|
234
|
+
intro("Check this project's setup");
|
|
235
|
+
await runDoctor(args);
|
|
236
|
+
}
|
|
228
237
|
else {
|
|
229
238
|
const intros = {
|
|
230
239
|
add: "Add a module to this project",
|