@waelio/cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -54
- package/dist/index.js +14 -0
- package/dist/scaffold.d.ts +35 -0
- package/dist/scaffold.js +333 -0
- package/dist/server.js +83 -1
- package/package.json +5 -4
- package/ui/dist/assets/{index-BJ1Dzrgp.css → index-C3Eg0a_I.css} +1 -1
- package/ui/dist/assets/index-Cde-X4PD.js +18 -0
- package/ui/dist/index.html +2 -2
- package/dist/localRepos.test.d.ts +0 -1
- package/dist/localRepos.test.js +0 -51
- package/dist/siteforge.test.d.ts +0 -1
- package/dist/siteforge.test.js +0 -53
- package/ui/dist/assets/index-DS_pYwiX.js +0 -18
package/README.md
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
# waelio
|
|
1
|
+
# @waelio/cli
|
|
2
2
|
|
|
3
|
-
TypeScript toolkit for building [`waelio/siteforge`](https://github.com/waelio/siteforge) from
|
|
3
|
+
TypeScript CLI + browser UI toolkit for building [`waelio/siteforge`](https://github.com/waelio/siteforge) from the terminal or a local web interface.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
This repo now uses **Vite + TypeScript + Vue** for the on-screen UI.
|
|
8
|
-
|
|
9
|
-
Why this stack fits well here:
|
|
10
|
-
|
|
11
|
-
- **Vite** keeps development fast
|
|
12
|
-
- **TypeScript** matches the rest of the repo and the build pipeline
|
|
13
|
-
- **Vue** fits naturally with the broader waelio ecosystem and is quick to iterate on
|
|
5
|
+
Built with **Vite + TypeScript + Vue**.
|
|
14
6
|
|
|
15
7
|
## What it does
|
|
16
8
|
|
|
@@ -28,79 +20,170 @@ Why this stack fits well here:
|
|
|
28
20
|
- git
|
|
29
21
|
- Go
|
|
30
22
|
|
|
31
|
-
## Install
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
Run without installing:
|
|
32
26
|
|
|
33
27
|
```sh
|
|
34
|
-
|
|
28
|
+
npx @waelio/cli --help
|
|
35
29
|
```
|
|
36
30
|
|
|
37
|
-
|
|
31
|
+
Or install globally:
|
|
38
32
|
|
|
39
33
|
```sh
|
|
40
|
-
npm
|
|
34
|
+
npm install -g @waelio/cli
|
|
41
35
|
```
|
|
42
36
|
|
|
43
|
-
|
|
37
|
+
## CLI commands
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
- the Vite UI on `http://localhost:5173`
|
|
39
|
+
### `waelio --help`
|
|
47
40
|
|
|
48
|
-
|
|
41
|
+
Print the top-level help and list all available commands:
|
|
49
42
|
|
|
50
43
|
```sh
|
|
51
|
-
|
|
44
|
+
waelio --help
|
|
52
45
|
```
|
|
53
46
|
|
|
54
|
-
|
|
47
|
+
### `waelio doctor`
|
|
48
|
+
|
|
49
|
+
Check that all required tools (`git`, `npm`, `go`) are installed and accessible:
|
|
55
50
|
|
|
56
51
|
```sh
|
|
57
|
-
|
|
52
|
+
waelio doctor
|
|
58
53
|
```
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
### `waelio build`
|
|
56
|
+
|
|
57
|
+
Clone and build the `waelio/siteforge` website:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
waelio build
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**All options:**
|
|
61
64
|
|
|
62
|
-
|
|
65
|
+
```sh
|
|
66
|
+
waelio build \
|
|
67
|
+
--repo https://github.com/waelio/siteforge.git \ # custom repository URL (default: waelio/siteforge)
|
|
68
|
+
--ref main \ # branch, tag, or commit to checkout
|
|
69
|
+
--source ./my-local-siteforge \ # skip cloning, use an existing checkout
|
|
70
|
+
--workdir ./build-tmp \ # directory to clone into
|
|
71
|
+
--dry-run # print the plan without running anything
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Common examples:**
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
# default — clone and build waelio/siteforge
|
|
78
|
+
waelio build
|
|
79
|
+
|
|
80
|
+
# preview what would happen without executing
|
|
81
|
+
waelio build --dry-run
|
|
82
|
+
|
|
83
|
+
# build a specific branch
|
|
84
|
+
waelio build --ref feat/new-homepage
|
|
85
|
+
|
|
86
|
+
# build from a local checkout you already have
|
|
87
|
+
waelio build --source ~/Code/GitHub/waelio/siteforge
|
|
88
|
+
|
|
89
|
+
# clone into a custom directory
|
|
90
|
+
waelio build --workdir /tmp/siteforge-build
|
|
91
|
+
|
|
92
|
+
# build a fork
|
|
93
|
+
waelio build --repo https://github.com/yourname/siteforge.git
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `waelio ui`
|
|
97
|
+
|
|
98
|
+
Start the local web UI and API server (browser dashboard with live build logs):
|
|
63
99
|
|
|
64
100
|
```sh
|
|
65
|
-
|
|
101
|
+
waelio ui
|
|
66
102
|
```
|
|
67
103
|
|
|
68
|
-
|
|
104
|
+
**Options:**
|
|
69
105
|
|
|
70
106
|
```sh
|
|
71
|
-
|
|
107
|
+
waelio ui --port 4000 # default port is 3000
|
|
72
108
|
```
|
|
73
109
|
|
|
74
|
-
|
|
110
|
+
Open `http://localhost:3000` in your browser after running this.
|
|
75
111
|
|
|
76
|
-
|
|
77
|
-
- `--ref <ref>` — branch, tag, or commit to checkout before building
|
|
78
|
-
- `--source <path>` — use an existing local checkout instead of cloning
|
|
79
|
-
- `--workdir <path>` — directory to clone into when `--source` is not provided
|
|
80
|
-
- `--dry-run` — print the build plan without executing it
|
|
112
|
+
### `waelio scaffold <blueprint>`
|
|
81
113
|
|
|
82
|
-
|
|
114
|
+
Scaffold a full Next.js (frontend) + NestJS (backend) project from a siteforge blueprint JSON:
|
|
83
115
|
|
|
84
116
|
```sh
|
|
85
|
-
|
|
117
|
+
waelio scaffold ./blueprint.json
|
|
86
118
|
```
|
|
87
119
|
|
|
88
|
-
|
|
120
|
+
**All options:**
|
|
89
121
|
|
|
90
122
|
```sh
|
|
91
|
-
|
|
123
|
+
waelio scaffold ./blueprint.json \
|
|
124
|
+
--out ./sites \ # output root directory (default: siteforge/sites)
|
|
125
|
+
--no-git # skip git init and the initial commit
|
|
92
126
|
```
|
|
93
127
|
|
|
94
|
-
|
|
128
|
+
**Common examples:**
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
# scaffold with defaults
|
|
132
|
+
waelio scaffold ./blueprint.json
|
|
95
133
|
|
|
96
|
-
|
|
97
|
-
|
|
134
|
+
# scaffold into a custom output directory
|
|
135
|
+
waelio scaffold ./blueprint.json --out ~/projects/my-site
|
|
98
136
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
137
|
+
# scaffold without initialising a git repository
|
|
138
|
+
waelio scaffold ./blueprint.json --no-git
|
|
139
|
+
|
|
140
|
+
# combine options
|
|
141
|
+
waelio scaffold ./blueprint.json --out ./sites --no-git
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The blueprint JSON is produced by siteforge and describes the project name, slug,
|
|
145
|
+
pages, features, integrations, locales, roles, brand tones, visual styles,
|
|
146
|
+
content models, and SEO focuses. The scaffolder generates both workspaces from
|
|
147
|
+
those selections.
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
### Install dependencies
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
npm install
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Run in development
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
npm run dev
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Starts:
|
|
164
|
+
|
|
165
|
+
- API/build server on `http://localhost:3000`
|
|
166
|
+
- Vite UI on `http://localhost:5173`
|
|
167
|
+
|
|
168
|
+
### Build everything
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
npm run build
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Run the built server
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
npm start
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Other scripts
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
npm run typecheck # tsc --noEmit + vue-tsc for the UI
|
|
184
|
+
npm test # run *.test.ts via tsx --test
|
|
185
|
+
npm run check # typecheck + tests
|
|
186
|
+
```
|
|
104
187
|
|
|
105
188
|
## Helper repos surfaced in the UI
|
|
106
189
|
|
|
@@ -118,15 +201,6 @@ locales ship by default:
|
|
|
118
201
|
|
|
119
202
|
RTL locales (`ar`, `he`) should be considered when adding or editing UI copy.
|
|
120
203
|
|
|
121
|
-
## Scripts
|
|
122
|
-
|
|
123
|
-
- `npm run dev` — run the API server and Vite UI together
|
|
124
|
-
- `npm run build` — build the CLI (`tsc`) and the UI (`vite build`)
|
|
125
|
-
- `npm start` — run the built server (`dist/server.js`)
|
|
126
|
-
- `npm run typecheck` — `tsc --noEmit` plus `vue-tsc` for the UI
|
|
127
|
-
- `npm test` — run `*.test.ts` via `tsx --test`
|
|
128
|
-
- `npm run check` — typecheck + tests
|
|
129
|
-
|
|
130
204
|
## Local repository discovery
|
|
131
205
|
|
|
132
206
|
The UI and API now scan your local GitHub workspace and build a sanitized repository index.
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { DEFAULT_SITEFORGE_REPO, runBuild, runDoctor } from "./siteforge.js";
|
|
4
|
+
import { scaffold } from "./scaffold.js";
|
|
4
5
|
async function main() {
|
|
5
6
|
const program = new Command();
|
|
6
7
|
program
|
|
@@ -43,6 +44,19 @@ async function main() {
|
|
|
43
44
|
}
|
|
44
45
|
await startServer({ port });
|
|
45
46
|
});
|
|
47
|
+
program
|
|
48
|
+
.command("scaffold <blueprint>")
|
|
49
|
+
.description("Scaffold a Next.js + NestJS project from a siteforge blueprint JSON")
|
|
50
|
+
.option("--out <dir>", "output root (default: siteforge/sites)")
|
|
51
|
+
.option("--no-git", "skip git init / initial commit")
|
|
52
|
+
.action(async (blueprint, options) => {
|
|
53
|
+
const result = await scaffold({
|
|
54
|
+
blueprintPath: blueprint,
|
|
55
|
+
outRoot: options.out,
|
|
56
|
+
initGit: options.git,
|
|
57
|
+
});
|
|
58
|
+
console.log(`Scaffolded "${result.slug}" (${result.pageCount} pages) at:\n ${result.outDir}`);
|
|
59
|
+
});
|
|
46
60
|
await program.parseAsync(process.argv);
|
|
47
61
|
}
|
|
48
62
|
main().catch((error) => {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface BlueprintSelections {
|
|
2
|
+
selectedPages?: string[];
|
|
3
|
+
selectedFeatures?: string[];
|
|
4
|
+
selectedIntegrations?: string[];
|
|
5
|
+
selectedLocales?: string[];
|
|
6
|
+
selectedRoles?: string[];
|
|
7
|
+
selectedBrandTones?: string[];
|
|
8
|
+
selectedVisualStyles?: string[];
|
|
9
|
+
selectedContentModels?: string[];
|
|
10
|
+
selectedSEOFocuses?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface Blueprint {
|
|
13
|
+
id?: string;
|
|
14
|
+
createdAt?: string;
|
|
15
|
+
projectName?: string;
|
|
16
|
+
slug?: string;
|
|
17
|
+
selections?: BlueprintSelections;
|
|
18
|
+
}
|
|
19
|
+
export interface ScaffoldOptions {
|
|
20
|
+
blueprintPath: string;
|
|
21
|
+
outRoot?: string;
|
|
22
|
+
initGit?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface ScaffoldFromBlueprintOptions {
|
|
25
|
+
blueprint: Blueprint;
|
|
26
|
+
outRoot?: string;
|
|
27
|
+
initGit?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface ScaffoldResult {
|
|
30
|
+
slug: string;
|
|
31
|
+
outDir: string;
|
|
32
|
+
pageCount: number;
|
|
33
|
+
}
|
|
34
|
+
export declare function scaffoldFromBlueprint(options: ScaffoldFromBlueprintOptions): Promise<ScaffoldResult>;
|
|
35
|
+
export declare function scaffold(options: ScaffoldOptions): Promise<ScaffoldResult>;
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const DEFAULT_OUT_ROOT = "/Users/waelio/Code/GitHub/waelio/siteforge/sites";
|
|
7
|
+
function slugify(value) {
|
|
8
|
+
return value
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
11
|
+
.replace(/^-+|-+$/g, "")
|
|
12
|
+
|| "site";
|
|
13
|
+
}
|
|
14
|
+
function pageToRoute(name) {
|
|
15
|
+
const trimmed = name.trim();
|
|
16
|
+
if (!trimmed || trimmed.toLowerCase() === "home")
|
|
17
|
+
return "/";
|
|
18
|
+
return "/" + slugify(trimmed);
|
|
19
|
+
}
|
|
20
|
+
function pagePath(route) {
|
|
21
|
+
if (route === "/")
|
|
22
|
+
return "app/page.tsx";
|
|
23
|
+
return `app${route}/page.tsx`;
|
|
24
|
+
}
|
|
25
|
+
async function writeFileEnsured(filePath, content) {
|
|
26
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
27
|
+
await writeFile(filePath, content, "utf8");
|
|
28
|
+
}
|
|
29
|
+
function frontendPackageJson(name) {
|
|
30
|
+
return JSON.stringify({
|
|
31
|
+
name: `${name}-frontend`,
|
|
32
|
+
version: "0.0.1",
|
|
33
|
+
private: true,
|
|
34
|
+
scripts: {
|
|
35
|
+
dev: "next dev -p 3001",
|
|
36
|
+
build: "next build",
|
|
37
|
+
start: "next start -p 3001",
|
|
38
|
+
lint: "next lint",
|
|
39
|
+
},
|
|
40
|
+
dependencies: {
|
|
41
|
+
next: "^14.2.0",
|
|
42
|
+
react: "^18.3.1",
|
|
43
|
+
"react-dom": "^18.3.1",
|
|
44
|
+
},
|
|
45
|
+
devDependencies: {
|
|
46
|
+
typescript: "^5.4.0",
|
|
47
|
+
"@types/node": "^20.0.0",
|
|
48
|
+
"@types/react": "^18.3.0",
|
|
49
|
+
"@types/react-dom": "^18.3.0",
|
|
50
|
+
},
|
|
51
|
+
}, null, 2);
|
|
52
|
+
}
|
|
53
|
+
const NEXT_TSCONFIG = `{
|
|
54
|
+
"compilerOptions": {
|
|
55
|
+
"target": "ES2020",
|
|
56
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
57
|
+
"allowJs": false,
|
|
58
|
+
"skipLibCheck": true,
|
|
59
|
+
"strict": true,
|
|
60
|
+
"noEmit": true,
|
|
61
|
+
"esModuleInterop": true,
|
|
62
|
+
"module": "esnext",
|
|
63
|
+
"moduleResolution": "bundler",
|
|
64
|
+
"resolveJsonModule": true,
|
|
65
|
+
"isolatedModules": true,
|
|
66
|
+
"jsx": "preserve",
|
|
67
|
+
"incremental": true,
|
|
68
|
+
"plugins": [{ "name": "next" }],
|
|
69
|
+
"baseUrl": ".",
|
|
70
|
+
"paths": { "@/*": ["./*"] }
|
|
71
|
+
},
|
|
72
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
|
73
|
+
"exclude": ["node_modules"]
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
const NEXT_CONFIG = `/** @type {import('next').NextConfig} */
|
|
77
|
+
const nextConfig = { reactStrictMode: true };
|
|
78
|
+
module.exports = nextConfig;
|
|
79
|
+
`;
|
|
80
|
+
const NEXT_ENV_DTS = `/// <reference types="next" />
|
|
81
|
+
/// <reference types="next/image-types/global" />
|
|
82
|
+
`;
|
|
83
|
+
function rootLayout(name, pages) {
|
|
84
|
+
const navItems = pages
|
|
85
|
+
.map((p) => `<a href="${p.route}" style={{ marginRight: 16 }}>${p.name}</a>`)
|
|
86
|
+
.join("\n ");
|
|
87
|
+
return `import type { ReactNode } from "react";
|
|
88
|
+
|
|
89
|
+
export const metadata = {
|
|
90
|
+
title: "${name}",
|
|
91
|
+
description: "Generated by waelio/cli + siteforge",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
95
|
+
return (
|
|
96
|
+
<html lang="en">
|
|
97
|
+
<body style={{ fontFamily: "system-ui, sans-serif", margin: 0 }}>
|
|
98
|
+
<header style={{ padding: 16, borderBottom: "1px solid #eee" }}>
|
|
99
|
+
<strong>${name}</strong>
|
|
100
|
+
<nav style={{ marginTop: 8 }}>
|
|
101
|
+
${navItems}
|
|
102
|
+
</nav>
|
|
103
|
+
</header>
|
|
104
|
+
<main style={{ padding: 24 }}>{children}</main>
|
|
105
|
+
</body>
|
|
106
|
+
</html>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
function pageComponent(pageName, projectName) {
|
|
112
|
+
const isHome = pageName.toLowerCase() === "home" || pageName === "/";
|
|
113
|
+
const heading = isHome ? projectName : pageName;
|
|
114
|
+
return `export default function Page() {
|
|
115
|
+
return (
|
|
116
|
+
<section>
|
|
117
|
+
<h1>${heading}</h1>
|
|
118
|
+
<p>Generated ${pageName} page.</p>
|
|
119
|
+
</section>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
function backendPackageJson(name) {
|
|
125
|
+
return JSON.stringify({
|
|
126
|
+
name: `${name}-backend`,
|
|
127
|
+
version: "0.0.1",
|
|
128
|
+
private: true,
|
|
129
|
+
scripts: {
|
|
130
|
+
start: "node dist/main.js",
|
|
131
|
+
"start:dev": "ts-node-dev --respawn src/main.ts",
|
|
132
|
+
build: "tsc -p tsconfig.json",
|
|
133
|
+
},
|
|
134
|
+
dependencies: {
|
|
135
|
+
"@nestjs/common": "^10.3.0",
|
|
136
|
+
"@nestjs/core": "^10.3.0",
|
|
137
|
+
"@nestjs/platform-express": "^10.3.0",
|
|
138
|
+
"reflect-metadata": "^0.2.0",
|
|
139
|
+
rxjs: "^7.8.0",
|
|
140
|
+
},
|
|
141
|
+
devDependencies: {
|
|
142
|
+
"@types/node": "^20.0.0",
|
|
143
|
+
"ts-node-dev": "^2.0.0",
|
|
144
|
+
typescript: "^5.4.0",
|
|
145
|
+
},
|
|
146
|
+
}, null, 2);
|
|
147
|
+
}
|
|
148
|
+
const NEST_TSCONFIG = `{
|
|
149
|
+
"compilerOptions": {
|
|
150
|
+
"module": "commonjs",
|
|
151
|
+
"target": "es2021",
|
|
152
|
+
"outDir": "./dist",
|
|
153
|
+
"rootDir": "./src",
|
|
154
|
+
"experimentalDecorators": true,
|
|
155
|
+
"emitDecoratorMetadata": true,
|
|
156
|
+
"esModuleInterop": true,
|
|
157
|
+
"strict": true,
|
|
158
|
+
"skipLibCheck": true
|
|
159
|
+
},
|
|
160
|
+
"include": ["src/**/*.ts"],
|
|
161
|
+
"exclude": ["node_modules", "dist"]
|
|
162
|
+
}
|
|
163
|
+
`;
|
|
164
|
+
function nestMain(modules) {
|
|
165
|
+
const moduleList = modules.map((m) => `"${m}"`).join(", ");
|
|
166
|
+
return `import "reflect-metadata";
|
|
167
|
+
import { NestFactory } from "@nestjs/core";
|
|
168
|
+
import { AppModule } from "./app.module.js";
|
|
169
|
+
|
|
170
|
+
async function bootstrap() {
|
|
171
|
+
const app = await NestFactory.create(AppModule);
|
|
172
|
+
app.enableCors();
|
|
173
|
+
const port = Number(process.env.PORT ?? 3002);
|
|
174
|
+
await app.listen(port);
|
|
175
|
+
console.log(\`backend listening on http://localhost:\${port}\`);
|
|
176
|
+
console.log("modules:", [${moduleList}]);
|
|
177
|
+
}
|
|
178
|
+
bootstrap();
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
const NEST_APP_MODULE = `import { Module } from "@nestjs/common";
|
|
182
|
+
import { HealthController } from "./health.controller.js";
|
|
183
|
+
|
|
184
|
+
@Module({
|
|
185
|
+
controllers: [HealthController],
|
|
186
|
+
})
|
|
187
|
+
export class AppModule {}
|
|
188
|
+
`;
|
|
189
|
+
const NEST_HEALTH_CONTROLLER = `import { Controller, Get } from "@nestjs/common";
|
|
190
|
+
|
|
191
|
+
@Controller("health")
|
|
192
|
+
export class HealthController {
|
|
193
|
+
@Get()
|
|
194
|
+
ok() {
|
|
195
|
+
return { status: "ok", at: new Date().toISOString() };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
199
|
+
function rootReadme(name, slug, blueprint) {
|
|
200
|
+
const sel = blueprint.selections ?? {};
|
|
201
|
+
const list = (xs) => xs && xs.length ? xs.map((x) => `- ${x}`).join("\n") : "_(none)_";
|
|
202
|
+
return `# ${name}
|
|
203
|
+
|
|
204
|
+
Generated by **waelio/cli** from a siteforge blueprint.
|
|
205
|
+
|
|
206
|
+
- **Slug:** \`${slug}\`
|
|
207
|
+
- **Generated:** ${new Date().toISOString()}
|
|
208
|
+
|
|
209
|
+
## Layout
|
|
210
|
+
|
|
211
|
+
- \`frontend/\` — Next.js app (port 3001)
|
|
212
|
+
- \`backend/\` — NestJS app (port 3002)
|
|
213
|
+
|
|
214
|
+
## Run
|
|
215
|
+
|
|
216
|
+
\`\`\`bash
|
|
217
|
+
cd frontend && npm install && npm run dev
|
|
218
|
+
cd ../backend && npm install && npm run start:dev
|
|
219
|
+
\`\`\`
|
|
220
|
+
|
|
221
|
+
## Blueprint selections
|
|
222
|
+
|
|
223
|
+
### Pages
|
|
224
|
+
${list(sel.selectedPages)}
|
|
225
|
+
|
|
226
|
+
### Features
|
|
227
|
+
${list(sel.selectedFeatures)}
|
|
228
|
+
|
|
229
|
+
### Integrations
|
|
230
|
+
${list(sel.selectedIntegrations)}
|
|
231
|
+
|
|
232
|
+
### Locales
|
|
233
|
+
${list(sel.selectedLocales)}
|
|
234
|
+
|
|
235
|
+
### Roles
|
|
236
|
+
${list(sel.selectedRoles)}
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
const ROOT_GITIGNORE = `node_modules/
|
|
240
|
+
dist/
|
|
241
|
+
.next/
|
|
242
|
+
.env
|
|
243
|
+
.env.local
|
|
244
|
+
*.log
|
|
245
|
+
`;
|
|
246
|
+
async function pathExists(target) {
|
|
247
|
+
try {
|
|
248
|
+
await stat(target);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function uniqueOutDir(outRoot, slug) {
|
|
256
|
+
const base = path.join(outRoot, slug);
|
|
257
|
+
if (!(await pathExists(base)))
|
|
258
|
+
return base;
|
|
259
|
+
for (let i = 2; i < 1000; i++) {
|
|
260
|
+
const candidate = path.join(outRoot, `${slug}-${i}`);
|
|
261
|
+
if (!(await pathExists(candidate)))
|
|
262
|
+
return candidate;
|
|
263
|
+
}
|
|
264
|
+
throw new Error(`Unable to find a free output directory for slug "${slug}"`);
|
|
265
|
+
}
|
|
266
|
+
export async function scaffoldFromBlueprint(options) {
|
|
267
|
+
const blueprint = options.blueprint;
|
|
268
|
+
const projectName = blueprint.projectName?.trim() || "Siteforge Project";
|
|
269
|
+
const slugBase = slugify(blueprint.slug || projectName);
|
|
270
|
+
const outRoot = options.outRoot || DEFAULT_OUT_ROOT;
|
|
271
|
+
const outDir = await uniqueOutDir(outRoot, slugBase);
|
|
272
|
+
const slug = path.basename(outDir);
|
|
273
|
+
return writeScaffold(blueprint, projectName, slug, outDir, options.initGit);
|
|
274
|
+
}
|
|
275
|
+
export async function scaffold(options) {
|
|
276
|
+
const raw = await readFile(options.blueprintPath, "utf8");
|
|
277
|
+
const blueprint = JSON.parse(raw);
|
|
278
|
+
return scaffoldFromBlueprint({
|
|
279
|
+
blueprint,
|
|
280
|
+
outRoot: options.outRoot,
|
|
281
|
+
initGit: options.initGit,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async function writeScaffold(blueprint, projectName, slug, outDir, initGit) {
|
|
285
|
+
const sel = blueprint.selections ?? {};
|
|
286
|
+
const pageNames = Array.from(new Set([
|
|
287
|
+
"Home",
|
|
288
|
+
...(sel.selectedPages ?? []),
|
|
289
|
+
]));
|
|
290
|
+
const pages = pageNames.map((name) => ({ name, route: pageToRoute(name) }));
|
|
291
|
+
// Frontend
|
|
292
|
+
await writeFileEnsured(path.join(outDir, "frontend/package.json"), frontendPackageJson(slug));
|
|
293
|
+
await writeFileEnsured(path.join(outDir, "frontend/tsconfig.json"), NEXT_TSCONFIG);
|
|
294
|
+
await writeFileEnsured(path.join(outDir, "frontend/next.config.js"), NEXT_CONFIG);
|
|
295
|
+
await writeFileEnsured(path.join(outDir, "frontend/next-env.d.ts"), NEXT_ENV_DTS);
|
|
296
|
+
await writeFileEnsured(path.join(outDir, "frontend/app/layout.tsx"), rootLayout(projectName, pages));
|
|
297
|
+
for (const page of pages) {
|
|
298
|
+
await writeFileEnsured(path.join(outDir, "frontend", pagePath(page.route)), pageComponent(page.name, projectName));
|
|
299
|
+
}
|
|
300
|
+
// Backend
|
|
301
|
+
const features = sel.selectedFeatures ?? [];
|
|
302
|
+
const modules = ["Health", ...features];
|
|
303
|
+
await writeFileEnsured(path.join(outDir, "backend/package.json"), backendPackageJson(slug));
|
|
304
|
+
await writeFileEnsured(path.join(outDir, "backend/tsconfig.json"), NEST_TSCONFIG);
|
|
305
|
+
await writeFileEnsured(path.join(outDir, "backend/src/main.ts"), nestMain(modules));
|
|
306
|
+
await writeFileEnsured(path.join(outDir, "backend/src/app.module.ts"), NEST_APP_MODULE);
|
|
307
|
+
await writeFileEnsured(path.join(outDir, "backend/src/health.controller.ts"), NEST_HEALTH_CONTROLLER);
|
|
308
|
+
// Root
|
|
309
|
+
await writeFileEnsured(path.join(outDir, "README.md"), rootReadme(projectName, slug, blueprint));
|
|
310
|
+
await writeFileEnsured(path.join(outDir, ".gitignore"), ROOT_GITIGNORE);
|
|
311
|
+
await writeFileEnsured(path.join(outDir, "blueprint.json"), JSON.stringify(blueprint, null, 2));
|
|
312
|
+
if (initGit !== false) {
|
|
313
|
+
try {
|
|
314
|
+
await execFileAsync("git", ["init", "-q"], { cwd: outDir });
|
|
315
|
+
await execFileAsync("git", ["add", "."], { cwd: outDir });
|
|
316
|
+
await execFileAsync("git", ["commit", "-q", "-m", "scaffold from siteforge blueprint"], {
|
|
317
|
+
cwd: outDir,
|
|
318
|
+
env: {
|
|
319
|
+
...process.env,
|
|
320
|
+
GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME ?? "waelio-cli",
|
|
321
|
+
GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL ?? "cli@waelio.dev",
|
|
322
|
+
GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME ?? "waelio-cli",
|
|
323
|
+
GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL ?? "cli@waelio.dev",
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
// Non-fatal: leave folder unversioned if git init fails.
|
|
329
|
+
console.warn(`git init skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return { slug, outDir, pageCount: pages.length };
|
|
333
|
+
}
|