@genesislcap/genx 15.23.2 → 15.24.0-FUI-2603.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,6 +26,39 @@ To enable this module in your application, follow the steps below.
26
26
 
27
27
  ## [API Docs](./docs/api/index.md)
28
28
 
29
+ ## Agent rules
30
+
31
+ `genx agent-rules` installs the shared AI agent rules that ship with this package
32
+ ([`agent-rules/`](./agent-rules)) into a consumer project, and wires them into whichever assistants
33
+ that project uses.
34
+
35
+ ```bash
36
+ npx genx agent-rules # install and wire up detected assistants
37
+ npx genx agent-rules .. # target another folder (see below)
38
+ npx genx agent-rules --tools claude,cursor,gemini # wire up specific assistants
39
+ npx genx agent-rules --check # CI: fail when installed rules are stale
40
+ ```
41
+
42
+ `[folder]` defaults to the current directory, and it should be the folder the assistants read -
43
+ usually the repo root. In a Genesis app that is one level **above** `client/`, where `package.json`
44
+ lives, so run `npx genx agent-rules ..` from `client/`. Run bare in a folder with no assistant
45
+ files, the command looks up the tree as far as the repo root and, if it finds the real root, points
46
+ at it and writes nothing rather than leaving an orphaned second copy behind (`--tools` installs in
47
+ the current folder anyway).
48
+
49
+ The rule markdown is copied to `docs/agent-rules/` and referenced from a managed block in
50
+ `CLAUDE.md`, `GEMINI.md`, or `AGENTS.md`, and from a generated `.cursor/rules/<rule>.mdc` for
51
+ Cursor. Content outside the managed block is never touched. Installed copies are managed — re-run
52
+ the command after `genx upgrade` to pick up rule changes, and propose edits in
53
+ [foundation-ui](https://github.com/genesislcap/foundation-ui) rather than locally.
54
+
55
+ Currently shipped:
56
+
57
+ | Rule | Purpose |
58
+ | --- | --- |
59
+ | `report-upstream-bug.md` | Something **broken** upstream: file a bug in `genesislcap/foundation-ui` — with the human's confirmation — instead of quietly working around it in the consumer project. |
60
+ | `propose-upstream-feature.md` | Something **missing** upstream: propose the capability instead of building the fifth private copy of it. Includes a generality test so app code stays app code. |
61
+
29
62
  ## Event type codegen
30
63
 
31
64
  When `eventTypes.enabled` is set in `genx.config.*` or `package.json` (`genx` / `genesis` keys), `genx build` and `genx generate event-types` emit typed `EventDetailsMap` from Kotlin handler DTOs and generated table DAOs. See [@genesislcap/event-type-codegen](../event-type-codegen/README.md) for configuration, limitations, and migration.
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": 1,
3
+ "targetDir": "docs/agent-rules",
4
+ "rules": [
5
+ {
6
+ "file": "report-upstream-bug.md",
7
+ "name": "report-upstream-bug",
8
+ "title": "Report Foundation UI bugs upstream",
9
+ "description": "File a bug upstream in genesislcap/foundation-ui instead of working around it locally. Use whenever a defect is traced into a @genesislcap/* package - a stack frame in node_modules/@genesislcap, or behaviour contradicting the package's docs or types - or before writing any monkey-patch, patch-package entry, vendored copy, version pin, or cast that exists to route around upstream behaviour.",
10
+ "cursorDescription": "Use when a bug is traced into a @genesislcap package, when weighing a workaround for upstream behaviour, or before patching node_modules or pinning a @genesislcap version."
11
+ },
12
+ {
13
+ "file": "propose-upstream-feature.md",
14
+ "name": "propose-upstream-feature",
15
+ "title": "Propose platform features upstream",
16
+ "description": "Propose a missing platform capability in genesislcap/foundation-ui instead of building the fifth private copy of it. Use before building a generic component, formatter, or adapter with no business rules in it, before wrapping or re-implementing a @genesislcap/* component that lacks one option, or when the same thing has already been built in another Genesis app.",
17
+ "cursorDescription": "Use before building a generic component, utility, or adapter that has no business rules in it, when extending a @genesislcap component that lacks an option, or when the same capability already exists privately in another Genesis app."
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,219 @@
1
+ <!-- Managed by `genx agent-rules` (@genesislcap/genx). Do not edit this copy — it is overwritten on every run. Propose changes in genesislcap/foundation-ui. -->
2
+
3
+ # Propose platform features upstream (don't build the fifth private copy)
4
+
5
+ > **Purpose**: Some of what a Genesis app needs is app-specific, and some of it is platform
6
+ > capability that simply hasn't been built yet. The second kind gets built privately in one app,
7
+ > then again in the next, and every copy has to be maintained against upstream API changes forever.
8
+ > This rule catches that at the moment of writing: if the thing belongs in a `@genesislcap/*`
9
+ > package, it gets proposed in
10
+ > [`genesislcap/foundation-ui`](https://github.com/genesislcap/foundation-ui) — and only then, with
11
+ > approval, built locally as a temporary seam.
12
+ >
13
+ > The sibling rule for defects is `report-upstream-bug.md`. Something that is *broken* upstream is a
14
+ > bug; something that is *missing* upstream is this rule.
15
+
16
+ ## When this rule applies
17
+
18
+ Apply it before writing code, as soon as any of these is true:
19
+
20
+ - You are about to build a generic capability — a UI component, a formatter, a datasource or grid
21
+ adapter, an auth/comms/layout concern — with no business rules in it.
22
+ - You are extending an existing `@genesislcap/*` component by wrapping, subclassing, or
23
+ re-implementing it, because it lacks one attribute, slot, event, or option.
24
+ - Someone says "we built this in the other app" or you can see code copied in from another Genesis
25
+ project.
26
+ - The thing would have to track an upstream API to keep working — a private fork of platform
27
+ behaviour rather than a consumer of it.
28
+ - Upstream documents or types the capability partially and you are filling the gap locally.
29
+
30
+ It does **not** apply to app code, and most code is app code:
31
+
32
+ - Business rules, pricing, workflow, entitlements, domain validation.
33
+ - Client-specific branding, copy, layout, or one-off screens.
34
+ - Anything whose API you cannot describe without this app's domain nouns.
35
+ - Spikes and prototypes you intend to delete.
36
+ - Work under a deadline where waiting on upstream is not an option — build it locally now, and
37
+ propose it in parallel (Step 6 covers the seam).
38
+
39
+ ## Non-negotiables
40
+
41
+ 1. **Apply the generality test in Step 2 before proposing anything.** A proposal for app-specific
42
+ code wastes the platform team's time and teaches them to ignore the next one.
43
+ 2. **Never** run `gh issue create` unprompted. Draft the proposal, show it to the human, wait for a
44
+ clear yes. Filing is an outward-facing action on a shared repo.
45
+ 3. **Never** fork or vendor an upstream component to change a few lines without approval — propose
46
+ the option upstream and wrap locally instead.
47
+ 4. **Building the local version needs explicit human approval** (Step 6), like any workaround.
48
+ 5. **Don't overclaim demand.** "Two apps need this" is only true if you can name them.
49
+
50
+ ---
51
+
52
+ ## Step 1 — Check it doesn't already exist
53
+
54
+ Platform surface is large and under-documented; assume it might already be there.
55
+
56
+ ```bash
57
+ # Search the installed packages' public types for the capability.
58
+ grep -rl "<CapabilityName>" node_modules/@genesislcap/*/dist/dts 2>/dev/null | head
59
+
60
+ # What does the package actually export?
61
+ cat node_modules/@genesislcap/<pkg>/dist/dts/index.d.ts | grep -i "<keyword>"
62
+
63
+ # Newer versions may already have it.
64
+ npm view @genesislcap/<pkg> version
65
+ npm view @genesislcap/<pkg> dist-tags
66
+ ```
67
+
68
+ Also check the package's `README.md` and `docs/api/` inside `node_modules`, and the showcase apps
69
+ if you have the monorepo to hand. If it exists, use it and stop here.
70
+
71
+ ## Step 2 — Apply the generality test
72
+
73
+ Answer all four. Any "no" means it is app code: build it locally and stop.
74
+
75
+ 1. **Name two independent consumers.** Two real projects or teams that would use this, not two
76
+ screens in this app.
77
+ 2. **Describe the API without this app's domain nouns.** If the signature needs
78
+ `Trade`, `Counterparty`, or your client's name to make sense, it is not platform capability.
79
+ 3. **No app-specific dependencies.** It must not reach into this app's services, routes, config
80
+ shape, or backend contract.
81
+ 4. **The behaviour is stable, not still being discovered.** If the design is likely to change twice
82
+ more this sprint, it is not ready to be someone else's API.
83
+
84
+ Borderline cases exist. When it half-passes, the honest proposal is usually smaller than the thing
85
+ you were about to build: a new attribute, slot, event, or option on an existing component rather
86
+ than a new component.
87
+
88
+ ## Step 3 — Check it isn't already proposed
89
+
90
+ ```bash
91
+ gh issue list --repo genesislcap/foundation-ui --state all --search "<keywords>" --limit 20
92
+ gh pr list --repo genesislcap/foundation-ui --state all --search "<keywords>" --limit 20
93
+ ```
94
+
95
+ - **An open issue matches** → add your use case and your second consumer to it, after the human
96
+ approves. That is worth more to the platform team than a duplicate: it is evidence of demand.
97
+ - **A closed issue matches** → read why. If it was declined, respect the decision and build locally
98
+ (Step 6); the reasoning usually tells you what shape *would* be accepted.
99
+ - **A merged PR matches** → it may be released already, or available on a pre-release dist-tag.
100
+
101
+ ## Step 4 — Decide the shape, smallest first
102
+
103
+ Pick the least invasive option that solves it, and say in the proposal which you chose and why:
104
+
105
+ | Shape | Use when | Cost to upstream |
106
+ | --- | --- | --- |
107
+ | New attribute / slot / event / option on an existing component | The component does 90% of it | Lowest — usually additive and non-breaking |
108
+ | New export from an existing package | It is a utility, formatter, or type that fits a package's remit | Low |
109
+ | New component in an existing package | It is a peer of what that package already ships | Medium |
110
+ | New package | Nothing existing has the remit | Highest — expect to justify it |
111
+
112
+ Additive and backward-compatible beats elegant-but-breaking. If your idea only works as a breaking
113
+ change, say so explicitly and expect it to be scheduled rather than accepted outright.
114
+
115
+ ## Step 5 — Draft the proposal, then file it once the human confirms
116
+
117
+ Title: what the capability is, in the platform's vocabulary — not your app's.
118
+
119
+ - Good: `grid-pro: expose a column-level formatter hook for cell values`
120
+ - Bad: `Need to format trade dates in the blotter`
121
+
122
+ Body:
123
+
124
+ ````markdown
125
+ ## What's missing
126
+
127
+ One paragraph: the capability, and where it would live (`@genesislcap/<pkg>`, ideally the exact
128
+ component or module).
129
+
130
+ ## Who needs it
131
+
132
+ The projects or teams that would use this, named. If it has already been built privately more
133
+ than once, say where — a duplicated implementation is the strongest argument for adopting it.
134
+
135
+ ## What we do today
136
+
137
+ The local implementation or workaround, with its cost: lines of code, what it forks, what breaks
138
+ when upstream changes.
139
+
140
+ ## Proposed API
141
+
142
+ ```ts
143
+ // The smallest shape that solves it — attribute, option, export, or signature.
144
+ ```
145
+
146
+ Include the default, and confirm existing behaviour is unchanged when the new option is absent.
147
+
148
+ ## Alternatives considered
149
+
150
+ Including "keep it app-side", and why that loses.
151
+
152
+ ## Breaking change?
153
+
154
+ No / yes, with the migration if yes.
155
+
156
+ ## Can we contribute it?
157
+
158
+ Whether this team can raise the PR, and by when.
159
+ ````
160
+
161
+ Then, after an explicit yes — with the body in a relative file, so this works on Windows too, and
162
+ deleted afterwards rather than committed:
163
+
164
+ ```bash
165
+ gh issue create \
166
+ --repo genesislcap/foundation-ui \
167
+ --title "<title>" \
168
+ --body-file temp-upstream-proposal.md \
169
+ --label "Enhancement" \
170
+ --label "<area label>"
171
+ ```
172
+
173
+ Use `gh label list --repo genesislcap/foundation-ui --limit 100` for the area label — the same
174
+ areas as the bug rule (`Grids`, `COMMS`, `Design System`, `Forms`, `Layout`,
175
+ `Frontends Compatibility`, …). Add `Next` when it is capability the platform is expected to grow
176
+ into rather than something needed this sprint. Delete the temp file, then report the URL.
177
+
178
+ ## Step 6 — Build locally as a seam, with approval
179
+
180
+ A filed proposal does not ship your feature. Once the issue URL exists:
181
+
182
+ 1. **Present the options and their costs** — build behind a local seam now, wait for upstream, or
183
+ contribute the PR yourself and consume a pre-release. Include "wait" when it is viable.
184
+ 2. **Wait for an explicit choice.** Do not start editing before that.
185
+ 3. **Once approved**, build it so it can be deleted rather than untangled:
186
+ - Keep it behind **one boundary** — an adapter, a wrapper component, a single module the app
187
+ imports from. When upstream lands, one file changes, not thirty call sites.
188
+ - Match the proposed upstream API, so adopting the real thing is an import swap.
189
+ - Mark it with the issue and the removal condition:
190
+
191
+ ```ts
192
+ // UPSTREAM-CANDIDATE(genesislcap/foundation-ui#2501):
193
+ // Column-level value formatter; grid-pro has no hook for this yet.
194
+ // Remove when: the hook ships in @genesislcap/grid-pro and this app adopts it.
195
+ ```
196
+
197
+ 4. **Log it** in the shared tracking file (create it if absent — `docs/upstream-tracking.md`):
198
+
199
+ ````markdown
200
+ | Kind | Issue | Package | What we do locally | Remove when |
201
+ | --- | --- | --- | --- | --- |
202
+ | feature | genesislcap/foundation-ui#2501 | grid-pro | local formatter adapter | hook released and adopted |
203
+ ````
204
+
205
+ ## Step 7 — Offer the PR
206
+
207
+ Proposals with a PR attached land far sooner than proposals without one, and you have already
208
+ written the implementation once. `foundation-ui` accepts contributions from consumer teams — see its
209
+ [CONTRIBUTING.md](https://github.com/genesislcap/foundation-ui/blob/master/CONTRIBUTING.md) for
210
+ setup, commit format, and the PR template. A pre-release can be cut from the branch so this app can
211
+ adopt it before it merges (`@genesislcap/<pkg>@<dist-tag>`); ask in the issue.
212
+
213
+ ## Never
214
+
215
+ - Propose app business logic as platform capability.
216
+ - Fork or vendor an upstream component to change a few lines instead of proposing the option.
217
+ - Claim demand you cannot name, or a use case you have not built.
218
+ - Design an API around this app's backend contract and present it as generic.
219
+ - Leave the local implementation as the only record — the marker must name the issue.
@@ -0,0 +1,253 @@
1
+ <!-- Managed by `genx agent-rules` (@genesislcap/genx). Do not edit this copy — it is overwritten on every run. Propose changes in genesislcap/foundation-ui. -->
2
+
3
+ # Report Foundation UI bugs upstream (don't silently work around them)
4
+
5
+ > **Purpose**: When a bug in this project turns out to live inside a `@genesislcap/*` package,
6
+ > the fix belongs in [`genesislcap/foundation-ui`](https://github.com/genesislcap/foundation-ui),
7
+ > not in a local patch. A workaround that is never reported keeps every other consumer project
8
+ > broken and quietly becomes permanent. This rule makes "raise it upstream" the default path and
9
+ > a local workaround the deliberate, approved, clearly-marked exception.
10
+ >
11
+ > The sibling rule for missing capability is `propose-upstream-feature.md`. Something that is
12
+ > *broken* upstream is this rule; something that is *missing* upstream is that one.
13
+
14
+ ## When this rule applies
15
+
16
+ Apply it as soon as any of these is true:
17
+
18
+ - A stack frame, breakpoint, or console error points into `node_modules/@genesislcap/**`.
19
+ - The behaviour of a `@genesislcap/*` package contradicts its own docs, types, JSDoc, or
20
+ README — including a `CustomEvent<unknown>` where a typed detail is documented.
21
+ - You are about to write, or have caught yourself considering, any of these:
22
+ - a monkey-patch, prototype override, or `patch-package` entry against a `@genesislcap/*` package;
23
+ - a copy of an upstream component/function into this repo so a few lines can be changed;
24
+ - `as any` / `@ts-expect-error` / `// eslint-disable` purely to route around a wrong upstream type;
25
+ - a CSS override fighting an upstream style (`!important` into a shadow part, etc.);
26
+ - pinning or downgrading a `@genesislcap/*` version to dodge a regression;
27
+ - a `setTimeout`, extra re-render, retry, or "nudge" that exists only to compensate for
28
+ upstream timing.
29
+
30
+ It does **not** apply when the cause is on this side: wrong usage of a documented API, a version
31
+ mismatch between `@genesislcap/*` packages, missing peer dependencies, or local configuration.
32
+ Step 1 exists to tell those apart.
33
+
34
+ ## Non-negotiables
35
+
36
+ 1. **The upstream issue is filed before local code is changed** to route around the bug.
37
+ 2. **Never** modify anything under `node_modules/` (or add a `patch-package` patch, or vendor a
38
+ copy of upstream source) without explicit human approval.
39
+ 3. **Never** run `gh issue create` unprompted. Draft the issue, show it to the human, wait for a
40
+ clear yes, then file it. Filing an issue is an outward-facing action on a shared repo.
41
+ 4. **Any workaround needs explicit human approval** — present the options and the trade-offs, do
42
+ not pick one and start editing (see Step 6).
43
+ 5. **Be honest about what you verified.** Separate "confirmed by running it" from "suspected".
44
+ A confidently-wrong root cause wastes more upstream time than no root cause at all.
45
+
46
+ ---
47
+
48
+ ## Step 1 — Rule out the boring causes
49
+
50
+ Do this before writing a single line of the issue.
51
+
52
+ ```bash
53
+ # What is actually installed (the lockfile, not the range in package.json)?
54
+ npm ls @genesislcap/<package> # or: pnpm why @genesislcap/<package>
55
+
56
+ # Is it the latest? Is there a newer patch or a prerelease dist-tag?
57
+ npm view @genesislcap/<package> version
58
+ npm view @genesislcap/<package> dist-tags
59
+
60
+ # Are the @genesislcap/* packages on matching versions? Mixed majors/minors cause
61
+ # symptoms that look exactly like upstream bugs but are dependency drift.
62
+ npm ls --all 2>/dev/null | grep @genesislcap
63
+ ```
64
+
65
+ Then confirm the calling code follows the documented API — read the package's own `dist/dts/*.d.ts`
66
+ and README rather than assuming. If the answer is "we were holding it wrong", fix it locally and
67
+ stop here; this rule is done.
68
+
69
+ ## Step 2 — Check it isn't already known or already fixed
70
+
71
+ ```bash
72
+ # Open AND closed issues — a closed one may name the release that fixed it.
73
+ gh issue list --repo genesislcap/foundation-ui --state all --search "<keywords>" --limit 20
74
+
75
+ # Same for PRs — a fix may be merged but not yet released.
76
+ gh pr list --repo genesislcap/foundation-ui --state all --search "<keywords>" --limit 20
77
+ ```
78
+
79
+ Outcomes:
80
+
81
+ - **An open issue matches** → don't file a duplicate. Add a comment with your new evidence
82
+ (your version, your repro, your environment) after the human approves, and link it in your report.
83
+ - **A closed issue matches and a newer version exists** → the fix is likely released. Recommend the
84
+ upgrade instead of an issue.
85
+ - **Nothing matches** → continue to Step 3.
86
+
87
+ ## Step 3 — Collect the evidence
88
+
89
+ An upstream maintainer needs to reproduce this without access to your app. Gather:
90
+
91
+ | What | How |
92
+ | --- | --- |
93
+ | Root-cause package + exact version | The package the broken code lives in, e.g. `@genesislcap/foundation-comms@15.3.1` |
94
+ | Symptom package + exact version | Where it *appears* to break, if different, e.g. `@genesislcap/grid-pro@15.3.1` |
95
+ | The offending code | Real path + quoted snippet from `node_modules/@genesislcap/<pkg>/dist/...` |
96
+ | Minimal reproduction | Numbered steps. Say plainly whether you executed them or are proposing them |
97
+ | Expected vs actual | Both stated as observable behaviour, not as internal state |
98
+ | Environment | Framework + version (React 19, Angular, etc.), bundler, browser, and relevant third-party versions (`ag-grid-community`, `@microsoft/fast-element`, …) |
99
+ | Evidence | Console output, network/websocket trace, screenshots — trimmed to the relevant lines |
100
+ | Workaround status | What unblocks the app today (even if it's "nothing yet") |
101
+
102
+ Redact anything client-confidential: hostnames, credentials, real user data, and business-specific
103
+ resource names. Replace them with placeholders (`SOME_DATASERVER_QUERY`) — a repro does not need
104
+ them to be real.
105
+
106
+ ## Step 4 — Draft the issue
107
+
108
+ Title: a specific, searchable one-liner that names the mechanism, not just the symptom. No
109
+ conventional-commit prefix.
110
+
111
+ - Good: `sendForStream() defers DATA_LOGON via DOM.queueUpdate — StrictMode double-mount discards it, leaving every dataserver grid empty`
112
+ - Bad: `Grid is empty`
113
+
114
+ Body — use this structure (it mirrors
115
+ [#2442](https://github.com/genesislcap/foundation-ui/issues/2442), a good reference report):
116
+
117
+ ````markdown
118
+ ## Package + version
119
+
120
+ Root cause: **`@genesislcap/<pkg>@<version>`**
121
+ Visible symptom in: **`@genesislcap/<pkg>@<version>`** (if different)
122
+
123
+ From the consumer's `package.json`:
124
+
125
+ ```
126
+ "@genesislcap/<pkg>": "<version>"
127
+ "<relevant third-party dep>": "<version>"
128
+ ```
129
+
130
+ ## Summary
131
+
132
+ One paragraph: the mechanism and its user-visible consequence.
133
+
134
+ ## Reproduction
135
+
136
+ State up front whether this was confirmed end-to-end or is a proposed reduction.
137
+
138
+ 1. …
139
+ 2. …
140
+ 3. Observe: …
141
+
142
+ ## Expected vs Actual
143
+
144
+ **Expected**
145
+ - …
146
+
147
+ **Actual**
148
+ - …
149
+
150
+ ## Root cause
151
+
152
+ `<pkg>/dist/<path>`:
153
+
154
+ ```js
155
+ // the offending snippet, quoted
156
+ ```
157
+
158
+ Explain why this is wrong. If parts are inferred rather than observed, say so explicitly.
159
+
160
+ ## Impact
161
+
162
+ Who and what is affected — one app, every consumer using this component, dev-only, production.
163
+
164
+ ## Workaround in place downstream
165
+
166
+ What the consumer project is doing right now to stay unblocked, or "none".
167
+
168
+ ## Environment
169
+
170
+ - Framework: …
171
+ - Browser / Node: …
172
+ - Other relevant versions: …
173
+ ````
174
+
175
+ Show the draft to the human. Do not file it yet.
176
+
177
+ ## Step 5 — File it (after the human says yes)
178
+
179
+ Write the body to a file in the project first — passing markdown inline mangles backticks and
180
+ special characters. Keep the path relative so this works on Windows as well as macOS and Linux, and
181
+ delete it afterwards rather than committing it:
182
+
183
+ ```bash
184
+ gh issue create \
185
+ --repo genesislcap/foundation-ui \
186
+ --title "<title>" \
187
+ --body-file temp-upstream-issue.md \
188
+ --label "Bug" \
189
+ --label "<area label>"
190
+ ```
191
+
192
+ Pick area labels from the repo's own list — `gh label list --repo genesislcap/foundation-ui --limit 100`.
193
+ Common ones:
194
+
195
+ | Area | Label |
196
+ | --- | --- |
197
+ | grid-pro, data-grid, rapid-grid-pro | `Grids` |
198
+ | foundation-comms, datasources, websockets | `COMMS`, `Datasource` |
199
+ | design system, FAST, rapid components | `Design System`, `Rapid` |
200
+ | foundation-forms / criteria / filters | `Forms`, `Criteria` |
201
+ | foundation-layout, golden/dynamic layout | `Layout` |
202
+ | foundation-entity-management | `Entity Management` |
203
+ | React/Angular/Vue wrappers, typings | `Frontends Compatibility` |
204
+ | build, bundling, genx/CLI | `Build`, `Rollup & Webpack`, `CLI & GENX` |
205
+ | production-blocking | `High Priority`, or `CRITICAL` if the app is down |
206
+
207
+ Then delete the temp file, and report the issue URL back to the human.
208
+
209
+ ## Step 6 — Workaround: only after the issue exists, only with approval
210
+
211
+ Filing the issue does not unblock this app. Once the issue URL exists:
212
+
213
+ 1. **Present the options**, with the cost of each — e.g. upgrade/downgrade, use a different
214
+ documented API, local subclass/override, wait for the upstream fix, `patch-package`.
215
+ Include "do nothing yet" when it is viable.
216
+ 2. **Wait for an explicit choice.** Do not start editing consumer code before that.
217
+ 3. **Once approved**, keep it minimal and make it self-deleting on upgrade:
218
+ - Mark every touched site with a marker comment carrying the issue URL and the removal condition:
219
+
220
+ ```ts
221
+ // UPSTREAM-WORKAROUND(genesislcap/foundation-ui#2442):
222
+ // foundation-comms defers DATA_LOGON via DOM.queueUpdate, so a StrictMode remount drops it.
223
+ // Remove when: @genesislcap/foundation-comms ships the fix (> 15.3.1) and this app upgrades.
224
+ ```
225
+
226
+ - Prefer wrapping over forking: a subclass, a decorator, or an adapter at one boundary beats a
227
+ vendored copy of upstream source scattered across the codebase.
228
+ - Never leave the workaround as the only record of the bug. The marker must name the issue.
229
+ 4. **Log it** in a single tracking file the team can grep at upgrade time, shared with the feature
230
+ rule (create it if absent — `docs/upstream-tracking.md`):
231
+
232
+ ````markdown
233
+ | Kind | Issue | Package + version | What we do locally | Remove when |
234
+ | --- | --- | --- | --- | --- |
235
+ | bug | genesislcap/foundation-ui#2442 | foundation-comms@15.3.1 | ... | fix released and adopted |
236
+ ````
237
+
238
+ ## Step 7 — Offer the upstream fix
239
+
240
+ If the fix is small and you can see it, say so in the issue and offer a PR. `foundation-ui` accepts
241
+ contributions from consumer teams — see its
242
+ [CONTRIBUTING.md](https://github.com/genesislcap/foundation-ui/blob/master/CONTRIBUTING.md) for
243
+ setup, commit format, and the PR template. A pre-release build can be cut from a branch so this app
244
+ can validate the fix before it lands (`@genesislcap/<pkg>@<dist-tag>`); ask in the issue.
245
+
246
+ ## Never
247
+
248
+ - Edit `node_modules/` directly, or add a `patch-package` patch, without approval — and never
249
+ without an issue link in the patch's commit message.
250
+ - Present a suspected root cause as confirmed.
251
+ - Paste client-confidential data (hostnames, credentials, real records) into a shared repo.
252
+ - Delete, skip, or weaken a test to make an upstream bug invisible.
253
+ - Ship a workaround with no marker comment and no issue reference.
@@ -0,0 +1,6 @@
1
+ declare const _default: (folder?: string, options?: {
2
+ tools?: string;
3
+ check?: boolean;
4
+ }) => Promise<void>;
5
+ export default _default;
6
+ //# sourceMappingURL=agent-rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-rules.d.ts","sourceRoot":"","sources":["../../src/commands/agent-rules.ts"],"names":[],"mappings":"yBAyCE,eAAsB,EACtB,UAAS;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO;AAFnD,wBAkEE"}
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const build_kit_1 = require("@genesislcap/build-kit");
7
+ const consola_1 = tslib_1.__importDefault(require("consola"));
8
+ const agent_rules_1 = require("../utils/agent-rules");
9
+ /** The packaged rules live at the root of this package, alongside `bin` and `dist`. */
10
+ const packagedRulesDir = () => (0, node_path_1.resolve)(__dirname, '../../agent-rules');
11
+ const loadManifest = (rulesDir) => JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(rulesDir, 'manifest.json'), 'utf8'));
12
+ const readRuleContents = (rulesDir, manifest) => manifest.rules.reduce((contents, rule) => {
13
+ contents[rule.file] = (0, node_fs_1.readFileSync)((0, node_path_1.join)(rulesDir, rule.file), 'utf8');
14
+ return contents;
15
+ }, {});
16
+ const resolveTools = (dir, requested) => {
17
+ if (!requested) {
18
+ return (0, agent_rules_1.detectTools)(dir);
19
+ }
20
+ try {
21
+ return (0, agent_rules_1.parseTools)(requested);
22
+ }
23
+ catch (error) {
24
+ // A typo in --tools is a user error, not a bug: report it without a stack trace.
25
+ consola_1.default.error((0, agent_rules_1.errorMessage)(error));
26
+ process.exit(1);
27
+ }
28
+ };
29
+ exports.default = (...args_1) => tslib_1.__awaiter(void 0, [...args_1], void 0, function* (folder = process.cwd(), options = {}) {
30
+ const dir = (0, node_path_1.resolve)(folder);
31
+ const rulesDir = packagedRulesDir();
32
+ const manifest = loadManifest(rulesDir);
33
+ const ruleContents = readRuleContents(rulesDir, manifest);
34
+ const tools = resolveTools(dir, options.tools);
35
+ if (!tools.length && !options.tools) {
36
+ // Writing rule files into a package directory whose assistant files live higher up leaves an
37
+ // orphaned second copy behind, so point at the real root instead of quietly installing here.
38
+ const assistantRoot = (0, agent_rules_1.findAssistantRoot)(dir);
39
+ if (assistantRoot) {
40
+ consola_1.default.error(`No assistant detected in ${dir}, but ${assistantRoot} has one.`);
41
+ consola_1.default.info(`Run \`genx agent-rules ${assistantRoot}\` to install there instead.`);
42
+ consola_1.default.info('Pass --tools to install in this folder anyway.');
43
+ process.exit(1);
44
+ }
45
+ consola_1.default.warn(`No assistant detected in ${dir}. The rule files will be installed, but nothing will be wired up. Pass --tools claude,cursor,gemini,agents to wire one explicitly.`);
46
+ }
47
+ let planned;
48
+ try {
49
+ planned = (0, agent_rules_1.planFiles)(dir, manifest, tools, ruleContents, (path) => (0, node_fs_1.readFileSync)(path, 'utf8'));
50
+ }
51
+ catch (error) {
52
+ // A hand-mangled managed block is the user's to repair - we will not guess and overwrite prose.
53
+ consola_1.default.error((0, agent_rules_1.errorMessage)(error));
54
+ process.exit(1);
55
+ }
56
+ const drift = (0, agent_rules_1.findDrift)(dir, planned);
57
+ if (options.check) {
58
+ if (drift.length) {
59
+ consola_1.default.error(`Agent rules are out of date in ${dir}:`);
60
+ drift.forEach((file) => consola_1.default.log((0, build_kit_1.white)(` ${(0, build_kit_1.bold)(file.path)} ${(0, node_fs_1.existsSync)((0, node_path_1.join)(dir, file.path)) ? 'differs' : 'missing'}`)));
61
+ consola_1.default.info('Run `genx agent-rules` to update.');
62
+ process.exit(1);
63
+ }
64
+ consola_1.default.success(`Agent rules are up to date (${manifest.rules.length} rule(s)).`);
65
+ return;
66
+ }
67
+ if (!drift.length) {
68
+ consola_1.default.info('Agent rules already up to date. Nothing to do.');
69
+ return;
70
+ }
71
+ drift.forEach((file) => {
72
+ const path = (0, node_path_1.join)(dir, file.path);
73
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
74
+ (0, node_fs_1.writeFileSync)(path, file.contents, 'utf8');
75
+ consola_1.default.log((0, build_kit_1.white)(` ${(0, build_kit_1.bold)(file.path)}`));
76
+ });
77
+ consola_1.default.success(`Installed ${manifest.rules.length} agent rule(s)${tools.length ? ` for: ${tools.join(', ')}` : ''}.`);
78
+ consola_1.default.info('Commit the changes so the whole team gets them.');
79
+ });
@@ -1,4 +1,8 @@
1
1
  export declare const commands: {
2
+ agentRules: () => Promise<(folder?: string, options?: {
3
+ tools?: string;
4
+ check?: boolean;
5
+ }) => Promise<void>>;
2
6
  clean: () => Promise<(paths?: string[]) => Promise<void>>;
3
7
  generateEventTypes: () => Promise<(folder?: string, options?: {
4
8
  fromMetadata?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,QAAQ;;;oBAEI,CAAC;;;;;;;CAMzB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,QAAQ;;aAY2qB,CAAC;aAAe,CAAC;;;;oBAVlsB,CAAA;;;;;;;CAOd,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,QAAQ,CAAC"}
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.commands = void 0;
37
37
  // lazy loading command definitions
38
38
  exports.commands = {
39
+ agentRules: () => Promise.resolve().then(() => __importStar(require('./agent-rules'))).then((r) => r.default),
39
40
  clean: () => Promise.resolve().then(() => __importStar(require('./clean'))).then((r) => r.default),
40
41
  generateEventTypes: () => Promise.resolve().then(() => __importStar(require('./generate-event-types'))).then((r) => r.default),
41
42
  run: () => Promise.resolve().then(() => __importStar(require('./run'))).then((r) => r.default),
package/dist/index.js CHANGED
@@ -42,6 +42,23 @@ cli
42
42
  consola_1.consola.error(`Unknown generate type "${type}". Supported: event-types`);
43
43
  process.exit(1);
44
44
  }));
45
+ cli
46
+ .command('agent-rules [folder]', 'Install shared AI agent rules into a project')
47
+ .option('-t, --tools <list>', 'Comma-separated assistants to wire: claude, cursor, gemini, agents (defaults to those detected)')
48
+ .option('-c, --check', 'Verify the installed rules are up to date, exit 1 otherwise (for CI)')
49
+ .action((folder, options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
50
+ const action = yield commands_1.commands.agentRules();
51
+ yield action(folder, options);
52
+ })).example(`
53
+ ${(0, build_kit_1.bold)('genx agent-rules')}
54
+ Install the shared rules into docs/agent-rules and wire up every assistant detected in the project
55
+
56
+ ${(0, build_kit_1.bold)('genx agent-rules --tools claude,cursor')}
57
+ Wire up Claude Code and Cursor explicitly
58
+
59
+ ${(0, build_kit_1.bold)('genx agent-rules --check')}
60
+ Fail if the installed rules are missing or out of date (use in CI)
61
+ `);
45
62
  cli
46
63
  .command('clean [...paths]', 'Delete specified paths (defaults to dist folder)')
47
64
  .action((paths) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Shareable agent rules are markdown files that ship with this package and are installed into a
3
+ * consumer project by `genx agent-rules`. The rule body is tool-agnostic; per-assistant wiring is
4
+ * generated from the manifest below.
5
+ */
6
+ export interface AgentRule {
7
+ /** File name of the rule markdown, relative to the packaged `agent-rules` folder. */
8
+ file: string;
9
+ /** Slug used for generated file names, e.g. the Cursor `.mdc`. */
10
+ name: string;
11
+ /** Human readable title, used in logs and generated pointers. */
12
+ title: string;
13
+ /** When the rule applies. Used as the Claude skill / pointer description. */
14
+ description: string;
15
+ /** Cursor's trigger phrasing for the `.mdc` front matter. */
16
+ cursorDescription: string;
17
+ }
18
+ export interface AgentRulesManifest {
19
+ version: number;
20
+ /** Where the rule markdown is installed inside the consumer project. */
21
+ targetDir: string;
22
+ rules: AgentRule[];
23
+ }
24
+ export declare const AGENT_TOOLS: readonly ["claude", "cursor", "gemini", "agents"];
25
+ export type AgentTool = (typeof AGENT_TOOLS)[number];
26
+ /** Entry file each assistant reads, for the tools wired via a managed block. */
27
+ export declare const TOOL_ENTRY_FILES: Record<Exclude<AgentTool, 'cursor'>, string>;
28
+ export declare const MANAGED_BEGIN = "<!-- BEGIN genx agent-rules -->";
29
+ export declare const MANAGED_END = "<!-- END genx agent-rules -->";
30
+ export interface PlannedFile {
31
+ /** Path relative to the consumer project root. */
32
+ path: string;
33
+ contents: string;
34
+ }
35
+ export type ExistsFn = (path: string) => boolean;
36
+ export type ReadFn = (path: string) => string;
37
+ /**
38
+ * Narrowed message for a caught value. `catch` bindings are only typed loosely because this
39
+ * package has not enabled `useUnknownInCatchVariables` yet; don't rely on that staying true.
40
+ */
41
+ export declare const errorMessage: (error: unknown) => string;
42
+ /**
43
+ * Replaces the genx-managed block in `content`, or appends one when absent. Everything outside the
44
+ * markers is left alone — these files belong to the consumer project, not to us.
45
+ *
46
+ * Throws when the markers are malformed (an unpaired marker, `END` before `BEGIN`, or more than one
47
+ * pair). Every way of guessing what the author meant risks eating their prose: appending a second
48
+ * block leaves the file to be truncated by the *next* run, and treating an unpaired `BEGIN` as
49
+ * running to end-of-file discards whatever they wrote below it. Refusing to write is the only
50
+ * option that cannot lose someone's instructions.
51
+ */
52
+ export declare const upsertManagedBlock: (content: string, body: string) => string;
53
+ /** Import line understood by Claude Code (`CLAUDE.md`) and Gemini CLI (`GEMINI.md`). */
54
+ export declare const renderImportBlock: (rules: AgentRule[], targetDir: string) => string;
55
+ /** Plain pointer for assistants without an import syntax (AGENTS.md, Copilot, Windsurf, …). */
56
+ export declare const renderPointerBlock: (rules: AgentRule[], targetDir: string) => string;
57
+ /** A fully generated Cursor rule that defers to the shared markdown. */
58
+ export declare const renderCursorRule: (rule: AgentRule, targetDir: string) => string;
59
+ /**
60
+ * Which assistants a project already uses. Wiring an assistant nobody uses is just litter, so we
61
+ * only touch what we can see evidence of.
62
+ */
63
+ export declare const detectTools: (dir: string, exists?: ExistsFn) => AgentTool[];
64
+ /**
65
+ * The nearest ancestor of `dir` that looks like the project's assistant root, or `null`.
66
+ *
67
+ * Genesis apps keep `package.json` in `client/` while `CLAUDE.md` and friends sit at the repo
68
+ * root, so running the installer from the package directory would wire up nothing and leave a
69
+ * second, orphaned set of rule files behind. Walk up as far as the repo root looking for the
70
+ * directory the assistants actually read.
71
+ */
72
+ export declare const findAssistantRoot: (dir: string, exists?: ExistsFn) => string | null;
73
+ /** Parses and validates a `--tools` value. Throws on anything unknown rather than guessing. */
74
+ export declare const parseTools: (value: string) => AgentTool[];
75
+ /**
76
+ * Everything the command intends to write, as (path, contents) pairs. Kept separate from the
77
+ * writing itself so `--check` can compare against disk without touching it.
78
+ */
79
+ export declare const planFiles: (dir: string, manifest: AgentRulesManifest, tools: AgentTool[], ruleContents: Record<string, string>, read: ReadFn, exists?: ExistsFn) => PlannedFile[];
80
+ /** Planned files whose contents differ from what is on disk. */
81
+ export declare const findDrift: (dir: string, planned: PlannedFile[], read?: ReadFn, exists?: ExistsFn) => PlannedFile[];
82
+ //# sourceMappingURL=agent-rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-rules.d.ts","sourceRoot":"","sources":["../../src/utils/agent-rules.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,mDAAoD,CAAC;AAC7E,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,gFAAgF;AAChF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,CAIzE,CAAC;AAEF,eAAO,MAAM,aAAa,oCAAoC,CAAC;AAC/D,eAAO,MAAM,WAAW,kCAAkC,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC1B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;AACjD,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;AAK9C;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MACU,CAAC;AAEzD;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,EAAE,MAAM,MAAM,KAAG,MAsBlE,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,iBAAiB,GAAI,OAAO,SAAS,EAAE,EAAE,WAAW,MAAM,KAAG,MAI5D,CAAC;AAEf,+FAA+F;AAC/F,eAAO,MAAM,kBAAkB,GAAI,OAAO,SAAS,EAAE,EAAE,WAAW,MAAM,KAAG,MAI7D,CAAC;AAUf,wEAAwE;AACxE,eAAO,MAAM,gBAAgB,GAAI,MAAM,SAAS,EAAE,WAAW,MAAM,KAAG,MAarE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,EAAE,SAAQ,QAAqB,KAAG,SAAS,EAQjF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,EAAE,SAAQ,QAAqB,KAAG,MAAM,GAAG,IAiBvF,CAAC;AAEF,+FAA+F;AAC/F,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,KAAG,SAAS,EAYnD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,SAAS,GACpB,KAAK,MAAM,EACX,UAAU,kBAAkB,EAC5B,OAAO,SAAS,EAAE,EAClB,cAAc,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,MAAM,MAAM,EACZ,SAAQ,QAAqB,KAC5B,WAAW,EAiCb,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,SAAS,GACpB,KAAK,MAAM,EACX,SAAS,WAAW,EAAE,EACtB,OAAM,MAA6C,EACnD,SAAQ,QAAqB,KAC5B,WAAW,EAIV,CAAC"}
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findDrift = exports.planFiles = exports.parseTools = exports.findAssistantRoot = exports.detectTools = exports.renderCursorRule = exports.renderPointerBlock = exports.renderImportBlock = exports.upsertManagedBlock = exports.errorMessage = exports.MANAGED_END = exports.MANAGED_BEGIN = exports.TOOL_ENTRY_FILES = exports.AGENT_TOOLS = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ exports.AGENT_TOOLS = ['claude', 'cursor', 'gemini', 'agents'];
7
+ /** Entry file each assistant reads, for the tools wired via a managed block. */
8
+ exports.TOOL_ENTRY_FILES = {
9
+ claude: 'CLAUDE.md',
10
+ gemini: 'GEMINI.md',
11
+ agents: 'AGENTS.md',
12
+ };
13
+ exports.MANAGED_BEGIN = '<!-- BEGIN genx agent-rules -->';
14
+ exports.MANAGED_END = '<!-- END genx agent-rules -->';
15
+ const countOccurrences = (haystack, needle) => haystack.split(needle).length - 1;
16
+ /**
17
+ * Narrowed message for a caught value. `catch` bindings are only typed loosely because this
18
+ * package has not enabled `useUnknownInCatchVariables` yet; don't rely on that staying true.
19
+ */
20
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
21
+ exports.errorMessage = errorMessage;
22
+ /**
23
+ * Replaces the genx-managed block in `content`, or appends one when absent. Everything outside the
24
+ * markers is left alone — these files belong to the consumer project, not to us.
25
+ *
26
+ * Throws when the markers are malformed (an unpaired marker, `END` before `BEGIN`, or more than one
27
+ * pair). Every way of guessing what the author meant risks eating their prose: appending a second
28
+ * block leaves the file to be truncated by the *next* run, and treating an unpaired `BEGIN` as
29
+ * running to end-of-file discards whatever they wrote below it. Refusing to write is the only
30
+ * option that cannot lose someone's instructions.
31
+ */
32
+ const upsertManagedBlock = (content, body) => {
33
+ const block = `${exports.MANAGED_BEGIN}\n${body.trim()}\n${exports.MANAGED_END}`;
34
+ const begins = countOccurrences(content, exports.MANAGED_BEGIN);
35
+ const ends = countOccurrences(content, exports.MANAGED_END);
36
+ const begin = content.indexOf(exports.MANAGED_BEGIN);
37
+ const end = content.indexOf(exports.MANAGED_END);
38
+ if (begins === 1 && ends === 1 && end > begin) {
39
+ const before = content.slice(0, begin);
40
+ const after = content.slice(end + exports.MANAGED_END.length);
41
+ return `${before}${block}${after}`;
42
+ }
43
+ if (begins === 0 && ends === 0) {
44
+ return content.trim() ? `${content.replace(/\s*$/, '')}\n\n${block}\n` : `${block}\n`;
45
+ }
46
+ throw new Error(`the genx agent-rules block is malformed: found ${begins} "${exports.MANAGED_BEGIN}" and ${ends} ` +
47
+ `"${exports.MANAGED_END}" marker(s), expected one matching pair or none. Restore the pair or delete ` +
48
+ `the block by hand, then run again — refusing to guess, so nothing you wrote is lost.`);
49
+ };
50
+ exports.upsertManagedBlock = upsertManagedBlock;
51
+ /** Import line understood by Claude Code (`CLAUDE.md`) and Gemini CLI (`GEMINI.md`). */
52
+ const renderImportBlock = (rules, targetDir) => [
53
+ '<!-- Installed by `genx agent-rules`. Re-run it to update; edits inside this block are lost. -->',
54
+ ...rules.map((rule) => `@${targetDir}/${rule.file}`),
55
+ ].join('\n');
56
+ exports.renderImportBlock = renderImportBlock;
57
+ /** Plain pointer for assistants without an import syntax (AGENTS.md, Copilot, Windsurf, …). */
58
+ const renderPointerBlock = (rules, targetDir) => [
59
+ '<!-- Installed by `genx agent-rules`. Re-run it to update; edits inside this block are lost. -->',
60
+ ...rules.map((rule) => `- ${rule.description} Read and follow \`${targetDir}/${rule.file}\`.`),
61
+ ].join('\n');
62
+ exports.renderPointerBlock = renderPointerBlock;
63
+ /**
64
+ * YAML double-quoted scalar for a rule description. A colon, a quote, or a leading `-` in the
65
+ * manifest would otherwise produce front matter Cursor cannot parse — and the rule would then be
66
+ * silently inert rather than visibly broken.
67
+ */
68
+ const yamlString = (value) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
69
+ /** A fully generated Cursor rule that defers to the shared markdown. */
70
+ const renderCursorRule = (rule, targetDir) => `---
71
+ description: ${yamlString(rule.cursorDescription)}
72
+ alwaysApply: false
73
+ ---
74
+
75
+ <!-- Installed by \`genx agent-rules\`. Re-run it to update; local edits are lost. -->
76
+
77
+ # ${rule.title}
78
+
79
+ @${targetDir}/${rule.file}
80
+
81
+ If this reference does not resolve, open \`${targetDir}/${rule.file}\` and follow it directly.
82
+ `;
83
+ exports.renderCursorRule = renderCursorRule;
84
+ /**
85
+ * Which assistants a project already uses. Wiring an assistant nobody uses is just litter, so we
86
+ * only touch what we can see evidence of.
87
+ */
88
+ const detectTools = (dir, exists = node_fs_1.existsSync) => {
89
+ const markers = {
90
+ claude: ['CLAUDE.md', '.claude'],
91
+ cursor: ['.cursor', '.cursorrules'],
92
+ gemini: ['GEMINI.md', '.gemini'],
93
+ agents: ['AGENTS.md'],
94
+ };
95
+ return exports.AGENT_TOOLS.filter((tool) => markers[tool].some((marker) => exists((0, node_path_1.join)(dir, marker))));
96
+ };
97
+ exports.detectTools = detectTools;
98
+ /**
99
+ * The nearest ancestor of `dir` that looks like the project's assistant root, or `null`.
100
+ *
101
+ * Genesis apps keep `package.json` in `client/` while `CLAUDE.md` and friends sit at the repo
102
+ * root, so running the installer from the package directory would wire up nothing and leave a
103
+ * second, orphaned set of rule files behind. Walk up as far as the repo root looking for the
104
+ * directory the assistants actually read.
105
+ */
106
+ const findAssistantRoot = (dir, exists = node_fs_1.existsSync) => {
107
+ let current = (0, node_path_1.dirname)(dir);
108
+ for (;;) {
109
+ if ((0, exports.detectTools)(current, exists).length) {
110
+ return current;
111
+ }
112
+ // The repo root is as far as we go - above it is someone else's project.
113
+ if (exists((0, node_path_1.join)(current, '.git'))) {
114
+ return null;
115
+ }
116
+ const parent = (0, node_path_1.dirname)(current);
117
+ if (parent === current) {
118
+ return null;
119
+ }
120
+ current = parent;
121
+ }
122
+ };
123
+ exports.findAssistantRoot = findAssistantRoot;
124
+ /** Parses and validates a `--tools` value. Throws on anything unknown rather than guessing. */
125
+ const parseTools = (value) => {
126
+ const requested = value
127
+ .split(',')
128
+ .map((tool) => tool.trim().toLowerCase())
129
+ .filter(Boolean);
130
+ const unknown = requested.filter((tool) => !exports.AGENT_TOOLS.includes(tool));
131
+ if (unknown.length) {
132
+ throw new Error(`Unknown agent tool(s): ${unknown.join(', ')}. Supported: ${exports.AGENT_TOOLS.join(', ')}`);
133
+ }
134
+ return exports.AGENT_TOOLS.filter((tool) => requested.includes(tool));
135
+ };
136
+ exports.parseTools = parseTools;
137
+ /**
138
+ * Everything the command intends to write, as (path, contents) pairs. Kept separate from the
139
+ * writing itself so `--check` can compare against disk without touching it.
140
+ */
141
+ const planFiles = (dir, manifest, tools, ruleContents, read, exists = node_fs_1.existsSync) => {
142
+ const { targetDir, rules } = manifest;
143
+ const planned = rules.map((rule) => ({
144
+ path: `${targetDir}/${rule.file}`,
145
+ contents: ruleContents[rule.file],
146
+ }));
147
+ tools.forEach((tool) => {
148
+ if (tool === 'cursor') {
149
+ rules.forEach((rule) => {
150
+ planned.push({
151
+ path: `.cursor/rules/${rule.name}.mdc`,
152
+ contents: (0, exports.renderCursorRule)(rule, targetDir),
153
+ });
154
+ });
155
+ return;
156
+ }
157
+ const entryFile = exports.TOOL_ENTRY_FILES[tool];
158
+ const entryPath = (0, node_path_1.join)(dir, entryFile);
159
+ const current = exists(entryPath) ? read(entryPath) : '';
160
+ const body = tool === 'agents'
161
+ ? (0, exports.renderPointerBlock)(rules, targetDir)
162
+ : (0, exports.renderImportBlock)(rules, targetDir);
163
+ try {
164
+ planned.push({ path: entryFile, contents: (0, exports.upsertManagedBlock)(current, body) });
165
+ }
166
+ catch (error) {
167
+ throw new Error(`${entryFile}: ${(0, exports.errorMessage)(error)}`);
168
+ }
169
+ });
170
+ return planned;
171
+ };
172
+ exports.planFiles = planFiles;
173
+ /** Planned files whose contents differ from what is on disk. */
174
+ const findDrift = (dir, planned, read = (path) => (0, node_fs_1.readFileSync)(path, 'utf8'), exists = node_fs_1.existsSync) => planned.filter((file) => {
175
+ const path = (0, node_path_1.join)(dir, file.path);
176
+ return !exists(path) || read(path) !== file.contents;
177
+ });
178
+ exports.findDrift = findDrift;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/genx",
3
3
  "description": "Genx CLI",
4
- "version": "15.23.2",
4
+ "version": "15.24.0-FUI-2603.10",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "engines": {
7
7
  "node": ">=22.0.0"
@@ -10,6 +10,7 @@
10
10
  "build": "npm run clean && tsc -b ./tsconfig.json",
11
11
  "clean": "rimraf dist temp tsconfig.tsbuildinfo",
12
12
  "dev": "tsc -b ./tsconfig.json -w",
13
+ "test": "genx test",
13
14
  "lint": "genx lint -l ox",
14
15
  "lint:fix": "genx lint -l ox --fix"
15
16
  },
@@ -17,18 +18,21 @@
17
18
  "genx": "./bin/genx"
18
19
  },
19
20
  "dependencies": {
20
- "@genesislcap/build-kit": "15.23.2",
21
- "@genesislcap/eslint-stylelint-builder": "15.23.2",
22
- "@genesislcap/event-type-codegen": "15.23.2",
23
- "@genesislcap/rollup-builder": "15.23.2",
24
- "@genesislcap/ts-builder": "15.23.2",
25
- "@genesislcap/uvu-playwright-builder": "15.23.2",
26
- "@genesislcap/vite-builder": "15.23.2",
27
- "@genesislcap/webpack-builder": "15.23.2",
21
+ "@genesislcap/build-kit": "15.24.0-FUI-2603.10",
22
+ "@genesislcap/eslint-stylelint-builder": "15.24.0-FUI-2603.10",
23
+ "@genesislcap/event-type-codegen": "15.24.0-FUI-2603.10",
24
+ "@genesislcap/rollup-builder": "15.24.0-FUI-2603.10",
25
+ "@genesislcap/ts-builder": "15.24.0-FUI-2603.10",
26
+ "@genesislcap/uvu-playwright-builder": "15.24.0-FUI-2603.10",
27
+ "@genesislcap/vite-builder": "15.24.0-FUI-2603.10",
28
+ "@genesislcap/webpack-builder": "15.24.0-FUI-2603.10",
28
29
  "cac": "^6.7.14",
29
30
  "consola": "^3.0.2",
30
31
  "serve-handler": "^6.1.5"
31
32
  },
33
+ "devDependencies": {
34
+ "@genesislcap/foundation-testing": "15.24.0-FUI-2603.10"
35
+ },
32
36
  "repository": {
33
37
  "type": "git",
34
38
  "url": "git+https://github.com/genesislcap/foundation-ui.git",