@nanoforge-dev/schematics 2.3.1 → 3.0.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/collection.json +5 -0
  3. package/dist/index.cjs +4 -2
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +2 -2
  6. package/dist/index.d.cts.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/libs/component/component.factory.d.ts +1 -4
  12. package/dist/libs/component/component.factory.d.ts.map +1 -1
  13. package/dist/libs/component/component.factory.js +2 -4
  14. package/dist/libs/component/component.factory.js.map +1 -1
  15. package/dist/libs/plugin/files/README.md +23 -0
  16. package/dist/libs/plugin/files/__dot__gitignore +2 -0
  17. package/dist/libs/plugin/files/nanoforge.manifest.json.template +49 -0
  18. package/dist/libs/plugin/files/package.json.template +21 -0
  19. package/dist/libs/plugin/files/src/Panel.svelte +35 -0
  20. package/dist/libs/plugin/files/src/counter.ts +4 -0
  21. package/dist/libs/plugin/files/src/index.ts +52 -0
  22. package/dist/libs/plugin/files/svelte.config.js +2 -0
  23. package/dist/libs/plugin/files/tsconfig.json +14 -0
  24. package/dist/libs/plugin/files/vite.config.ts +12 -0
  25. package/dist/libs/plugin/plugin.factory.d.ts +18 -0
  26. package/dist/libs/plugin/plugin.factory.d.ts.map +1 -0
  27. package/dist/libs/plugin/plugin.factory.js +111 -0
  28. package/dist/libs/plugin/plugin.factory.js.map +1 -0
  29. package/dist/libs/plugin/schema.json +25 -0
  30. package/dist/libs/plugin/theme-files/README.md +21 -0
  31. package/dist/libs/plugin/theme-files/__dot__gitignore +2 -0
  32. package/dist/libs/plugin/theme-files/build.mjs +29 -0
  33. package/dist/libs/plugin/theme-files/icons/leaf.svg +1 -0
  34. package/dist/libs/plugin/theme-files/nanoforge.manifest.json.template +30 -0
  35. package/dist/libs/plugin/theme-files/package.json.template +11 -0
  36. package/dist/libs/project/files/js/AGENTS.md.template +126 -0
  37. package/dist/libs/project/files/js/CLAUDE.md.template +1 -0
  38. package/dist/libs/project/files/js/__dot__gitignore +4 -0
  39. package/dist/libs/project/files/ts/AGENTS.md.template +114 -0
  40. package/dist/libs/project/files/ts/CLAUDE.md.template +1 -0
  41. package/dist/libs/project/files/ts/__dot__gitignore +4 -0
  42. package/dist/libs/project/project.factory.d.ts +5 -4
  43. package/dist/libs/project/project.factory.d.ts.map +1 -1
  44. package/dist/libs/project/project.factory.js +5 -6
  45. package/dist/libs/project/project.factory.js.map +1 -1
  46. package/dist/libs/project/schema.json +5 -0
  47. package/dist/libs/system/system.factory.d.ts +1 -4
  48. package/dist/libs/system/system.factory.d.ts.map +1 -1
  49. package/dist/libs/system/system.factory.js +2 -4
  50. package/dist/libs/system/system.factory.js.map +1 -1
  51. package/dist/libs/workspace/files/js/AGENTS.md.template +49 -0
  52. package/dist/libs/workspace/files/js/CLAUDE.md.template +1 -0
  53. package/dist/libs/workspace/files/js/__dot__gitignore +4 -0
  54. package/dist/libs/workspace/files/ts/AGENTS.md.template +49 -0
  55. package/dist/libs/workspace/files/ts/CLAUDE.md.template +1 -0
  56. package/dist/libs/workspace/files/ts/__dot__gitignore +4 -0
  57. package/dist/libs/workspace/schema.json +5 -0
  58. package/dist/libs/workspace/workspace.factory.d.ts +5 -4
  59. package/dist/libs/workspace/workspace.factory.d.ts.map +1 -1
  60. package/dist/libs/workspace/workspace.factory.js +5 -6
  61. package/dist/libs/workspace/workspace.factory.js.map +1 -1
  62. package/package.json +15 -16
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The theme has no code: its build copies the manifest and the icons into dist/, the folder the
3
+ * editor loads. With --watch, it copies them again on every change (the editor then reloads it).
4
+ */
5
+ import { cpSync, existsSync, mkdirSync, rmSync, watch } from "node:fs";
6
+
7
+ const build = () => {
8
+ rmSync("dist", { recursive: true, force: true });
9
+ mkdirSync("dist");
10
+ cpSync("nanoforge.manifest.json", "dist/nanoforge.manifest.json");
11
+ if (existsSync("icons")) cpSync("icons", "dist/icons", { recursive: true });
12
+ console.log("Built dist/");
13
+ };
14
+
15
+ // An editor saving mid-rename makes cpSync throw ENOENT: log it and wait for the next change
16
+ // rather than killing the watcher.
17
+ const rebuild = () => {
18
+ try {
19
+ build();
20
+ } catch (error) {
21
+ console.error(`Build failed: ${error.message}`);
22
+ }
23
+ };
24
+
25
+ build();
26
+ if (process.argv.includes("--watch")) {
27
+ for (const target of ["nanoforge.manifest.json", "icons"])
28
+ if (existsSync(target)) watch(target, { recursive: true }, rebuild);
29
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21c0-9 6-15 16-16-1 10-7 16-16 16Z"/><path d="M5 21 14 12"/></svg>
@@ -0,0 +1,30 @@
1
+ {
2
+ "type": "plugin",
3
+ "name": "<%= name %>",
4
+ "version": "0.1.0",
5
+ "displayName": "<%= title %>",
6
+ "description": "A color theme for the NanoForge editor.",
7
+ "keywords": ["theme"],
8
+ "engines": {
9
+ "editor": ">=0.1.0"
10
+ },
11
+ "contributes": {
12
+ "themes": [
13
+ {
14
+ "id": "<%= short %>",
15
+ "label": "<%= title %>",
16
+ "extends": "nanoforge-dark",
17
+ "colors": {
18
+ "accent": "#2f7d6d",
19
+ "accent-hover": "#286a5d",
20
+ "accent-text": "#7fd6c2"
21
+ },
22
+ "syntax": {
23
+ "keyword": "#7fd6c2",
24
+ "string": "#e6c07b"
25
+ }
26
+ }
27
+ ],
28
+ "icons": [{ "id": "<%= short %>-leaf", "path": "icons/leaf.svg" }]
29
+ }
30
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "<%= short %>-editor-theme",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "<%= title %>: a NanoForge editor theme",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "node build.mjs",
9
+ "dev": "node build.mjs --watch"
10
+ }
11
+ }
@@ -0,0 +1,126 @@
1
+ <% var nf = { npm: "npm exec --no -- nf", pnpm: "pnpm exec nf", yarn: "yarn nf", bun: "bun run nf" }[packageManager]; var add = { npm: "npm install", pnpm: "pnpm add", yarn: "yarn add", bun: "bun add" }[packageManager]; var networked = part === "server" || hasServer; %># <%= packageName %>
2
+
3
+ Notes for AI coding agents. This is a NanoForge <%= part %> app, written in JavaScript: ES
4
+ modules, with types in JSDoc comments. NanoForge is a game engine built on an entity component
5
+ system (ECS).
6
+ <% if (part === "client") { %>The game runs in the browser.<% } else { %>This app runs in Bun: it has no graphics, input or DOM.<% } %>
7
+ <% if (workspace) { %>
8
+ This app is part of a workspace: read `../../AGENTS.md` first.
9
+ <% } %>
10
+ ## Files
11
+
12
+ - `nanoforge.config.js`: the app config that `nf` reads (`type: "<%= part %>"`). Keep its
13
+ `entryFile`, `editor.entryFile` and `language: "js"`: the defaults are for TypeScript. Fields
14
+ and defaults: https://docs.nanoforge.eu/docs/cli/configuration/configuration-file
15
+ - `src/main.js`: the entry file. The loader calls its exported `main(options)` from the bundled
16
+ `main.js`, so keep the file name and the export.
17
+ - `src/components/`, `src/systems/`: one component or system per file,
18
+ `<kebab-name>.component.js` and `<kebab-name>.system.js`.
19
+ - `assets/`: copied to the root of `dist/` on each build, so `assets/hero.png` is served as
20
+ `/hero.png`. Read it with `ctx.assets.getAsset("/hero.png")`, an `NfFile` whose `path` is its URL.
21
+ - `dist/` is the build output, emptied on every build. `.nanoforge/` belongs to the NanoForge
22
+ tools: don't edit it.
23
+
24
+ ## Commands
25
+
26
+ `nf` comes from the `@nanoforge-dev/cli` dev dependency: run it as `<%= nf %> <command>`.
27
+ <% if (workspace) { %>
28
+ Run `nf dev` and `nf start` from the workspace root: they run this app with the other one, and
29
+ they read the `.env` of their `-d` folder (the current folder by default). To build this app
30
+ alone, run `<%= nf %> build -d apps/<%= part %>` at the root.
31
+ <% } else { %>
32
+ - `nf dev`: runs `nf build --watch` and `nf start --watch` together.<% if (part === "client") { %> The game is on
33
+ http://localhost:3000 (`port` in the config).<% } %>
34
+ - `nf build`: bundles `src/main.js` with Bun into `dist/`. It does not check the JSDoc types.
35
+ - `nf start`: <% if (part === "client") { %>serves<% } else { %>runs<% } %> the last build.
36
+ <% } %>
37
+ <% if (workspace && packageManager === "npm") { %>Add a package with `npm install <package> -w apps/<%= part %>` at the workspace root.<% } else { %>Add a package with `<%= add %> <package>` in this folder.<% } %>
38
+
39
+ The `@nanoforge-dev/*` packages are on `^2`. The online docs follow the engine's development
40
+ branch and can describe APIs that the installed release lacks: the installed types
41
+ (`@nanoforge-dev/<package>/dist` in `node_modules`) are the reference.
42
+
43
+ ## Game code
44
+
45
+ - `main` creates the app with `NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>`, registers each library with
46
+ `app.use(...)`, then calls `await app.init(options)`. `app.use` throws after `init`, and
47
+ `ecs.registry` throws before it. `await app.run()` starts the tick loop and returns.
48
+ - Components are plain classes. The registry keys them by their `name` field: give each class a
49
+ `name` equal to its class name (the generated ones use `this.constructor.name`), and never store
50
+ game data in `name`. Names must be unique in the app, and `entity` and `id` are reserved. An
51
+ entity has one component per class: adding a second one replaces the first.
52
+ - Systems are `(registry, ctx) => void` functions. They run once per tick, in the order of the
53
+ `registry.addSystem` calls. Their result is ignored, so an async system is not awaited.
54
+ - `registry.getZipper([A, B])` returns one object per entity that has both, keyed by component
55
+ name: `({ A, B }) => …`. The values are the live components: change them in place.
56
+ `getIndexedZipper` adds the entity `id`, and `registry.entityFromIndex(id)` turns it back into an
57
+ entity for `killEntity` or `removeComponent`.
58
+ - `ctx.app.delta` is the time since the last tick, in milliseconds. `tickRate` in `main` is the
59
+ number of ticks per second. `ctx.app.requestStop()` stops the game.<% if (part === "client") { %>
60
+ - Graphics use Konva: `@nanoforge-dev/graphics-2d` re-exports its shapes (`Circle`, `Rect`,
61
+ `Layer`…), and `ctx.graphics.baseLayer` is a layer already on the stage. Coordinates are in a
62
+ 1920×1080 design space scaled to the window (the `viewport` option of `createClient`).
63
+ - Keys: `ctx.input.isKeyPressed(InputEnum.KeyW)`, with `InputEnum` from `@nanoforge-dev/input`. Its
64
+ values are `KeyboardEvent.code` names.<% } %>
65
+ <% if (networked) { %>
66
+ ## Network
67
+
68
+ - The client and the server connect over TCP, a WebSocket, and over UDP, a WebRTC data channel
69
+ set up through a WebSocket. Their address and ports are `NANOFORGE_*` variables, which `nf start`
70
+ reads from the `.env` of <%= workspace ? "the workspace root" : "its `-d` folder" %>. It gives
71
+ `NANOFORGE_CLIENT_X` to the client and `NANOFORGE_SERVER_X` to the server, as `X` in
72
+ `options.env`. Everything the client gets reaches the browser: never put a secret there.
73
+ https://docs.nanoforge.eu/docs/cli/configuration/environment-variables
74
+ - The generated systems use `ctx.network.tcp` and encode JSON with `TextEncoder`. Check the
75
+ installed `@nanoforge-dev/network` types before using another network API.
76
+ <% } %>
77
+ ## Example
78
+
79
+ A component and a system, as the rules above want them:
80
+
81
+ ```js
82
+ // src/components/velocity-2d.component.js
83
+ /** Speed of an entity, in pixels per second. */
84
+ export class Velocity2D {
85
+ name = "Velocity2D";
86
+
87
+ /**
88
+ * @param {number} [x]
89
+ * @param {number} [y]
90
+ */
91
+ constructor(x = 0, y = 0) {
92
+ this.x = x;
93
+ this.y = y;
94
+ }
95
+ }
96
+
97
+ // src/systems/apply-velocity-2d.system.js
98
+ import { Position2D } from "../components/position-2d.component";
99
+ import { Velocity2D } from "../components/velocity-2d.component";
100
+
101
+ /**
102
+ * @typedef {import("@nanoforge-dev/common").Context} Context
103
+ * @typedef {import("@nanoforge-dev/ecs").Registry} Registry
104
+ */
105
+
106
+ /**
107
+ * Moves every entity that has a position and a velocity.
108
+ * @param {Registry} registry
109
+ * @param {Context} ctx
110
+ */
111
+ export const applyVelocity2D = (registry, ctx) => {
112
+ const seconds = ctx.app.delta / 1000;
113
+
114
+ registry.getZipper([Position2D, Velocity2D]).forEach(({ Position2D, Velocity2D }) => {
115
+ Position2D.x += Velocity2D.x * seconds;
116
+ Position2D.y += Velocity2D.y * seconds;
117
+ });
118
+ };
119
+ ```
120
+
121
+ Register it in `main` with `registry.addSystem(applyVelocity2D)`.
122
+
123
+ ## Docs
124
+
125
+ - `nf` commands: https://docs.nanoforge.eu/docs/cli/commands/overview
126
+ - Everything else: https://docs.nanoforge.eu
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -82,6 +82,10 @@ build/Release
82
82
 
83
83
  # Dependency directories
84
84
  node_modules/
85
+
86
+ # NanoForge registry packages (nf install restores them from nanoforge.packages.lock.json)
87
+ nf_modules/
88
+
85
89
  jspm_packages/
86
90
 
87
91
  # Snowpack dependency directory (https://snowpack.dev/)
@@ -0,0 +1,114 @@
1
+ <% var nf = { npm: "npm exec --no -- nf", pnpm: "pnpm exec nf", yarn: "yarn nf", bun: "bun run nf" }[packageManager]; var add = { npm: "npm install", pnpm: "pnpm add", yarn: "yarn add", bun: "bun add" }[packageManager]; var networked = part === "server" || hasServer; %># <%= packageName %>
2
+
3
+ Notes for AI coding agents. This is a NanoForge <%= part %> app, written in TypeScript.
4
+ NanoForge is a game engine built on an entity component system (ECS).
5
+ <% if (part === "client") { %>The game runs in the browser.<% } else { %>This app runs in Bun: it has no graphics, input or DOM.<% } %>
6
+ <% if (workspace) { %>
7
+ This app is part of a workspace: read `../../AGENTS.md` first.
8
+ <% } %>
9
+ ## Files
10
+
11
+ - `nanoforge.config.ts`: the app config that `nf` reads (`type: "<%= part %>"`). Fields and defaults:
12
+ https://docs.nanoforge.eu/docs/cli/configuration/configuration-file
13
+ - `src/main.ts`: the entry file. The loader calls its exported `main(options)` from the bundled
14
+ `main.js`, so keep the file name and the export.
15
+ - `src/components/`, `src/systems/`: one component or system per file,
16
+ `<kebab-name>.component.ts` and `<kebab-name>.system.ts`.
17
+ - `assets/`: copied to the root of `dist/` on each build, so `assets/hero.png` is served as
18
+ `/hero.png`. Read it with `ctx.assets.getAsset("/hero.png")`, an `NfFile` whose `path` is its URL.
19
+ - `dist/` is the build output, emptied on every build. `.nanoforge/` belongs to the NanoForge
20
+ tools: don't edit it.
21
+
22
+ ## Commands
23
+
24
+ `nf` comes from the `@nanoforge-dev/cli` dev dependency: run it as `<%= nf %> <command>`.
25
+ <% if (workspace) { %>
26
+ Run `nf dev` and `nf start` from the workspace root: they run this app with the other one, and
27
+ they read the `.env` of their `-d` folder (the current folder by default). To build this app
28
+ alone, run `<%= nf %> build -d apps/<%= part %>` at the root.
29
+ <% } else { %>
30
+ - `nf dev`: runs `nf build --watch` and `nf start --watch` together.<% if (part === "client") { %> The game is on
31
+ http://localhost:3000 (`port` in the config).<% } %>
32
+ - `nf build`: bundles `src/main.ts` with Bun into `dist/`. It does not type-check.
33
+ - `nf start`: <% if (part === "client") { %>serves<% } else { %>runs<% } %> the last build.
34
+ <% } %>
35
+ <% if (workspace && packageManager === "npm") { %>Add a package with `npm install <package> -w apps/<%= part %>` at the workspace root.<% } else { %>Add a package with `<%= add %> <package>` in this folder.<% } %>
36
+
37
+ The `@nanoforge-dev/*` packages are on `^2`. The online docs follow the engine's development
38
+ branch and can describe APIs that the installed release lacks: the installed types
39
+ (`@nanoforge-dev/<package>/dist` in `node_modules`) are the reference.
40
+
41
+ ## Game code
42
+
43
+ - `main` creates the app with `NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>`, registers each library with
44
+ `app.use(...)`, then calls `await app.init(options)`. `app.use` throws after `init`, and
45
+ `ecs.registry` throws before it. `await app.run()` starts the tick loop and returns.
46
+ - Components are plain classes. The registry keys them by their `name` field: give each class a
47
+ `name` equal to its class name (the generated ones use `this.constructor.name`), and never store
48
+ game data in `name`. Names must be unique in the app, and `entity` and `id` are reserved. An
49
+ entity has one component per class: adding a second one replaces the first.
50
+ - Systems are `(registry, ctx) => void` functions. They run once per tick, in the order of the
51
+ `registry.addSystem` calls. Their result is ignored, so an async system is not awaited.
52
+ - `registry.getZipper([A, B])` returns one object per entity that has both, keyed by component
53
+ name: `({ A, B }) => …`. The values are the live components: change them in place.
54
+ `getIndexedZipper` adds the entity `id`, and `registry.entityFromIndex(id)` turns it back into an
55
+ entity for `killEntity` or `removeComponent`.
56
+ - `ctx.app.delta` is the time since the last tick, in milliseconds. `tickRate` in `main` is the
57
+ number of ticks per second. `ctx.app.requestStop()` stops the game.<% if (part === "client") { %>
58
+ - Graphics use Konva: `@nanoforge-dev/graphics-2d` re-exports its shapes (`Circle`, `Rect`,
59
+ `Layer`…), and `ctx.graphics.baseLayer` is a layer already on the stage. Coordinates are in a
60
+ 1920×1080 design space scaled to the window (the `viewport` option of `createClient`).
61
+ - Keys: `ctx.input.isKeyPressed(InputEnum.KeyW)`, with `InputEnum` from `@nanoforge-dev/input`. Its
62
+ values are `KeyboardEvent.code` names.<% } %>
63
+ <% if (networked) { %>
64
+ ## Network
65
+
66
+ - The client and the server connect over TCP, a WebSocket, and over UDP, a WebRTC data channel
67
+ set up through a WebSocket. Their address and ports are `NANOFORGE_*` variables, which `nf start`
68
+ reads from the `.env` of <%= workspace ? "the workspace root" : "its `-d` folder" %>. It gives
69
+ `NANOFORGE_CLIENT_X` to the client and `NANOFORGE_SERVER_X` to the server, as `X` in
70
+ `options.env`. Everything the client gets reaches the browser: never put a secret there.
71
+ https://docs.nanoforge.eu/docs/cli/configuration/environment-variables
72
+ - The generated systems use `ctx.network.tcp` and encode JSON with `TextEncoder`. Check the
73
+ installed `@nanoforge-dev/network` types before using another network API.
74
+ <% } %>
75
+ ## Example
76
+
77
+ A component and a system, as the rules above want them:
78
+
79
+ ```ts
80
+ // src/components/velocity-2d.component.ts
81
+ /** Speed of an entity, in pixels per second. */
82
+ export class Velocity2D {
83
+ name = "Velocity2D";
84
+
85
+ constructor(
86
+ public x = 0,
87
+ public y = 0,
88
+ ) {}
89
+ }
90
+
91
+ // src/systems/apply-velocity-2d.system.ts
92
+ import { type Context } from "@nanoforge-dev/common";
93
+ import { type Registry } from "@nanoforge-dev/ecs";
94
+
95
+ import { Position2D } from "../components/position-2d.component";
96
+ import { Velocity2D } from "../components/velocity-2d.component";
97
+
98
+ /** Moves every entity that has a position and a velocity. */
99
+ export const applyVelocity2D = (registry: Registry, ctx: Context): void => {
100
+ const seconds = ctx.app.delta / 1000;
101
+
102
+ registry.getZipper([Position2D, Velocity2D]).forEach(({ Position2D, Velocity2D }) => {
103
+ Position2D.x += Velocity2D.x * seconds;
104
+ Position2D.y += Velocity2D.y * seconds;
105
+ });
106
+ };
107
+ ```
108
+
109
+ Register it in `main` with `registry.addSystem(applyVelocity2D)`.
110
+
111
+ ## Docs
112
+
113
+ - `nf` commands: https://docs.nanoforge.eu/docs/cli/commands/overview
114
+ - Everything else: https://docs.nanoforge.eu
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -82,6 +82,10 @@ build/Release
82
82
 
83
83
  # Dependency directories
84
84
  node_modules/
85
+
86
+ # NanoForge registry packages (nf install restores them from nanoforge.packages.lock.json)
87
+ nf_modules/
88
+
85
89
  jspm_packages/
86
90
 
87
91
  # Snowpack dependency directory (https://snowpack.dev/)
@@ -1,5 +1,4 @@
1
1
  import { Rule } from "@angular-devkit/schematics";
2
- //#region src/libs/project/project.schema.d.ts
3
2
  interface ProjectSchema {
4
3
  /**
5
4
  * The type of project to generate
@@ -56,9 +55,11 @@ interface ProjectSchema {
56
55
  * Add editor dependencies
57
56
  */
58
57
  editor: boolean;
58
+ /**
59
+ * Generate an AGENTS.md file documenting the project for AI coding agents, and a CLAUDE.md that imports it
60
+ */
61
+ agents: boolean;
59
62
  }
60
- //#endregion
61
- //#region src/libs/project/project.factory.d.ts
62
63
  export declare const main: (schema: ProjectSchema) => Rule;
63
- //#endregion
64
+ export {};
64
65
  //# sourceMappingURL=project.factory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"project.factory.d.ts","names":[],"sources":["../../../src/libs/project/project.schema.d.ts","../../../src/libs/project/project.factory.ts"],"mappings":";;UAAiB;;;;EAIf;;;;;;EAOA;;;;;;;EAQA;;;;;;EAOA;;;;EAKA;;;;EAKA;;;;EAKA;;;;EAKA;;;;;EAMA;;;;;;;EAQA;;;;EAKA;;;;qBCmEW,OAAQ,QAAQ,kBAAgB"}
1
+ {"version":3,"file":"project.factory.d.ts","names":[],"sources":["../../../src/libs/project/project.schema.d.ts","../../../src/libs/project/project.factory.ts"],"mappings":";UAAiB;;;;EAIf;;;;;;EAOA;;;;;;;EAQA;;;;;;EAOA;;;;EAKA;;;;EAKA;;;;EAKA;;;;EAKA;;;;;EAMA;;;;;;;EAQA;;;;EAKA;;;;EAKA;;qBCoEW,OAAQ,QAAQ,kBAAgB"}
@@ -1,6 +1,6 @@
1
1
  import { join, normalize, strings } from "@angular-devkit/core";
2
2
  import { apply, filter, mergeWith, move, renameTemplateFiles, template, url } from "@angular-devkit/schematics";
3
- //#region src/utils/formatting.ts
3
+ // src/utils/formatting.ts
4
4
  const toWords = (str) => {
5
5
  return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[-_]+/g, " ").trim().split(/\s+/).filter(Boolean);
6
6
  };
@@ -9,8 +9,7 @@ const toKebabCase = (str) => {
9
9
  };
10
10
  /** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */
11
11
  const DEFAULT_CLI_DEPENDENCY_VERSION = "latest";
12
- //#endregion
13
- //#region src/utils/registry.ts
12
+ // src/utils/registry.ts
14
13
  const REGISTRY_URL = "https://registry.npmjs.org";
15
14
  const DEFAULT_TIMEOUT_MS = 3e3;
16
15
  const RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
@@ -48,8 +47,7 @@ const fetchTrustedVersion = async (packageName, fallback, options = {}) => {
48
47
  clearTimeout(timeout);
49
48
  }
50
49
  };
51
- //#endregion
52
- //#region src/libs/project/project.factory.ts
50
+ // src/libs/project/project.factory.ts
53
51
  const resolveDepVersions = async (part) => {
54
52
  const isClient = part === "client";
55
53
  const [engine, cli, ecs, network, graphics2d, input] = await Promise.all([
@@ -92,6 +90,7 @@ const transform = async (schema) => {
92
90
  hasServer: schema.hasServer,
93
91
  docker: schema.docker ?? false,
94
92
  editor: schema.editor ?? false,
93
+ agents: schema.agents ?? true,
95
94
  allowBuilds: workspace ? [] : ["bun"]
96
95
  };
97
96
  };
@@ -104,6 +103,7 @@ const generate = (options, path) => {
104
103
  }),
105
104
  filter((path) => options.docker && !options.workspace || !["Dockerfile", ".dockerignore"].includes(path.split("/").at(-1) ?? "")),
106
105
  filter((path) => options.packageManager === "pnpm" && options.allowBuilds.length > 0 || !(path.split("/").at(-1) ?? "").startsWith("pnpm-workspace.yaml")),
106
+ filter((path) => options.agents || !["AGENTS.md", "CLAUDE.md"].some((name) => (path.split("/").at(-1) ?? "").startsWith(name))),
107
107
  filter((path) => {
108
108
  const filename = path.split("/").at(-1) ?? "";
109
109
  if (filename.startsWith("drawable-circle-2d.component") || filename.startsWith("draw-2d.system")) return options.part === "client";
@@ -121,7 +121,6 @@ const main = (schema) => {
121
121
  return mergeWith(generate(options, schema.directory));
122
122
  };
123
123
  };
124
- //#endregion
125
124
  export { main };
126
125
 
127
126
  //# sourceMappingURL=project.factory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"project.factory.js","names":[],"sources":["../../../src/utils/formatting.ts","../../../src/defaults.ts","../../../src/utils/registry.ts","../../../src/libs/project/project.factory.ts"],"sourcesContent":["const toWords = (str: string): string[] => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n};\n\nexport const toKebabCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.toLowerCase())\n .join(\"-\");\n};\n\nexport const toPascalCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n};\n\nexport const toCamelCase = (str: string): string => {\n const words = toWords(str);\n return words\n .map((word, i) =>\n i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),\n )\n .join(\"\");\n};\n","export const DEFAULT_APP_NAME = \"nanoforge-app\";\nexport const DEFAULT_LANGUAGE = \"ts\";\nexport const DEFAULT_PACKAGE_MANAGER = \"npm\";\nexport const DEFAULT_NANOFORGE_DEPENDENCY_VERSION = \"^2\";\nexport const DEFAULT_ENGINE_VERSION = \"^2\";\n\n/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */\nexport const DEFAULT_CLI_DEPENDENCY_VERSION = \"latest\";\n\n/**\n * Fallback used when the registry lookup for `typescript` fails. A specific\n * version, not a range — non-nanoforge packages are pinned exactly.\n */\nexport const DEFAULT_TYPESCRIPT_VERSION = \"6.0.3\";\n\n/**\n * Mirrors pnpm's own `minimumReleaseAge` supply-chain protection: when\n * resolving a pinned dependency version at generation time, only consider\n * versions published at least this long ago, so a freshly (and potentially\n * maliciously) published release isn't picked up immediately.\n */\nexport const MIN_RELEASE_AGE_HOURS = 48;\n","import { MIN_RELEASE_AGE_HOURS } from \"~/defaults\";\n\nconst REGISTRY_URL = \"https://registry.npmjs.org\";\nconst DEFAULT_TIMEOUT_MS = 3000;\n\nconst RELEASE_VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+$/;\n\nconst compareVersions = (a: string, b: string): number => {\n const partsA = a.split(\".\").map(Number);\n const partsB = b.split(\".\").map(Number);\n for (let i = 0; i < 3; i++) {\n const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);\n if (diff !== 0) {\n return diff;\n }\n }\n return 0;\n};\n\nexport interface FetchTrustedVersionOptions {\n /**\n * Only consider versions published at least this many hours ago, mirroring\n * pnpm's `minimumReleaseAge` supply-chain protection for a freshly\n * published (and potentially compromised) release.\n * @default MIN_RELEASE_AGE_HOURS\n */\n minAgeHours?: number;\n\n /**\n * Restrict candidates to this major version (e.g. `6` only considers\n * `6.x.x`). Useful to avoid silently jumping a generated project onto a\n * new major release.\n */\n major?: number;\n\n /** Network timeout in milliseconds. @default 3000 */\n timeoutMs?: number;\n}\n\n/**\n * Resolves the newest published version of an npm package that is at least\n * `minAgeHours` old, so freshly (and potentially maliciously) published\n * versions aren't picked up immediately. Falls back to `fallback` on any\n * network error, timeout, missing package, or when no version satisfies the\n * constraints, so callers never have to handle a rejected promise.\n */\nexport const fetchTrustedVersion = async (\n packageName: string,\n fallback: string,\n options: FetchTrustedVersionOptions = {},\n): Promise<string> => {\n const { minAgeHours = MIN_RELEASE_AGE_HOURS, major, timeoutMs = DEFAULT_TIMEOUT_MS } = options;\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(`${REGISTRY_URL}/${encodeURIComponent(packageName)}`, {\n signal: controller.signal,\n });\n if (!response.ok) {\n return fallback;\n }\n\n const data = (await response.json()) as { time?: Record<string, string> };\n const time = data.time;\n if (!time) {\n return fallback;\n }\n\n const cutoff = Date.now() - minAgeHours * 60 * 60 * 1000;\n const eligible = Object.entries(time)\n .filter(([version]) => RELEASE_VERSION_PATTERN.test(version))\n .filter(([version]) => major === undefined || Number(version.split(\".\")[0]) === major)\n .filter(([, publishedAt]) => new Date(publishedAt).getTime() <= cutoff)\n .map(([version]) => version)\n .sort(compareVersions);\n\n return eligible.at(-1) ? `^${eligible.at(-1)}` : fallback;\n } catch {\n return fallback;\n } finally {\n clearTimeout(timeout);\n }\n};\n\n/**\n * Resolves several packages' trusted versions in parallel. Keys of `specs`\n * are the package name to query the registry for; values are the fallback\n * to use if that lookup fails. Returns a map from the *same* keys to the\n * resolved (or fallback) version — typed so callers don't get `| undefined`\n * back for a key they just passed in.\n */\nexport const fetchTrustedVersions = async <\n T extends Record<string, { fallback: string; major?: number }>,\n>(\n specs: T,\n): Promise<{ [K in keyof T]: string }> => {\n const entries = await Promise.all(\n Object.entries(specs).map(async ([packageName, { fallback, major }]) => {\n const version = await fetchTrustedVersion(packageName, fallback, { major });\n return [packageName, version] as const;\n }),\n );\n return Object.fromEntries(entries) as { [K in keyof T]: string };\n};\n","import { type Path, join, normalize, strings } from \"@angular-devkit/core\";\nimport {\n type Rule,\n type Source,\n apply,\n filter,\n mergeWith,\n move,\n renameTemplateFiles,\n template,\n url,\n} from \"@angular-devkit/schematics\";\n\nimport { toKebabCase } from \"@utils/formatting\";\nimport { fetchTrustedVersion } from \"@utils/registry\";\n\nimport {\n DEFAULT_APP_NAME,\n DEFAULT_CLI_DEPENDENCY_VERSION,\n DEFAULT_ENGINE_VERSION,\n DEFAULT_LANGUAGE,\n DEFAULT_NANOFORGE_DEPENDENCY_VERSION,\n DEFAULT_PACKAGE_MANAGER,\n} from \"~/defaults\";\n\nimport { type ProjectOptions } from \"./project.options\";\nimport { type ProjectSchema } from \"./project.schema\";\n\ninterface DepVersions {\n ecs: string;\n graphics2d: string;\n input: string;\n network: string;\n engine: string;\n cli: string;\n}\n\nconst resolveDepVersions = async (part: ProjectSchema[\"part\"]): Promise<DepVersions> => {\n const isClient = part === \"client\";\n\n const [engine, cli, ecs, network, graphics2d, input] = await Promise.all([\n fetchTrustedVersion(\"@nanoforge-dev/core\", DEFAULT_ENGINE_VERSION),\n fetchTrustedVersion(\"@nanoforge-dev/cli\", DEFAULT_CLI_DEPENDENCY_VERSION),\n fetchTrustedVersion(\"@nanoforge-dev/ecs\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION),\n fetchTrustedVersion(\"@nanoforge-dev/network\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION),\n isClient\n ? fetchTrustedVersion(\"@nanoforge-dev/graphics-2d\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION)\n : DEFAULT_NANOFORGE_DEPENDENCY_VERSION,\n isClient\n ? fetchTrustedVersion(\"@nanoforge-dev/input\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION)\n : DEFAULT_NANOFORGE_DEPENDENCY_VERSION,\n ]);\n\n return {\n ecs,\n graphics2d,\n input,\n network,\n engine,\n cli,\n };\n};\n\nconst transform = async (schema: ProjectSchema): Promise<ProjectOptions> => {\n const packageManager = schema.packageManager ?? DEFAULT_PACKAGE_MANAGER;\n const workspace = schema.workspace ?? true;\n const depVersions = await resolveDepVersions(schema.part);\n\n const name = schema.name ?? schema.part;\n const packageName = schema.workspaceName\n ? `${toKebabCase(schema.workspaceName)}-${toKebabCase(name)}`\n : toKebabCase(schema.name ?? DEFAULT_APP_NAME);\n\n return {\n part: schema.part,\n appClass: schema.part === \"client\" ? \"NanoforgeClient\" : \"NanoforgeServer\",\n packageName,\n language: schema.language ?? DEFAULT_LANGUAGE,\n strict: schema.strict ?? true,\n packageManager,\n workspace,\n ecsVersion: depVersions.ecs,\n graphics2dVersion: depVersions.graphics2d,\n inputVersion: depVersions.input,\n networkVersion: depVersions.network,\n engineVersion: depVersions.engine,\n cliVersion: depVersions.cli,\n hasServer: schema.hasServer,\n docker: schema.docker ?? false,\n editor: schema.editor ?? false,\n allowBuilds: workspace ? [] : [\"bun\"],\n };\n};\n\nconst generate = (options: ProjectOptions, path: string): Source => {\n return apply(url(join(\"./files\" as Path, options.language)), [\n template({\n dot: \".\",\n ...strings,\n ...options,\n }),\n filter(\n (path) =>\n (options.docker && !options.workspace) ||\n ![\"Dockerfile\", \".dockerignore\"].includes(path.split(\"/\").at(-1) ?? \"\"),\n ),\n filter(\n (path) =>\n (options.packageManager === \"pnpm\" && options.allowBuilds.length > 0) ||\n !(path.split(\"/\").at(-1) ?? \"\").startsWith(\"pnpm-workspace.yaml\"),\n ),\n filter((path) => {\n const filename = path.split(\"/\").at(-1) ?? \"\";\n if (\n filename.startsWith(\"drawable-circle-2d.component\") ||\n filename.startsWith(\"draw-2d.system\")\n ) {\n return options.part === \"client\";\n }\n if (filename.startsWith(\"move-2d.system\")) {\n return options.part === \"server\";\n }\n if (filename.startsWith(\"position-sync.system\")) {\n return options.part === \"client\" && options.hasServer;\n }\n return true;\n }),\n renameTemplateFiles(),\n move(normalize(path)),\n ]);\n};\n\nexport const main = (schema: ProjectSchema): Rule => {\n return async () => {\n const options = await transform(schema);\n\n return mergeWith(generate(options, schema.directory));\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"project.factory.js","names":[],"sources":["../../../src/utils/formatting.ts","../../../src/defaults.ts","../../../src/utils/registry.ts","../../../src/libs/project/project.factory.ts"],"sourcesContent":["const toWords = (str: string): string[] => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n};\n\nexport const toKebabCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.toLowerCase())\n .join(\"-\");\n};\n\nexport const toPascalCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n};\n\nexport const toCamelCase = (str: string): string => {\n const words = toWords(str);\n return words\n .map((word, i) =>\n i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),\n )\n .join(\"\");\n};\n","export const DEFAULT_APP_NAME = \"nanoforge-app\";\nexport const DEFAULT_LANGUAGE = \"ts\";\nexport const DEFAULT_PACKAGE_MANAGER = \"npm\";\nexport const DEFAULT_NANOFORGE_DEPENDENCY_VERSION = \"^2\";\nexport const DEFAULT_ENGINE_VERSION = \"^2\";\n\n/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */\nexport const DEFAULT_CLI_DEPENDENCY_VERSION = \"latest\";\n\n/**\n * Fallback used when the registry lookup for `typescript` fails. A specific\n * version, not a range — non-nanoforge packages are pinned exactly.\n */\nexport const DEFAULT_TYPESCRIPT_VERSION = \"6.0.3\";\n\n/**\n * Mirrors pnpm's own `minimumReleaseAge` supply-chain protection: when\n * resolving a pinned dependency version at generation time, only consider\n * versions published at least this long ago, so a freshly (and potentially\n * maliciously) published release isn't picked up immediately.\n */\nexport const MIN_RELEASE_AGE_HOURS = 48;\n\n/** Fallback for the editor packages (`@nanoforge-dev/editor-*`), which share one version line. */\nexport const DEFAULT_EDITOR_VERSION = \"^1.0.0\";\n","import { MIN_RELEASE_AGE_HOURS } from \"~/defaults\";\n\nconst REGISTRY_URL = \"https://registry.npmjs.org\";\nconst DEFAULT_TIMEOUT_MS = 3000;\n\nconst RELEASE_VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+$/;\n\nconst compareVersions = (a: string, b: string): number => {\n const partsA = a.split(\".\").map(Number);\n const partsB = b.split(\".\").map(Number);\n for (let i = 0; i < 3; i++) {\n const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);\n if (diff !== 0) {\n return diff;\n }\n }\n return 0;\n};\n\nexport interface FetchTrustedVersionOptions {\n /**\n * Only consider versions published at least this many hours ago, mirroring\n * pnpm's `minimumReleaseAge` supply-chain protection for a freshly\n * published (and potentially compromised) release.\n * @default MIN_RELEASE_AGE_HOURS\n */\n minAgeHours?: number;\n\n /**\n * Restrict candidates to this major version (e.g. `6` only considers\n * `6.x.x`). Useful to avoid silently jumping a generated project onto a\n * new major release.\n */\n major?: number;\n\n /** Network timeout in milliseconds. @default 3000 */\n timeoutMs?: number;\n}\n\n/**\n * Resolves the newest published version of an npm package that is at least\n * `minAgeHours` old, so freshly (and potentially maliciously) published\n * versions aren't picked up immediately. Falls back to `fallback` on any\n * network error, timeout, missing package, or when no version satisfies the\n * constraints, so callers never have to handle a rejected promise.\n */\nexport const fetchTrustedVersion = async (\n packageName: string,\n fallback: string,\n options: FetchTrustedVersionOptions = {},\n): Promise<string> => {\n const { minAgeHours = MIN_RELEASE_AGE_HOURS, major, timeoutMs = DEFAULT_TIMEOUT_MS } = options;\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(`${REGISTRY_URL}/${encodeURIComponent(packageName)}`, {\n signal: controller.signal,\n });\n if (!response.ok) {\n return fallback;\n }\n\n const data = (await response.json()) as { time?: Record<string, string> };\n const time = data.time;\n if (!time) {\n return fallback;\n }\n\n const cutoff = Date.now() - minAgeHours * 60 * 60 * 1000;\n const eligible = Object.entries(time)\n .filter(([version]) => RELEASE_VERSION_PATTERN.test(version))\n .filter(([version]) => major === undefined || Number(version.split(\".\")[0]) === major)\n .filter(([, publishedAt]) => new Date(publishedAt).getTime() <= cutoff)\n .map(([version]) => version)\n .sort(compareVersions);\n\n return eligible.at(-1) ? `^${eligible.at(-1)}` : fallback;\n } catch {\n return fallback;\n } finally {\n clearTimeout(timeout);\n }\n};\n\n/**\n * Resolves several packages' trusted versions in parallel. Keys of `specs`\n * are the package name to query the registry for; values are the fallback\n * to use if that lookup fails. Returns a map from the *same* keys to the\n * resolved (or fallback) version — typed so callers don't get `| undefined`\n * back for a key they just passed in.\n */\nexport const fetchTrustedVersions = async <\n T extends Record<string, { fallback: string; major?: number }>,\n>(\n specs: T,\n): Promise<{ [K in keyof T]: string }> => {\n const entries = await Promise.all(\n Object.entries(specs).map(async ([packageName, { fallback, major }]) => {\n const version = await fetchTrustedVersion(packageName, fallback, { major });\n return [packageName, version] as const;\n }),\n );\n return Object.fromEntries(entries) as { [K in keyof T]: string };\n};\n","import { type Path, join, normalize, strings } from \"@angular-devkit/core\";\nimport {\n type Rule,\n type Source,\n apply,\n filter,\n mergeWith,\n move,\n renameTemplateFiles,\n template,\n url,\n} from \"@angular-devkit/schematics\";\n\nimport { toKebabCase } from \"@utils/formatting\";\nimport { fetchTrustedVersion } from \"@utils/registry\";\n\nimport {\n DEFAULT_APP_NAME,\n DEFAULT_CLI_DEPENDENCY_VERSION,\n DEFAULT_ENGINE_VERSION,\n DEFAULT_LANGUAGE,\n DEFAULT_NANOFORGE_DEPENDENCY_VERSION,\n DEFAULT_PACKAGE_MANAGER,\n} from \"~/defaults\";\n\nimport { type ProjectOptions } from \"./project.options\";\nimport { type ProjectSchema } from \"./project.schema\";\n\ninterface DepVersions {\n ecs: string;\n graphics2d: string;\n input: string;\n network: string;\n engine: string;\n cli: string;\n}\n\nconst resolveDepVersions = async (part: ProjectSchema[\"part\"]): Promise<DepVersions> => {\n const isClient = part === \"client\";\n\n const [engine, cli, ecs, network, graphics2d, input] = await Promise.all([\n fetchTrustedVersion(\"@nanoforge-dev/core\", DEFAULT_ENGINE_VERSION),\n fetchTrustedVersion(\"@nanoforge-dev/cli\", DEFAULT_CLI_DEPENDENCY_VERSION),\n fetchTrustedVersion(\"@nanoforge-dev/ecs\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION),\n fetchTrustedVersion(\"@nanoforge-dev/network\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION),\n isClient\n ? fetchTrustedVersion(\"@nanoforge-dev/graphics-2d\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION)\n : DEFAULT_NANOFORGE_DEPENDENCY_VERSION,\n isClient\n ? fetchTrustedVersion(\"@nanoforge-dev/input\", DEFAULT_NANOFORGE_DEPENDENCY_VERSION)\n : DEFAULT_NANOFORGE_DEPENDENCY_VERSION,\n ]);\n\n return {\n ecs,\n graphics2d,\n input,\n network,\n engine,\n cli,\n };\n};\n\nconst transform = async (schema: ProjectSchema): Promise<ProjectOptions> => {\n const packageManager = schema.packageManager ?? DEFAULT_PACKAGE_MANAGER;\n const workspace = schema.workspace ?? true;\n const depVersions = await resolveDepVersions(schema.part);\n\n const name = schema.name ?? schema.part;\n const packageName = schema.workspaceName\n ? `${toKebabCase(schema.workspaceName)}-${toKebabCase(name)}`\n : toKebabCase(schema.name ?? DEFAULT_APP_NAME);\n\n return {\n part: schema.part,\n appClass: schema.part === \"client\" ? \"NanoforgeClient\" : \"NanoforgeServer\",\n packageName,\n language: schema.language ?? DEFAULT_LANGUAGE,\n strict: schema.strict ?? true,\n packageManager,\n workspace,\n ecsVersion: depVersions.ecs,\n graphics2dVersion: depVersions.graphics2d,\n inputVersion: depVersions.input,\n networkVersion: depVersions.network,\n engineVersion: depVersions.engine,\n cliVersion: depVersions.cli,\n hasServer: schema.hasServer,\n docker: schema.docker ?? false,\n editor: schema.editor ?? false,\n agents: schema.agents ?? true,\n allowBuilds: workspace ? [] : [\"bun\"],\n };\n};\n\nconst generate = (options: ProjectOptions, path: string): Source => {\n return apply(url(join(\"./files\" as Path, options.language)), [\n template({\n dot: \".\",\n ...strings,\n ...options,\n }),\n filter(\n (path) =>\n (options.docker && !options.workspace) ||\n ![\"Dockerfile\", \".dockerignore\"].includes(path.split(\"/\").at(-1) ?? \"\"),\n ),\n filter(\n (path) =>\n (options.packageManager === \"pnpm\" && options.allowBuilds.length > 0) ||\n !(path.split(\"/\").at(-1) ?? \"\").startsWith(\"pnpm-workspace.yaml\"),\n ),\n filter(\n (path) =>\n options.agents ||\n ![\"AGENTS.md\", \"CLAUDE.md\"].some((name) => (path.split(\"/\").at(-1) ?? \"\").startsWith(name)),\n ),\n filter((path) => {\n const filename = path.split(\"/\").at(-1) ?? \"\";\n if (\n filename.startsWith(\"drawable-circle-2d.component\") ||\n filename.startsWith(\"draw-2d.system\")\n ) {\n return options.part === \"client\";\n }\n if (filename.startsWith(\"move-2d.system\")) {\n return options.part === \"server\";\n }\n if (filename.startsWith(\"position-sync.system\")) {\n return options.part === \"client\" && options.hasServer;\n }\n return true;\n }),\n renameTemplateFiles(),\n move(normalize(path)),\n ]);\n};\n\nexport const main = (schema: ProjectSchema): Rule => {\n return async () => {\n const options = await transform(schema);\n\n return mergeWith(generate(options, schema.directory));\n };\n};\n"],"mappings":"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"}
@@ -66,6 +66,11 @@
66
66
  "description": "Add editor dependencies",
67
67
  "default": false
68
68
  },
69
+ "agents": {
70
+ "type": "boolean",
71
+ "description": "Generate an AGENTS.md file documenting the project for AI coding agents, and a CLAUDE.md that imports it",
72
+ "default": true
73
+ },
69
74
  "libs": {
70
75
  "type": "array",
71
76
  "items": { "type": "string" },
@@ -1,5 +1,4 @@
1
1
  import { Rule } from "@angular-devkit/schematics";
2
- //#region src/libs/system/system.schema.d.ts
3
2
  interface SystemSchema {
4
3
  /**
5
4
  * System destination directory
@@ -18,8 +17,6 @@ interface SystemSchema {
18
17
  */
19
18
  language: "js" | "ts";
20
19
  }
21
- //#endregion
22
- //#region src/libs/system/system.factory.d.ts
23
20
  export declare const main: (schema: SystemSchema) => Rule;
24
- //#endregion
21
+ export {};
25
22
  //# sourceMappingURL=system.factory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"system.factory.d.ts","names":[],"sources":["../../../src/libs/system/system.schema.d.ts","../../../src/libs/system/system.factory.ts"],"mappings":";;UAAiB;;;;EAIf;;;;EAKA;;;;EAKA;;;;EAKA;;;;qBCiBW,OAAQ,QAAQ,iBAAe"}
1
+ {"version":3,"file":"system.factory.d.ts","names":[],"sources":["../../../src/libs/system/system.schema.d.ts","../../../src/libs/system/system.factory.ts"],"mappings":";UAAiB;;;;EAIf;;;;EAKA;;;;EAKA;;;;EAKA;;qBCiBW,OAAQ,QAAQ,iBAAe"}
@@ -1,6 +1,6 @@
1
1
  import { join, normalize, strings } from "@angular-devkit/core";
2
2
  import { apply, mergeWith, move, template, url } from "@angular-devkit/schematics";
3
- //#region src/utils/formatting.ts
3
+ // src/utils/formatting.ts
4
4
  const toWords = (str) => {
5
5
  return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[-_]+/g, " ").trim().split(/\s+/).filter(Boolean);
6
6
  };
@@ -10,8 +10,7 @@ const toKebabCase = (str) => {
10
10
  const toCamelCase = (str) => {
11
11
  return toWords(str).map((word, i) => i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
12
12
  };
13
- //#endregion
14
- //#region src/libs/system/system.factory.ts
13
+ // src/libs/system/system.factory.ts
15
14
  const transform = (schema) => {
16
15
  return {
17
16
  part: schema.part,
@@ -30,7 +29,6 @@ const main = (schema) => {
30
29
  const options = transform(schema);
31
30
  return mergeWith(generate(options, schema.directory, schema.language));
32
31
  };
33
- //#endregion
34
32
  export { main };
35
33
 
36
34
  //# sourceMappingURL=system.factory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"system.factory.js","names":[],"sources":["../../../src/utils/formatting.ts","../../../src/libs/system/system.factory.ts"],"sourcesContent":["const toWords = (str: string): string[] => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n};\n\nexport const toKebabCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.toLowerCase())\n .join(\"-\");\n};\n\nexport const toPascalCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n};\n\nexport const toCamelCase = (str: string): string => {\n const words = toWords(str);\n return words\n .map((word, i) =>\n i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),\n )\n .join(\"\");\n};\n","import { type Path, join, normalize, strings } from \"@angular-devkit/core\";\nimport {\n type Rule,\n type Source,\n apply,\n mergeWith,\n move,\n template,\n url,\n} from \"@angular-devkit/schematics\";\n\nimport { toCamelCase, toKebabCase } from \"@utils/formatting\";\n\nimport { type SystemOptions } from \"./system.options\";\nimport { type SystemSchema } from \"./system.schema\";\n\nconst transform = (schema: SystemSchema): SystemOptions => {\n return {\n part: schema.part,\n functionName: toCamelCase(schema.name),\n fileName: toKebabCase(schema.name),\n };\n};\n\nconst generate = (options: SystemOptions, path: string, language: \"ts\" | \"js\"): Source => {\n const rules = [\n template({\n ...strings,\n ...options,\n }),\n move(normalize(path)),\n ];\n\n return apply(url(join(\"./files\" as Path, language)), rules);\n};\n\nexport const main = (schema: SystemSchema): Rule => {\n const options = transform(schema);\n\n return mergeWith(generate(options, schema.directory, schema.language));\n};\n"],"mappings":";;;AAAA,MAAM,WAAW,QAA0B;CACzC,OAAO,IACJ,QAAQ,mBAAmB,OAAO,CAAC,CACnC,QAAQ,yBAAyB,OAAO,CAAC,CACzC,QAAQ,UAAU,GAAG,CAAC,CACtB,KAAK,CAAC,CACN,MAAM,KAAK,CAAC,CACZ,OAAO,OAAO;AACnB;AAEA,MAAa,eAAe,QAAwB;CAClD,OAAO,QAAQ,GAAG,CAAC,CAChB,KAAK,SAAS,KAAK,YAAY,CAAC,CAAC,CACjC,KAAK,GAAG;AACb;AAQA,MAAa,eAAe,QAAwB;CAElD,OADc,QAAQ,GACf,CAAA,CACJ,KAAK,MAAM,MACV,MAAM,IAAI,KAAK,YAAY,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAC1F,CAAC,CACA,KAAK,EAAE;AACZ;;;ACbA,MAAM,aAAa,WAAwC;CACzD,OAAO;EACL,MAAM,OAAO;EACb,cAAc,YAAY,OAAO,IAAI;EACrC,UAAU,YAAY,OAAO,IAAI;CACnC;AACF;AAEA,MAAM,YAAY,SAAwB,MAAc,aAAkC;CACxF,MAAM,QAAQ,CACZ,SAAS;EACP,GAAG;EACH,GAAG;CACL,CAAC,GACD,KAAK,UAAU,IAAI,CAAC,CACtB;CAEA,OAAO,MAAM,IAAI,KAAK,WAAmB,QAAQ,CAAC,GAAG,KAAK;AAC5D;AAEA,MAAa,QAAQ,WAA+B;CAClD,MAAM,UAAU,UAAU,MAAM;CAEhC,OAAO,UAAU,SAAS,SAAS,OAAO,WAAW,OAAO,QAAQ,CAAC;AACvE"}
1
+ {"version":3,"file":"system.factory.js","names":[],"sources":["../../../src/utils/formatting.ts","../../../src/libs/system/system.factory.ts"],"sourcesContent":["const toWords = (str: string): string[] => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n};\n\nexport const toKebabCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.toLowerCase())\n .join(\"-\");\n};\n\nexport const toPascalCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n};\n\nexport const toCamelCase = (str: string): string => {\n const words = toWords(str);\n return words\n .map((word, i) =>\n i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),\n )\n .join(\"\");\n};\n","import { type Path, join, normalize, strings } from \"@angular-devkit/core\";\nimport {\n type Rule,\n type Source,\n apply,\n mergeWith,\n move,\n template,\n url,\n} from \"@angular-devkit/schematics\";\n\nimport { toCamelCase, toKebabCase } from \"@utils/formatting\";\n\nimport { type SystemOptions } from \"./system.options\";\nimport { type SystemSchema } from \"./system.schema\";\n\nconst transform = (schema: SystemSchema): SystemOptions => {\n return {\n part: schema.part,\n functionName: toCamelCase(schema.name),\n fileName: toKebabCase(schema.name),\n };\n};\n\nconst generate = (options: SystemOptions, path: string, language: \"ts\" | \"js\"): Source => {\n const rules = [\n template({\n ...strings,\n ...options,\n }),\n move(normalize(path)),\n ];\n\n return apply(url(join(\"./files\" as Path, language)), rules);\n};\n\nexport const main = (schema: SystemSchema): Rule => {\n const options = transform(schema);\n\n return mergeWith(generate(options, schema.directory, schema.language));\n};\n"],"mappings":";;;AAAA,MAAM,WAAW,QAA0B;CACzC,OAAO,IACJ,QAAQ,mBAAmB,OAAO,CAAC,CACnC,QAAQ,yBAAyB,OAAO,CAAC,CACzC,QAAQ,UAAU,GAAG,CAAC,CACtB,KAAK,CAAC,CACN,MAAM,KAAK,CAAC,CACZ,OAAO,OAAO;AACnB;AAEA,MAAa,eAAe,QAAwB;CAClD,OAAO,QAAQ,GAAG,CAAC,CAChB,KAAK,SAAS,KAAK,YAAY,CAAC,CAAC,CACjC,KAAK,GAAG;AACb;AAQA,MAAa,eAAe,QAAwB;CAElD,OADc,QAAQ,GACf,CAAA,CACJ,KAAK,MAAM,MACV,MAAM,IAAI,KAAK,YAAY,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAC1F,CAAC,CACA,KAAK,EAAE;AACZ;;ACbA,MAAM,aAAa,WAAwC;CACzD,OAAO;EACL,MAAM,OAAO;EACb,cAAc,YAAY,OAAO,IAAI;EACrC,UAAU,YAAY,OAAO,IAAI;CACnC;AACF;AAEA,MAAM,YAAY,SAAwB,MAAc,aAAkC;CACxF,MAAM,QAAQ,CACZ,SAAS;EACP,GAAG;EACH,GAAG;CACL,CAAC,GACD,KAAK,UAAU,IAAI,CAAC,CACtB;CAEA,OAAO,MAAM,IAAI,KAAK,WAAmB,QAAQ,CAAC,GAAG,KAAK;AAC5D;AAEA,MAAa,QAAQ,WAA+B;CAClD,MAAM,UAAU,UAAU,MAAM;CAEhC,OAAO,UAAU,SAAS,SAAS,OAAO,WAAW,OAAO,QAAQ,CAAC;AACvE"}
@@ -0,0 +1,49 @@
1
+ <% var nf = { npm: "npm exec --no -- nf", pnpm: "pnpm exec nf", yarn: "yarn nf", bun: "bun run nf" }[packageManager]; %># <%= name %>
2
+
3
+ Notes for AI coding agents. This is a NanoForge game workspace, written in JavaScript (ES
4
+ modules, with types in JSDoc comments): a client app that runs in the browser and a server app
5
+ that runs in Bun. NanoForge is a game engine built on an entity component system (ECS).
6
+
7
+ ## Files
8
+
9
+ - `nanoforge.config.js`: `type: "workspace"`, with `packages: ["apps/*"]`. Each folder it matches
10
+ is an app with its own `nanoforge.config.js`.
11
+ - `apps/client/`, `apps/server/`: the two apps. Each has its own `AGENTS.md`: read the one of the
12
+ app you change.
13
+ - `.env`: the `NANOFORGE_*` variables that connect the client to the server (address, TCP and UDP
14
+ ports). Change both sides together. Git ignores it.
15
+ - `jsconfig.json`: the options your code editor reads the JSDoc types with, extended by each app.
16
+ - `package.json`<% if (packageManager === "pnpm") { %> and `pnpm-workspace.yaml`<% } %>: the <%= packageManager %> workspace. Install every app with
17
+ `<%= packageManager %> install` here.
18
+
19
+ ## Commands
20
+
21
+ `nf` comes from the `@nanoforge-dev/cli` dev dependency. Run it from this folder, as
22
+ `<%= nf %> <command>`.
23
+
24
+ - `nf dev`: runs `nf build --watch` and `nf start --watch` together. The game is on
25
+ http://localhost:3000, with the server running.
26
+ - `nf build`: bundles every app with Bun into its `dist/`. It does not check the JSDoc
27
+ types. `-d apps/client` or `-d apps/server` builds one app.
28
+ - `nf start`: runs the last build of both apps.
29
+
30
+ `nf dev` and `nf start` read the `.env` of their `-d` folder (the current folder by default), so
31
+ run them here.
32
+
33
+ ## Client and server
34
+
35
+ - They connect over TCP, a WebSocket, and over UDP, a WebRTC data channel set up through a
36
+ WebSocket. `nf start` gives `NANOFORGE_CLIENT_X` to the client and `NANOFORGE_SERVER_X` to the
37
+ server, as `X` in the `options.env` of `main`. A plain `NANOFORGE_X` goes to both. Everything
38
+ the client gets reaches the browser: never put a secret there.
39
+ https://docs.nanoforge.eu/docs/cli/configuration/environment-variables
40
+ - In the generated game, the server moves the entity and sends its position to every client
41
+ (`apps/server/src/systems/move-2d.system.js`), and the client sets the position it receives
42
+ (`apps/client/src/systems/position-sync.system.js`).
43
+ - The apps share no code: each defines its own `Position2D`.
44
+
45
+ ## Docs
46
+
47
+ - `nf` commands: https://docs.nanoforge.eu/docs/cli/commands/overview
48
+ - The workspace config: https://docs.nanoforge.eu/docs/cli/configuration/configuration-file
49
+ - Everything else: https://docs.nanoforge.eu
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -82,6 +82,10 @@ build/Release
82
82
 
83
83
  # Dependency directories
84
84
  node_modules/
85
+
86
+ # NanoForge registry packages (nf install restores them from nanoforge.packages.lock.json)
87
+ nf_modules/
88
+
85
89
  jspm_packages/
86
90
 
87
91
  # Snowpack dependency directory (https://snowpack.dev/)