From 2bbfdfd23aadf733e8fd8ab1b67901b8237e413c Mon Sep 17 00:00:00 2001 From: Harper Date: Fri, 23 Oct 2026 17:00:43 +0200 Subject: [PATCH] feat: new CLI options for AGENTS.md documentation --- README.md | 1 + docs/docs/commands/2-commands/1-new.mdx | 1 + .../schematics/2-schematics/1-workspace.mdx | 2 + .../schematics/2-schematics/2-project.mdx | 2 + libs/schematics/e2e/project.test.ts | 55 +++++++ libs/schematics/e2e/workspace.test.ts | 34 ++++ .../src/libs/project/files/js/AGENTS.md | 155 ++++++++++++++++++ .../src/libs/project/files/ts/AGENTS.md | 139 ++++++++++++++++ .../src/libs/project/project.factory.ts | 2 + .../src/libs/project/project.options.d.ts | 5 + .../src/libs/project/project.schema.d.ts | 5 + libs/schematics/src/libs/project/schema.json | 5 + .../src/libs/workspace/files/js/AGENTS.md | 58 +++++++ .../src/libs/workspace/files/ts/AGENTS.md | 58 +++++++ .../schematics/src/libs/workspace/schema.json | 5 + .../src/libs/workspace/workspace.factory.ts | 2 + .../src/libs/workspace/workspace.options.d.ts | 5 + .../src/libs/workspace/workspace.schema.d.ts | 5 + src/action/actions/new.action.ts | 5 + src/command/commands/new.command.ts | 3 + src/lib/input/inputs/new/agents.input.spec.ts | 20 +++ src/lib/input/inputs/new/agents.input.ts | 6 + src/lib/input/inputs/new/index.ts | 1 + 23 files changed, 574 insertions(+) create mode 100644 libs/schematics/src/libs/project/files/js/AGENTS.md create mode 100644 libs/schematics/src/libs/project/files/ts/AGENTS.md create mode 100644 libs/schematics/src/libs/workspace/files/js/AGENTS.md create mode 100644 libs/schematics/src/libs/workspace/files/ts/AGENTS.md create mode 100644 src/lib/input/inputs/new/agents.input.spec.ts create mode 100644 src/lib/input/inputs/new/agents.input.ts diff --git a/README.md b/README.md index a54d6a3..c5dc7c1 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ Used to create a new nanoforge project. - `--docker` generate docker files. - `--no-docker` do not generate docker files. - `--no-lint` do not generate lint files. +- `--no-agents` do not generate `AGENTS.md` files for AI coding agents. - `--editor` add editor dependencies. - `--git` generate a git repository. - `--no-git` do not generate a git repository. diff --git a/docs/docs/commands/2-commands/1-new.mdx b/docs/docs/commands/2-commands/1-new.mdx index dd11d75..46c0194 100644 --- a/docs/docs/commands/2-commands/1-new.mdx +++ b/docs/docs/commands/2-commands/1-new.mdx @@ -30,6 +30,7 @@ nf new | `--skip-install` / `--no-skip-install` | Skip installing dependencies after scaffolding. | | `--docker` / `--no-docker` | Generate Docker files. | | `--no-lint` | Skip generating lint configuration files. | +| `--no-agents` | Skip generating the `AGENTS.md` files for AI coding agents. | | `--editor` | Add editor dependencies. | | `--git` / `--no-git` | Initialize a git repository. | | `--git-remote ` / `--no-git-remote` | Set up a git remote (requires `--git`). | diff --git a/docs/docs/schematics/2-schematics/1-workspace.mdx b/docs/docs/schematics/2-schematics/1-workspace.mdx index 5f836f2..b6213eb 100644 --- a/docs/docs/schematics/2-schematics/1-workspace.mdx +++ b/docs/docs/schematics/2-schematics/1-workspace.mdx @@ -27,6 +27,7 @@ schematics @nanoforge-dev/schematics:workspace --name=my-game | `strict` | `boolean` | `true` | Enable strict mode | | `packageManager` | `"npm" \| "yarn" \| "pnpm" \| "bun"` | `npm` | Package manager to target | | `allowBuilds` | `string[]` | `[]` | Packages allowed to run install/build scripts, in addition to `bun` | +| `agents` | `boolean` | `true` | Generate an AGENTS.md file | ## Generated structure @@ -40,6 +41,7 @@ my-game/ ├── nanoforge.config.ts # (or .js) ├── .gitignore ├── README.md +├── AGENTS.md # if agents: true └── apps/ # generated with the `project` schematic ``` diff --git a/docs/docs/schematics/2-schematics/2-project.mdx b/docs/docs/schematics/2-schematics/2-project.mdx index 1ba4517..fff23c1 100644 --- a/docs/docs/schematics/2-schematics/2-project.mdx +++ b/docs/docs/schematics/2-schematics/2-project.mdx @@ -36,6 +36,7 @@ schematics @nanoforge-dev/schematics:project --part=client --workspaceName=my-ga | `editor` | `boolean` | `false` | Reserved for future editor build support | | `libs` | `string[]` | `[]` | Relative paths to shared libs this project depends on, written into `nanoforge.config.ts` | | `allowBuilds` | `string[]` | `[]` | Packages allowed to run install/build scripts (standalone projects only) | +| `agents` | `boolean` | `true` | Generate an AGENTS.md file | ## Generated structure @@ -48,6 +49,7 @@ schematics @nanoforge-dev/schematics:project --part=client --workspaceName=my-ga ├── pnpm-workspace.yaml # if !workspace && packageManager: pnpm && allowBuilds.length > 0 ├── Dockerfile # if docker: true ├── .dockerignore # if docker: true +├── AGENTS.md # if agents: true ├── src/ │ ├── main.ts │ ├── components/ diff --git a/libs/schematics/e2e/project.test.ts b/libs/schematics/e2e/project.test.ts index d43a8ca..a0366dd 100644 --- a/libs/schematics/e2e/project.test.ts +++ b/libs/schematics/e2e/project.test.ts @@ -428,4 +428,59 @@ describe("project schematic", () => { expect(bunPackageJson.trustedDependencies).toEqual(["bun"]); }); }); + + describe("AGENTS.md", () => { + it("is generated by default with the project specifics", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + workspaceName: "my-workspace", + directory: "apps/client", + editor: true, + }); + expect(tree.files).toContain("/apps/client/AGENTS.md"); + const content = tree.readContent("/apps/client/AGENTS.md"); + expect(content).toContain( + "`my-workspace-client` is a [NanoForge](https://nanoforge.eu) **client** app", + ); + expect(content).toContain("read the workspace root `AGENTS.md` first"); + expect(content).toContain("NanoforgeFactory.createClient"); + expect(content).toContain("`nf editor .`"); + expect(content).not.toContain("<%"); + }); + + it("describes a standalone server project without workspace references", async () => { + const tree = await runner.runSchematic("project", { + part: "server", + name: "solo", + directory: "solo", + workspace: false, + }); + const content = tree.readContent("/solo/AGENTS.md"); + expect(content).toContain("**server** app"); + expect(content).toContain("NanoforgeFactory.createServer"); + expect(content).not.toContain("workspace root"); + expect(content).not.toContain("`nf editor .`"); + }); + + it("uses JavaScript conventions for a JavaScript project", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + directory: "apps/client", + language: "js", + }); + const content = tree.readContent("/apps/client/AGENTS.md"); + expect(content).toContain("JavaScript (ES modules, typed with JSDoc"); + expect(content).toContain("src/main.js"); + expect(content).not.toContain("<%"); + }); + + it("is not generated when agents is false", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + directory: "apps/client", + agents: false, + }); + expect(tree.files).not.toContain("/apps/client/AGENTS.md"); + }); + }); }); diff --git a/libs/schematics/e2e/workspace.test.ts b/libs/schematics/e2e/workspace.test.ts index 111c5e0..e7e71b2 100644 --- a/libs/schematics/e2e/workspace.test.ts +++ b/libs/schematics/e2e/workspace.test.ts @@ -265,4 +265,38 @@ describe("workspace schematic", () => { }, ); }); + + describe("AGENTS.md", () => { + it("is generated by default at the workspace root", async () => { + const tree = await runner.runSchematic("workspace", { + name: "my-workspace", + packageManager: "pnpm", + }); + expect(tree.files).toContain("/my-workspace/AGENTS.md"); + const content = tree.readContent("/my-workspace/AGENTS.md"); + expect(content).toContain( + "`my-workspace` is a [NanoForge](https://nanoforge.eu) multiplayer game", + ); + expect(content).toContain("`pnpm run build`"); + expect(content).not.toContain("<%"); + }); + + it("references the JavaScript config files for a JavaScript workspace", async () => { + const tree = await runner.runSchematic("workspace", { + name: "js-workspace", + language: "js", + }); + const content = tree.readContent("/js-workspace/AGENTS.md"); + expect(content).toContain("nanoforge.config.js"); + expect(content).toContain("jsconfig.json"); + }); + + it("is not generated when agents is false", async () => { + const tree = await runner.runSchematic("workspace", { + name: "no-agents", + agents: false, + }); + expect(tree.files).not.toContain("/no-agents/AGENTS.md"); + }); + }); }); diff --git a/libs/schematics/src/libs/project/files/js/AGENTS.md b/libs/schematics/src/libs/project/files/js/AGENTS.md new file mode 100644 index 0000000..9ab488d --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/AGENTS.md @@ -0,0 +1,155 @@ +# AGENTS.md + +Instructions for AI coding agents (Codex, Cursor, GitHub Copilot, Gemini CLI, Claude Code, ...) +working on this project. Human-oriented documentation lives in `README.md`. + +## Project overview + +`<%= packageName %>` is a [NanoForge](https://nanoforge.eu) **<%= part %>** app written in +JavaScript (ES modules, typed with JSDoc<% if (strict) { %>, strict checks enabled in `jsconfig.json`<% } %>), +using **<%= packageManager %>**.<% if (workspace) { %> +It is one app of a NanoForge workspace: read the workspace root `AGENTS.md` first.<% } %> + +NanoForge is a web game engine built on an ECS (Entity Component System): + +- **Entities** are plain ids spawned from the registry. +- **Components** are classes that only hold data, attached to entities. +- **Systems** are functions run every tick on the entities that have a given set of components. +- **Libraries** (graphics, input, network, ...) are plugged into the app with `app.use(...)`. + +## Commands + +| Task | Command | +| ---------------------------- | ------------------------------------------------ | +| Install dependencies | `<%= packageManager %> install` | +| Develop with hot reload | `nf dev` | +| Build for production | `nf build` (output in `dist/`) | +| Run the built app | `nf start` | +| Scaffold a component | `nf create component -n ` | +| Scaffold a system | `nf create system -n ` | +| Install a registry component | `nf install ` |<% if (editor) { %> +| Open the visual editor | `nf editor .` |<% } %> + +<% if (workspace) { %>Install dependencies from the workspace root. <% } %>There is no test runner configured. After a change, run `nf build` to check that the project +still compiles. + +## Project structure + +``` +. +├── assets/ # Static assets (images, sounds, ...) +├── src/ +│ ├── main.js # Entry point: creates the app, registers libraries, entities and systems +│ ├── components/ # ECS components (*.component.js) +│ └── systems/ # ECS systems (*.system.js) +├── nanoforge.config.js # NanoForge project configuration (type: "<%= part %>") +├── package.json +└── jsconfig.json +``` + +Do not edit generated or installed files: `dist/`, `.nanoforge/` and `nf_modules/`. + +## Conventions + +### Components + +- One component per file in `src/components/`, named `.component.js`. +- A component is a class with a `name = this.constructor.name;` field and its data assigned + in the constructor. Document parameter types with JSDoc. +- Keep `export default .name;`: it is required by the code generation. +- Keep `EDITOR_COMPONENT_MANIFEST` in sync with the constructor parameters (same names, order + and types): it is used by the editor. + +```js +/** + * @typedef {import("@nanoforge-dev/ecs").EditorComponentManifest} EditorComponentManifest + */ + +export class Velocity2D { + name = this.constructor.name; + + /** + * @param {number} x + * @param {number} y + */ + constructor(x, y) { + this.x = x; + this.y = y; + } +} + +export default Velocity2D.name; + +/** @type {EditorComponentManifest} */ +export const EDITOR_COMPONENT_MANIFEST = { + name: "Velocity2D", + description: "Velocity of an entity in pixel per tick", + params: [ + { type: "number", name: "x", description: "Horizontal velocity", example: 1, default: 0 }, + { type: "number", name: "y", description: "Vertical velocity", example: 1, default: 0 }, + ], +}; +``` + +### Systems + +- One system per file in `src/systems/`, named `.system.js`. +- A system is a function `(registry, ctx) => void`. Query entities with + `registry.getZipper([ComponentA, ComponentB])`. +- Keep `export default .name;` and `EDITOR_SYSTEM_MANIFEST`, whose `dependencies` lists + the names of the components the system reads. + +```js +/** + * @typedef {import("@nanoforge-dev/common").Context} Context + * @typedef {import("@nanoforge-dev/ecs").EditorSystemManifest} EditorSystemManifest + * @typedef {import("@nanoforge-dev/ecs").Registry} Registry + */ + +import { Position2D } from "../components/position-2d.component"; +import { Velocity2D } from "../components/velocity-2d.component"; + +/** + * @param {Registry} registry + * @param {Context} _ctx + */ +export function applyVelocity(registry, _ctx) { + registry.getZipper([Position2D, Velocity2D]).forEach(({ Position2D, Velocity2D }) => { + Position2D.x += Velocity2D.x; + Position2D.y += Velocity2D.y; + }); +} + +export default applyVelocity.name; + +/** @type {EditorSystemManifest} */ +export const EDITOR_SYSTEM_MANIFEST = { + name: "applyVelocity", + description: "Moves entities according to their velocity", + dependencies: ["Position2D", "Velocity2D"], +}; +``` + +### Entry point + +`src/main.js` exports `main(options)`, which: + +1. creates the app with `NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>(...)`, +2. registers the libraries with `app.use(...)`, then calls `await app.init(options)`, +3. spawns entities with `registry.spawnEntity()` and `registry.addComponent(...)`, +4. registers systems with `registry.addSystem(...)`, then calls `await app.run()`. + +Scaffolding a component or a system does not register it: add the system to `src/main.js` +yourself. + +### Imports + +- Types (JSDoc `import(...)` only): `@nanoforge-dev/common` (`Context`) and `@nanoforge-dev/ecs` + (`Registry`, manifests). +- Libraries: `@nanoforge-dev/ecs/<%= part %>`, `@nanoforge-dev/network/<%= part %>`<% if (part === "client") { %>, + `@nanoforge-dev/graphics-2d` and `@nanoforge-dev/input`<% } %>. + +## Documentation + +- NanoForge documentation: +- CLI reference: `nf --help` and `nf --help` diff --git a/libs/schematics/src/libs/project/files/ts/AGENTS.md b/libs/schematics/src/libs/project/files/ts/AGENTS.md new file mode 100644 index 0000000..eb4bb43 --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/AGENTS.md @@ -0,0 +1,139 @@ +# AGENTS.md + +Instructions for AI coding agents (Codex, Cursor, GitHub Copilot, Gemini CLI, Claude Code, ...) +working on this project. Human-oriented documentation lives in `README.md`. + +## Project overview + +`<%= packageName %>` is a [NanoForge](https://nanoforge.eu) **<%= part %>** app written in +TypeScript<% if (strict) { %> (strict mode)<% } %>, using **<%= packageManager %>**.<% if (workspace) { %> +It is one app of a NanoForge workspace: read the workspace root `AGENTS.md` first.<% } %> + +NanoForge is a web game engine built on an ECS (Entity Component System): + +- **Entities** are plain ids spawned from the registry. +- **Components** are classes that only hold data, attached to entities. +- **Systems** are functions run every tick on the entities that have a given set of components. +- **Libraries** (graphics, input, network, ...) are plugged into the app with `app.use(...)`. + +## Commands + +| Task | Command | +| ---------------------------- | ------------------------------------------------ | +| Install dependencies | `<%= packageManager %> install` | +| Develop with hot reload | `nf dev` | +| Build for production | `nf build` (output in `dist/`) | +| Run the built app | `nf start` | +| Scaffold a component | `nf create component -n ` | +| Scaffold a system | `nf create system -n ` | +| Install a registry component | `nf install ` |<% if (editor) { %> +| Open the visual editor | `nf editor .` |<% } %> + +<% if (workspace) { %>Install dependencies from the workspace root. <% } %>There is no test runner configured. After a change, run `nf build` to check that the project +still compiles. + +## Project structure + +``` +. +├── assets/ # Static assets (images, sounds, ...) +├── src/ +│ ├── main.ts # Entry point: creates the app, registers libraries, entities and systems +│ ├── components/ # ECS components (*.component.ts) +│ └── systems/ # ECS systems (*.system.ts) +├── nanoforge.config.ts # NanoForge project configuration (type: "<%= part %>") +├── package.json +└── tsconfig.json +``` + +Do not edit generated or installed files: `dist/`, `.nanoforge/` and `nf_modules/`. + +## Conventions + +### Components + +- One component per file in `src/components/`, named `.component.ts`. +- A component is a class with a `name = this.constructor.name;` field and its data as + constructor parameters. +- Keep `export default .name;`: it is required by the code generation. +- Keep `EDITOR_COMPONENT_MANIFEST` in sync with the constructor parameters (same names, order + and types): it is used by the editor. + +```ts +import type { EditorComponentManifest } from "@nanoforge-dev/ecs"; + +export class Velocity2D { + name = this.constructor.name; + + constructor( + public x: number, + public y: number, + ) {} +} + +export default Velocity2D.name; + +export const EDITOR_COMPONENT_MANIFEST: EditorComponentManifest = { + name: "Velocity2D", + description: "Velocity of an entity in pixel per tick", + params: [ + { type: "number", name: "x", description: "Horizontal velocity", example: 1, default: 0 }, + { type: "number", name: "y", description: "Vertical velocity", example: 1, default: 0 }, + ], +}; +``` + +### Systems + +- One system per file in `src/systems/`, named `.system.ts`. +- A system is a function `(registry: Registry, ctx: Context) => void`. Query entities with + `registry.getZipper([ComponentA, ComponentB])`. +- Keep `export default .name;` and `EDITOR_SYSTEM_MANIFEST`, whose `dependencies` lists + the names of the components the system reads. + +```ts +import type { Context } from "@nanoforge-dev/common"; +import type { EditorSystemManifest, Registry } from "@nanoforge-dev/ecs"; + +import { Position2D } from "../components/position-2d.component"; +import { Velocity2D } from "../components/velocity-2d.component"; + +export function applyVelocity(registry: Registry, _ctx: Context) { + registry.getZipper([Position2D, Velocity2D]).forEach(({ Position2D, Velocity2D }) => { + Position2D.x += Velocity2D.x; + Position2D.y += Velocity2D.y; + }); +} + +export default applyVelocity.name; + +export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = { + name: "applyVelocity", + description: "Moves entities according to their velocity", + dependencies: ["Position2D", "Velocity2D"], +}; +``` + +### Entry point + +`src/main.ts` exports `main(options)`, which: + +1. creates the app with `NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>(...)`, +2. registers the libraries with `app.use(...)`, then calls `await app.init(options)`, +3. spawns entities with `registry.spawnEntity()` and `registry.addComponent(...)`, +4. registers systems with `registry.addSystem(...)`, then calls `await app.run()`. + +Scaffolding a component or a system does not register it: add the system to `src/main.ts` +yourself. + +### Imports + +- Types: `@nanoforge-dev/common` (`Context`) and `@nanoforge-dev/ecs` (`Registry`, manifests). +- Libraries: `@nanoforge-dev/ecs/<%= part %>`, `@nanoforge-dev/network/<%= part %>`<% if (part === "client") { %>, + `@nanoforge-dev/graphics-2d` and `@nanoforge-dev/input`<% } %>. +- Use `import type` for type-only imports. + +## Documentation + +- NanoForge documentation: +- CLI reference: `nf --help` and `nf --help` diff --git a/libs/schematics/src/libs/project/project.factory.ts b/libs/schematics/src/libs/project/project.factory.ts index a15be4e..c360168 100644 --- a/libs/schematics/src/libs/project/project.factory.ts +++ b/libs/schematics/src/libs/project/project.factory.ts @@ -88,6 +88,7 @@ const transform = async (schema: ProjectSchema): Promise => { hasServer: schema.hasServer, docker: schema.docker ?? false, editor: schema.editor ?? false, + agents: schema.agents ?? true, allowBuilds: workspace ? [] : ["bun"], }; }; @@ -109,6 +110,7 @@ const generate = (options: ProjectOptions, path: string): Source => { (options.packageManager === "pnpm" && options.allowBuilds.length > 0) || !(path.split("/").at(-1) ?? "").startsWith("pnpm-workspace.yaml"), ), + filter((path) => options.agents || (path.split("/").at(-1) ?? "") !== "AGENTS.md"), filter((path) => { const filename = path.split("/").at(-1) ?? ""; if ( diff --git a/libs/schematics/src/libs/project/project.options.d.ts b/libs/schematics/src/libs/project/project.options.d.ts index 55cec52..b75383e 100644 --- a/libs/schematics/src/libs/project/project.options.d.ts +++ b/libs/schematics/src/libs/project/project.options.d.ts @@ -85,4 +85,9 @@ export interface ProjectOptions { * `workspace` is false — see {@link ProjectSchema.allowBuilds}. */ allowBuilds: string[]; + + /** + * Generate an AGENTS.md file documenting the project for AI coding agents + */ + agents: boolean; } diff --git a/libs/schematics/src/libs/project/project.schema.d.ts b/libs/schematics/src/libs/project/project.schema.d.ts index 5f00a9c..61c6884 100644 --- a/libs/schematics/src/libs/project/project.schema.d.ts +++ b/libs/schematics/src/libs/project/project.schema.d.ts @@ -64,4 +64,9 @@ export interface ProjectSchema { * Add editor dependencies */ editor: boolean; + + /** + * Generate an AGENTS.md file documenting the project for AI coding agents + */ + agents: boolean; } diff --git a/libs/schematics/src/libs/project/schema.json b/libs/schematics/src/libs/project/schema.json index cc933df..9445ae2 100644 --- a/libs/schematics/src/libs/project/schema.json +++ b/libs/schematics/src/libs/project/schema.json @@ -66,6 +66,11 @@ "description": "Add editor dependencies", "default": false }, + "agents": { + "type": "boolean", + "description": "Generate an AGENTS.md file documenting the project for AI coding agents", + "default": true + }, "libs": { "type": "array", "items": { "type": "string" }, diff --git a/libs/schematics/src/libs/workspace/files/js/AGENTS.md b/libs/schematics/src/libs/workspace/files/js/AGENTS.md new file mode 100644 index 0000000..ff6a65c --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/js/AGENTS.md @@ -0,0 +1,58 @@ +# AGENTS.md + +Instructions for AI coding agents (Codex, Cursor, GitHub Copilot, Gemini CLI, Claude Code, ...) +working on this repository. Human-oriented documentation lives in `README.md`. + +## Project overview + +`<%= name %>` is a [NanoForge](https://nanoforge.eu) multiplayer game, organized as a +**<%= packageManager %>** workspace written in JavaScript (ES modules, typed with JSDoc). + +NanoForge is a web game engine built on an ECS (Entity Component System): entities are ids, +components are data classes and systems are functions run every tick. + +## Structure + +``` +. +├── apps/ +│ ├── client/ # Browser app: rendering, input, receives the game state from the server +│ └── server/ # Authoritative game logic, broadcasts the game state to the clients +├── nanoforge.config.js # Workspace configuration (type: "workspace", lists the apps) +├── package.json +└── jsconfig.json # Shared JavaScript configuration, extended by each app +``` + +Each app has its own `AGENTS.md` describing its layout and conventions: read it before +working inside `apps/`. + +## Commands + +Run from the workspace root: + +| Task | Command | +| ----------------------- | ---------------------------------------- | +| Install dependencies | `<%= packageManager %> install` | +| Develop with hot reload | `<%= packageManager %> run dev` | +| Build every app | `<%= packageManager %> run build` | +| Run every built app | `<%= packageManager %> run start` | + +Target a single app with `-d apps/`, e.g. `nf build -d apps/client`. Scaffold components +and systems from inside an app (`nf create component -n -d apps/client`), never from +the workspace root. + +There is no test runner configured. After a change, run the build to check that every app +still compiles. + +## Conventions + +- Client and server communicate through `@nanoforge-dev/network` (`ctx.network` in systems). + When you change the shape of a message sent by one side, update the other side too. +- Keep the game logic on the server; the client renders and sends input. +- Install dependencies from the root only; do not add a lockfile inside `apps/*`. +- Do not edit generated files: `dist/`, `.nanoforge/` and `nf_modules/`. + +## Documentation + +- NanoForge documentation: +- CLI reference: `nf --help` and `nf --help` diff --git a/libs/schematics/src/libs/workspace/files/ts/AGENTS.md b/libs/schematics/src/libs/workspace/files/ts/AGENTS.md new file mode 100644 index 0000000..734a741 --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/ts/AGENTS.md @@ -0,0 +1,58 @@ +# AGENTS.md + +Instructions for AI coding agents (Codex, Cursor, GitHub Copilot, Gemini CLI, Claude Code, ...) +working on this repository. Human-oriented documentation lives in `README.md`. + +## Project overview + +`<%= name %>` is a [NanoForge](https://nanoforge.eu) multiplayer game, organized as a +**<%= packageManager %>** workspace written in TypeScript<% if (strict) { %> (strict mode)<% } %>. + +NanoForge is a web game engine built on an ECS (Entity Component System): entities are ids, +components are data classes and systems are functions run every tick. + +## Structure + +``` +. +├── apps/ +│ ├── client/ # Browser app: rendering, input, receives the game state from the server +│ └── server/ # Authoritative game logic, broadcasts the game state to the clients +├── nanoforge.config.ts # Workspace configuration (type: "workspace", lists the apps) +├── package.json +└── tsconfig.json # Shared TypeScript configuration, extended by each app +``` + +Each app has its own `AGENTS.md` describing its layout and conventions: read it before +working inside `apps/`. + +## Commands + +Run from the workspace root: + +| Task | Command | +| ----------------------- | ---------------------------------------- | +| Install dependencies | `<%= packageManager %> install` | +| Develop with hot reload | `<%= packageManager %> run dev` | +| Build every app | `<%= packageManager %> run build` | +| Run every built app | `<%= packageManager %> run start` | + +Target a single app with `-d apps/`, e.g. `nf build -d apps/client`. Scaffold components +and systems from inside an app (`nf create component -n -d apps/client`), never from +the workspace root. + +There is no test runner configured. After a change, run the build to check that every app +still compiles. + +## Conventions + +- Client and server communicate through `@nanoforge-dev/network` (`ctx.network` in systems). + When you change the shape of a message sent by one side, update the other side too. +- Keep the game logic on the server; the client renders and sends input. +- Install dependencies from the root only; do not add a lockfile inside `apps/*`. +- Do not edit generated files: `dist/`, `.nanoforge/` and `nf_modules/`. + +## Documentation + +- NanoForge documentation: +- CLI reference: `nf --help` and `nf --help` diff --git a/libs/schematics/src/libs/workspace/schema.json b/libs/schematics/src/libs/workspace/schema.json index 9d843e6..1963a06 100644 --- a/libs/schematics/src/libs/workspace/schema.json +++ b/libs/schematics/src/libs/workspace/schema.json @@ -40,6 +40,11 @@ "type": "boolean", "description": "Generate a Dockerfile building every app in the workspace into one image", "default": false + }, + "agents": { + "type": "boolean", + "description": "Generate an AGENTS.md file documenting the project for AI coding agents", + "default": true } } } diff --git a/libs/schematics/src/libs/workspace/workspace.factory.ts b/libs/schematics/src/libs/workspace/workspace.factory.ts index 3c82966..4660d48 100644 --- a/libs/schematics/src/libs/workspace/workspace.factory.ts +++ b/libs/schematics/src/libs/workspace/workspace.factory.ts @@ -50,6 +50,7 @@ const transform = async (schema: WorkspaceSchema): Promise => packageManager, allowBuilds: Array.from(new Set([...ALWAYS_ALLOWED_BUILDS, ...(schema.allowBuilds ?? [])])), docker: schema.docker ?? false, + agents: schema.agents ?? true, }; }; @@ -69,6 +70,7 @@ const generate = (options: WorkspaceOptions, path: string): Source => { (path) => options.docker || !["Dockerfile", ".dockerignore"].includes(path.split("/").at(-1) ?? ""), ), + filter((path) => options.agents || (path.split("/").at(-1) ?? "") !== "AGENTS.md"), renameTemplateFiles(), move(normalize(path)), ]); diff --git a/libs/schematics/src/libs/workspace/workspace.options.d.ts b/libs/schematics/src/libs/workspace/workspace.options.d.ts index 26b0a83..4bd1649 100644 --- a/libs/schematics/src/libs/workspace/workspace.options.d.ts +++ b/libs/schematics/src/libs/workspace/workspace.options.d.ts @@ -41,4 +41,9 @@ export interface WorkspaceOptions { * Generate a Dockerfile building every app in the workspace into one image */ docker: boolean; + + /** + * Generate an AGENTS.md file documenting the project for AI coding agents + */ + agents: boolean; } diff --git a/libs/schematics/src/libs/workspace/workspace.schema.d.ts b/libs/schematics/src/libs/workspace/workspace.schema.d.ts index fd61e17..72b9aef 100644 --- a/libs/schematics/src/libs/workspace/workspace.schema.d.ts +++ b/libs/schematics/src/libs/workspace/workspace.schema.d.ts @@ -38,4 +38,9 @@ export interface WorkspaceSchema { * Generate a Dockerfile building every app in the workspace into one image */ docker: boolean; + + /** + * Generate an AGENTS.md file documenting the project for AI coding agents + */ + agents: boolean; } diff --git a/src/action/actions/new.action.ts b/src/action/actions/new.action.ts index bab6fb8..ff2f20c 100644 --- a/src/action/actions/new.action.ts +++ b/src/action/actions/new.action.ts @@ -5,6 +5,7 @@ import { type Input, getDirectoryInput, getEditorInput, + getNewAgentsInput, getNewDockerOrAsk, getNewLanguageInputOrAsk, getNewLintInput, @@ -34,6 +35,7 @@ interface NewValues { skipInstall: boolean; docker: boolean; lint: boolean; + agents: boolean; editor: boolean; git: boolean; gitRemote: string | null; @@ -73,6 +75,7 @@ export class NewAction extends AbstractAction { skipInstall: await getNewSkipInstallOrAsk(inputs), docker: await getNewDockerOrAsk(inputs), lint: getNewLintInput(inputs), + agents: getNewAgentsInput(inputs), editor: getEditorInput(inputs), git: await getNewGitOrAsk(inputs), }; @@ -111,6 +114,7 @@ export class NewAction extends AbstractAction { strict: values.strict, packageManager: values.packageManager, docker: values.docker, + agents: values.agents, }); } @@ -148,6 +152,7 @@ export class NewAction extends AbstractAction { workspace: values.multiplayer, docker: values.docker, editor: values.editor, + agents: values.agents, }; } diff --git a/src/command/commands/new.command.ts b/src/command/commands/new.command.ts index dfa8132..7a21bac 100644 --- a/src/command/commands/new.command.ts +++ b/src/command/commands/new.command.ts @@ -14,6 +14,7 @@ interface NewOptions { skipInstall?: boolean; docker?: boolean; lint?: boolean; + agents?: boolean; editor?: boolean; git?: boolean; gitRemote?: string | false; @@ -43,6 +44,7 @@ export class NewCommand extends AbstractCommand { .option("--docker", "generate docker files") .option("--no-docker", "do not generate docker files") .option("--no-lint", "do not generate lint files") + .option("--no-agents", "do not generate AGENTS.md files for AI coding agents") .option("--editor", "do add editor dependencies") .option("--git", "generate git repository") .option("--no-git", "do not generate git repository") @@ -64,6 +66,7 @@ export class NewCommand extends AbstractCommand { skipInstall: rawOptions.skipInstall, docker: rawOptions.docker, lint: rawOptions.lint, + agents: rawOptions.agents, editor: rawOptions.editor, git: rawOptions.git, }); diff --git a/src/lib/input/inputs/new/agents.input.spec.ts b/src/lib/input/inputs/new/agents.input.spec.ts new file mode 100644 index 0000000..6d5100b --- /dev/null +++ b/src/lib/input/inputs/new/agents.input.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; + +import { type Input } from "../../input.type"; +import { getNewAgentsInput } from "./agents.input"; + +const createInput = (entries: [string, any][]): Input => { + return new Map(entries.map(([key, value]) => [key, { value }])); +}; + +describe("getNewAgentsInput", () => { + it("should return the agents value when provided", () => { + const input = createInput([["agents", false]]); + expect(getNewAgentsInput(input)).toBe(false); + }); + + it("should return true as default when agents is missing", () => { + const input = createInput([]); + expect(getNewAgentsInput(input)).toBe(true); + }); +}); diff --git a/src/lib/input/inputs/new/agents.input.ts b/src/lib/input/inputs/new/agents.input.ts new file mode 100644 index 0000000..38d1dea --- /dev/null +++ b/src/lib/input/inputs/new/agents.input.ts @@ -0,0 +1,6 @@ +import { getBooleanInputWithDefault } from "../../base-inputs"; +import { type Input } from "../../input.type"; + +export const getNewAgentsInput = (inputs: Input): boolean => { + return getBooleanInputWithDefault(inputs, "agents", true); +}; diff --git a/src/lib/input/inputs/new/index.ts b/src/lib/input/inputs/new/index.ts index af35a3a..3d96883 100644 --- a/src/lib/input/inputs/new/index.ts +++ b/src/lib/input/inputs/new/index.ts @@ -1,3 +1,4 @@ +export * from "./agents.input"; export * from "./docker.input"; export * from "./language.input"; export * from "./lint.input";