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