@genesislcap/genx 15.35.0 → 15.35.1-GENC-1632.1
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 +11 -5
- package/agent-rules/propose-upstream-feature.md +42 -17
- package/agent-rules/report-upstream-bug.md +55 -12
- package/dist/commands/agent-rules.d.ts.map +1 -1
- package/dist/commands/agent-rules.js +57 -16
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/utils/agent-rules.d.ts +45 -6
- package/dist/utils/agent-rules.d.ts.map +1 -1
- package/dist/utils/agent-rules.js +112 -23
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -46,11 +46,17 @@ files, the command looks up the tree as far as the repo root and, if it finds th
|
|
|
46
46
|
at it and writes nothing rather than leaving an orphaned second copy behind (`--tools` installs in
|
|
47
47
|
the current folder anyway).
|
|
48
48
|
|
|
49
|
-
The rule markdown is copied to `docs/agent-rules
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
The rule markdown is copied to `docs/agent-rules/`, and each assistant is wired in its own idiom:
|
|
50
|
+
a `.claude/skills/<rule>/SKILL.md` for Claude Code, a `.cursor/rules/<rule>.mdc` for Cursor, and a
|
|
51
|
+
pointer line inside a managed block in `GEMINI.md` or `AGENTS.md` for the rest. All four load the
|
|
52
|
+
rule body on demand rather than on every session. Content outside the managed block is never
|
|
53
|
+
touched.
|
|
54
|
+
|
|
55
|
+
Installed copies are managed: re-run the command after `genx upgrade` **and `npm install`** to pick
|
|
56
|
+
up rule changes, and propose edits in
|
|
57
|
+
[foundation-ui](https://github.com/genesislcap/foundation-ui) rather than locally. The install
|
|
58
|
+
records what it did in `docs/agent-rules/.genx-agent-rules.json`, which is what lets `--check`
|
|
59
|
+
verify the assistants actually chosen and lets a later run sweep away rules retired upstream.
|
|
54
60
|
|
|
55
61
|
Currently shipped:
|
|
56
62
|
|
|
@@ -15,10 +15,14 @@
|
|
|
15
15
|
|
|
16
16
|
## When this rule applies
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Most of what a consumer project builds is app code, and app code is not this rule's business.
|
|
19
|
+
The signal is not "I am writing a component" — it is "I am writing something a *different* Genesis
|
|
20
|
+
project would want unchanged". Apply the rule before writing code when that is plausible and any of
|
|
21
|
+
these is true:
|
|
19
22
|
|
|
20
23
|
- 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
|
|
24
|
+
adapter, an auth/comms/layout concern — with no business rules in it, and Step 2's test looks
|
|
25
|
+
likely to pass.
|
|
22
26
|
- You are extending an existing `@genesislcap/*` component by wrapping, subclassing, or
|
|
23
27
|
re-implementing it, because it lacks one attribute, slot, event, or option.
|
|
24
28
|
- Someone says "we built this in the other app" or you can see code copied in from another Genesis
|
|
@@ -53,27 +57,40 @@ It does **not** apply to app code, and most code is app code:
|
|
|
53
57
|
|
|
54
58
|
Platform surface is large and under-documented; assume it might already be there.
|
|
55
59
|
|
|
60
|
+
First find `node_modules`: in a Genesis app it is under `client/`, while these rules are installed
|
|
61
|
+
at the repo root, so a relative path from your cwd will usually miss. Resolve it once, and do not
|
|
62
|
+
silence the errors — "not installed" and "searched, found nothing" must not look alike:
|
|
63
|
+
|
|
56
64
|
```bash
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
MODULES=$(cd client 2>/dev/null || cd .; npm root) # absolute path to node_modules
|
|
66
|
+
ls "$MODULES/@genesislcap" | head
|
|
67
|
+
```
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
Then search for the **behaviour**, not for the name you are about to invent, and search the leaf
|
|
70
|
+
declarations — `index.d.ts` is a 20-line barrel of `export *`, so grepping it answers nothing, and
|
|
71
|
+
`docs/api/` is not published (the tarball ships an empty `docs/`; `README.md` and `dist/dts` do):
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
```bash
|
|
74
|
+
grep -rin "<behaviour keyword>" "$MODULES/@genesislcap/<pkg>/dist/dts" | head
|
|
75
|
+
grep -rin "<behaviour keyword>" "$MODULES/@genesislcap/<pkg>/README.md" | head
|
|
76
|
+
|
|
77
|
+
# Newer versions may already have it. Read the release notes, not the dist-tag list.
|
|
78
|
+
npm view @genesislcap/<pkg> dist-tags.latest
|
|
66
79
|
```
|
|
67
80
|
|
|
68
|
-
|
|
69
|
-
if you have the monorepo to hand. If it exists, use it and stop here.
|
|
81
|
+
Try two or three phrasings of the behaviour before concluding it is absent, and check the showcase
|
|
82
|
+
apps if you have the monorepo to hand. If it exists, use it and stop here.
|
|
70
83
|
|
|
71
84
|
## Step 2 — Apply the generality test
|
|
72
85
|
|
|
73
86
|
Answer all four. Any "no" means it is app code: build it locally and stop.
|
|
74
87
|
|
|
75
88
|
1. **Name two independent consumers.** Two real projects or teams that would use this, not two
|
|
76
|
-
screens in this app.
|
|
89
|
+
screens in this app. You are inside one checkout and cannot see the others, so the honest
|
|
90
|
+
answers are: name them because the human or the repo told you, **ask the human to name them**,
|
|
91
|
+
or write "demand unestablished — one project so far" in the proposal. Never invent a team or an
|
|
92
|
+
app name; a fabricated `## Who needs it` is worse than an empty one, because it is the field the
|
|
93
|
+
platform team prioritises on.
|
|
77
94
|
2. **Describe the API without this app's domain nouns.** If the signature needs
|
|
78
95
|
`Trade`, `Counterparty`, or your client's name to make sense, it is not platform capability.
|
|
79
96
|
3. **No app-specific dependencies.** It must not reach into this app's services, routes, config
|
|
@@ -119,7 +136,8 @@ Title: what the capability is, in the platform's vocabulary — not your app's.
|
|
|
119
136
|
- Good: `grid-pro: expose a column-level formatter hook for cell values`
|
|
120
137
|
- Bad: `Need to format trade dates in the blotter`
|
|
121
138
|
|
|
122
|
-
Body
|
|
139
|
+
Body — the repo ships an issue form at `.github/ISSUE_TEMPLATE/upstream-proposal.yml` whose fields
|
|
140
|
+
are the headings below; fill the same ones when filing from the CLI:
|
|
123
141
|
|
|
124
142
|
````markdown
|
|
125
143
|
## What's missing
|
|
@@ -175,6 +193,12 @@ areas as the bug rule (`Grids`, `COMMS`, `Design System`, `Forms`, `Layout`,
|
|
|
175
193
|
`Frontends Compatibility`, …). Add `Next` when it is capability the platform is expected to grow
|
|
176
194
|
into rather than something needed this sprint. Delete the temp file, then report the URL.
|
|
177
195
|
|
|
196
|
+
> **If the tracker is unreachable** — `genesislcap/foundation-ui` is private and many consumer
|
|
197
|
+
> developers will not have access (`gh repo view genesislcap/foundation-ui --json name` tells you in
|
|
198
|
+
> one call) — or the human declines, say so plainly, record the proposal in
|
|
199
|
+
> `docs/upstream-tracking.md` as **unfiled**, and carry on to Step 6 with
|
|
200
|
+
> `UPSTREAM-CANDIDATE(unfiled)`. Nothing here should block the app.
|
|
201
|
+
|
|
178
202
|
## Step 6 — Build locally as a seam, with approval
|
|
179
203
|
|
|
180
204
|
A filed proposal does not ship your feature. Once the issue URL exists:
|
|
@@ -205,10 +229,11 @@ A filed proposal does not ship your feature. Once the issue URL exists:
|
|
|
205
229
|
## Step 7 — Offer the PR
|
|
206
230
|
|
|
207
231
|
Proposals with a PR attached land far sooner than proposals without one, and you have already
|
|
208
|
-
written the implementation once.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
232
|
+
written the implementation once. Ask in the issue for the two things a consumer team cannot provide
|
|
233
|
+
itself: a Jira ticket reference for the commit message (commit linting enforces one) and, if you
|
|
234
|
+
need to adopt the change before it merges, a pre-release — cutting one is an internal Actions
|
|
235
|
+
dispatch a platform engineer runs. Setup and PR conventions are in
|
|
236
|
+
[CONTRIBUTING.md](https://github.com/genesislcap/foundation-ui/blob/master/CONTRIBUTING.md).
|
|
212
237
|
|
|
213
238
|
## Never
|
|
214
239
|
|
|
@@ -13,9 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
## When this rule applies
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
First, a caution about the trigger: in an app built on the platform, `@genesislcap` frames appear
|
|
17
|
+
in nearly every stack — FAST's template and observable machinery, the router, DI and comms all call
|
|
18
|
+
back into app code. A platform frame is *not* on its own a sign of a platform bug. What matters is
|
|
19
|
+
the **innermost non-platform frame**: if that is your own code, this is your bug.
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
Apply the rule when that test points upstream and any of these is true:
|
|
22
|
+
|
|
23
|
+
- The innermost non-platform frame is inside `node_modules/@genesislcap/**`.
|
|
19
24
|
- The behaviour of a `@genesislcap/*` package contradicts its own docs, types, JSDoc, or
|
|
20
25
|
README — including a `CustomEvent<unknown>` where a typed detail is documented.
|
|
21
26
|
- You are about to write, or have caught yourself considering, any of these:
|
|
@@ -43,6 +48,30 @@ Step 1 exists to tell those apart.
|
|
|
43
48
|
5. **Be honest about what you verified.** Separate "confirmed by running it" from "suspected".
|
|
44
49
|
A confidently-wrong root cause wastes more upstream time than no root cause at all.
|
|
45
50
|
|
|
51
|
+
## Before you start: can you reach the tracker?
|
|
52
|
+
|
|
53
|
+
`genesislcap/foundation-ui` is private. Filing needs GitHub CLI authenticated as someone with
|
|
54
|
+
access to it (`gh auth status`), which many consumer-project developers will not have — the failure
|
|
55
|
+
looks like `Could not resolve to a Repository with the name 'genesislcap/foundation-ui'`.
|
|
56
|
+
|
|
57
|
+
Check once, before promising the human an issue:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
gh repo view genesislcap/foundation-ui --json name >/dev/null && echo reachable
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**If it is not reachable, or the human declines to file, or they simply do not answer**, the rule
|
|
64
|
+
does not leave the app stuck. Non-negotiable 1 is lifted, and instead:
|
|
65
|
+
|
|
66
|
+
1. Say plainly that the upstream issue could not be filed, and why.
|
|
67
|
+
2. Write the report you would have filed into `docs/upstream-tracking.md` (or the project's own
|
|
68
|
+
notes) so nothing is lost, marked as **unfiled**.
|
|
69
|
+
3. Proceed to Step 6's workaround flow — still with explicit approval, still marked — using
|
|
70
|
+
`UPSTREAM-WORKAROUND(unfiled)` in place of the issue reference.
|
|
71
|
+
4. Tell the human who to ask for repo access, or to have someone with access file it.
|
|
72
|
+
|
|
73
|
+
A production outage does not wait for a tracker.
|
|
74
|
+
|
|
46
75
|
---
|
|
47
76
|
|
|
48
77
|
## Step 1 — Rule out the boring causes
|
|
@@ -53,15 +82,20 @@ Do this before writing a single line of the issue.
|
|
|
53
82
|
# What is actually installed (the lockfile, not the range in package.json)?
|
|
54
83
|
npm ls @genesislcap/<package> # or: pnpm why @genesislcap/<package>
|
|
55
84
|
|
|
56
|
-
# Is it the latest?
|
|
57
|
-
npm view @genesislcap/<package>
|
|
58
|
-
npm view @genesislcap/<package>
|
|
85
|
+
# Is it the latest?
|
|
86
|
+
npm view @genesislcap/<package> dist-tags.latest
|
|
87
|
+
npm view @genesislcap/<package> versions --json | tail -20
|
|
59
88
|
|
|
60
89
|
# Are the @genesislcap/* packages on matching versions? Mixed majors/minors cause
|
|
61
90
|
# symptoms that look exactly like upstream bugs but are dependency drift.
|
|
62
91
|
npm ls --all 2>/dev/null | grep @genesislcap
|
|
63
92
|
```
|
|
64
93
|
|
|
94
|
+
Do not scan the full dist-tag list for something newer: the packages carry ~200 per-ticket
|
|
95
|
+
pre-release tags, most of them on older majors (`bny: 14.49.0-bny.2`, `pa-659: 13.2.0-pa-659.2`),
|
|
96
|
+
they are not ordered, and their names imply nothing about content. "Upgrading" to one is usually a
|
|
97
|
+
downgrade. Check release notes instead.
|
|
98
|
+
|
|
65
99
|
Then confirm the calling code follows the documented API — read the package's own `dist/dts/*.d.ts`
|
|
66
100
|
and README rather than assuming. If the answer is "we were holding it wrong", fix it locally and
|
|
67
101
|
stop here; this rule is done.
|
|
@@ -111,8 +145,10 @@ conventional-commit prefix.
|
|
|
111
145
|
- Good: `sendForStream() defers DATA_LOGON via DOM.queueUpdate — StrictMode double-mount discards it, leaving every dataserver grid empty`
|
|
112
146
|
- Bad: `Grid is empty`
|
|
113
147
|
|
|
114
|
-
Body —
|
|
115
|
-
|
|
148
|
+
Body — the repo ships an issue form at `.github/ISSUE_TEMPLATE/upstream-bug.yml`, and its fields
|
|
149
|
+
are the structure below. Fill the same headings when filing from the CLI, so a CLI-filed issue and
|
|
150
|
+
a browser-filed one read alike ([#2442](https://github.com/genesislcap/foundation-ui/issues/2442)
|
|
151
|
+
is a good worked example):
|
|
116
152
|
|
|
117
153
|
````markdown
|
|
118
154
|
## Package + version
|
|
@@ -237,11 +273,18 @@ Filing the issue does not unblock this app. Once the issue URL exists:
|
|
|
237
273
|
|
|
238
274
|
## Step 7 — Offer the upstream fix
|
|
239
275
|
|
|
240
|
-
If the fix is small and you can see it, say so in the issue and offer a PR
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
276
|
+
If the fix is small and you can see it, say so in the issue and offer a PR — but ask for two things
|
|
277
|
+
in the issue rather than trying and failing:
|
|
278
|
+
|
|
279
|
+
- **A ticket reference.** Every commit needs an `FUI-`/`PLAT-` style Jira reference and it is
|
|
280
|
+
enforced by commit linting, which consumer teams cannot mint for themselves. Ask a maintainer to
|
|
281
|
+
supply one on the issue.
|
|
282
|
+
- **A pre-release, if you need to validate before the fix merges.** Cutting one is an internal
|
|
283
|
+
Actions dispatch a platform engineer runs; it is not something a consumer team can trigger. Ask
|
|
284
|
+
for it in the issue and say why you need it, rather than assuming it is on tap.
|
|
285
|
+
|
|
286
|
+
Setup and PR conventions are in
|
|
287
|
+
[CONTRIBUTING.md](https://github.com/genesislcap/foundation-ui/blob/master/CONTRIBUTING.md).
|
|
245
288
|
|
|
246
289
|
## Never
|
|
247
290
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-rules.d.ts","sourceRoot":"","sources":["../../src/commands/agent-rules.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agent-rules.d.ts","sourceRoot":"","sources":["../../src/commands/agent-rules.ts"],"names":[],"mappings":"yBAkEE,eAAsB,EACtB,UAAS;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO;AAFnD,wBAmFE"}
|
|
@@ -8,31 +8,56 @@ const consola_1 = tslib_1.__importDefault(require("consola"));
|
|
|
8
8
|
const agent_rules_1 = require("../utils/agent-rules");
|
|
9
9
|
/** The packaged rules live at the root of this package, alongside `bin` and `dist`. */
|
|
10
10
|
const packagedRulesDir = () => (0, node_path_1.resolve)(__dirname, '../../agent-rules');
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
return
|
|
15
|
-
}
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
/** A packaging fault, not a user error — say which file is missing instead of dumping a stack. */
|
|
12
|
+
const fail = (message) => {
|
|
13
|
+
consola_1.default.error(message);
|
|
14
|
+
return process.exit(1);
|
|
15
|
+
};
|
|
16
|
+
const loadManifest = (rulesDir) => {
|
|
17
|
+
const path = (0, node_path_1.join)(rulesDir, 'manifest.json');
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
|
|
19
20
|
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return fail(`Could not read the packaged rules manifest at ${path}: ${(0, agent_rules_1.errorMessage)(error)}`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const readRuleContents = (rulesDir, manifest) => manifest.rules.reduce((contents, rule) => {
|
|
20
26
|
try {
|
|
21
|
-
|
|
27
|
+
contents[rule.file] = (0, node_fs_1.readFileSync)((0, node_path_1.join)(rulesDir, rule.file), 'utf8');
|
|
22
28
|
}
|
|
23
29
|
catch (error) {
|
|
24
|
-
|
|
25
|
-
consola_1.default.error((0, agent_rules_1.errorMessage)(error));
|
|
26
|
-
process.exit(1);
|
|
30
|
+
fail(`The manifest lists "${rule.file}" but the packaged rule is missing: ${(0, agent_rules_1.errorMessage)(error)}`);
|
|
27
31
|
}
|
|
32
|
+
return contents;
|
|
33
|
+
}, {});
|
|
34
|
+
/**
|
|
35
|
+
* What was installed beats what happens to be lying around. Re-detecting on every run would tell a
|
|
36
|
+
* team that installed `--tools claude` that their deliberate omissions are drift, and would let a
|
|
37
|
+
* hand-edited file go unchecked when CI passes a narrower `--tools` than the install did.
|
|
38
|
+
*/
|
|
39
|
+
const resolveTools = (dir, targetDir, requested) => {
|
|
40
|
+
var _a;
|
|
41
|
+
if (requested !== undefined) {
|
|
42
|
+
try {
|
|
43
|
+
return (0, agent_rules_1.parseTools)(requested);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
// A typo in --tools is a user error, not a bug: report it without a stack trace.
|
|
47
|
+
return fail((0, agent_rules_1.errorMessage)(error));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const installed = (0, agent_rules_1.readInstalledState)(dir, targetDir);
|
|
51
|
+
return ((_a = installed === null || installed === void 0 ? void 0 : installed.tools) === null || _a === void 0 ? void 0 : _a.length) ? installed.tools : (0, agent_rules_1.detectTools)(dir);
|
|
28
52
|
};
|
|
29
53
|
exports.default = (...args_1) => tslib_1.__awaiter(void 0, [...args_1], void 0, function* (folder = process.cwd(), options = {}) {
|
|
30
54
|
const dir = (0, node_path_1.resolve)(folder);
|
|
31
55
|
const rulesDir = packagedRulesDir();
|
|
32
56
|
const manifest = loadManifest(rulesDir);
|
|
33
57
|
const ruleContents = readRuleContents(rulesDir, manifest);
|
|
34
|
-
const tools = resolveTools(dir, options.tools);
|
|
35
|
-
|
|
58
|
+
const tools = resolveTools(dir, manifest.targetDir, options.tools);
|
|
59
|
+
const installed = (0, agent_rules_1.readInstalledState)(dir, manifest.targetDir);
|
|
60
|
+
if (!tools.length && options.tools === undefined) {
|
|
36
61
|
// Writing rule files into a package directory whose assistant files live higher up leaves an
|
|
37
62
|
// orphaned second copy behind, so point at the real root instead of quietly installing here.
|
|
38
63
|
const assistantRoot = (0, agent_rules_1.findAssistantRoot)(dir);
|
|
@@ -54,17 +79,21 @@ exports.default = (...args_1) => tslib_1.__awaiter(void 0, [...args_1], void 0,
|
|
|
54
79
|
process.exit(1);
|
|
55
80
|
}
|
|
56
81
|
const drift = (0, agent_rules_1.findDrift)(dir, planned);
|
|
82
|
+
// Rules retired upstream: their files linger, and a stale .mdc keeps pointing agents at a rule
|
|
83
|
+
// nobody maintains any more.
|
|
84
|
+
const stale = (0, agent_rules_1.findStale)(installed, planned).filter((path) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, path)));
|
|
57
85
|
if (options.check) {
|
|
58
|
-
if (drift.length) {
|
|
86
|
+
if (drift.length || stale.length) {
|
|
59
87
|
consola_1.default.error(`Agent rules are out of date in ${dir}:`);
|
|
60
88
|
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'}`)));
|
|
89
|
+
stale.forEach((path) => consola_1.default.log((0, build_kit_1.white)(` ${(0, build_kit_1.bold)(path)} no longer shipped`)));
|
|
61
90
|
consola_1.default.info('Run `genx agent-rules` to update.');
|
|
62
91
|
process.exit(1);
|
|
63
92
|
}
|
|
64
93
|
consola_1.default.success(`Agent rules are up to date (${manifest.rules.length} rule(s)).`);
|
|
65
94
|
return;
|
|
66
95
|
}
|
|
67
|
-
if (!drift.length) {
|
|
96
|
+
if (!drift.length && !stale.length) {
|
|
68
97
|
consola_1.default.info('Agent rules already up to date. Nothing to do.');
|
|
69
98
|
return;
|
|
70
99
|
}
|
|
@@ -74,6 +103,18 @@ exports.default = (...args_1) => tslib_1.__awaiter(void 0, [...args_1], void 0,
|
|
|
74
103
|
(0, node_fs_1.writeFileSync)(path, file.contents, 'utf8');
|
|
75
104
|
consola_1.default.log((0, build_kit_1.white)(` ${(0, build_kit_1.bold)(file.path)}`));
|
|
76
105
|
});
|
|
106
|
+
stale.forEach((relative) => {
|
|
107
|
+
const path = (0, node_path_1.join)(dir, relative);
|
|
108
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
109
|
+
try {
|
|
110
|
+
// Skill folders are one-file directories; leaving the husk behind is just litter.
|
|
111
|
+
(0, node_fs_1.rmdirSync)((0, node_path_1.dirname)(path));
|
|
112
|
+
}
|
|
113
|
+
catch (_a) {
|
|
114
|
+
// Not empty, or shared with something else. Fine either way.
|
|
115
|
+
}
|
|
116
|
+
consola_1.default.log((0, build_kit_1.white)(` ${(0, build_kit_1.bold)(relative)} removed`));
|
|
117
|
+
});
|
|
77
118
|
consola_1.default.success(`Installed ${manifest.rules.length} agent rule(s)${tools.length ? ` for: ${tools.join(', ')}` : ''}.`);
|
|
78
119
|
consola_1.default.info('Commit the changes so the whole team gets them.');
|
|
79
120
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,QAAQ;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,QAAQ;;aAYspD,CAAC;aAAe,CAAC;;;;oBAV7qD,CAAA;;;;;;;CAOd,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,QAAQ,CAAC"}
|
|
@@ -24,7 +24,21 @@ export interface AgentRulesManifest {
|
|
|
24
24
|
export declare const AGENT_TOOLS: readonly ["claude", "cursor", "gemini", "agents"];
|
|
25
25
|
export type AgentTool = (typeof AGENT_TOOLS)[number];
|
|
26
26
|
/** Entry file each assistant reads, for the tools wired via a managed block. */
|
|
27
|
-
export declare const TOOL_ENTRY_FILES: Record<
|
|
27
|
+
export declare const TOOL_ENTRY_FILES: Record<Extract<AgentTool, 'gemini' | 'agents'>, string>;
|
|
28
|
+
/** Where the installed state lives, relative to `targetDir`. */
|
|
29
|
+
export declare const STATE_FILE = ".genx-agent-rules.json";
|
|
30
|
+
/**
|
|
31
|
+
* What the last install actually did. `--check` reads this instead of re-detecting, so a team that
|
|
32
|
+
* installed `--tools claude` is not told its deliberate omissions are drift; and the file list is
|
|
33
|
+
* what lets a later run sweep away rules that have since been retired upstream.
|
|
34
|
+
*/
|
|
35
|
+
export interface InstalledState {
|
|
36
|
+
/** `manifest.version` at install time — the hook for changing the sweep rules later. */
|
|
37
|
+
version: number;
|
|
38
|
+
tools: AgentTool[];
|
|
39
|
+
/** Every path this command owns, relative to the project root. */
|
|
40
|
+
files: string[];
|
|
41
|
+
}
|
|
28
42
|
export declare const MANAGED_BEGIN = "<!-- BEGIN genx agent-rules -->";
|
|
29
43
|
export declare const MANAGED_END = "<!-- END genx agent-rules -->";
|
|
30
44
|
export interface PlannedFile {
|
|
@@ -34,6 +48,13 @@ export interface PlannedFile {
|
|
|
34
48
|
}
|
|
35
49
|
export type ExistsFn = (path: string) => boolean;
|
|
36
50
|
export type ReadFn = (path: string) => string;
|
|
51
|
+
/**
|
|
52
|
+
* Line endings, ignored. A Windows checkout of the consumer repo hands us CRLF (Git for Windows
|
|
53
|
+
* defaults to `core.autocrlf=true`, and `* text=auto` does it for everyone) while the packaged
|
|
54
|
+
* rules stay LF, because npm tarballs are not EOL-converted. Comparing raw text would fail
|
|
55
|
+
* `--check` on every CI run, and rewriting would not fix it — the next checkout re-normalises.
|
|
56
|
+
*/
|
|
57
|
+
export declare const normalizeEol: (value: string) => string;
|
|
37
58
|
/**
|
|
38
59
|
* Narrowed message for a caught value. `catch` bindings are only typed loosely because this
|
|
39
60
|
* package has not enabled `useUnknownInCatchVariables` yet; don't rely on that staying true.
|
|
@@ -50,9 +71,14 @@ export declare const errorMessage: (error: unknown) => string;
|
|
|
50
71
|
* option that cannot lose someone's instructions.
|
|
51
72
|
*/
|
|
52
73
|
export declare const upsertManagedBlock: (content: string, body: string) => string;
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Progressive-disclosure wiring for Claude Code: a project skill whose front matter carries the
|
|
76
|
+
* trigger, so the rule body is read when it is relevant instead of on every session start. This is
|
|
77
|
+
* the analogue of Cursor's `alwaysApply: false` — an `@import` in `CLAUDE.md` would load ~5k tokens
|
|
78
|
+
* of rule text into every session in the project, relevant or not.
|
|
79
|
+
*/
|
|
80
|
+
export declare const renderClaudeSkill: (rule: AgentRule, targetDir: string) => string;
|
|
81
|
+
/** Plain pointer for assistants without a skill or rules mechanism (GEMINI.md, AGENTS.md, …). */
|
|
56
82
|
export declare const renderPointerBlock: (rules: AgentRule[], targetDir: string) => string;
|
|
57
83
|
/** A fully generated Cursor rule that defers to the shared markdown. */
|
|
58
84
|
export declare const renderCursorRule: (rule: AgentRule, targetDir: string) => string;
|
|
@@ -70,13 +96,26 @@ export declare const detectTools: (dir: string, exists?: ExistsFn) => AgentTool[
|
|
|
70
96
|
* directory the assistants actually read.
|
|
71
97
|
*/
|
|
72
98
|
export declare const findAssistantRoot: (dir: string, exists?: ExistsFn) => string | null;
|
|
73
|
-
/**
|
|
74
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Parses and validates a `--tools` value. Throws on anything unknown, and on anything that names
|
|
101
|
+
* no tool at all — the CLI parser hands us whatever the shell gave it (`--tools ""` arrives as the
|
|
102
|
+
* number `0`, repeated `-t` flags as an array), and an explicit flag that wires nothing is far
|
|
103
|
+
* more likely a typo than an intent.
|
|
104
|
+
*/
|
|
105
|
+
export declare const parseTools: (value: unknown) => AgentTool[];
|
|
75
106
|
/**
|
|
76
107
|
* Everything the command intends to write, as (path, contents) pairs. Kept separate from the
|
|
77
108
|
* writing itself so `--check` can compare against disk without touching it.
|
|
78
109
|
*/
|
|
79
110
|
export declare const planFiles: (dir: string, manifest: AgentRulesManifest, tools: AgentTool[], ruleContents: Record<string, string>, read: ReadFn, exists?: ExistsFn) => PlannedFile[];
|
|
111
|
+
/** The state recorded by the last install, or `null` when there is none to read. */
|
|
112
|
+
export declare const readInstalledState: (dir: string, targetDir: string, read?: ReadFn, exists?: ExistsFn) => InstalledState | null;
|
|
113
|
+
/**
|
|
114
|
+
* Files a previous install owned that the current manifest no longer produces — a rule renamed or
|
|
115
|
+
* retired upstream. Left in place, a stale `.mdc` keeps pointing Cursor at a withdrawn rule and
|
|
116
|
+
* agents keep following it, with `--check` none the wiser.
|
|
117
|
+
*/
|
|
118
|
+
export declare const findStale: (state: InstalledState | null, planned: PlannedFile[]) => string[];
|
|
80
119
|
/** Planned files whose contents differ from what is on disk. */
|
|
81
120
|
export declare const findDrift: (dir: string, planned: PlannedFile[], read?: ReadFn, exists?: ExistsFn) => PlannedFile[];
|
|
82
121
|
//# sourceMappingURL=agent-rules.d.ts.map
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"agent-rules.d.ts","sourceRoot":"","sources":["../../src/utils/agent-rules.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.findDrift = exports.planFiles = exports.parseTools = exports.findAssistantRoot = exports.detectTools = exports.renderCursorRule = exports.renderPointerBlock = exports.
|
|
3
|
+
exports.findDrift = exports.findStale = exports.readInstalledState = exports.planFiles = exports.parseTools = exports.findAssistantRoot = exports.detectTools = exports.renderCursorRule = exports.renderPointerBlock = exports.renderClaudeSkill = exports.upsertManagedBlock = exports.errorMessage = exports.normalizeEol = exports.MANAGED_END = exports.MANAGED_BEGIN = exports.STATE_FILE = exports.TOOL_ENTRY_FILES = exports.AGENT_TOOLS = void 0;
|
|
4
4
|
const node_fs_1 = require("node:fs");
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
6
|
exports.AGENT_TOOLS = ['claude', 'cursor', 'gemini', 'agents'];
|
|
7
7
|
/** Entry file each assistant reads, for the tools wired via a managed block. */
|
|
8
8
|
exports.TOOL_ENTRY_FILES = {
|
|
9
|
-
claude: 'CLAUDE.md',
|
|
10
9
|
gemini: 'GEMINI.md',
|
|
11
10
|
agents: 'AGENTS.md',
|
|
12
11
|
};
|
|
12
|
+
/** Where the installed state lives, relative to `targetDir`. */
|
|
13
|
+
exports.STATE_FILE = '.genx-agent-rules.json';
|
|
13
14
|
exports.MANAGED_BEGIN = '<!-- BEGIN genx agent-rules -->';
|
|
14
15
|
exports.MANAGED_END = '<!-- END genx agent-rules -->';
|
|
15
16
|
const countOccurrences = (haystack, needle) => haystack.split(needle).length - 1;
|
|
17
|
+
/**
|
|
18
|
+
* Line endings, ignored. A Windows checkout of the consumer repo hands us CRLF (Git for Windows
|
|
19
|
+
* defaults to `core.autocrlf=true`, and `* text=auto` does it for everyone) while the packaged
|
|
20
|
+
* rules stay LF, because npm tarballs are not EOL-converted. Comparing raw text would fail
|
|
21
|
+
* `--check` on every CI run, and rewriting would not fix it — the next checkout re-normalises.
|
|
22
|
+
*/
|
|
23
|
+
const normalizeEol = (value) => value.replace(/\r\n/g, '\n');
|
|
24
|
+
exports.normalizeEol = normalizeEol;
|
|
25
|
+
const usesCrlf = (value) => value.includes('\r\n');
|
|
26
|
+
const toCrlf = (value) => (0, exports.normalizeEol)(value).replace(/\n/g, '\r\n');
|
|
16
27
|
/**
|
|
17
28
|
* Narrowed message for a caught value. `catch` bindings are only typed loosely because this
|
|
18
29
|
* package has not enabled `useUnknownInCatchVariables` yet; don't rely on that staying true.
|
|
@@ -35,37 +46,51 @@ const upsertManagedBlock = (content, body) => {
|
|
|
35
46
|
const ends = countOccurrences(content, exports.MANAGED_END);
|
|
36
47
|
const begin = content.indexOf(exports.MANAGED_BEGIN);
|
|
37
48
|
const end = content.indexOf(exports.MANAGED_END);
|
|
49
|
+
// Keep the file's own line endings, or a CRLF file drifts again on the next checkout.
|
|
50
|
+
const write = (value) => (usesCrlf(content) ? toCrlf(value) : value);
|
|
38
51
|
if (begins === 1 && ends === 1 && end > begin) {
|
|
39
52
|
const before = content.slice(0, begin);
|
|
40
53
|
const after = content.slice(end + exports.MANAGED_END.length);
|
|
41
|
-
return `${before}${block}${after}
|
|
54
|
+
return write(`${before}${block}${after}`);
|
|
42
55
|
}
|
|
43
56
|
if (begins === 0 && ends === 0) {
|
|
44
|
-
return content.trim() ? `${content.replace(/\s*$/, '')}\n\n${block}\n` : `${block}\n
|
|
57
|
+
return write(content.trim() ? `${content.replace(/\s*$/, '')}\n\n${block}\n` : `${block}\n`);
|
|
45
58
|
}
|
|
46
59
|
throw new Error(`the genx agent-rules block is malformed: found ${begins} "${exports.MANAGED_BEGIN}" and ${ends} ` +
|
|
47
60
|
`"${exports.MANAGED_END}" marker(s), expected one matching pair or none. Restore the pair or delete ` +
|
|
48
61
|
`the block by hand, then run again — refusing to guess, so nothing you wrote is lost.`);
|
|
49
62
|
};
|
|
50
63
|
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
|
/**
|
|
64
65
|
* YAML double-quoted scalar for a rule description. A colon, a quote, or a leading `-` in the
|
|
65
66
|
* manifest would otherwise produce front matter Cursor cannot parse — and the rule would then be
|
|
66
67
|
* silently inert rather than visibly broken.
|
|
67
68
|
*/
|
|
68
69
|
const yamlString = (value) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
70
|
+
/**
|
|
71
|
+
* Progressive-disclosure wiring for Claude Code: a project skill whose front matter carries the
|
|
72
|
+
* trigger, so the rule body is read when it is relevant instead of on every session start. This is
|
|
73
|
+
* the analogue of Cursor's `alwaysApply: false` — an `@import` in `CLAUDE.md` would load ~5k tokens
|
|
74
|
+
* of rule text into every session in the project, relevant or not.
|
|
75
|
+
*/
|
|
76
|
+
const renderClaudeSkill = (rule, targetDir) => `---
|
|
77
|
+
name: ${rule.name}
|
|
78
|
+
description: ${yamlString(rule.description)}
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
<!-- Installed by \`genx agent-rules\`. Re-run it to update; local edits are lost. -->
|
|
82
|
+
|
|
83
|
+
# ${rule.title}
|
|
84
|
+
|
|
85
|
+
Read \`${targetDir}/${rule.file}\` and follow it for the rest of this task.
|
|
86
|
+
`;
|
|
87
|
+
exports.renderClaudeSkill = renderClaudeSkill;
|
|
88
|
+
/** Plain pointer for assistants without a skill or rules mechanism (GEMINI.md, AGENTS.md, …). */
|
|
89
|
+
const renderPointerBlock = (rules, targetDir) => [
|
|
90
|
+
'<!-- Installed by `genx agent-rules`. Re-run it to update; edits inside this block are lost. -->',
|
|
91
|
+
...rules.map((rule) => `- ${rule.description} Read and follow \`${targetDir}/${rule.file}\`.`),
|
|
92
|
+
].join('\n');
|
|
93
|
+
exports.renderPointerBlock = renderPointerBlock;
|
|
69
94
|
/** A fully generated Cursor rule that defers to the shared markdown. */
|
|
70
95
|
const renderCursorRule = (rule, targetDir) => `---
|
|
71
96
|
description: ${yamlString(rule.cursorDescription)}
|
|
@@ -104,6 +129,11 @@ exports.detectTools = detectTools;
|
|
|
104
129
|
* directory the assistants actually read.
|
|
105
130
|
*/
|
|
106
131
|
const findAssistantRoot = (dir, exists = node_fs_1.existsSync) => {
|
|
132
|
+
// Checked before the walk: `dir` is very often the repo root itself, and one level above it
|
|
133
|
+
// `~/.claude` looks exactly like a project asking for the rules.
|
|
134
|
+
if (exists((0, node_path_1.join)(dir, '.git'))) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
107
137
|
let current = (0, node_path_1.dirname)(dir);
|
|
108
138
|
for (;;) {
|
|
109
139
|
if ((0, exports.detectTools)(current, exists).length) {
|
|
@@ -121,9 +151,20 @@ const findAssistantRoot = (dir, exists = node_fs_1.existsSync) => {
|
|
|
121
151
|
}
|
|
122
152
|
};
|
|
123
153
|
exports.findAssistantRoot = findAssistantRoot;
|
|
124
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* Parses and validates a `--tools` value. Throws on anything unknown, and on anything that names
|
|
156
|
+
* no tool at all — the CLI parser hands us whatever the shell gave it (`--tools ""` arrives as the
|
|
157
|
+
* number `0`, repeated `-t` flags as an array), and an explicit flag that wires nothing is far
|
|
158
|
+
* more likely a typo than an intent.
|
|
159
|
+
*/
|
|
125
160
|
const parseTools = (value) => {
|
|
126
|
-
|
|
161
|
+
// `--tools ""` reaches us as the number 0, and a number never names an assistant.
|
|
162
|
+
const raw = Array.isArray(value)
|
|
163
|
+
? value.join(',')
|
|
164
|
+
: typeof value === 'number'
|
|
165
|
+
? ''
|
|
166
|
+
: String(value !== null && value !== void 0 ? value : '');
|
|
167
|
+
const requested = raw
|
|
127
168
|
.split(',')
|
|
128
169
|
.map((tool) => tool.trim().toLowerCase())
|
|
129
170
|
.filter(Boolean);
|
|
@@ -131,6 +172,9 @@ const parseTools = (value) => {
|
|
|
131
172
|
if (unknown.length) {
|
|
132
173
|
throw new Error(`Unknown agent tool(s): ${unknown.join(', ')}. Supported: ${exports.AGENT_TOOLS.join(', ')}`);
|
|
133
174
|
}
|
|
175
|
+
if (!requested.length) {
|
|
176
|
+
throw new Error(`--tools named no assistant. Supported: ${exports.AGENT_TOOLS.join(', ')}`);
|
|
177
|
+
}
|
|
134
178
|
return exports.AGENT_TOOLS.filter((tool) => requested.includes(tool));
|
|
135
179
|
};
|
|
136
180
|
exports.parseTools = parseTools;
|
|
@@ -154,25 +198,70 @@ const planFiles = (dir, manifest, tools, ruleContents, read, exists = node_fs_1.
|
|
|
154
198
|
});
|
|
155
199
|
return;
|
|
156
200
|
}
|
|
201
|
+
if (tool === 'claude') {
|
|
202
|
+
rules.forEach((rule) => {
|
|
203
|
+
planned.push({
|
|
204
|
+
path: `.claude/skills/${rule.name}/SKILL.md`,
|
|
205
|
+
contents: (0, exports.renderClaudeSkill)(rule, targetDir),
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
157
210
|
const entryFile = exports.TOOL_ENTRY_FILES[tool];
|
|
158
211
|
const entryPath = (0, node_path_1.join)(dir, entryFile);
|
|
159
212
|
const current = exists(entryPath) ? read(entryPath) : '';
|
|
160
|
-
const body = tool === 'agents'
|
|
161
|
-
? (0, exports.renderPointerBlock)(rules, targetDir)
|
|
162
|
-
: (0, exports.renderImportBlock)(rules, targetDir);
|
|
163
213
|
try {
|
|
164
|
-
planned.push({
|
|
214
|
+
planned.push({
|
|
215
|
+
path: entryFile,
|
|
216
|
+
contents: (0, exports.upsertManagedBlock)(current, (0, exports.renderPointerBlock)(rules, targetDir)),
|
|
217
|
+
});
|
|
165
218
|
}
|
|
166
219
|
catch (error) {
|
|
167
220
|
throw new Error(`${entryFile}: ${(0, exports.errorMessage)(error)}`);
|
|
168
221
|
}
|
|
169
222
|
});
|
|
223
|
+
// Written last so it can record every path above, itself included.
|
|
224
|
+
const statePath = `${targetDir}/${exports.STATE_FILE}`;
|
|
225
|
+
const state = {
|
|
226
|
+
version: manifest.version,
|
|
227
|
+
tools,
|
|
228
|
+
files: [...planned.map((file) => file.path), statePath].sort(),
|
|
229
|
+
};
|
|
230
|
+
planned.push({ path: statePath, contents: `${JSON.stringify(state, null, 2)}\n` });
|
|
170
231
|
return planned;
|
|
171
232
|
};
|
|
172
233
|
exports.planFiles = planFiles;
|
|
234
|
+
/** The state recorded by the last install, or `null` when there is none to read. */
|
|
235
|
+
const readInstalledState = (dir, targetDir, read = (path) => (0, node_fs_1.readFileSync)(path, 'utf8'), exists = node_fs_1.existsSync) => {
|
|
236
|
+
const path = (0, node_path_1.join)(dir, targetDir, exports.STATE_FILE);
|
|
237
|
+
if (!exists(path)) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
return JSON.parse(read(path));
|
|
242
|
+
}
|
|
243
|
+
catch (_a) {
|
|
244
|
+
// A corrupt state file must not block an install — the next write repairs it.
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
exports.readInstalledState = readInstalledState;
|
|
249
|
+
/**
|
|
250
|
+
* Files a previous install owned that the current manifest no longer produces — a rule renamed or
|
|
251
|
+
* retired upstream. Left in place, a stale `.mdc` keeps pointing Cursor at a withdrawn rule and
|
|
252
|
+
* agents keep following it, with `--check` none the wiser.
|
|
253
|
+
*/
|
|
254
|
+
const findStale = (state, planned) => {
|
|
255
|
+
if (!(state === null || state === void 0 ? void 0 : state.files)) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
const current = new Set(planned.map((file) => file.path));
|
|
259
|
+
return state.files.filter((path) => !current.has(path));
|
|
260
|
+
};
|
|
261
|
+
exports.findStale = findStale;
|
|
173
262
|
/** Planned files whose contents differ from what is on disk. */
|
|
174
263
|
const findDrift = (dir, planned, read = (path) => (0, node_fs_1.readFileSync)(path, 'utf8'), exists = node_fs_1.existsSync) => planned.filter((file) => {
|
|
175
264
|
const path = (0, node_path_1.join)(dir, file.path);
|
|
176
|
-
return !exists(path) || read(path) !== file.contents;
|
|
265
|
+
return !exists(path) || (0, exports.normalizeEol)(read(path)) !== (0, exports.normalizeEol)(file.contents);
|
|
177
266
|
});
|
|
178
267
|
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.35.
|
|
4
|
+
"version": "15.35.1-GENC-1632.1",
|
|
5
5
|
"license": "SEE LICENSE IN license.txt",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.0.0"
|
|
@@ -18,20 +18,20 @@
|
|
|
18
18
|
"genx": "./bin/genx"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@genesislcap/build-kit": "15.35.
|
|
22
|
-
"@genesislcap/eslint-stylelint-builder": "15.35.
|
|
23
|
-
"@genesislcap/event-type-codegen": "15.35.
|
|
24
|
-
"@genesislcap/rollup-builder": "15.35.
|
|
25
|
-
"@genesislcap/ts-builder": "15.35.
|
|
26
|
-
"@genesislcap/uvu-playwright-builder": "15.35.
|
|
27
|
-
"@genesislcap/vite-builder": "15.35.
|
|
28
|
-
"@genesislcap/webpack-builder": "15.35.
|
|
21
|
+
"@genesislcap/build-kit": "15.35.1-GENC-1632.1",
|
|
22
|
+
"@genesislcap/eslint-stylelint-builder": "15.35.1-GENC-1632.1",
|
|
23
|
+
"@genesislcap/event-type-codegen": "15.35.1-GENC-1632.1",
|
|
24
|
+
"@genesislcap/rollup-builder": "15.35.1-GENC-1632.1",
|
|
25
|
+
"@genesislcap/ts-builder": "15.35.1-GENC-1632.1",
|
|
26
|
+
"@genesislcap/uvu-playwright-builder": "15.35.1-GENC-1632.1",
|
|
27
|
+
"@genesislcap/vite-builder": "15.35.1-GENC-1632.1",
|
|
28
|
+
"@genesislcap/webpack-builder": "15.35.1-GENC-1632.1",
|
|
29
29
|
"cac": "^6.7.14",
|
|
30
30
|
"consola": "^3.0.2",
|
|
31
31
|
"serve-handler": "^6.1.5"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@genesislcap/foundation-testing": "15.35.
|
|
34
|
+
"@genesislcap/foundation-testing": "15.35.1-GENC-1632.1"
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|