@clubedge/create-clubedge-app 0.2.1 → 0.3.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 +11 -0
- package/README.md +50 -35
- package/bin/create.mjs +2 -469
- package/dist/cli/args.js +49 -0
- package/dist/cli/ui.js +85 -0
- package/dist/core/customize.js +56 -0
- package/dist/core/execute.js +60 -0
- package/dist/core/manifest.js +81 -0
- package/dist/core/plan.js +67 -0
- package/dist/core/source.js +107 -0
- package/dist/index.js +125 -0
- package/dist/package-info.js +8 -0
- package/dist/steps/git.js +11 -0
- package/dist/steps/install.js +19 -0
- package/dist/utils/fs.js +97 -0
- package/dist/utils/names.js +14 -0
- package/dist/utils/process.js +49 -0
- package/package.json +18 -7
- package/template/.dockerignore +8 -0
- package/template/.env.example +25 -0
- package/template/.gitattributes +6 -0
- package/template/.github/ISSUE_TEMPLATE/bug_report.yml +35 -0
- package/template/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/template/.github/ISSUE_TEMPLATE/feature_request.yml +24 -0
- package/template/.github/pull_request_template.md +20 -0
- package/template/.github/workflows/ci.yml +49 -0
- package/template/.prettierignore +1 -0
- package/template/.prettierrc.json +1 -0
- package/template/AGENTS.md +31 -0
- package/template/CODE_OF_CONDUCT.md +22 -0
- package/template/CONTRIBUTING.md +53 -0
- package/template/Dockerfile +32 -0
- package/template/LICENSE +201 -0
- package/template/NOTICE +6 -0
- package/template/PUBLISHING.md +27 -0
- package/template/README.md +157 -0
- package/template/SECURITY.md +15 -0
- package/template/SETUP.md +174 -0
- package/template/SUPPORT.md +8 -0
- package/template/_gitignore +13 -0
- package/template/apps/web/AGENTS.md +9 -0
- package/template/apps/web/components.json +21 -0
- package/template/apps/web/e2e/auth.spec.ts +37 -0
- package/template/apps/web/e2e/home.spec.ts +15 -0
- package/template/apps/web/next-env.d.ts +7 -0
- package/template/apps/web/next.config.ts +32 -0
- package/template/apps/web/package.json +33 -0
- package/template/apps/web/postcss.config.mjs +3 -0
- package/template/apps/web/public/.gitkeep +0 -0
- package/template/apps/web/public/.well-known/logo.svg +1 -0
- package/template/apps/web/src/app/actions/auth.ts +85 -0
- package/template/apps/web/src/app/api/health/route.ts +11 -0
- package/template/apps/web/src/app/auth/callback/route.ts +21 -0
- package/template/apps/web/src/app/dashboard/_components/account-button.tsx +34 -0
- package/template/apps/web/src/app/dashboard/_components/app-sidebar.tsx +91 -0
- package/template/apps/web/src/app/dashboard/page.tsx +352 -0
- package/template/apps/web/src/app/favicon.ico +0 -0
- package/template/apps/web/src/app/layout.tsx +36 -0
- package/template/apps/web/src/app/login/_components/auth-form-controls.tsx +88 -0
- package/template/apps/web/src/app/login/page.tsx +149 -0
- package/template/apps/web/src/app/page.tsx +229 -0
- package/template/apps/web/src/app/theme-provider.tsx +43 -0
- package/template/apps/web/src/app/theme-toggle.tsx +23 -0
- package/template/apps/web/src/components/branding/clubedge-mark.tsx +25 -0
- package/template/apps/web/src/components/landing/copy-command.tsx +48 -0
- package/template/apps/web/src/config/site.json +11 -0
- package/template/apps/web/src/config/site.ts +20 -0
- package/template/apps/web/src/env/client.ts +22 -0
- package/template/apps/web/src/env/server.test.ts +35 -0
- package/template/apps/web/src/env/server.ts +45 -0
- package/template/apps/web/src/lib/login-url.test.ts +16 -0
- package/template/apps/web/src/lib/login-url.ts +16 -0
- package/template/apps/web/src/lib/utils.ts +1 -0
- package/template/apps/web/src/proxy.ts +31 -0
- package/template/apps/web/src/server/auth.ts +69 -0
- package/template/apps/web/src/server/cache.ts +38 -0
- package/template/apps/web/src/server/db.ts +16 -0
- package/template/apps/web/src/server/storage.ts +39 -0
- package/template/apps/web/src/server/users.ts +17 -0
- package/template/apps/web/tsconfig.json +36 -0
- package/template/clubedge.template.json +13 -0
- package/template/docker-compose.yml +8 -0
- package/template/eslint.config.mjs +63 -0
- package/template/package.json +47 -0
- package/template/packages/auth/package.json +23 -0
- package/template/packages/auth/src/index.ts +9 -0
- package/template/packages/auth/src/supabase.test.ts +126 -0
- package/template/packages/auth/src/supabase.ts +65 -0
- package/template/packages/auth/src/types.ts +59 -0
- package/template/packages/auth/tsconfig.json +4 -0
- package/template/packages/cache/package.json +20 -0
- package/template/packages/cache/src/index.test.ts +67 -0
- package/template/packages/cache/src/index.ts +19 -0
- package/template/packages/cache/src/rate-limit.test.ts +52 -0
- package/template/packages/cache/src/rate-limit.ts +74 -0
- package/template/packages/cache/src/redis.ts +106 -0
- package/template/packages/cache/tsconfig.json +4 -0
- package/template/packages/core/package.json +17 -0
- package/template/packages/core/src/client-ip.test.ts +19 -0
- package/template/packages/core/src/client-ip.ts +11 -0
- package/template/packages/core/src/errors.test.ts +20 -0
- package/template/packages/core/src/errors.ts +20 -0
- package/template/packages/core/src/index.ts +4 -0
- package/template/packages/core/src/redirect.test.ts +27 -0
- package/template/packages/core/src/redirect.ts +20 -0
- package/template/packages/core/src/result.ts +10 -0
- package/template/packages/core/tsconfig.json +4 -0
- package/template/packages/db/drizzle/0000_initial.sql +10 -0
- package/template/packages/db/drizzle/meta/_journal.json +13 -0
- package/template/packages/db/drizzle.config.ts +9 -0
- package/template/packages/db/package.json +30 -0
- package/template/packages/db/scripts/load-env.ts +14 -0
- package/template/packages/db/scripts/migrate.ts +11 -0
- package/template/packages/db/scripts/seed.ts +14 -0
- package/template/packages/db/src/client.ts +22 -0
- package/template/packages/db/src/index.ts +4 -0
- package/template/packages/db/src/schema/index.ts +1 -0
- package/template/packages/db/src/schema/users.ts +14 -0
- package/template/packages/db/src/users.ts +16 -0
- package/template/packages/db/tsconfig.json +4 -0
- package/template/packages/storage/package.json +24 -0
- package/template/packages/storage/src/index.ts +1 -0
- package/template/packages/storage/src/s3.test.ts +86 -0
- package/template/packages/storage/src/s3.ts +86 -0
- package/template/packages/storage/src/supabase.ts +69 -0
- package/template/packages/storage/src/types.ts +18 -0
- package/template/packages/storage/tsconfig.json +4 -0
- package/template/packages/ui/components.json +21 -0
- package/template/packages/ui/package.json +32 -0
- package/template/packages/ui/src/components/badge.tsx +48 -0
- package/template/packages/ui/src/components/breadcrumb.tsx +102 -0
- package/template/packages/ui/src/components/button.tsx +57 -0
- package/template/packages/ui/src/components/card.tsx +87 -0
- package/template/packages/ui/src/components/input.tsx +19 -0
- package/template/packages/ui/src/components/label.tsx +19 -0
- package/template/packages/ui/src/components/separator.tsx +20 -0
- package/template/packages/ui/src/components/sheet.tsx +125 -0
- package/template/packages/ui/src/components/sidebar.tsx +692 -0
- package/template/packages/ui/src/components/skeleton.tsx +13 -0
- package/template/packages/ui/src/components/switch.tsx +31 -0
- package/template/packages/ui/src/components/table.tsx +88 -0
- package/template/packages/ui/src/components/tooltip.tsx +53 -0
- package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
- package/template/packages/ui/src/lib/utils.ts +1 -0
- package/template/packages/ui/src/styles/globals.css +126 -0
- package/template/packages/ui/tsconfig.json +19 -0
- package/template/playwright.config.ts +19 -0
- package/template/pnpm-lock.yaml +3624 -0
- package/template/pnpm-workspace.yaml +3 -0
- package/template/scripts/start-standalone.mjs +37 -0
- package/template/tsconfig.base.json +16 -0
- package/template/turbo.json +17 -0
- package/template/vitest.config.ts +8 -0
- package/template.lock.json +142 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.3.0
|
|
6
|
+
|
|
7
|
+
- Default to Starter `v0.3.0`, which adds the `clubedge.template.json` template manifest.
|
|
8
|
+
- Rewrite the CLI in TypeScript, organized into argument parsing, planning, execution, and steps.
|
|
9
|
+
- Bundle the pinned Starter release into the package. The default scaffold no longer downloads from GitHub or requires Git.
|
|
10
|
+
- Read the Starter's `clubedge.template.json` manifest and refuse Starters with a newer template schema.
|
|
11
|
+
- Add `--dry-run`, `--yes`, and `--template-dir`.
|
|
12
|
+
- Remove everything written when scaffolding fails, instead of leaving a partial project.
|
|
13
|
+
- Check the Node.js version before starting, and fall back to `corepack pnpm` when `pnpm` is not on the path.
|
|
14
|
+
- Never copy local environment files, dependencies, or build output from a template directory, even when it sits inside another Git repository.
|
|
15
|
+
|
|
5
16
|
## 0.2.1
|
|
6
17
|
|
|
7
18
|
- Publish the default Starter pin in the `clubedge` field of `package.json`. The CLI and release workflow read it from there, and the npm registry exposes it so the website always shows the Starter each CLI version scaffolds.
|
package/README.md
CHANGED
|
@@ -12,34 +12,34 @@ The CLI downloads a tested Starter release, names the generated project, creates
|
|
|
12
12
|
Each CLI release is tested against one specific Starter release:
|
|
13
13
|
|
|
14
14
|
```text
|
|
15
|
-
create-clubedge-app v0.
|
|
15
|
+
create-clubedge-app v0.3.0
|
|
16
16
|
|
|
|
17
17
|
v
|
|
18
|
-
Clubedge Starter v0.
|
|
19
|
-
commit
|
|
18
|
+
Clubedge Starter v0.3.0
|
|
19
|
+
commit 60f1691920c91a5cf83e480a3ce98c3ddefdef55
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
The current relationship is:
|
|
23
23
|
|
|
24
24
|
| CLI release | Default Starter release | Meaning |
|
|
25
25
|
| --- | --- | --- |
|
|
26
|
-
| `create-clubedge-app@0.
|
|
26
|
+
| `create-clubedge-app@0.3.0` | `clubedge-starter@v0.3.0` | The CLI's required, tested default |
|
|
27
27
|
|
|
28
28
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
29
29
|
|
|
30
|
-
The `v0.
|
|
31
|
-
`
|
|
30
|
+
The `v0.3.0` tag is expected to resolve to commit
|
|
31
|
+
`60f1691920c91a5cf83e480a3ce98c3ddefdef55`. The CLI verifies this before
|
|
32
32
|
scaffolding with the default reference and fails rather than silently using a
|
|
33
33
|
moved tag.
|
|
34
34
|
|
|
35
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.
|
|
35
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.3.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.
|
|
36
36
|
|
|
37
37
|
## Requirements
|
|
38
38
|
|
|
39
39
|
- Node.js 22.12 or newer.
|
|
40
|
-
- pnpm 10.9.0 or newer
|
|
40
|
+
- pnpm 10.9.0 or newer. The generated project is a pnpm workspace, so dependencies are installed with pnpm even when you start the CLI with `npx`; the CLI falls back to `corepack pnpm` when `pnpm` is not on your path.
|
|
41
41
|
- Git, when using the default Git initialization.
|
|
42
|
-
- Internet access to
|
|
42
|
+
- Internet access to install dependencies. The default Starter release ships inside the CLI package, so GitHub is only contacted for `--ref`.
|
|
43
43
|
|
|
44
44
|
## Usage
|
|
45
45
|
|
|
@@ -73,53 +73,68 @@ pnpm dlx @clubedge/create-clubedge-app
|
|
|
73
73
|
```text
|
|
74
74
|
Usage: pnpm dlx @clubedge/create-clubedge-app [project-directory] [options]
|
|
75
75
|
|
|
76
|
-
--ref <ref>
|
|
77
|
-
--
|
|
78
|
-
--
|
|
79
|
-
-
|
|
80
|
-
-
|
|
76
|
+
--ref <ref> Download this Starter tag, branch, or commit from GitHub instead
|
|
77
|
+
--template-dir <path> Scaffold from a local Starter checkout (for Starter development)
|
|
78
|
+
--dry-run Show what would be created without writing anything
|
|
79
|
+
-y, --yes Accept defaults and never prompt (default directory: my-app)
|
|
80
|
+
--no-install Skip dependency installation
|
|
81
|
+
--no-git Skip Git initialization
|
|
82
|
+
-h, --help Show help
|
|
83
|
+
-v, --version Show the CLI version
|
|
81
84
|
```
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
The target directory must be empty or not exist. The CLI never deletes or overwrites existing files, and if scaffolding fails it removes whatever it wrote, so a failed run leaves nothing behind.
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref <starter-tag-or-commit>
|
|
87
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --no-install
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
The target directory must be empty or not exist. The CLI will not delete or overwrite files in a non-empty target directory. The default is the tested `v0.2.0` Starter release, so the same CLI version produces the same Starter revision. Use `--ref` to explicitly select a tag, branch, or commit.
|
|
91
|
-
|
|
92
|
-
The default is the tested Starter release tag `v0.2.0`. Every generated project records its CLI version, Starter repository, exact Starter ref, and resolved Starter commit in `package.json` and `README.md`.
|
|
93
|
-
|
|
94
|
-
Use `--ref` only when you intentionally want a different Starter revision:
|
|
88
|
+
Every generated project records its CLI version, Starter repository, Starter ref, and Starter commit in `package.json` and `README.md`.
|
|
95
89
|
|
|
96
90
|
```sh
|
|
97
|
-
#
|
|
98
|
-
pnpm dlx @clubedge/create-clubedge-app my-app
|
|
91
|
+
# Preview the files and edits without writing anything
|
|
92
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --dry-run
|
|
93
|
+
|
|
94
|
+
# Non-interactive, for scripts and CI
|
|
95
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git
|
|
99
96
|
|
|
100
97
|
# Try an unreleased Starter change
|
|
101
98
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
102
99
|
|
|
103
100
|
# Reproduce a specific Starter release or commit
|
|
104
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.
|
|
101
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.3.0
|
|
105
102
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
|
|
103
|
+
|
|
104
|
+
# Test local Starter changes before releasing them
|
|
105
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --template-dir ../clubedge-starter
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
## How it works
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
Each CLI release bundles one Starter release. When the release is built, `scripts/bundle-template.mjs` clones the pinned Starter tag, verifies that it resolves to the pinned commit, and copies its tracked files into the package (`template/` and `template.lock.json`). Running the CLI then needs no GitHub access for the default Starter.
|
|
111
|
+
|
|
112
|
+
The Starter describes itself in `clubedge.template.json`: its app directory, site config, environment files, Docker image name, and files that stay out of generated projects. The CLI refuses a Starter whose manifest schema is newer than it understands and asks you to update. Starters before v0.3.0 have no manifest and use the previous layout.
|
|
113
|
+
|
|
114
|
+
The CLI first builds a plan (the files to copy and the edits to make), which `--dry-run` prints, and then applies it. The source is organized the same way:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
src/
|
|
118
|
+
cli/ argument parsing and terminal output
|
|
119
|
+
core/ manifest, template sources, customizations, planning, and execution
|
|
120
|
+
steps/ Git initialization and dependency installation
|
|
121
|
+
utils/ file listing and copying, processes, and naming
|
|
122
|
+
```
|
|
111
123
|
|
|
112
124
|
## Development
|
|
113
125
|
|
|
114
126
|
```sh
|
|
115
127
|
corepack enable
|
|
116
128
|
pnpm install
|
|
117
|
-
|
|
118
|
-
pnpm
|
|
119
|
-
pnpm
|
|
129
|
+
pnpm check # type-check sources and tests
|
|
130
|
+
pnpm test # unit and scaffolding tests
|
|
131
|
+
pnpm build # compile src/ to dist/
|
|
132
|
+
node ./bin/create.mjs my-app --template-dir ../clubedge-starter --dry-run
|
|
120
133
|
```
|
|
121
134
|
|
|
122
|
-
|
|
135
|
+
Without a bundled template, a local build downloads the pinned Starter release from GitHub and verifies its commit. Run `pnpm bundle-template` to bundle it, or `pnpm bundle-template --from ../clubedge-starter` to bundle a local checkout.
|
|
136
|
+
|
|
137
|
+
CI packs the CLI with `npm pack`, scaffolds a clean project from the packed tarball, installs its dependencies, runs linting, typechecking, unit tests, browser checks, and a production build, then starts the generated app and checks the landing page, sign-up page, dashboard, health endpoint, logo, and favicon. A scheduled, non-blocking compatibility job also checks Starter `main`.
|
|
123
138
|
|
|
124
139
|
## Release workflow
|
|
125
140
|
|
|
@@ -128,11 +143,11 @@ CI scaffolds a clean project from the pinned Starter release, installs its depen
|
|
|
128
143
|
1. Make and validate changes in `clubedge-starter`.
|
|
129
144
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
130
145
|
3. Update the Starter package versions.
|
|
131
|
-
4. Commit `main`, create a release tag such as `v0.
|
|
146
|
+
4. Commit `main`, create a release tag such as `v0.3.0`, and publish the Starter release.
|
|
132
147
|
|
|
133
148
|
### CLI maintainers
|
|
134
149
|
|
|
135
|
-
1.
|
|
150
|
+
1. Set `clubedge.starterRef` and `clubedge.starterCommit` in [`package.json`](package.json) to the new tested Starter tag, and bump `version`.
|
|
136
151
|
2. Update the version relationship table and changelog.
|
|
137
152
|
3. Generate a clean project from that exact tag.
|
|
138
153
|
4. Run install, lint, typecheck, unit tests, browser tests, production build, and runtime smoke checks.
|
package/bin/create.mjs
CHANGED
|
@@ -1,471 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { main } from "../dist/index.js";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
import { downloadTemplate } from "giget";
|
|
5
|
-
import { access, cp, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
7
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
8
|
-
import process from "node:process";
|
|
9
|
-
import { pathToFileURL } from "node:url";
|
|
10
|
-
|
|
11
|
-
// package.json is the single source for the version and the tested Starter pin. It is
|
|
12
|
-
// published with the package, so the npm registry exposes exactly what each release scaffolds.
|
|
13
|
-
const packageManifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
14
|
-
const packageVersion = packageManifest.version;
|
|
15
|
-
const {
|
|
16
|
-
starterRepository,
|
|
17
|
-
starterRef: defaultStarterRef,
|
|
18
|
-
starterCommit: defaultStarterCommit,
|
|
19
|
-
} = packageManifest.clubedge;
|
|
20
|
-
const siteUrl = "https://clubedge.live";
|
|
21
|
-
const cliUrl = "https://starter.clubedge.live";
|
|
22
|
-
|
|
23
|
-
/* ------------------------------------------------------------------ */
|
|
24
|
-
/* Branding: big block-letter banner */
|
|
25
|
-
/* ------------------------------------------------------------------ */
|
|
26
|
-
|
|
27
|
-
// Each glyph is 6 rows tall. Rows are padded to a common width when rendered.
|
|
28
|
-
const glyphs = {
|
|
29
|
-
C: [" ██████╗", "██╔════╝", "██║ ", "██║ ", "╚██████╗", " ╚═════╝"],
|
|
30
|
-
L: ["██╗ ", "██║ ", "██║ ", "██║ ", "███████╗", "╚══════╝"],
|
|
31
|
-
U: ["██╗ ██╗", "██║ ██║", "██║ ██║", "██║ ██║", "╚██████╔╝", " ╚═════╝ "],
|
|
32
|
-
B: ["██████╗ ", "██╔══██╗", "██████╔╝", "██╔══██╗", "██████╔╝", "╚═════╝ "],
|
|
33
|
-
E: ["███████╗", "██╔════╝", "█████╗ ", "██╔══╝ ", "███████╗", "╚══════╝"],
|
|
34
|
-
D: ["██████╗ ", "██╔══██╗", "██║ ██║", "██║ ██║", "██████╔╝", "╚═════╝ "],
|
|
35
|
-
G: [" ██████╗ ", "██╔════╝ ", "██║ ███╗", "██║ ██║", "╚██████╔╝", " ╚═════╝ "],
|
|
36
|
-
S: ["███████╗", "██╔════╝", "███████╗", "╚════██║", "███████║", "╚══════╝"],
|
|
37
|
-
T: ["████████╗", "╚══██╔══╝", " ██║ ", " ██║ ", " ██║ ", " ╚═╝ "],
|
|
38
|
-
A: [" █████╗ ", "██╔══██╗", "███████║", "██╔══██║", "██║ ██║", "╚═╝ ╚═╝"],
|
|
39
|
-
R: ["██████╗ ", "██╔══██╗", "██████╔╝", "██╔══██╗", "██║ ██║", "╚═╝ ╚═╝"],
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
function renderWord(word) {
|
|
43
|
-
const letters = [...word].map((letter) => {
|
|
44
|
-
const rows = glyphs[letter];
|
|
45
|
-
const width = Math.max(...rows.map((row) => row.length));
|
|
46
|
-
return rows.map((row) => row.padEnd(width, " "));
|
|
47
|
-
});
|
|
48
|
-
return letters[0].map((_, rowIndex) => letters.map((letter) => letter[rowIndex]).join(""));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const colorEnabled = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
52
|
-
const paint = (rgb, value) => (colorEnabled ? `\x1b[38;2;${rgb.join(";")}m${value}\x1b[0m` : value);
|
|
53
|
-
const dim = (value) => (colorEnabled ? `\x1b[2m${value}\x1b[0m` : value);
|
|
54
|
-
const bold = (value) => (colorEnabled ? `\x1b[1m${value}\x1b[0m` : value);
|
|
55
|
-
|
|
56
|
-
// Light violet -> Clubedge brand violet, top to bottom across the banner.
|
|
57
|
-
const gradientFrom = [165, 140, 255];
|
|
58
|
-
const gradientTo = [91, 61, 245];
|
|
59
|
-
const mix = (from, to, t) => from.map((channel, index) => Math.round(channel + (to[index] - channel) * t));
|
|
60
|
-
|
|
61
|
-
function showBanner() {
|
|
62
|
-
// Stay quiet when output is piped (CI, logs, scripts).
|
|
63
|
-
if (!process.stdout.isTTY) return;
|
|
64
|
-
|
|
65
|
-
const columns = process.stdout.columns ?? 80;
|
|
66
|
-
const lines = [...renderWord("CLUBEDGE"), ...renderWord("STARTER")];
|
|
67
|
-
const widest = Math.max(...lines.map((line) => line.length));
|
|
68
|
-
|
|
69
|
-
console.log("");
|
|
70
|
-
if (columns >= widest + 2) {
|
|
71
|
-
lines.forEach((line, index) => {
|
|
72
|
-
console.log(paint(mix(gradientFrom, gradientTo, index / (lines.length - 1)), line));
|
|
73
|
-
});
|
|
74
|
-
} else {
|
|
75
|
-
// Narrow terminal: compact wordmark instead of wrapping the big letters.
|
|
76
|
-
console.log(paint(gradientTo, bold("CLUBEDGE STARTER")));
|
|
77
|
-
}
|
|
78
|
-
console.log("");
|
|
79
|
-
console.log(` ${bold("create-clubedge-app")} ${dim(`v${packageVersion}`)} ${dim("·")} ${paint(gradientFrom, cliUrl)}`);
|
|
80
|
-
console.log(` ${dim("By Clubedge")} ${dim("·")} ${paint(gradientFrom, siteUrl)}`);
|
|
81
|
-
console.log("");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/* ------------------------------------------------------------------ */
|
|
85
|
-
|
|
86
|
-
function showHelp() {
|
|
87
|
-
console.log(`
|
|
88
|
-
create-clubedge-app ${packageVersion}
|
|
89
|
-
|
|
90
|
-
Create a Next.js project from the Clubedge Starter reference repository.
|
|
91
|
-
This CLI release uses Starter ${defaultStarterRef} by default.
|
|
92
|
-
Expected Starter commit: ${defaultStarterCommit}
|
|
93
|
-
|
|
94
|
-
Usage:
|
|
95
|
-
pnpm dlx @clubedge/create-clubedge-app [project-directory] [options]
|
|
96
|
-
|
|
97
|
-
Options:
|
|
98
|
-
--ref <ref> Starter tag, branch, or commit to scaffold (default: ${defaultStarterRef})
|
|
99
|
-
--no-install Skip dependency installation
|
|
100
|
-
--no-git Skip Git repository initialization
|
|
101
|
-
-h, --help Show this help
|
|
102
|
-
-v, --version Show the CLI version
|
|
103
|
-
|
|
104
|
-
Examples:
|
|
105
|
-
pnpm dlx @clubedge/create-clubedge-app my-app
|
|
106
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref <starter-tag-or-commit>
|
|
107
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --no-install
|
|
108
|
-
|
|
109
|
-
Release model:
|
|
110
|
-
CLI ${packageVersion} -> Starter ${defaultStarterRef}
|
|
111
|
-
|
|
112
|
-
Learn more:
|
|
113
|
-
CLI ${cliUrl}
|
|
114
|
-
Clubedge ${siteUrl}
|
|
115
|
-
`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function parseArguments(args) {
|
|
119
|
-
const options = { install: true, git: true, ref: defaultStarterRef, projectDirectory: undefined };
|
|
120
|
-
|
|
121
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
122
|
-
const argument = args[index];
|
|
123
|
-
|
|
124
|
-
if (argument === "--help" || argument === "-h") {
|
|
125
|
-
return { ...options, help: true };
|
|
126
|
-
}
|
|
127
|
-
if (argument === "--version" || argument === "-v") {
|
|
128
|
-
return { ...options, version: true };
|
|
129
|
-
}
|
|
130
|
-
if (argument === "--no-install") {
|
|
131
|
-
options.install = false;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (argument === "--no-git") {
|
|
135
|
-
options.git = false;
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (argument === "--ref" || argument.startsWith("--ref=")) {
|
|
139
|
-
const value = argument === "--ref" ? args[++index] : argument.slice("--ref=".length);
|
|
140
|
-
if (!value || value.startsWith("-")) {
|
|
141
|
-
throw new Error("--ref requires a branch or tag name.");
|
|
142
|
-
}
|
|
143
|
-
options.ref = value;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (argument.startsWith("-")) {
|
|
147
|
-
throw new Error(`Unknown option: ${argument}. Run with --help to see available options.`);
|
|
148
|
-
}
|
|
149
|
-
if (options.projectDirectory) {
|
|
150
|
-
throw new Error("Only one project directory can be provided.");
|
|
151
|
-
}
|
|
152
|
-
options.projectDirectory = argument;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return options;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function isCancelled(value) {
|
|
159
|
-
if (isCancel(value)) {
|
|
160
|
-
cancel("Operation cancelled.");
|
|
161
|
-
process.exit(0);
|
|
162
|
-
}
|
|
163
|
-
return value;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function packageNameFromDirectory(directory) {
|
|
167
|
-
return (
|
|
168
|
-
basename(directory)
|
|
169
|
-
.toLowerCase()
|
|
170
|
-
.replace(/[^a-z0-9._-]+/g, "-")
|
|
171
|
-
.replace(/^[._-]+|[._-]+$/g, "") || "my-app"
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function displayNameFromPackageName(packageName) {
|
|
176
|
-
return packageName
|
|
177
|
-
.split(/[-._]+/)
|
|
178
|
-
.filter(Boolean)
|
|
179
|
-
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
180
|
-
.join(" ");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function run(command, args, cwd) {
|
|
184
|
-
await new Promise((resolvePromise, rejectPromise) => {
|
|
185
|
-
const child = spawn(command, args, {
|
|
186
|
-
cwd,
|
|
187
|
-
stdio: "inherit",
|
|
188
|
-
shell: process.platform === "win32",
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
child.once("error", rejectPromise);
|
|
192
|
-
child.once("exit", (code, signal) => {
|
|
193
|
-
if (code === 0) {
|
|
194
|
-
resolvePromise();
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
rejectPromise(new Error(`${command} exited with ${signal ? `signal ${signal}` : `code ${code}`}.`));
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function runCapture(command, args, cwd) {
|
|
203
|
-
return await new Promise((resolvePromise, rejectPromise) => {
|
|
204
|
-
const child = spawn(command, args, {
|
|
205
|
-
cwd,
|
|
206
|
-
shell: false,
|
|
207
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
208
|
-
});
|
|
209
|
-
let stdout = "";
|
|
210
|
-
let stderr = "";
|
|
211
|
-
child.stdout.on("data", (chunk) => {
|
|
212
|
-
stdout += chunk;
|
|
213
|
-
});
|
|
214
|
-
child.stderr.on("data", (chunk) => {
|
|
215
|
-
stderr += chunk;
|
|
216
|
-
});
|
|
217
|
-
child.once("error", rejectPromise);
|
|
218
|
-
child.once("exit", (code, signal) => {
|
|
219
|
-
if (code === 0) {
|
|
220
|
-
resolvePromise(stdout.trim());
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
rejectPromise(
|
|
224
|
-
new Error(
|
|
225
|
-
`${command} exited with ${signal ? `signal ${signal}` : `code ${code}`}${
|
|
226
|
-
stderr.trim() ? `: ${stderr.trim()}` : "."
|
|
227
|
-
}`,
|
|
228
|
-
),
|
|
229
|
-
);
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function verifyDefaultStarterReference(starterRef) {
|
|
235
|
-
if (starterRef !== defaultStarterRef) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const remote = `https://github.com/${starterRepository}.git`;
|
|
240
|
-
const resolvedReference = await runCapture("git", [
|
|
241
|
-
"ls-remote",
|
|
242
|
-
remote,
|
|
243
|
-
`refs/tags/${starterRef}^{}`,
|
|
244
|
-
]);
|
|
245
|
-
const resolvedCommit = resolvedReference.split(/\s+/)[0];
|
|
246
|
-
if (resolvedCommit !== defaultStarterCommit) {
|
|
247
|
-
throw new Error(
|
|
248
|
-
`Starter tag ${starterRef} resolved to ${resolvedCommit || "no commit"}, expected ${defaultStarterCommit}.`,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function ensureTargetIsSafe(directory) {
|
|
254
|
-
await mkdir(dirname(directory), { recursive: true });
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
const entries = await readdir(directory);
|
|
258
|
-
if (entries.length > 0) {
|
|
259
|
-
throw new Error(`The target directory is not empty: ${directory}`);
|
|
260
|
-
}
|
|
261
|
-
} catch (error) {
|
|
262
|
-
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
throw error;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async function fileExists(path) {
|
|
270
|
-
try {
|
|
271
|
-
await access(path);
|
|
272
|
-
return true;
|
|
273
|
-
} catch {
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** Starter v0.2+ keeps project identity in apps/web/src/config/site.json. */
|
|
279
|
-
async function customizeSiteConfig(siteConfigPath, packageName, displayName) {
|
|
280
|
-
const siteConfig = JSON.parse(await readFile(siteConfigPath, "utf8"));
|
|
281
|
-
siteConfig.name = displayName;
|
|
282
|
-
siteConfig.shortName = displayName;
|
|
283
|
-
siteConfig.description = `${displayName} application foundation.`;
|
|
284
|
-
siteConfig.serviceId = packageName;
|
|
285
|
-
siteConfig.workspaceLabel = "Application workspace";
|
|
286
|
-
await writeFile(siteConfigPath, `${JSON.stringify(siteConfig, null, 2)}\n`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** Starter v0.1.x has no site.json, so names are replaced in known source files. */
|
|
290
|
-
async function customizeLegacySources(directory, displayName) {
|
|
291
|
-
const layoutPath = join(directory, "apps", "web", "src", "app", "layout.tsx");
|
|
292
|
-
const layout = await readFile(layoutPath, "utf8");
|
|
293
|
-
const customizedLayout = layout
|
|
294
|
-
.replaceAll('"Clubedge Starter"', JSON.stringify(displayName))
|
|
295
|
-
.replace(`%s \u00b7 Clubedge Starter`, `%s \u00b7 ${displayName}`)
|
|
296
|
-
.replace(
|
|
297
|
-
"A production-minded foundation for Clubedge applications.",
|
|
298
|
-
`${displayName} application foundation.`,
|
|
299
|
-
);
|
|
300
|
-
await writeFile(layoutPath, customizedLayout);
|
|
301
|
-
|
|
302
|
-
const appFiles = [
|
|
303
|
-
join(directory, "apps", "web", "src", "app", "page.tsx"),
|
|
304
|
-
join(directory, "apps", "web", "src", "app", "dashboard", "_components", "app-sidebar.tsx"),
|
|
305
|
-
join(directory, "apps", "web", "src", "app", "dashboard", "page.tsx"),
|
|
306
|
-
join(directory, "apps", "web", "src", "app", "login", "page.tsx"),
|
|
307
|
-
];
|
|
308
|
-
for (const appFile of appFiles) {
|
|
309
|
-
let source;
|
|
310
|
-
try {
|
|
311
|
-
source = await readFile(appFile, "utf8");
|
|
312
|
-
} catch (error) {
|
|
313
|
-
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
throw error;
|
|
317
|
-
}
|
|
318
|
-
const customizedSource = source
|
|
319
|
-
.replaceAll("Clubedge Starter", displayName)
|
|
320
|
-
.replaceAll('>Clubedge<', `>${displayName}<`)
|
|
321
|
-
.replaceAll("Starter workspace", "Application workspace");
|
|
322
|
-
await writeFile(appFile, customizedSource);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
async function customizeProject(
|
|
327
|
-
directory,
|
|
328
|
-
packageName,
|
|
329
|
-
starterRef = defaultStarterRef,
|
|
330
|
-
starterCommit = starterRef === defaultStarterRef ? defaultStarterCommit : undefined,
|
|
331
|
-
) {
|
|
332
|
-
const rootManifestPath = join(directory, "package.json");
|
|
333
|
-
const rootManifest = JSON.parse(await readFile(rootManifestPath, "utf8"));
|
|
334
|
-
rootManifest.name = packageName;
|
|
335
|
-
rootManifest.clubedge = {
|
|
336
|
-
cliVersion: packageVersion,
|
|
337
|
-
starterRepository,
|
|
338
|
-
starterRef,
|
|
339
|
-
...(starterCommit ? { starterCommit } : {}),
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
for (const scriptName of ["docker:build", "docker:start"]) {
|
|
343
|
-
if (rootManifest.scripts?.[scriptName]) {
|
|
344
|
-
rootManifest.scripts[scriptName] = rootManifest.scripts[scriptName].replace(
|
|
345
|
-
/clubedge-starter/g,
|
|
346
|
-
packageName,
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
await writeFile(rootManifestPath, `${JSON.stringify(rootManifest, null, 2)}\n`);
|
|
351
|
-
|
|
352
|
-
const readmePath = join(directory, "README.md");
|
|
353
|
-
const readme = await readFile(readmePath, "utf8");
|
|
354
|
-
const displayName = displayNameFromPackageName(packageName);
|
|
355
|
-
const starterCommitLine = starterCommit ? `\n- Starter commit: \`${starterCommit}\`` : "";
|
|
356
|
-
const customizedReadme = readme
|
|
357
|
-
.replace(/^# Clubedge Starter$/m, `# ${displayName}`)
|
|
358
|
-
.replace(
|
|
359
|
-
/^# (.+)$/m,
|
|
360
|
-
`$&\n\n## Generated from\n\n- Starter repository: \`${starterRepository}\`\n- Starter ref: \`${starterRef}\`${starterCommitLine}\n- CLI version: \`${packageVersion}\``,
|
|
361
|
-
);
|
|
362
|
-
await writeFile(readmePath, customizedReadme);
|
|
363
|
-
|
|
364
|
-
const siteConfigPath = join(directory, "apps", "web", "src", "config", "site.json");
|
|
365
|
-
if (await fileExists(siteConfigPath)) {
|
|
366
|
-
await customizeSiteConfig(siteConfigPath, packageName, displayName);
|
|
367
|
-
} else {
|
|
368
|
-
await customizeLegacySources(directory, displayName);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const exampleEnvironment = join(directory, ".env.example");
|
|
372
|
-
const localEnvironment = join(directory, "apps", "web", ".env.local");
|
|
373
|
-
await cp(exampleEnvironment, localEnvironment, { errorOnExist: true });
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function initializeGit(directory) {
|
|
377
|
-
try {
|
|
378
|
-
await run("git", ["init", "-b", "main"], directory);
|
|
379
|
-
} catch {
|
|
380
|
-
await run("git", ["init"], directory);
|
|
381
|
-
await run("git", ["branch", "-M", "main"], directory);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
async function createProject(options) {
|
|
386
|
-
let requestedDirectory = options.projectDirectory;
|
|
387
|
-
if (!requestedDirectory) {
|
|
388
|
-
requestedDirectory = isCancelled(
|
|
389
|
-
await text({
|
|
390
|
-
message: "Where should the new project be created?",
|
|
391
|
-
placeholder: "my-app",
|
|
392
|
-
validate(value) {
|
|
393
|
-
if (!value.trim()) return "Enter a project directory name.";
|
|
394
|
-
},
|
|
395
|
-
}),
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const projectDirectory = resolve(process.cwd(), requestedDirectory);
|
|
400
|
-
if (projectDirectory === resolve(dirname(projectDirectory))) {
|
|
401
|
-
throw new Error("Choose a project subdirectory rather than a filesystem root.");
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
await ensureTargetIsSafe(projectDirectory);
|
|
405
|
-
|
|
406
|
-
const projectName = packageNameFromDirectory(projectDirectory);
|
|
407
|
-
await verifyDefaultStarterReference(options.ref);
|
|
408
|
-
const source = `gh:${starterRepository}#${options.ref}`;
|
|
409
|
-
const downloadSpinner = spinner();
|
|
410
|
-
downloadSpinner.start(`Downloading Clubedge Starter (${options.ref})`);
|
|
411
|
-
try {
|
|
412
|
-
await downloadTemplate(source, { dir: projectDirectory, force: true });
|
|
413
|
-
await customizeProject(projectDirectory, projectName, options.ref);
|
|
414
|
-
downloadSpinner.stop("Starter files downloaded and configured");
|
|
415
|
-
} catch (error) {
|
|
416
|
-
downloadSpinner.stop("Project scaffold failed");
|
|
417
|
-
throw error;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (options.git) {
|
|
421
|
-
console.log("\nInitializing Git...");
|
|
422
|
-
await initializeGit(projectDirectory);
|
|
423
|
-
console.log("Git repository initialized on main");
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (options.install) {
|
|
427
|
-
console.log("\nInstalling dependencies with pnpm...\n");
|
|
428
|
-
try {
|
|
429
|
-
await run("pnpm", ["install"], projectDirectory);
|
|
430
|
-
console.log("\nDependencies installed");
|
|
431
|
-
} catch (error) {
|
|
432
|
-
console.error("\nProject created; dependency installation did not complete.");
|
|
433
|
-
console.error(error instanceof Error ? error.message : error);
|
|
434
|
-
console.error("Install pnpm with Corepack, then run `pnpm install` in the new project.");
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const relativeDirectory = process.cwd() === projectDirectory ? "." : requestedDirectory;
|
|
439
|
-
outro(
|
|
440
|
-
`Created ${projectName}.\n\nNext steps:\n cd ${relativeDirectory}\n pnpm dev\n\nSee SETUP.md for database and provider configuration.\n\nDocs: ${cliUrl}\nClubedge: ${siteUrl}`,
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
async function main() {
|
|
445
|
-
try {
|
|
446
|
-
const options = parseArguments(process.argv.slice(2));
|
|
447
|
-
if (options.version) {
|
|
448
|
-
console.log(packageVersion);
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
showBanner();
|
|
453
|
-
|
|
454
|
-
if (options.help) {
|
|
455
|
-
showHelp();
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
intro("Create a project with Clubedge Starter");
|
|
460
|
-
await createProject(options);
|
|
461
|
-
} catch (error) {
|
|
462
|
-
console.error(`\n${error instanceof Error ? error.message : String(error)}`);
|
|
463
|
-
process.exitCode = 1;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
export { customizeProject, displayNameFromPackageName, packageNameFromDirectory };
|
|
468
|
-
|
|
469
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
470
|
-
await main();
|
|
471
|
-
}
|
|
4
|
+
await main();
|