@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 CHANGED
@@ -1,16 +1,8 @@
1
- # waelio CLI + UI
1
+ # @waelio/cli
2
2
 
3
- TypeScript toolkit for building [`waelio/siteforge`](https://github.com/waelio/siteforge) from either the terminal or a browser UI.
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
- ## Recommended UI stack
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 dependencies
23
+ ## Install
24
+
25
+ Run without installing:
32
26
 
33
27
  ```sh
34
- npm install
28
+ npx @waelio/cli --help
35
29
  ```
36
30
 
37
- ## Run the UI in development
31
+ Or install globally:
38
32
 
39
33
  ```sh
40
- npm run dev
34
+ npm install -g @waelio/cli
41
35
  ```
42
36
 
43
- That starts:
37
+ ## CLI commands
44
38
 
45
- - the API/build server on `http://localhost:3000`
46
- - the Vite UI on `http://localhost:5173`
39
+ ### `waelio --help`
47
40
 
48
- ## Build everything
41
+ Print the top-level help and list all available commands:
49
42
 
50
43
  ```sh
51
- npm run build
44
+ waelio --help
52
45
  ```
53
46
 
54
- ## Run the built UI + API server
47
+ ### `waelio doctor`
48
+
49
+ Check that all required tools (`git`, `npm`, `go`) are installed and accessible:
55
50
 
56
51
  ```sh
57
- npm start
52
+ waelio doctor
58
53
  ```
59
54
 
60
- ## CLI commands
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
- ### Check prerequisites
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
- node dist/index.js doctor
101
+ waelio ui
66
102
  ```
67
103
 
68
- ### Build `siteforge`
104
+ **Options:**
69
105
 
70
106
  ```sh
71
- node dist/index.js build
107
+ waelio ui --port 4000 # default port is 3000
72
108
  ```
73
109
 
74
- Options:
110
+ Open `http://localhost:3000` in your browser after running this.
75
111
 
76
- - `--repo <url>` — repository URL to build (defaults to `waelio/siteforge`)
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
- ### Start the local UI/API server from the CLI
114
+ Scaffold a full Next.js (frontend) + NestJS (backend) project from a siteforge blueprint JSON:
83
115
 
84
116
  ```sh
85
- node dist/index.js ui --port 3000
117
+ waelio scaffold ./blueprint.json
86
118
  ```
87
119
 
88
- ### Scaffold a Next.js + NestJS project from a blueprint
120
+ **All options:**
89
121
 
90
122
  ```sh
91
- node dist/index.js scaffold ./blueprint.json --out ./sites
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
- Options:
128
+ **Common examples:**
129
+
130
+ ```sh
131
+ # scaffold with defaults
132
+ waelio scaffold ./blueprint.json
95
133
 
96
- - `--out <dir>` output root (defaults to `siteforge/sites`)
97
- - `--no-git` skip `git init` and the initial commit
134
+ # scaffold into a custom output directory
135
+ waelio scaffold ./blueprint.json --out ~/projects/my-site
98
136
 
99
- The blueprint is a JSON file produced by siteforge that describes the project
100
- name, slug, and selections (pages, features, integrations, locales, roles,
101
- brand tones, visual styles, content models, and SEO focuses). The scaffolder
102
- generates a frontend (Next.js) and backend (NestJS) workspace from those
103
- selections.
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>;
@@ -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
+ }