@averildwi/nest-skills 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +99 -2
- package/bin/cli.js +22 -0
- package/docs/USAGE.md +108 -0
- package/package.json +45 -4
- package/src/cli.js +124 -0
- package/src/config/agents.js +49 -0
- package/src/errors.js +27 -0
- package/src/main.js +54 -0
- package/src/preflight.js +35 -0
- package/src/prompts.js +59 -0
- package/src/runner.js +170 -0
- package/src/steps/decide.js +144 -0
- package/src/steps/index.js +15 -0
- package/src/steps/installSkills.js +22 -0
- package/src/steps/writeClaudeMd.js +37 -0
- package/src/steps/writeIndex.js +53 -0
- package/src/steps/writeManifest.js +47 -0
- package/src/ui/colors.js +36 -0
- package/src/ui/output.js +40 -0
- package/src/utils/fsHelpers.js +37 -0
- package/src/utils/manifest.js +38 -0
- package/src/utils/templates.js +30 -0
- package/templates/AGENTS.md +70 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/skills/git-workflow/SKILL.md +110 -0
- package/templates/skills/nest-architecture/SKILL.md +179 -0
- package/templates/skills/nest-auth-and-security/SKILL.md +220 -0
- package/templates/skills/nest-prisma/SKILL.md +200 -0
- package/templates/skills/nest-response-and-errors/SKILL.md +191 -0
- package/templates/skills/nest-testing/SKILL.md +125 -0
- package/templates/skills/nest-validation-and-config/SKILL.md +171 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 averildwi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,100 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @averildwi/nest-skills
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Install Everill's NestJS + Prisma coding standards into a project as **agent skills**
|
|
4
|
+
plus an **`AGENTS.md`** index, so AI coding agents (OpenCode, Claude Code) write code
|
|
5
|
+
that follows the same rules as the rest of the team.
|
|
6
|
+
|
|
7
|
+
It only writes Markdown. It never touches your code, `package.json` or `.env*`.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @averildwi/nest-skills --agent opencode
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Recommended order on a new project
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @nestjs/cli@11 new my-app --strict
|
|
17
|
+
cd my-app
|
|
18
|
+
npx @averildwi/nest-prisma --db postgres # client in generated/prisma (root)
|
|
19
|
+
npx @averildwi/nest-common
|
|
20
|
+
npx @averildwi/nest-skills --agent opencode # or claude / both
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Commit after each tool, on its own (see the `git-workflow` skill). The standard
|
|
24
|
+
targets Nest 11 (CommonJS + Jest + ESLint); pin `@nestjs/cli@11` when creating
|
|
25
|
+
the project.
|
|
26
|
+
|
|
27
|
+
## What gets installed
|
|
28
|
+
|
|
29
|
+
| Agent | Skills folder | Index | Extra |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| `opencode` | `.opencode/skills/<name>/SKILL.md` | `AGENTS.md` | |
|
|
32
|
+
| `claude` | `.claude/skills/<name>/SKILL.md` | `AGENTS.md` | `CLAUDE.md` with `@AGENTS.md` |
|
|
33
|
+
| `both` | `.claude/skills/<name>/SKILL.md` (OpenCode reads this folder too) | `AGENTS.md` | `CLAUDE.md` with `@AGENTS.md` |
|
|
34
|
+
|
|
35
|
+
Plus `.nest-skills.json`, which records a hash of every file the tool wrote. Commit it.
|
|
36
|
+
|
|
37
|
+
Skills:
|
|
38
|
+
|
|
39
|
+
| Skill | Covers |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `nest-architecture` | folder layout, layering, naming, `main.ts` order, `trust proxy`, Swagger prefixes |
|
|
42
|
+
| `nest-response-and-errors` | response envelope, marker classes, exception filters and their order |
|
|
43
|
+
| `nest-validation-and-config` | DTOs, `PaginationDto`, `ValidationPipe`, Joi env schema, `ConfigService` |
|
|
44
|
+
| `nest-prisma` | ID type (asked first), `@@map`, timestamps, `select`, transactions, destructive commands |
|
|
45
|
+
| `nest-auth-and-security` | JWT, guards, roles, scrypt hashing, OTP, throttling, CORS, `:id` pipes |
|
|
46
|
+
| `nest-testing` | service unit tests with mocked Prisma and transactions |
|
|
47
|
+
| `git-workflow` | branch, verify, stage specific files, Conventional Commits, local commits only |
|
|
48
|
+
|
|
49
|
+
`AGENTS.md` maps tasks to skills, lists the global "never do" rules, and says how to
|
|
50
|
+
behave when existing scaffold code differs from the standard.
|
|
51
|
+
|
|
52
|
+
## Options
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
npx @averildwi/nest-skills [install] [options]
|
|
56
|
+
npx @averildwi/nest-skills update [options]
|
|
57
|
+
|
|
58
|
+
-a, --agent <name> opencode | claude | both
|
|
59
|
+
--dir <path> install skills in another folder (relative, inside the project)
|
|
60
|
+
--force overwrite files you edited (old copy kept in .nest-skills-backup/)
|
|
61
|
+
-y, --yes no prompts, no confirmation
|
|
62
|
+
--dry-run show the plan only
|
|
63
|
+
--no-color plain output
|
|
64
|
+
-v, --version print the version
|
|
65
|
+
-h, --help show help
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Your files are safe
|
|
69
|
+
|
|
70
|
+
- Your own **`AGENTS.md` / `CLAUDE.md`** (not written by nest-skills) are **never** overwritten,
|
|
71
|
+
not even with `--force`.
|
|
72
|
+
- Own `AGENTS.md`: the index goes to `AGENTS.nest-skills.md` and the CLI prints the line to add.
|
|
73
|
+
- Own `CLAUDE.md`: left as is; the CLI tells you to add `@AGENTS.md` (or the fallback name).
|
|
74
|
+
- A skill file with the same name that you wrote yourself is kept; `--force` replaces it
|
|
75
|
+
and saves yours under `.nest-skills-backup/<timestamp>/`.
|
|
76
|
+
- A file nest-skills wrote and **you edited** is kept by `install` and `update`.
|
|
77
|
+
`--force` replaces it and saves your version under `.nest-skills-backup/<timestamp>/`.
|
|
78
|
+
That folder sits outside the skills folder (the agent never loads a backup) and
|
|
79
|
+
contains its own `.gitignore`, so it is never committed by accident.
|
|
80
|
+
- Running the command twice changes nothing.
|
|
81
|
+
- CRLF vs LF does not count as an edit.
|
|
82
|
+
|
|
83
|
+
## How it works
|
|
84
|
+
|
|
85
|
+
The CLI follows the same pattern as `@averildwi/nest-common` and `@averildwi/nest-prisma`:
|
|
86
|
+
a list of steps (`src/steps/*`), each with `plan(ctx)` (reads only) and `run(ctx, data)`.
|
|
87
|
+
The full plan is printed first, then applied after confirmation (or `--yes`).
|
|
88
|
+
|
|
89
|
+
See [docs/USAGE.md](docs/USAGE.md) for details and edge cases.
|
|
90
|
+
|
|
91
|
+
## Development
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install
|
|
95
|
+
npm test # node:test, unit + integration (spawns the real CLI in temp folders)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { main } = require('../src/main');
|
|
4
|
+
const { CliError, CancelledError } = require('../src/errors');
|
|
5
|
+
const ui = require('../src/ui/output');
|
|
6
|
+
|
|
7
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
8
|
+
if (err instanceof CancelledError) {
|
|
9
|
+
ui.out();
|
|
10
|
+
ui.warn(err.message);
|
|
11
|
+
process.exit(err.exitCode);
|
|
12
|
+
}
|
|
13
|
+
if (err instanceof CliError) {
|
|
14
|
+
ui.error(err.failedStep ? `${err.failedStep} failed: ${err.message}` : err.message);
|
|
15
|
+
for (const h of err.hints) ui.hint(h);
|
|
16
|
+
process.stderr.write('\n');
|
|
17
|
+
process.exit(err.exitCode);
|
|
18
|
+
}
|
|
19
|
+
ui.error('Unexpected error. This is likely a bug in nest-skills.');
|
|
20
|
+
process.stderr.write(`${err && err.stack ? err.stack : err}\n\n`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
package/docs/USAGE.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Usage
|
|
2
|
+
|
|
3
|
+
## 1. Install
|
|
4
|
+
|
|
5
|
+
Run from the root of a NestJS project (a `package.json` that lists `@nestjs/core`).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @averildwi/nest-skills # asks which agent
|
|
9
|
+
npx @averildwi/nest-skills --agent claude # no question
|
|
10
|
+
npx @averildwi/nest-skills --agent both --yes
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Without a terminal (CI, scripts) or with `--yes`, `--agent` is required on the first install.
|
|
14
|
+
Later runs read the agent and folder from `.nest-skills.json`.
|
|
15
|
+
|
|
16
|
+
### Which agent?
|
|
17
|
+
|
|
18
|
+
| Choose | When |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `opencode` | Only OpenCode is used. Skills go to `.opencode/skills`. |
|
|
21
|
+
| `claude` | Only Claude Code is used. Skills go to `.claude/skills`, plus a `CLAUDE.md` that imports `AGENTS.md`. |
|
|
22
|
+
| `both` | The team uses both. One copy in `.claude/skills`, which OpenCode also reads. Installing into two folders would make OpenCode see every skill twice, and skill names must be unique. |
|
|
23
|
+
|
|
24
|
+
Sources: OpenCode [skills](https://opencode.ai/docs/skills/) and [rules](https://opencode.ai/docs/rules/),
|
|
25
|
+
Claude Code [skills](https://code.claude.com/docs/en/skills) and [memory](https://code.claude.com/docs/en/memory).
|
|
26
|
+
Claude Code reads `AGENTS.md` by itself only when there is no `CLAUDE.md` and only on newer
|
|
27
|
+
versions. The `@AGENTS.md` import in `CLAUDE.md` works in every case.
|
|
28
|
+
|
|
29
|
+
### Custom folder
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @averildwi/nest-skills --agent opencode --dir .agents/skills
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`AGENTS.md` is rendered with the folder you choose, so the agent is told the real path.
|
|
36
|
+
The folder must be relative and inside the project.
|
|
37
|
+
|
|
38
|
+
## 2. Commit
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git switch -c chore/agent-skills
|
|
42
|
+
git add AGENTS.md .opencode/skills .nest-skills.json # adjust to the printed list
|
|
43
|
+
git commit -m "chore: add agent skills"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Commit generated files on their own, before you edit anything (`git-workflow` rule 6).
|
|
47
|
+
|
|
48
|
+
## 3. Update to a newer version
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx @averildwi/nest-skills@latest update --dry-run # see what would change
|
|
52
|
+
npx @averildwi/nest-skills@latest update
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| File state | `install` | `update` | with `--force` |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| missing | created | created | created |
|
|
58
|
+
| same as bundled | skipped | skipped | skipped |
|
|
59
|
+
| written by us, unedited, older | reported ("run update") | **refreshed** | refreshed |
|
|
60
|
+
| written by us, **edited by you** | kept | kept | replaced, your copy backed up |
|
|
61
|
+
| **not written by us**: a skill file | kept | kept | replaced, your copy backed up |
|
|
62
|
+
| **not written by us**: `AGENTS.md` / `CLAUDE.md` | kept | kept | **kept** |
|
|
63
|
+
|
|
64
|
+
Backups go to `.nest-skills-backup/<timestamp>/<original path>`, never next to the skill
|
|
65
|
+
(a stray `SKILL.md.bak` inside the skills folder would be committed with `git add <skillsDir>`).
|
|
66
|
+
The backup folder contains a `.gitignore` with `*`, so git ignores it without any change to
|
|
67
|
+
your own `.gitignore`. Delete it when you no longer need the copies.
|
|
68
|
+
|
|
69
|
+
"Written by us" means the path and its hash are in `.nest-skills.json`. If you delete that
|
|
70
|
+
file, every installed file counts as yours and will not be updated.
|
|
71
|
+
|
|
72
|
+
## 4. Switching agent
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx @averildwi/nest-skills --agent claude
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`AGENTS.md` (if unedited) is rewritten to point at the new folder and `CLAUDE.md` is added.
|
|
79
|
+
The old skills folder is **not deleted**; the CLI prints the `git rm -r` command to run.
|
|
80
|
+
|
|
81
|
+
## 5. When you already have AGENTS.md or CLAUDE.md
|
|
82
|
+
|
|
83
|
+
- Own `AGENTS.md`: kept. The index is written to `AGENTS.nest-skills.md`. Add a line to your
|
|
84
|
+
`AGENTS.md` such as `Read AGENTS.nest-skills.md before writing NestJS code and follow it.`
|
|
85
|
+
- Own `CLAUDE.md`: kept. Add `@AGENTS.md` (or `@AGENTS.nest-skills.md`) as its first line.
|
|
86
|
+
If it already has that line, nothing is reported.
|
|
87
|
+
|
|
88
|
+
## 6. Existing scaffold code that differs from the skills
|
|
89
|
+
|
|
90
|
+
The skills describe the target standard. Code generated by older `nest-common`/`nest-prisma`
|
|
91
|
+
versions may differ (optional `page?` in `PaginationDto`, `string[]` roles, `any` in
|
|
92
|
+
interceptors, CORS `'*'`, no Jest `moduleNameMapper`). The section
|
|
93
|
+
"When the existing code differs from these skills" in `AGENTS.md` tells the agent to adapt
|
|
94
|
+
at the call site, not rewrite shared code silently, and to propose a separate commit.
|
|
95
|
+
|
|
96
|
+
## Exit codes
|
|
97
|
+
|
|
98
|
+
| Code | Meaning |
|
|
99
|
+
|---|---|
|
|
100
|
+
| 0 | done, or nothing to do |
|
|
101
|
+
| 1 | not a NestJS project, invalid `package.json` / `.nest-skills.json`, write error |
|
|
102
|
+
| 2 | bad arguments, `--agent` missing in non-interactive mode, `update` without an install |
|
|
103
|
+
| 130 | cancelled |
|
|
104
|
+
|
|
105
|
+
## Not included (ideas)
|
|
106
|
+
|
|
107
|
+
- `--with-commitlint`: decision N5 is "not yet", and it would have to edit `package.json`,
|
|
108
|
+
which this tool never does. Left out on purpose.
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@averildwi/nest-skills",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Install Everill's NestJS + Prisma coding standards as agent skills (OpenCode / Claude Code) plus an AGENTS.md index.",
|
|
5
|
+
"main": "bin/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nest-skills": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"templates",
|
|
13
|
+
"docs"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test test/unit.test.js test/integration.test.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"nestjs",
|
|
20
|
+
"prisma",
|
|
21
|
+
"agent-skills",
|
|
22
|
+
"agents-md",
|
|
23
|
+
"opencode",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"cli"
|
|
26
|
+
],
|
|
27
|
+
"author": "averildwi",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/averildwi/nest-skills.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/averildwi/nest-skills/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/averildwi/nest-skills#readme",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.17"
|
|
42
|
+
},
|
|
43
|
+
"type": "commonjs",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"inquirer": "^9.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const { parseArgs } = require('util');
|
|
2
|
+
|
|
3
|
+
const { CliError } = require('./errors');
|
|
4
|
+
const { AGENTS, BACKUP_DIR } = require('./config/agents');
|
|
5
|
+
|
|
6
|
+
const COMMANDS = ['install', 'update'];
|
|
7
|
+
|
|
8
|
+
const OPTIONS = {
|
|
9
|
+
help: { type: 'boolean', short: 'h' },
|
|
10
|
+
version: { type: 'boolean', short: 'v' },
|
|
11
|
+
yes: { type: 'boolean', short: 'y' },
|
|
12
|
+
'dry-run': { type: 'boolean' },
|
|
13
|
+
force: { type: 'boolean' },
|
|
14
|
+
agent: { type: 'string', short: 'a' },
|
|
15
|
+
dir: { type: 'string' },
|
|
16
|
+
color: { type: 'boolean' },
|
|
17
|
+
'no-color': { type: 'boolean' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parses argv into a normalised options object. Unspecified options stay
|
|
22
|
+
* `undefined` so the caller can prompt or fall back to the manifest.
|
|
23
|
+
*/
|
|
24
|
+
function parseCliArgs(argv = process.argv.slice(2)) {
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
28
|
+
} catch (err) {
|
|
29
|
+
throw new CliError(err.message.replace(/\. To specify.*$/s, '.'), {
|
|
30
|
+
hints: ['Run "nest-skills --help" to see all available options.'],
|
|
31
|
+
exitCode: 2,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const v = parsed.values;
|
|
36
|
+
const [command = 'install', ...extra] = parsed.positionals;
|
|
37
|
+
if (!COMMANDS.includes(command)) {
|
|
38
|
+
throw new CliError(`Unknown command "${command}".`, {
|
|
39
|
+
hints: [`Supported commands: ${COMMANDS.join(', ')}.`],
|
|
40
|
+
exitCode: 2,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (extra.length) {
|
|
44
|
+
throw new CliError(`Unexpected argument "${extra[0]}".`, { exitCode: 2 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let agent;
|
|
48
|
+
if (v.agent !== undefined) {
|
|
49
|
+
agent = v.agent.toLowerCase();
|
|
50
|
+
if (!AGENTS[agent]) {
|
|
51
|
+
throw new CliError(`Unknown agent "${v.agent}".`, {
|
|
52
|
+
hints: [`Supported values: ${Object.keys(AGENTS).join(', ')}.`],
|
|
53
|
+
exitCode: 2,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let dir;
|
|
59
|
+
if (v.dir !== undefined) {
|
|
60
|
+
dir = v.dir.trim().replace(/\\/g, '/').replace(/\/{2,}/g, '/').replace(/^(\.\/)+/, '').replace(/\/+$/, '');
|
|
61
|
+
const segments = dir.split('/');
|
|
62
|
+
// "." would put the skill folders straight into the project root, next to src/.
|
|
63
|
+
const unsafe =
|
|
64
|
+
!dir ||
|
|
65
|
+
dir.startsWith('/') ||
|
|
66
|
+
/^[a-z]:/i.test(dir) ||
|
|
67
|
+
segments.some((s) => s === '.' || s === '..') ||
|
|
68
|
+
segments[0] === BACKUP_DIR ||
|
|
69
|
+
segments[0] === 'node_modules';
|
|
70
|
+
if (unsafe) {
|
|
71
|
+
throw new CliError(`Invalid --dir "${v.dir}".`, {
|
|
72
|
+
hints: ['Use a sub-folder of the project root, e.g. --dir .agents/skills'],
|
|
73
|
+
exitCode: 2,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
command,
|
|
80
|
+
help: Boolean(v.help),
|
|
81
|
+
version: Boolean(v.version),
|
|
82
|
+
yes: Boolean(v.yes),
|
|
83
|
+
dryRun: Boolean(v['dry-run']),
|
|
84
|
+
force: Boolean(v.force),
|
|
85
|
+
agent,
|
|
86
|
+
dir,
|
|
87
|
+
color: v['no-color'] ? false : v.color ? true : undefined,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function helpText(pkg) {
|
|
92
|
+
return `
|
|
93
|
+
${pkg.name} v${pkg.version}
|
|
94
|
+
Install the NestJS + Prisma coding standards as agent skills and an AGENTS.md index.
|
|
95
|
+
|
|
96
|
+
Usage
|
|
97
|
+
$ npx ${pkg.name} [install] [options]
|
|
98
|
+
$ npx ${pkg.name} update [options]
|
|
99
|
+
|
|
100
|
+
Commands
|
|
101
|
+
install Add missing skills and the index (default). Never overwrites your edits.
|
|
102
|
+
update Also refresh files you have not edited to the bundled version.
|
|
103
|
+
|
|
104
|
+
Options
|
|
105
|
+
-a, --agent <name> opencode | claude | both (update: default from .nest-skills.json)
|
|
106
|
+
opencode -> .opencode/skills + AGENTS.md
|
|
107
|
+
claude -> .claude/skills + AGENTS.md + CLAUDE.md (imports AGENTS.md)
|
|
108
|
+
both -> .claude/skills (read by both tools) + AGENTS.md + CLAUDE.md
|
|
109
|
+
--dir <path> Install skills somewhere else (relative to the project root)
|
|
110
|
+
--force Overwrite files you edited or wrote yourself (old copy kept in .nest-skills-backup/)
|
|
111
|
+
-y, --yes Skip prompts and the confirmation
|
|
112
|
+
--dry-run Show the plan without writing anything
|
|
113
|
+
--no-color Disable coloured output
|
|
114
|
+
-v, --version Print the version
|
|
115
|
+
-h, --help Show this help
|
|
116
|
+
|
|
117
|
+
Examples
|
|
118
|
+
$ npx ${pkg.name} --agent opencode
|
|
119
|
+
$ npx ${pkg.name} --agent both --yes
|
|
120
|
+
$ npx ${pkg.name} update --dry-run
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { parseCliArgs, helpText, COMMANDS };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each agent reads skills and project instructions.
|
|
3
|
+
*
|
|
4
|
+
* Sources (checked 2026-10-13):
|
|
5
|
+
* - OpenCode, https://opencode.ai/docs/skills/ : project skills in
|
|
6
|
+
* `.opencode/skills/<name>/SKILL.md`, `.claude/skills/<name>/SKILL.md` or
|
|
7
|
+
* `.agents/skills/<name>/SKILL.md`; names must be unique across locations.
|
|
8
|
+
* - OpenCode, https://opencode.ai/docs/rules/ : project rules in `AGENTS.md`
|
|
9
|
+
* at the project root; `CLAUDE.md` is only a fallback when AGENTS.md is absent.
|
|
10
|
+
* - Claude Code, https://code.claude.com/docs/en/skills : project skills in
|
|
11
|
+
* `.claude/skills/<name>/SKILL.md`.
|
|
12
|
+
* - Claude Code, https://code.claude.com/docs/en/memory : reads `AGENTS.md`
|
|
13
|
+
* only when there is no `CLAUDE.md` (and only from v2.1.277); a `CLAUDE.md`
|
|
14
|
+
* that contains `@AGENTS.md` works in every version and setting.
|
|
15
|
+
*
|
|
16
|
+
* "both" installs one copy in `.claude/skills`, which both tools read, so the
|
|
17
|
+
* same skill name never appears twice for OpenCode.
|
|
18
|
+
*/
|
|
19
|
+
const AGENTS = {
|
|
20
|
+
opencode: {
|
|
21
|
+
value: 'opencode',
|
|
22
|
+
label: 'OpenCode',
|
|
23
|
+
skillsDir: '.opencode/skills',
|
|
24
|
+
claudeMd: false,
|
|
25
|
+
},
|
|
26
|
+
claude: {
|
|
27
|
+
value: 'claude',
|
|
28
|
+
label: 'Claude Code',
|
|
29
|
+
skillsDir: '.claude/skills',
|
|
30
|
+
claudeMd: true,
|
|
31
|
+
},
|
|
32
|
+
both: {
|
|
33
|
+
value: 'both',
|
|
34
|
+
label: 'OpenCode + Claude Code',
|
|
35
|
+
skillsDir: '.claude/skills',
|
|
36
|
+
claudeMd: true,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Fallback index name when the project already has its own AGENTS.md. */
|
|
41
|
+
const FALLBACK_INDEX = 'AGENTS.nest-skills.md';
|
|
42
|
+
const MANIFEST_FILE = '.nest-skills.json';
|
|
43
|
+
/**
|
|
44
|
+
* --force keeps the replaced file here, outside the skills folder, so the
|
|
45
|
+
* printed `git add <skillsDir>` never stages a backup and OpenCode never sees one.
|
|
46
|
+
*/
|
|
47
|
+
const BACKUP_DIR = '.nest-skills-backup';
|
|
48
|
+
|
|
49
|
+
module.exports = { AGENTS, FALLBACK_INDEX, MANIFEST_FILE, BACKUP_DIR };
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An expected, user-facing failure. Rendered by the entrypoint as a clean
|
|
3
|
+
* message plus hints instead of a stack trace.
|
|
4
|
+
*/
|
|
5
|
+
class CliError extends Error {
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} message
|
|
8
|
+
* @param {{ hints?: string[], exitCode?: number, cause?: unknown }} [options]
|
|
9
|
+
*/
|
|
10
|
+
constructor(message, { hints = [], exitCode = 1, cause } = {}) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'CliError';
|
|
13
|
+
this.hints = hints;
|
|
14
|
+
this.exitCode = exitCode;
|
|
15
|
+
if (cause) this.cause = cause;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Raised when the user aborts (Ctrl+C or answering "no" to the confirmation). */
|
|
20
|
+
class CancelledError extends CliError {
|
|
21
|
+
constructor(message = 'Cancelled. No changes were made.') {
|
|
22
|
+
super(message, { exitCode: 130 });
|
|
23
|
+
this.name = 'CancelledError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { CliError, CancelledError };
|
package/src/main.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const pkg = require('../package.json');
|
|
2
|
+
const { parseCliArgs, helpText } = require('./cli');
|
|
3
|
+
const { setColorEnabled } = require('./ui/colors');
|
|
4
|
+
const ui = require('./ui/output');
|
|
5
|
+
const { runPreflight } = require('./preflight');
|
|
6
|
+
const { resolveAgent, confirmPlan } = require('./prompts');
|
|
7
|
+
const { readManifest } = require('./utils/manifest');
|
|
8
|
+
const runner = require('./runner');
|
|
9
|
+
|
|
10
|
+
async function main(argv, { cwd = process.cwd(), isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY) } = {}) {
|
|
11
|
+
const options = parseCliArgs(argv);
|
|
12
|
+
if (options.color !== undefined) setColorEnabled(options.color);
|
|
13
|
+
|
|
14
|
+
if (options.help) {
|
|
15
|
+
process.stdout.write(helpText(pkg));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (options.version) {
|
|
19
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const interactive = isTTY && !options.yes;
|
|
24
|
+
ui.header('nest-skills', `v${pkg.version}`);
|
|
25
|
+
|
|
26
|
+
runPreflight(cwd);
|
|
27
|
+
const manifest = readManifest(cwd);
|
|
28
|
+
const agent = await resolveAgent(options, { interactive, manifest });
|
|
29
|
+
|
|
30
|
+
const ctx = runner.buildContext({ projectRoot: cwd, pkg, options, agent, manifest });
|
|
31
|
+
const plan = await runner.buildPlan(ctx);
|
|
32
|
+
runner.printPlan(ctx, plan, { dryRun: options.dryRun });
|
|
33
|
+
|
|
34
|
+
if (!plan.some((p) => p.actionable)) {
|
|
35
|
+
ui.out();
|
|
36
|
+
ui.success('Everything is already installed. Nothing to do.');
|
|
37
|
+
runner.printSummary(ctx, plan, { files: [] });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (options.dryRun) {
|
|
41
|
+
ui.out();
|
|
42
|
+
ui.info('Dry run complete. Re-run without --dry-run to apply these changes.');
|
|
43
|
+
ui.out();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ui.out();
|
|
48
|
+
if (interactive) await confirmPlan();
|
|
49
|
+
|
|
50
|
+
const summary = await runner.executePlan(ctx, plan);
|
|
51
|
+
runner.printSummary(ctx, plan, summary);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { main };
|
package/src/preflight.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const { CliError } = require('./errors');
|
|
5
|
+
|
|
6
|
+
function readPackageJson(projectRoot) {
|
|
7
|
+
const file = path.join(projectRoot, 'package.json');
|
|
8
|
+
if (!fs.existsSync(file)) {
|
|
9
|
+
throw new CliError('No package.json found in the current directory.', {
|
|
10
|
+
hints: [`Current directory: ${projectRoot}`, 'Run this command from the root of your NestJS project.'],
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
15
|
+
} catch (err) {
|
|
16
|
+
throw new CliError('package.json is not valid JSON.', { hints: [err.message], cause: err });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Validates that the current directory is a NestJS project. Reads only package.json. */
|
|
21
|
+
function runPreflight(projectRoot) {
|
|
22
|
+
const pkgJson = readPackageJson(projectRoot);
|
|
23
|
+
const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
24
|
+
if (!deps['@nestjs/core']) {
|
|
25
|
+
throw new CliError("This doesn't look like a NestJS project.", {
|
|
26
|
+
hints: [
|
|
27
|
+
'package.json does not list @nestjs/core.',
|
|
28
|
+
'Create one first with: npx @nestjs/cli new my-app',
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return { pkgJson };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { runPreflight, readPackageJson };
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { CancelledError, CliError } = require('./errors');
|
|
2
|
+
const { AGENTS } = require('./config/agents');
|
|
3
|
+
|
|
4
|
+
async function prompt(questions) {
|
|
5
|
+
// inquirer 9 is ESM-only, so it has to be loaded with a dynamic import.
|
|
6
|
+
const { default: inquirer } = await import('inquirer');
|
|
7
|
+
try {
|
|
8
|
+
return await inquirer.prompt(questions);
|
|
9
|
+
} catch (err) {
|
|
10
|
+
if (err && (err.name === 'ExitPromptError' || /force closed/i.test(err.message || ''))) {
|
|
11
|
+
throw new CancelledError();
|
|
12
|
+
}
|
|
13
|
+
throw err;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Decides the agent target. Priority: --agent, then the manifest of a previous
|
|
19
|
+
* install, then a prompt. Without a TTY (or with --yes) there is no prompt.
|
|
20
|
+
*/
|
|
21
|
+
async function resolveAgent(options, { interactive, manifest }) {
|
|
22
|
+
if (options.agent) return options.agent;
|
|
23
|
+
if (manifest && AGENTS[manifest.agent]) return manifest.agent;
|
|
24
|
+
|
|
25
|
+
if (options.command === 'update') {
|
|
26
|
+
throw new CliError('No previous install found (.nest-skills.json is missing).', {
|
|
27
|
+
hints: ['Run "npx @averildwi/nest-skills --agent <opencode|claude|both>" first.'],
|
|
28
|
+
exitCode: 2,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (!interactive) {
|
|
32
|
+
throw new CliError('Choose an agent with --agent when running non-interactively.', {
|
|
33
|
+
hints: [
|
|
34
|
+
`Supported values: ${Object.keys(AGENTS).join(', ')}.`,
|
|
35
|
+
'Example: npx @averildwi/nest-skills --agent opencode --yes',
|
|
36
|
+
],
|
|
37
|
+
exitCode: 2,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { agent } = await prompt([
|
|
42
|
+
{
|
|
43
|
+
type: 'list',
|
|
44
|
+
name: 'agent',
|
|
45
|
+
message: 'Which AI agent should read these skills?',
|
|
46
|
+
choices: Object.values(AGENTS).map((a) => ({ name: `${a.label} (${a.skillsDir})`, value: a.value })),
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
return agent;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function confirmPlan() {
|
|
53
|
+
const { proceed } = await prompt([
|
|
54
|
+
{ type: 'confirm', name: 'proceed', message: 'Apply these changes?', default: true },
|
|
55
|
+
]);
|
|
56
|
+
if (!proceed) throw new CancelledError();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { resolveAgent, confirmPlan };
|