create-cronus-app 0.6.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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/compose.d.ts +56 -0
- package/dist/compose.js +92 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +255 -0
- package/dist/scaffold.d.ts +31 -0
- package/dist/scaffold.js +118 -0
- package/dist/utils.d.ts +121 -0
- package/dist/utils.js +285 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +3 -0
- package/package.json +56 -0
- package/templates/dashboard/README.md +61 -0
- package/templates/dashboard/app/globals.css +16 -0
- package/templates/dashboard/app/layout.tsx +40 -0
- package/templates/dashboard/app/page.tsx +77 -0
- package/templates/dashboard/app/settings/page.tsx +21 -0
- package/templates/dashboard/components/dashboard-shell.tsx +120 -0
- package/templates/dashboard/components/orders-table.tsx +132 -0
- package/templates/dashboard/components/revenue-chart.tsx +50 -0
- package/templates/dashboard/components/settings-form.tsx +167 -0
- package/templates/dashboard/cronus-ui.json +17 -0
- package/templates/dashboard/gitignore +34 -0
- package/templates/dashboard/next.config.mjs +11 -0
- package/templates/dashboard/package.json +30 -0
- package/templates/dashboard/postcss.config.mjs +7 -0
- package/templates/dashboard/tsconfig.json +23 -0
- package/templates/default/README.md +46 -0
- package/templates/default/app/globals.css +16 -0
- package/templates/default/app/layout.tsx +39 -0
- package/templates/default/app/page.tsx +112 -0
- package/templates/default/cronus-ui.json +17 -0
- package/templates/default/gitignore +34 -0
- package/templates/default/next.config.mjs +11 -0
- package/templates/default/package.json +27 -0
- package/templates/default/postcss.config.mjs +7 -0
- package/templates/default/tsconfig.json +23 -0
- package/templates/marketing/README.md +65 -0
- package/templates/marketing/app/globals.css +16 -0
- package/templates/marketing/app/layout.tsx +39 -0
- package/templates/marketing/app/page.tsx +25 -0
- package/templates/marketing/components/faq.tsx +62 -0
- package/templates/marketing/components/feature-grid.tsx +67 -0
- package/templates/marketing/components/hero.tsx +61 -0
- package/templates/marketing/components/pricing.tsx +116 -0
- package/templates/marketing/components/site-footer.tsx +91 -0
- package/templates/marketing/components/site-header.tsx +47 -0
- package/templates/marketing/components/testimonials.tsx +91 -0
- package/templates/marketing/components/waitlist-cta.tsx +93 -0
- package/templates/marketing/cronus-ui.json +17 -0
- package/templates/marketing/gitignore +34 -0
- package/templates/marketing/next.config.mjs +11 -0
- package/templates/marketing/package.json +28 -0
- package/templates/marketing/postcss.config.mjs +7 -0
- package/templates/marketing/tsconfig.json +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cronus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# create-cronus-app
|
|
2
|
+
|
|
3
|
+
Scaffold a production-ready **Next.js + Cronus UI** app in one command.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx create-cronus-app my-app --template saas
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
That is the gold path: a composed multi-page SaaS product — split login/signup,
|
|
10
|
+
sidebar-shell dashboard, analytics, team, billing, and settings — built from
|
|
11
|
+
validated registry blocks (the generator's golden rule: pages only import and
|
|
12
|
+
stack installed blocks).
|
|
13
|
+
|
|
14
|
+
You get Next.js 16 (App Router) + React 19 + Tailwind v4 with
|
|
15
|
+
[`@cronus-ui/ui`](https://www.npmjs.com/package/@cronus-ui/ui), `@cronus-ui/tokens`,
|
|
16
|
+
and `@cronus-ui/theme` already wired — themeable, accessible components and an
|
|
17
|
+
anti-flash theme script.
|
|
18
|
+
|
|
19
|
+
The CLI default remains `--template default` (a single-page starter). Pass
|
|
20
|
+
`--template saas` when you want the full product.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
create-cronus-app [project-name] [options]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If you omit the project name, you are prompted (default: `my-cronus-app`).
|
|
29
|
+
Interactive runs also prompt for template, theme, mode, and the AI Kit.
|
|
30
|
+
|
|
31
|
+
### Options
|
|
32
|
+
|
|
33
|
+
| Flag | Description |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `--template <name>` | Starter template. CLI default: `default`. Bundled: `default`, `dashboard`, `marketing`. Composed: `store`, `landing`, `saas`, plus `landing-*` flavors (studio, ops, secure, care, shop, docs, premium, agents, coverage, broadcast, agency, glass). |
|
|
36
|
+
| `--theme <aurora\|neutral\|midnight\|sunset\|emerald>` | Theme preset to bake in (default: `aurora`). |
|
|
37
|
+
| `--mode <dark\|light>` | Default color mode (default: `dark`). |
|
|
38
|
+
| `--ai` / `--no-ai` | Include or skip the AI Kit (skills, rules & doctrine). |
|
|
39
|
+
| `--assistants <claude,cursor,copilot,windsurf,gemini\|all\|none>` | Assistants to configure (default: all). Codex CLI and Zed read the root `AGENTS.md` natively. |
|
|
40
|
+
| `--preset <standard\|fintech\|saas\|oss\|agency\|none>` | Engineering doctrine to ship (default: `standard`). |
|
|
41
|
+
| `--skills <ui-add,theme,compose,code-review,ship-pr,evidence-check\|all\|none>` | Claude Code skills to include (default: all). |
|
|
42
|
+
| `--pm <bun\|npm\|pnpm\|yarn>` | Package manager to install with (auto-detected otherwise). |
|
|
43
|
+
| `--no-install` | Skip installing dependencies. |
|
|
44
|
+
| `-y`, `--yes` | Accept defaults; skip interactive prompts. |
|
|
45
|
+
| `-h`, `--help` | Show help. |
|
|
46
|
+
| `-v`, `--version` | Show the version. |
|
|
47
|
+
|
|
48
|
+
There are no other flags.
|
|
49
|
+
|
|
50
|
+
### Templates
|
|
51
|
+
|
|
52
|
+
`default`, `dashboard`, and `marketing` copy a bundled directory.
|
|
53
|
+
|
|
54
|
+
`store`, `landing`, `saas`, the `landing-*` flavors, and the Pro pack
|
|
55
|
+
(`mail`, `chat`, `finance`) have **no** bundled template dir. They scaffold
|
|
56
|
+
the `default` base, then compose pages + chrome from validated registry
|
|
57
|
+
blocks (the same engine as `npx cronus-ui compose <template>`). OSS keeps
|
|
58
|
+
saas/store/landing; the Pro pack is additive.
|
|
59
|
+
|
|
60
|
+
| Template | What you get |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `default` | Single-page starter — metrics, table & cards. The CLI default (`--yes` without `--template` lands here). |
|
|
63
|
+
| `dashboard` | Bundled multi-page app — sidebar shell, KPIs, chart, data table, settings. |
|
|
64
|
+
| `marketing` | Bundled landing site — hero, features, pricing, testimonials, FAQ, waitlist. |
|
|
65
|
+
| `store` | Composed storefront — 9 navigable pages, real nav, from validated blocks. |
|
|
66
|
+
| `landing` | Composed marketing page — hero, features, pricing, testimonials, FAQ, CTA. |
|
|
67
|
+
| `saas` | Composed SaaS product — split auth + sidebar-shell dashboard, analytics, team, billing, settings. **Recommended for a full product.** |
|
|
68
|
+
| `landing-studio` | Dark AI studio — atmosphere hero, marquee, bento, stats, pricing. Midnight dark. |
|
|
69
|
+
| `landing-ops` | Workflow product — split hero, logos, features, integrations. |
|
|
70
|
+
| `landing-secure` | Infra/security — compact hero, metrics, usage pricing, split FAQ. Midnight dark. |
|
|
71
|
+
| `landing-care` | Healthcare conversion — waitlist hero, bento, proof. Emerald light. |
|
|
72
|
+
| `landing-shop` | Storefront — compact hero + editorial product showcase. Sunset light. |
|
|
73
|
+
| `landing-docs` | Developer tool — compact hero, logos, features, integrations. |
|
|
74
|
+
| `landing-premium` | Full SaaS marketing — split hero, toggle pricing, FAQ. Aurora light. |
|
|
75
|
+
| `landing-agents` | Automation — split hero, marquee, bento, stats. Emerald light. |
|
|
76
|
+
| `landing-coverage` | Services — hero, stats, testimonial grid, FAQ. Sunset light. |
|
|
77
|
+
| `landing-broadcast` | Studio/show — atmosphere hero, marquee, features, pricing. |
|
|
78
|
+
| `landing-agency` | Agency — split hero, about, services, stats. Midnight dark. |
|
|
79
|
+
| `landing-glass` | Glass dark — atmosphere hero, bento, split FAQ, split CTA. |
|
|
80
|
+
| `mail` | **Pro.** Inbox — notification panel, activity feed, compose, preferences. Midnight dark. |
|
|
81
|
+
| `chat` | **Pro.** Assistant — chat thread, prompt box, replies, settings. Aurora dark. |
|
|
82
|
+
| `finance` | **Pro.** Money — payouts, invoices, billing, analytics. Emerald light. |
|
|
83
|
+
|
|
84
|
+
### AI Kit
|
|
85
|
+
|
|
86
|
+
`--ai` writes the kit; `--no-ai` skips it. If you pass neither, an interactive
|
|
87
|
+
run prompts (default **yes**). `--yes` accepts that default, so
|
|
88
|
+
`npx create-cronus-app my-app -y` **does** include the kit — pass `--no-ai` to
|
|
89
|
+
skip it. `--assistants`, `--preset`, and `--skills` only take effect when the
|
|
90
|
+
kit is written.
|
|
91
|
+
|
|
92
|
+
### Examples
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
# Gold path — composed SaaS product.
|
|
96
|
+
npx create-cronus-app my-app --template saas
|
|
97
|
+
|
|
98
|
+
# Same, non-interactive, skip the AI Kit, install with pnpm.
|
|
99
|
+
npx create-cronus-app my-app --template saas --no-ai --pm pnpm --yes
|
|
100
|
+
|
|
101
|
+
# Bundled dashboard starter.
|
|
102
|
+
npx create-cronus-app my-app --template dashboard
|
|
103
|
+
|
|
104
|
+
# Theme + mode (any template).
|
|
105
|
+
npx create-cronus-app my-app --template saas --theme aurora --mode dark
|
|
106
|
+
|
|
107
|
+
# AI Kit for a subset of assistants.
|
|
108
|
+
npx create-cronus-app my-app --template saas --ai --assistants claude,cursor --preset fintech
|
|
109
|
+
|
|
110
|
+
# Files only; install later yourself.
|
|
111
|
+
npx create-cronus-app my-app --template saas --no-install
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## What you get
|
|
115
|
+
|
|
116
|
+
A `--template saas` app (after compose) looks like:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
my-app/
|
|
120
|
+
├─ app/
|
|
121
|
+
│ ├─ globals.css # Tailwind v4 + Cronus tokens + the @source opt-in
|
|
122
|
+
│ ├─ layout.tsx # <CronusUIProvider> + anti-flash <CronusThemeScript>
|
|
123
|
+
│ ├─ (bare)/login # split-variant sign-in
|
|
124
|
+
│ ├─ (bare)/signup
|
|
125
|
+
│ └─ (shell)/ # dashboard (/), analytics, team, billing, settings
|
|
126
|
+
├─ components/blocks/ # installed registry blocks; pages only stack them
|
|
127
|
+
├─ cronus-ui.json # so `npx cronus-ui add` / add-page / compose work here
|
|
128
|
+
├─ next.config.mjs
|
|
129
|
+
├─ postcss.config.mjs
|
|
130
|
+
├─ tsconfig.json
|
|
131
|
+
└─ package.json
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`default` is the single-page starter (`app/page.tsx` only). `store` and
|
|
135
|
+
`landing` compose the same way as `saas`, with their own routes and chrome.
|
|
136
|
+
|
|
137
|
+
After scaffolding:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
cd my-app
|
|
141
|
+
npm install # if you used --no-install
|
|
142
|
+
npm run dev
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Open [http://localhost:3000](http://localhost:3000).
|
|
146
|
+
|
|
147
|
+
## How to grow
|
|
148
|
+
|
|
149
|
+
Composed apps (`saas` / `store` / `landing`) grow with `add-page`, not by
|
|
150
|
+
hand-wiring routes. Switch theme or preview upgrades at any time:
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
npx cronus-ui add-page --route /pricing --blocks pricing,cta --nav Pricing
|
|
154
|
+
npx cronus-ui theme set aurora --mode dark
|
|
155
|
+
npx cronus-ui upgrade --all --dry-run
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Every app still has a `cronus-ui.json`, so individual components work too:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
npx cronus-ui add dialog table tabs
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Started from `default`, `dashboard`, or `marketing` and want a multi-page
|
|
165
|
+
product? Compose one onto the project:
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
npx cronus-ui compose saas
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-scaffold composition adapter.
|
|
3
|
+
*
|
|
4
|
+
* `store`/`landing` have no bundled template dir: create-cronus-app copies the
|
|
5
|
+
* `default` base, then this module calls `cronus-ui`'s `composeApp()` library
|
|
6
|
+
* entry to generate the pages + chrome from validated registry blocks.
|
|
7
|
+
*
|
|
8
|
+
* The import is dynamic so the rest of the CLI (and its tests) never pay for
|
|
9
|
+
* loading `cronus-ui/compose` — only composed templates reach here.
|
|
10
|
+
*
|
|
11
|
+
* Registry resolution (F1 honest cut — the chicken-and-egg):
|
|
12
|
+
* A freshly scaffolded app's `cronus-ui.json` pins the registry to the release
|
|
13
|
+
* remote (`v<version>/registry`), which only ships the `meta.json` sidecar
|
|
14
|
+
* compose needs from v0.4.0+. `composeApp` hard-requires that sidecar. So:
|
|
15
|
+
* 1. `CRONUS_UI_REGISTRY` (env) wins — the dev/CI escape hatch that points at
|
|
16
|
+
* the local workspace `registry/` dir (which has meta.json today).
|
|
17
|
+
* 2. Otherwise `composeApp` uses the project's own config registry (the
|
|
18
|
+
* release remote — correct once v0.4.0+ publishes meta.json).
|
|
19
|
+
* When the registry lacks meta.json, compose fails gracefully and the caller
|
|
20
|
+
* keeps the valid `default` scaffold with a clear "run compose later" hint.
|
|
21
|
+
*/
|
|
22
|
+
/** What create-cronus-app needs from the composer (a subset of `ComposeAppResult`). */
|
|
23
|
+
export interface ComposeTemplateOptions {
|
|
24
|
+
/** The scaffolded project root (contains cronus-ui.json). */
|
|
25
|
+
targetDir: string;
|
|
26
|
+
/** The composed template name (`store` / `landing`). */
|
|
27
|
+
template: string;
|
|
28
|
+
/** Brand string baked into chrome/hero copy (the project name). */
|
|
29
|
+
brand: string;
|
|
30
|
+
/** Skip the composer's own `pm add` (the final create-cronus-app install covers it). */
|
|
31
|
+
skipInstall?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Registry override. Defaults to `process.env.CRONUS_UI_REGISTRY` so dev/CI can
|
|
34
|
+
* point at the local workspace registry; falls through to the project config
|
|
35
|
+
* when unset.
|
|
36
|
+
*/
|
|
37
|
+
registry?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Structured outcome so the caller can log a page count or a graceful skip. */
|
|
40
|
+
export type ComposeTemplateResult = {
|
|
41
|
+
ok: true;
|
|
42
|
+
pageCount: number;
|
|
43
|
+
blockCount: number;
|
|
44
|
+
files: string[];
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
reason: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Compose a bundled app template into the freshly scaffolded project. Never
|
|
51
|
+
* throws: a compose failure (e.g. the pinned registry has no meta.json yet)
|
|
52
|
+
* returns `{ ok: false, reason }` so scaffolding still succeeds with the valid
|
|
53
|
+
* `default` base.
|
|
54
|
+
*/
|
|
55
|
+
export declare function composeTemplate(options: ComposeTemplateOptions): Promise<ComposeTemplateResult>;
|
|
56
|
+
//# sourceMappingURL=compose.d.ts.map
|
package/dist/compose.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-scaffold composition adapter.
|
|
3
|
+
*
|
|
4
|
+
* `store`/`landing` have no bundled template dir: create-cronus-app copies the
|
|
5
|
+
* `default` base, then this module calls `cronus-ui`'s `composeApp()` library
|
|
6
|
+
* entry to generate the pages + chrome from validated registry blocks.
|
|
7
|
+
*
|
|
8
|
+
* The import is dynamic so the rest of the CLI (and its tests) never pay for
|
|
9
|
+
* loading `cronus-ui/compose` — only composed templates reach here.
|
|
10
|
+
*
|
|
11
|
+
* Registry resolution (F1 honest cut — the chicken-and-egg):
|
|
12
|
+
* A freshly scaffolded app's `cronus-ui.json` pins the registry to the release
|
|
13
|
+
* remote (`v<version>/registry`), which only ships the `meta.json` sidecar
|
|
14
|
+
* compose needs from v0.4.0+. `composeApp` hard-requires that sidecar. So:
|
|
15
|
+
* 1. `CRONUS_UI_REGISTRY` (env) wins — the dev/CI escape hatch that points at
|
|
16
|
+
* the local workspace `registry/` dir (which has meta.json today).
|
|
17
|
+
* 2. Otherwise `composeApp` uses the project's own config registry (the
|
|
18
|
+
* release remote — correct once v0.4.0+ publishes meta.json).
|
|
19
|
+
* When the registry lacks meta.json, compose fails gracefully and the caller
|
|
20
|
+
* keeps the valid `default` scaffold with a clear "run compose later" hint.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, rmSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
/**
|
|
25
|
+
* True when `file` is a generated page that owns the root URL "/".
|
|
26
|
+
*
|
|
27
|
+
* The renderer emits the home page as `app/(<chromeGroup>)/page.tsx` (route "/"
|
|
28
|
+
* inside a path-transparent route group), never a bare `app/page.tsx`. So a
|
|
29
|
+
* generated `app/(<group>)/page.tsx` is the compose-authored home page.
|
|
30
|
+
*/
|
|
31
|
+
function isGeneratedRootPage(file) {
|
|
32
|
+
return /^app\/\([^/]+\)\/page\.tsx$/.test(file);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Remove the base template's `app/page.tsx` when compose generated its own home
|
|
36
|
+
* page. `create-cronus-app store|landing` scaffolds the `default` base (which
|
|
37
|
+
* ships `app/page.tsx`, the single-page dashboard starter) and THEN composes
|
|
38
|
+
* `app/(<group>)/page.tsx` for the manifest's "/" route. In the Next.js App
|
|
39
|
+
* Router a route group like `(site)` is path-transparent, so `app/page.tsx` and
|
|
40
|
+
* `app/(site)/page.tsx` both own "/". Left together, `next build` either errors
|
|
41
|
+
* ("two parallel pages resolve to /") or — on the Turbopack builder the template
|
|
42
|
+
* uses — silently serves the leftover base starter and drops the composed store
|
|
43
|
+
* home. Deleting the now-superseded base page leaves exactly one "/" page.
|
|
44
|
+
*
|
|
45
|
+
* The base `app/layout.tsx` (the required <html>/<body> root layout) is KEPT:
|
|
46
|
+
* Next allows a root `app/layout.tsx` plus nested route-group layouts
|
|
47
|
+
* (`app/(site)/layout.tsx`) to coexist, so only the PAGE collides.
|
|
48
|
+
*/
|
|
49
|
+
function removeSupersededBasePage(targetDir, generatedFiles) {
|
|
50
|
+
// Only delete the base page when compose actually authored a "/" page to
|
|
51
|
+
// replace it — never orphan the app of a home route.
|
|
52
|
+
if (!generatedFiles.some(isGeneratedRootPage))
|
|
53
|
+
return;
|
|
54
|
+
const basePage = join(targetDir, "app", "page.tsx");
|
|
55
|
+
if (existsSync(basePage))
|
|
56
|
+
rmSync(basePage);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Compose a bundled app template into the freshly scaffolded project. Never
|
|
60
|
+
* throws: a compose failure (e.g. the pinned registry has no meta.json yet)
|
|
61
|
+
* returns `{ ok: false, reason }` so scaffolding still succeeds with the valid
|
|
62
|
+
* `default` base.
|
|
63
|
+
*/
|
|
64
|
+
export async function composeTemplate(options) {
|
|
65
|
+
const registry = options.registry ?? process.env.CRONUS_UI_REGISTRY;
|
|
66
|
+
try {
|
|
67
|
+
// Dynamic import: only composed templates load the compose library, and a
|
|
68
|
+
// missing/unbuilt `cronus-ui/compose` degrades to a graceful skip rather than
|
|
69
|
+
// crashing the scaffolder.
|
|
70
|
+
const { composeApp } = await import("cronus-ui/compose");
|
|
71
|
+
const result = await composeApp({
|
|
72
|
+
targetDir: options.targetDir,
|
|
73
|
+
template: options.template,
|
|
74
|
+
choices: { brand: options.brand },
|
|
75
|
+
skipInstall: options.skipInstall ?? true,
|
|
76
|
+
...(registry !== undefined ? { registry } : {}),
|
|
77
|
+
});
|
|
78
|
+
// Compose owns the "/" route via app/(<group>)/page.tsx; drop the base
|
|
79
|
+
// template's colliding app/page.tsx so exactly one page resolves to "/".
|
|
80
|
+
removeSupersededBasePage(options.targetDir, result.generatedFiles);
|
|
81
|
+
return {
|
|
82
|
+
ok: true,
|
|
83
|
+
pageCount: result.generatedFiles.filter((f) => f.endsWith("page.tsx")).length,
|
|
84
|
+
blockCount: result.installedBlocks.length,
|
|
85
|
+
files: result.generatedFiles,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return { ok: false, reason: err.message };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=compose.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type Assistant, type DoctrinePreset, type Skill } from "@cronus-ui/ai-kit";
|
|
3
|
+
import { type ModeName, type PackageManager, type TemplateName, type Theme } from "./utils.js";
|
|
4
|
+
export declare const HELP: string;
|
|
5
|
+
interface ParsedCli {
|
|
6
|
+
/** Project name from positional arg, or undefined to prompt. */
|
|
7
|
+
name?: string;
|
|
8
|
+
pm?: PackageManager;
|
|
9
|
+
install: boolean;
|
|
10
|
+
/** Template from `--template`, or undefined to prompt. */
|
|
11
|
+
template?: TemplateName;
|
|
12
|
+
/** Theme from `--theme`, or undefined to prompt. */
|
|
13
|
+
theme?: Theme;
|
|
14
|
+
/** Mode from `--mode`, or undefined to prompt. */
|
|
15
|
+
mode?: ModeName;
|
|
16
|
+
/** `--yes`: accept defaults, skip prompts (also implied when not a TTY). */
|
|
17
|
+
yes: boolean;
|
|
18
|
+
/** AI Kit: true (`--ai`) / false (`--no-ai`) / undefined (decide later). */
|
|
19
|
+
ai?: boolean;
|
|
20
|
+
/** Assistants to configure (defaults to all). */
|
|
21
|
+
assistants?: readonly Assistant[];
|
|
22
|
+
/** Doctrine preset (defaults to "standard"). */
|
|
23
|
+
preset?: DoctrinePreset;
|
|
24
|
+
/** Claude Code skills to include (defaults to all). */
|
|
25
|
+
skills?: readonly Skill[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse argv into options. Throws a friendly Error on bad input (unknown flag,
|
|
29
|
+
* invalid `--pm`) so the caller can print and exit non-zero.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseCli(argv: string[]): ParsedCli;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readdirSync, realpathSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { argv } from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { ASSISTANTS, DEFAULT_PRESET, DOCTRINE_PRESETS, parseList, SKILLS, writeAiKit, writeDesignDocuments, } from "@cronus-ui/ai-kit";
|
|
8
|
+
import { composeTemplate } from "./compose.js";
|
|
9
|
+
import { runInstall, scaffold } from "./scaffold.js";
|
|
10
|
+
import { c, DEFAULT_MODE, DEFAULT_TEMPLATE, DEFAULT_THEME, dirNameFromProjectName, isComposedTemplate, isValidProjectName, log, MODES, PACKAGE_MANAGERS, promptConfirm, promptSelect, TEMPLATE_APPEARANCE, TEMPLATE_HINTS, TEMPLATES, THEME_HINTS, THEMES, } from "./utils.js";
|
|
11
|
+
import { CREATE_VERSION } from "./version.js";
|
|
12
|
+
export const HELP = `${c.bold("create-cronus-app")} — scaffold a Next.js + Cronus UI app.
|
|
13
|
+
|
|
14
|
+
${c.bold("Usage")}
|
|
15
|
+
create-cronus-app [project-name] [options]
|
|
16
|
+
|
|
17
|
+
${c.bold("Options")}
|
|
18
|
+
--template <name> Starter template (default: ${DEFAULT_TEMPLATE}).
|
|
19
|
+
Bundled: default, dashboard, marketing.
|
|
20
|
+
Composed: store, landing, saas, landing-* flavors, and Pro pack (mail, chat, finance).
|
|
21
|
+
--theme <${THEMES.join("|")}>
|
|
22
|
+
Theme preset to bake in (default: ${DEFAULT_THEME})
|
|
23
|
+
--mode <${MODES.join("|")}> Default color mode (default: ${DEFAULT_MODE})
|
|
24
|
+
--ai / --no-ai Include (or skip) the AI Kit: skills, rules & doctrine.
|
|
25
|
+
DESIGN.md still ships with --no-ai.
|
|
26
|
+
--assistants <${ASSISTANTS.join(",")}|all|none>
|
|
27
|
+
Which assistants to configure (default: all).
|
|
28
|
+
Codex CLI and Zed read the root AGENTS.md natively.
|
|
29
|
+
--preset <${DOCTRINE_PRESETS.join("|")}>
|
|
30
|
+
Which engineering doctrine to ship (default: standard)
|
|
31
|
+
--skills <name,...|all|none>
|
|
32
|
+
Which Claude Code skills to include (default: all)
|
|
33
|
+
--pm <bun|npm|pnpm|yarn> Package manager to install with (auto-detected otherwise)
|
|
34
|
+
--no-install Skip installing dependencies
|
|
35
|
+
-y, --yes Accept defaults; skip interactive prompts
|
|
36
|
+
-h, --help Show this help
|
|
37
|
+
-v, --version Show the version
|
|
38
|
+
|
|
39
|
+
${c.bold("Examples")}
|
|
40
|
+
npx create-cronus-app my-app --template saas
|
|
41
|
+
npx create-cronus-app my-app --template landing-studio
|
|
42
|
+
npx create-cronus-app my-app --template mail
|
|
43
|
+
npx create-cronus-app my-app --template dashboard
|
|
44
|
+
npx create-cronus-app my-app --theme sunset --mode light
|
|
45
|
+
npx create-cronus-app my-app --ai --assistants claude,cursor --preset fintech
|
|
46
|
+
npx create-cronus-app my-app --no-ai --pm pnpm --yes
|
|
47
|
+
`;
|
|
48
|
+
/**
|
|
49
|
+
* Parse argv into options. Throws a friendly Error on bad input (unknown flag,
|
|
50
|
+
* invalid `--pm`) so the caller can print and exit non-zero.
|
|
51
|
+
*/
|
|
52
|
+
export function parseCli(argv) {
|
|
53
|
+
// `node:util` parseArgs has no built-in `--no-foo` negation, so we declare an
|
|
54
|
+
// explicit `no-install` flag rather than rely on a `--no-install` convention.
|
|
55
|
+
const { values, positionals } = parseArgs({
|
|
56
|
+
args: argv,
|
|
57
|
+
allowPositionals: true,
|
|
58
|
+
options: {
|
|
59
|
+
template: { type: "string" },
|
|
60
|
+
theme: { type: "string" },
|
|
61
|
+
mode: { type: "string" },
|
|
62
|
+
ai: { type: "boolean", default: false },
|
|
63
|
+
"no-ai": { type: "boolean", default: false },
|
|
64
|
+
assistants: { type: "string" },
|
|
65
|
+
preset: { type: "string" },
|
|
66
|
+
skills: { type: "string" },
|
|
67
|
+
pm: { type: "string" },
|
|
68
|
+
"no-install": { type: "boolean", default: false },
|
|
69
|
+
yes: { type: "boolean", short: "y", default: false },
|
|
70
|
+
help: { type: "boolean", short: "h", default: false },
|
|
71
|
+
version: { type: "boolean", short: "v", default: false },
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
if (values.help)
|
|
75
|
+
return { install: true, yes: false, name: "--help" };
|
|
76
|
+
if (values.version)
|
|
77
|
+
return { install: true, yes: false, name: "--version" };
|
|
78
|
+
const pm = values.pm;
|
|
79
|
+
if (pm !== undefined && !PACKAGE_MANAGERS.includes(pm)) {
|
|
80
|
+
throw new Error(`Unknown --pm "${pm}". Use one of: ${PACKAGE_MANAGERS.join(", ")}.`);
|
|
81
|
+
}
|
|
82
|
+
const template = values.template;
|
|
83
|
+
if (template !== undefined && !TEMPLATES.includes(template)) {
|
|
84
|
+
throw new Error(`Unknown --template "${template}". Use one of: ${TEMPLATES.join(", ")}.`);
|
|
85
|
+
}
|
|
86
|
+
const theme = values.theme;
|
|
87
|
+
if (theme !== undefined && !THEMES.includes(theme)) {
|
|
88
|
+
throw new Error(`Unknown --theme "${theme}". Use one of: ${THEMES.join(", ")}.`);
|
|
89
|
+
}
|
|
90
|
+
const mode = values.mode;
|
|
91
|
+
if (mode !== undefined && !MODES.includes(mode)) {
|
|
92
|
+
throw new Error(`Unknown --mode "${mode}". Use one of: ${MODES.join(", ")}.`);
|
|
93
|
+
}
|
|
94
|
+
const preset = values.preset;
|
|
95
|
+
if (preset !== undefined && !DOCTRINE_PRESETS.includes(preset)) {
|
|
96
|
+
throw new Error(`Unknown --preset "${preset}". Use one of: ${DOCTRINE_PRESETS.join(", ")}.`);
|
|
97
|
+
}
|
|
98
|
+
// `parseList` validates each token and expands "all"/empty to the full set.
|
|
99
|
+
const assistants = parseList(values.assistants, ASSISTANTS, "assistant");
|
|
100
|
+
const skills = parseList(values.skills, SKILLS, "skill");
|
|
101
|
+
return {
|
|
102
|
+
name: positionals[0],
|
|
103
|
+
pm: pm,
|
|
104
|
+
install: !values["no-install"],
|
|
105
|
+
template: template,
|
|
106
|
+
theme: theme,
|
|
107
|
+
mode: mode,
|
|
108
|
+
yes: values.yes,
|
|
109
|
+
ai: values["no-ai"] ? false : values.ai ? true : undefined,
|
|
110
|
+
assistants,
|
|
111
|
+
preset: preset ?? DEFAULT_PRESET,
|
|
112
|
+
skills,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Prompt for a project name on the TTY; falls back to the default when piped. */
|
|
116
|
+
async function promptName(defaultName) {
|
|
117
|
+
if (!process.stdin.isTTY)
|
|
118
|
+
return defaultName;
|
|
119
|
+
const { createInterface } = await import("node:readline/promises");
|
|
120
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
121
|
+
try {
|
|
122
|
+
const answer = (await rl.question(`${c.bold("Project name")} ${c.dim(`(${defaultName})`)} `)).trim();
|
|
123
|
+
return answer || defaultName;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
rl.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Detect a package manager from the npm user-agent (set when run via npx/pnpm dlx). */
|
|
130
|
+
function detectPackageManager() {
|
|
131
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
132
|
+
if (ua.startsWith("bun"))
|
|
133
|
+
return "bun";
|
|
134
|
+
if (ua.startsWith("pnpm"))
|
|
135
|
+
return "pnpm";
|
|
136
|
+
if (ua.startsWith("yarn"))
|
|
137
|
+
return "yarn";
|
|
138
|
+
return "npm";
|
|
139
|
+
}
|
|
140
|
+
async function main() {
|
|
141
|
+
const parsed = parseCli(process.argv.slice(2));
|
|
142
|
+
if (parsed.name === "--help") {
|
|
143
|
+
process.stdout.write(`${HELP}\n`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (parsed.name === "--version") {
|
|
147
|
+
process.stdout.write(`${CREATE_VERSION}\n`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
log.intro();
|
|
151
|
+
const name = parsed.name ?? (await promptName("my-cronus-app"));
|
|
152
|
+
if (!isValidProjectName(name)) {
|
|
153
|
+
log.error(`"${name}" is not a valid package name. Use lowercase letters, digits, "-", "_", "." ` +
|
|
154
|
+
`(optionally scoped, e.g. @scope/name).`);
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const dirName = dirNameFromProjectName(name);
|
|
159
|
+
const targetDir = join(process.cwd(), dirName);
|
|
160
|
+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
|
|
161
|
+
log.error(`Directory "${dirName}" already exists and is not empty.`);
|
|
162
|
+
process.exitCode = 1;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Template + theme + mode: an explicit flag wins; otherwise prompt on a TTY
|
|
166
|
+
// (unless --yes), and fall back to the ship defaults when piped/CI.
|
|
167
|
+
const template = parsed.template ??
|
|
168
|
+
(parsed.yes
|
|
169
|
+
? DEFAULT_TEMPLATE
|
|
170
|
+
: await promptSelect("Template", TEMPLATES, DEFAULT_TEMPLATE, TEMPLATE_HINTS));
|
|
171
|
+
const appearance = TEMPLATE_APPEARANCE[template];
|
|
172
|
+
const theme = parsed.theme ??
|
|
173
|
+
(parsed.yes
|
|
174
|
+
? (appearance?.theme ?? DEFAULT_THEME)
|
|
175
|
+
: await promptSelect("Theme", THEMES, appearance?.theme ?? DEFAULT_THEME, THEME_HINTS));
|
|
176
|
+
const mode = parsed.mode ??
|
|
177
|
+
(parsed.yes
|
|
178
|
+
? (appearance?.mode ?? DEFAULT_MODE)
|
|
179
|
+
: await promptSelect("Default mode", MODES, appearance?.mode ?? DEFAULT_MODE));
|
|
180
|
+
log.step(`Scaffolding into ${c.cyan(dirName)}…`);
|
|
181
|
+
const { fileCount } = scaffold({ targetDir, name, theme, mode, template });
|
|
182
|
+
log.ok(`Created ${fileCount} files (${c.cyan(template)} template, ${c.cyan(theme)} theme, ${mode} mode).`);
|
|
183
|
+
// Cronus Compose: store/landing scaffold the `default` base above, then generate
|
|
184
|
+
// their pages + chrome from validated registry blocks. The blocks pull npm deps
|
|
185
|
+
// (e.g. lucide-react) that the base package.json lacks, so let compose record +
|
|
186
|
+
// install them via `pm add` when installing is enabled; a following `--no-install`
|
|
187
|
+
// scaffold records nothing (matching `cronus-ui add --no-install`).
|
|
188
|
+
if (isComposedTemplate(template)) {
|
|
189
|
+
log.step(`Composing the ${c.cyan(template)} app from validated blocks…`);
|
|
190
|
+
const composed = await composeTemplate({
|
|
191
|
+
targetDir,
|
|
192
|
+
template,
|
|
193
|
+
brand: name,
|
|
194
|
+
skipInstall: !parsed.install,
|
|
195
|
+
});
|
|
196
|
+
if (composed.ok) {
|
|
197
|
+
log.ok(`Composed ${composed.pageCount} page(s) from ${composed.blockCount} validated block(s).`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
log.warn(`Skipped composition (${composed.reason}). The ${c.cyan("default")} app was scaffolded; ` +
|
|
201
|
+
`run ${c.cyan(`npx cronus-ui compose ${template}`)} once the registry ships meta.json.`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// AI Kit: skills, rules & doctrine for Claude Code / Cursor / Copilot.
|
|
205
|
+
const wantAi = parsed.ai ??
|
|
206
|
+
(parsed.yes
|
|
207
|
+
? true
|
|
208
|
+
: await promptConfirm("Add the AI Kit (skills, rules & doctrine for AI assistants)?", true));
|
|
209
|
+
if (wantAi) {
|
|
210
|
+
const assistants = parsed.assistants ?? ASSISTANTS;
|
|
211
|
+
const preset = parsed.preset ?? DEFAULT_PRESET;
|
|
212
|
+
const skills = parsed.skills ?? SKILLS;
|
|
213
|
+
const { written } = writeAiKit({
|
|
214
|
+
targetDir,
|
|
215
|
+
name,
|
|
216
|
+
assistants,
|
|
217
|
+
preset,
|
|
218
|
+
skills,
|
|
219
|
+
theme,
|
|
220
|
+
});
|
|
221
|
+
log.ok(`AI Kit added — ${written.length} files for ${assistants.join(", ")} (${preset} doctrine).`);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const { written } = writeDesignDocuments(targetDir, { theme });
|
|
225
|
+
if (written.length > 0) {
|
|
226
|
+
log.ok("DESIGN.md added (visual taste).");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const pm = parsed.pm ?? detectPackageManager();
|
|
230
|
+
if (parsed.install) {
|
|
231
|
+
log.step(`Installing dependencies with ${c.cyan(pm)}…`);
|
|
232
|
+
try {
|
|
233
|
+
runInstall(pm, targetDir);
|
|
234
|
+
log.ok("Dependencies installed.");
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
log.warn(`Install failed (${err.message}). Install manually later.`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
log.outro(dirName, pm, parsed.install, template);
|
|
241
|
+
}
|
|
242
|
+
/** True when this module is the process entry point (not imported by a test). */
|
|
243
|
+
function isEntrypoint() {
|
|
244
|
+
const entry = argv[1];
|
|
245
|
+
if (!entry)
|
|
246
|
+
return false;
|
|
247
|
+
return existsSync(entry) && fileURLToPath(import.meta.url) === realpathSync(entry);
|
|
248
|
+
}
|
|
249
|
+
if (isEntrypoint()) {
|
|
250
|
+
main().catch((err) => {
|
|
251
|
+
log.error(err.message);
|
|
252
|
+
process.exitCode = 1;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ModeName, type PackageManager, type TemplateName, type Theme } from "./utils.js";
|
|
2
|
+
/**
|
|
3
|
+
* Replace template tokens. `__APP_NAME__` is the package name; `__THEME__` /
|
|
4
|
+
* `__MODE__` are the chosen theme preset + color mode (baked into layout.tsx and
|
|
5
|
+
* cronus-ui.json). Theme/mode default to the ship defaults so existing callers
|
|
6
|
+
* (and tests) that pass only a name keep working.
|
|
7
|
+
*/
|
|
8
|
+
export declare function applyTokens(content: string, name: string, theme?: Theme, mode?: ModeName): string;
|
|
9
|
+
export interface ScaffoldOptions {
|
|
10
|
+
/** Absolute path of the directory to create. */
|
|
11
|
+
targetDir: string;
|
|
12
|
+
/** Package name written into package.json / README (may be scoped). */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Theme preset baked into the app. @default "aurora" */
|
|
15
|
+
theme?: Theme;
|
|
16
|
+
/** Default color mode baked into the app. @default "dark" */
|
|
17
|
+
mode?: ModeName;
|
|
18
|
+
/** Starter template to copy. @default "default" */
|
|
19
|
+
template?: TemplateName;
|
|
20
|
+
}
|
|
21
|
+
export interface ScaffoldResult {
|
|
22
|
+
fileCount: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Recursively copy the chosen template into `targetDir`, replacing tokens in
|
|
26
|
+
* text files and restoring stripped dotfile names. Returns the file count.
|
|
27
|
+
*/
|
|
28
|
+
export declare function scaffold(options: ScaffoldOptions): ScaffoldResult;
|
|
29
|
+
/** Run the package manager's install in `cwd`. Throws if it exits non-zero. */
|
|
30
|
+
export declare function runInstall(pm: PackageManager, cwd: string): void;
|
|
31
|
+
//# sourceMappingURL=scaffold.d.ts.map
|