create-forgeon 0.0.4 → 0.1.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/README.md +10 -5
- package/bin/create-forgeon.mjs +9 -604
- package/package.json +6 -2
- package/src/cli/add-help.mjs +12 -0
- package/src/cli/add-options.mjs +54 -0
- package/src/cli/add-options.test.mjs +24 -0
- package/src/cli/help.mjs +20 -0
- package/src/cli/options.mjs +121 -0
- package/src/cli/options.test.mjs +41 -0
- package/src/cli/prompt-select.mjs +89 -0
- package/src/cli/prompt-select.test.mjs +34 -0
- package/src/constants.mjs +13 -0
- package/src/core/docs.mjs +128 -0
- package/src/core/docs.test.mjs +91 -0
- package/src/core/install.mjs +14 -0
- package/src/core/scaffold.mjs +57 -0
- package/src/core/validate.mjs +12 -0
- package/src/core/validate.test.mjs +73 -0
- package/src/databases/index.mjs +26 -0
- package/src/frameworks/index.mjs +32 -0
- package/src/infrastructure/proxy.mjs +12 -0
- package/src/modules/docs.mjs +70 -0
- package/src/modules/executor.mjs +40 -0
- package/src/modules/executor.test.mjs +62 -0
- package/src/modules/registry.mjs +37 -0
- package/src/presets/i18n.mjs +203 -0
- package/src/presets/index.mjs +2 -0
- package/src/presets/proxy.mjs +32 -0
- package/src/run-add-module.mjs +47 -0
- package/src/run-create-forgeon.mjs +72 -0
- package/src/utils/fs.mjs +26 -0
- package/src/utils/values.mjs +20 -0
- package/templates/base/docs/AI/MODULE_SPEC.md +56 -0
- package/templates/base/docs/AI/TASKS.md +17 -7
- package/templates/base/docs/README.md +2 -1
- package/templates/base/infra/docker/compose.none.yml +37 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/00_title.md +1 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/10_layout_base.md +6 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/11_layout_infra.md +1 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/12_layout_i18n_resources.md +1 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/20_env_base.md +4 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/21_env_i18n.md +3 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/30_default_db.md +7 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/31_docker_runtime.md +5 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/32_scope_freeze.md +5 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/40_docs_generation.md +9 -0
- package/templates/docs-fragments/AI_ARCHITECTURE/50_extension_points.md +8 -0
- package/templates/docs-fragments/AI_PROJECT/00_title.md +1 -0
- package/templates/docs-fragments/AI_PROJECT/10_what_is.md +3 -0
- package/templates/docs-fragments/AI_PROJECT/20_structure_base.md +5 -0
- package/templates/docs-fragments/AI_PROJECT/21_structure_i18n.md +2 -0
- package/templates/docs-fragments/AI_PROJECT/22_structure_docker.md +1 -0
- package/templates/docs-fragments/AI_PROJECT/23_structure_docs.md +1 -0
- package/templates/docs-fragments/AI_PROJECT/30_run_dev.md +8 -0
- package/templates/docs-fragments/AI_PROJECT/31_run_docker.md +5 -0
- package/templates/docs-fragments/AI_PROJECT/32_proxy_notes.md +5 -0
- package/templates/docs-fragments/AI_PROJECT/32_proxy_notes_none.md +5 -0
- package/templates/docs-fragments/AI_PROJECT/33_i18n_notes.md +4 -0
- package/templates/docs-fragments/AI_PROJECT/40_change_boundaries_base.md +3 -0
- package/templates/docs-fragments/AI_PROJECT/41_change_boundaries_docker.md +1 -0
- package/templates/docs-fragments/README/00_title.md +3 -0
- package/templates/docs-fragments/README/10_stack.md +8 -0
- package/templates/docs-fragments/README/20_quick_start_dev_intro.md +6 -0
- package/templates/docs-fragments/README/21_quick_start_dev_db_docker.md +4 -0
- package/templates/docs-fragments/README/21_quick_start_dev_db_local.md +1 -0
- package/templates/docs-fragments/README/22_quick_start_dev_outro.md +7 -0
- package/templates/docs-fragments/README/30_quick_start_docker.md +7 -0
- package/templates/docs-fragments/README/30_quick_start_docker_none.md +9 -0
- package/templates/docs-fragments/README/31_proxy_preset_caddy.md +9 -0
- package/templates/docs-fragments/README/31_proxy_preset_nginx.md +8 -0
- package/templates/docs-fragments/README/31_proxy_preset_none.md +6 -0
- package/templates/docs-fragments/README/32_prisma_container_start.md +5 -0
- package/templates/docs-fragments/README/40_i18n.md +10 -0
- package/templates/docs-fragments/README/90_next_steps.md +7 -0
- package/templates/module-fragments/jwt-auth/00_title.md +1 -0
- package/templates/module-fragments/jwt-auth/10_overview.md +6 -0
- package/templates/module-fragments/jwt-auth/20_scope.md +7 -0
- package/templates/module-fragments/jwt-auth/90_status_planned.md +3 -0
- package/templates/module-fragments/queue/00_title.md +1 -0
- package/templates/module-fragments/queue/10_overview.md +6 -0
- package/templates/module-fragments/queue/20_scope.md +7 -0
- package/templates/module-fragments/queue/90_status_planned.md +3 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { printHelp } from './cli/help.mjs';
|
|
5
|
+
import { parseCliArgs, promptForMissingOptions } from './cli/options.mjs';
|
|
6
|
+
import { DEFAULT_DB, DEFAULT_FRONTEND, DEFAULT_PROXY, FIXED_DOCKER_ENABLED } from './constants.mjs';
|
|
7
|
+
import { runInstall } from './core/install.mjs';
|
|
8
|
+
import { scaffoldProject } from './core/scaffold.mjs';
|
|
9
|
+
import { validatePresetSupport } from './core/validate.mjs';
|
|
10
|
+
import { parseBoolean } from './utils/values.mjs';
|
|
11
|
+
|
|
12
|
+
export async function runCreateForgeon(argv = process.argv.slice(2)) {
|
|
13
|
+
const { options: parsedOptions, positional } = parseCliArgs(argv);
|
|
14
|
+
const options = { ...parsedOptions };
|
|
15
|
+
|
|
16
|
+
if (options.help) {
|
|
17
|
+
printHelp();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!options.name && positional.length > 0) {
|
|
22
|
+
[options.name] = positional;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const promptedOptions = await promptForMissingOptions(options);
|
|
26
|
+
|
|
27
|
+
if (!promptedOptions.name || promptedOptions.name.trim().length === 0) {
|
|
28
|
+
throw new Error('Project name is required.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const frontend = (promptedOptions.frontend ?? DEFAULT_FRONTEND).toString().toLowerCase();
|
|
32
|
+
const db = (promptedOptions.db ?? DEFAULT_DB).toString().toLowerCase();
|
|
33
|
+
const i18nEnabled = parseBoolean(promptedOptions.i18n, true);
|
|
34
|
+
const dockerEnabled = FIXED_DOCKER_ENABLED;
|
|
35
|
+
const proxy = (promptedOptions.proxy ?? DEFAULT_PROXY).toString().toLowerCase();
|
|
36
|
+
const installEnabled = parseBoolean(promptedOptions.install, false);
|
|
37
|
+
|
|
38
|
+
validatePresetSupport({ frontend, db, dockerEnabled, proxy });
|
|
39
|
+
|
|
40
|
+
const projectName = promptedOptions.name.trim();
|
|
41
|
+
const targetRoot = path.resolve(process.cwd(), projectName);
|
|
42
|
+
if (fs.existsSync(targetRoot)) {
|
|
43
|
+
throw new Error(`Target directory already exists: ${targetRoot}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const srcDir = path.dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
const packageRoot = path.resolve(srcDir, '..');
|
|
48
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
49
|
+
|
|
50
|
+
scaffoldProject({
|
|
51
|
+
templateRoot,
|
|
52
|
+
packageRoot,
|
|
53
|
+
targetRoot,
|
|
54
|
+
projectName,
|
|
55
|
+
frontend,
|
|
56
|
+
db,
|
|
57
|
+
i18nEnabled,
|
|
58
|
+
proxy,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (installEnabled) {
|
|
62
|
+
runInstall(targetRoot);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log('Forgeon scaffold generated.');
|
|
66
|
+
console.log(`- path: ${targetRoot}`);
|
|
67
|
+
console.log(`- frontend: ${frontend}`);
|
|
68
|
+
console.log(`- db: ${db}`);
|
|
69
|
+
console.log(`- i18n: ${i18nEnabled}`);
|
|
70
|
+
console.log(`- docker: ${dockerEnabled}`);
|
|
71
|
+
console.log(`- proxy: ${proxy}`);
|
|
72
|
+
}
|
package/src/utils/fs.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function copyRecursive(source, destination) {
|
|
5
|
+
const stat = fs.statSync(source);
|
|
6
|
+
|
|
7
|
+
if (stat.isDirectory()) {
|
|
8
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
9
|
+
for (const entry of fs.readdirSync(source)) {
|
|
10
|
+
copyRecursive(path.join(source, entry), path.join(destination, entry));
|
|
11
|
+
}
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fs.copyFileSync(source, destination);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function writeJson(filePath, data) {
|
|
19
|
+
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function removeIfExists(targetPath) {
|
|
23
|
+
if (fs.existsSync(targetPath)) {
|
|
24
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function parseBoolean(value, fallback) {
|
|
2
|
+
if (value === undefined) return fallback;
|
|
3
|
+
if (typeof value === 'boolean') return value;
|
|
4
|
+
|
|
5
|
+
const normalized = String(value).trim().toLowerCase();
|
|
6
|
+
if (['true', '1', 'yes', 'y'].includes(normalized)) return true;
|
|
7
|
+
if (['false', '0', 'no', 'n'].includes(normalized)) return false;
|
|
8
|
+
|
|
9
|
+
throw new Error(`Invalid boolean value: ${value}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function toKebabCase(value) {
|
|
13
|
+
return (
|
|
14
|
+
value
|
|
15
|
+
.trim()
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
18
|
+
.replace(/^-+|-+$/g, '') || 'forgeon-app'
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# MODULE SPEC
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Define one repeatable fullstack pattern for Forgeon add-modules.
|
|
6
|
+
|
|
7
|
+
Each feature module should be split into:
|
|
8
|
+
|
|
9
|
+
1. `@forgeon/<feature>-contracts`
|
|
10
|
+
2. `@forgeon/<feature>-api`
|
|
11
|
+
3. `@forgeon/<feature>-web`
|
|
12
|
+
|
|
13
|
+
## 1) Contracts Package
|
|
14
|
+
|
|
15
|
+
Single source of truth shared by backend and frontend.
|
|
16
|
+
|
|
17
|
+
Must contain:
|
|
18
|
+
|
|
19
|
+
- DTO/request/response types
|
|
20
|
+
- route constants (`API.<feature>.*`)
|
|
21
|
+
- error codes (`<FEATURE>_*`)
|
|
22
|
+
- shared constants (header/cookie names)
|
|
23
|
+
|
|
24
|
+
Should contain:
|
|
25
|
+
|
|
26
|
+
- zod schemas + inferred TS types
|
|
27
|
+
|
|
28
|
+
## 2) API Package
|
|
29
|
+
|
|
30
|
+
NestJS module integrating contracts into backend runtime.
|
|
31
|
+
|
|
32
|
+
Must contain:
|
|
33
|
+
|
|
34
|
+
- module/service/controller
|
|
35
|
+
- guards/strategies (if auth/security related)
|
|
36
|
+
- config keys
|
|
37
|
+
- minimal e2e test path
|
|
38
|
+
- integration with `@forgeon/core` errors/logging
|
|
39
|
+
|
|
40
|
+
## 3) Web Package
|
|
41
|
+
|
|
42
|
+
React integration layer for the same feature.
|
|
43
|
+
|
|
44
|
+
Must contain:
|
|
45
|
+
|
|
46
|
+
- provider/hooks/store
|
|
47
|
+
- route guard (if feature requires auth/access)
|
|
48
|
+
- API client helpers using contracts route constants/types
|
|
49
|
+
- token/header/cookie wiring where relevant
|
|
50
|
+
|
|
51
|
+
## Acceptance Criteria
|
|
52
|
+
|
|
53
|
+
- No duplicate route strings across api/web.
|
|
54
|
+
- No duplicate error-code enums across api/web.
|
|
55
|
+
- Contracts package can be imported from both sides without circular dependencies.
|
|
56
|
+
- Module has docs under `docs/AI/MODULES/<module-id>.md`.
|
|
@@ -38,11 +38,21 @@ Run build checks and show changed files.
|
|
|
38
38
|
## Generate Preset
|
|
39
39
|
|
|
40
40
|
```text
|
|
41
|
-
Create
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
- update generated
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
Create or update create-forgeon preset flow:
|
|
42
|
+
- keep canonical stack fixed: NestJS + React + Prisma/Postgres + Docker
|
|
43
|
+
- allow only runtime proxy choice: caddy/nginx/none
|
|
44
|
+
- update generated docs fragments
|
|
45
|
+
- update docs/AI/ARCHITECTURE.md and docs/AI/MODULE_SPEC.md when scope changes
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Add Fullstack Module
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
Implement `create-forgeon add <module-id>` for a fullstack feature.
|
|
52
|
+
Requirements:
|
|
53
|
+
- split module into contracts/api/web packages
|
|
54
|
+
- contracts is source of truth for routes, DTOs, errors
|
|
55
|
+
- add docs note under docs/AI/MODULES/<module-id>.md
|
|
56
|
+
- keep backward compatibility
|
|
57
|
+
```
|
|
48
58
|
|
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
- `AI/PROJECT.md` - project overview and run modes
|
|
4
4
|
- `AI/ARCHITECTURE.md` - monorepo design and extension model
|
|
5
|
-
- `AI/
|
|
5
|
+
- `AI/MODULE_SPEC.md` - fullstack module contract (`contracts/api/web`)
|
|
6
|
+
- `AI/TASKS.md` - ready-to-use Codex prompts
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
services:
|
|
2
|
+
db:
|
|
3
|
+
image: postgres:16-alpine
|
|
4
|
+
restart: unless-stopped
|
|
5
|
+
environment:
|
|
6
|
+
POSTGRES_USER: ${POSTGRES_USER}
|
|
7
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
8
|
+
POSTGRES_DB: ${POSTGRES_DB}
|
|
9
|
+
ports:
|
|
10
|
+
- "5432:5432"
|
|
11
|
+
volumes:
|
|
12
|
+
- db_data:/var/lib/postgresql/data
|
|
13
|
+
healthcheck:
|
|
14
|
+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
|
|
15
|
+
interval: 10s
|
|
16
|
+
timeout: 5s
|
|
17
|
+
retries: 10
|
|
18
|
+
|
|
19
|
+
api:
|
|
20
|
+
build:
|
|
21
|
+
context: ../..
|
|
22
|
+
dockerfile: apps/api/Dockerfile
|
|
23
|
+
restart: unless-stopped
|
|
24
|
+
environment:
|
|
25
|
+
PORT: ${PORT}
|
|
26
|
+
DATABASE_URL: ${DATABASE_URL}
|
|
27
|
+
I18N_ENABLED: ${I18N_ENABLED}
|
|
28
|
+
I18N_DEFAULT_LANG: ${I18N_DEFAULT_LANG}
|
|
29
|
+
I18N_FALLBACK_LANG: ${I18N_FALLBACK_LANG}
|
|
30
|
+
depends_on:
|
|
31
|
+
db:
|
|
32
|
+
condition: service_healthy
|
|
33
|
+
ports:
|
|
34
|
+
- "3000:3000"
|
|
35
|
+
|
|
36
|
+
volumes:
|
|
37
|
+
db_data:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# ARCHITECTURE
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- `infra/*` - runtime infrastructure (compose, proxy, env examples)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- `resources/*` - static assets (includes translation dictionaries when i18n is enabled)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## Default DB Stack
|
|
2
|
+
|
|
3
|
+
Current default stack is `{{DB_LABEL}}`.
|
|
4
|
+
|
|
5
|
+
- Prisma schema and migrations live in `apps/api/prisma`
|
|
6
|
+
- DB access is encapsulated via `PrismaModule` (`apps/api/src/prisma`)
|
|
7
|
+
- Additional DB presets are intentionally out of scope for the current milestone.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## Docs Generation Pipeline
|
|
2
|
+
|
|
3
|
+
Project docs are assembled from markdown fragments in:
|
|
4
|
+
|
|
5
|
+
- `packages/create-forgeon/templates/docs-fragments/README`
|
|
6
|
+
- `packages/create-forgeon/templates/docs-fragments/AI_PROJECT`
|
|
7
|
+
- `packages/create-forgeon/templates/docs-fragments/AI_ARCHITECTURE`
|
|
8
|
+
|
|
9
|
+
During scaffold generation, the CLI selects fragments based on chosen flags and writes final docs into project root and `docs/AI`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## Extension Points
|
|
2
|
+
|
|
3
|
+
Future presets should extend both code and docs in parallel:
|
|
4
|
+
|
|
5
|
+
1. Add or update fullstack module templates (`contracts/api/web`).
|
|
6
|
+
2. Register modules in `create-forgeon add` registry.
|
|
7
|
+
3. Add module docs fragments and update module spec docs.
|
|
8
|
+
4. Keep canonical core stack stable unless a major version changes it.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# PROJECT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- `infra` - Docker Compose (always) + proxy preset (`{{PROXY_LABEL}}`)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- `docs` - documentation, AI prompts, and module spec contracts
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- Change carefully: proxy config and docker service names because routing depends on them.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## Generated Preset
|
|
2
|
+
|
|
3
|
+
- Stack: `NestJS + React + Prisma/Postgres + Docker`
|
|
4
|
+
- Frontend: `{{FRONTEND_LABEL}}` (fixed)
|
|
5
|
+
- Database: `{{DB_LABEL}}` (fixed)
|
|
6
|
+
- i18n: `{{I18N_STATUS}}`
|
|
7
|
+
- Docker/infra: `enabled` (fixed)
|
|
8
|
+
- Reverse proxy: `{{PROXY_LABEL}}` (`caddy|nginx|none`)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2. Ensure PostgreSQL is running locally and configure `DATABASE_URL` in `apps/api/.env`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## Quick Start (Docker)
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
docker compose --env-file infra/docker/.env.example -f infra/docker/compose.yml up --build
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Open API at `http://localhost:3000/api/health`.
|
|
8
|
+
|
|
9
|
+
Frontend is not served by Docker in `proxy=none` mode. Use `pnpm dev` for web.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
### Proxy Preset: Caddy
|
|
2
|
+
|
|
3
|
+
- `/api/*` is proxied to `api:3000`.
|
|
4
|
+
- Static frontend build is served by Caddy.
|
|
5
|
+
- Main proxy config: `infra/caddy/Caddyfile`.
|
|
6
|
+
- Compose service wiring: `infra/docker/compose.yml` (`caddy` service).
|
|
7
|
+
- Safe to edit: host ports and env values in `infra/docker/.env.example`.
|
|
8
|
+
- Avoid renaming service hosts (`api`, `db`) unless you also update proxy/compose config.
|
|
9
|
+
- Recommended preset for local SSL/OAuth-style testing.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
### Proxy Preset: Nginx
|
|
2
|
+
|
|
3
|
+
- `/api/*` is proxied to `api:3000`.
|
|
4
|
+
- Static frontend build is served by Nginx.
|
|
5
|
+
- Main proxy config: `infra/nginx/nginx.conf`.
|
|
6
|
+
- Compose service wiring: `infra/docker/compose.yml` (`nginx` service).
|
|
7
|
+
- Safe to edit: host ports and env values in `infra/docker/.env.example`.
|
|
8
|
+
- Avoid renaming service hosts (`api`, `db`) unless you also update proxy/compose config.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# MODULE: {{MODULE_LABEL}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# MODULE: {{MODULE_LABEL}}
|