spovishun-skills 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/.claude-plugin/.gitkeep +0 -0
- package/CHANGELOG.md +42 -0
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/adapters/.gitkeep +0 -0
- package/adapters/claude/index.js +123 -0
- package/adapters/claude/update.js +41 -0
- package/adapters/codex/build-agents-md.js +135 -0
- package/adapters/codex/index.js +109 -0
- package/adapters/windsurf/index.js +126 -0
- package/adapters/windsurf/update.js +46 -0
- package/agents/.gitkeep +0 -0
- package/agents/code-architecture-reviewer/AGENT.md +87 -0
- package/agents/code-architecture-reviewer/manifest.yaml +14 -0
- package/agents/code-refactor-master/AGENT.md +71 -0
- package/agents/code-refactor-master/manifest.yaml +17 -0
- package/agents/database-reviewer/AGENT.md +70 -0
- package/agents/database-reviewer/manifest.yaml +15 -0
- package/agents/doc-updater/AGENT.md +81 -0
- package/agents/doc-updater/manifest.yaml +26 -0
- package/agents/documentation-architect/AGENT.md +63 -0
- package/agents/documentation-architect/manifest.yaml +15 -0
- package/agents/kotlin-reviewer/AGENT.md +91 -0
- package/agents/kotlin-reviewer/manifest.yaml +15 -0
- package/agents/plan-reviewer/AGENT.md +58 -0
- package/agents/plan-reviewer/manifest.yaml +15 -0
- package/agents/refactor-planner/AGENT.md +77 -0
- package/agents/refactor-planner/manifest.yaml +15 -0
- package/agents/web-research-specialist/AGENT.md +70 -0
- package/agents/web-research-specialist/manifest.yaml +17 -0
- package/bin/doctor.js +422 -0
- package/bin/init.js +133 -0
- package/bin/install.js +66 -0
- package/bin/spovishun-skills.js +167 -0
- package/bin/sync.js +47 -0
- package/bin/update.js +232 -0
- package/hooks/.gitkeep +0 -0
- package/hooks/capture-learning.js +92 -0
- package/hooks/hooks.json +74 -0
- package/hooks/notion-task-inject.js +701 -0
- package/hooks/precompact-backup.js +26 -0
- package/hooks/session-end.js +67 -0
- package/hooks/session-start.js +42 -0
- package/lib/ajv.js +9 -0
- package/lib/artifact-loader.js +135 -0
- package/lib/checksum.js +10 -0
- package/lib/config-loader.js +37 -0
- package/lib/config-validator.js +77 -0
- package/lib/errors.js +13 -0
- package/lib/installed-files-loader.js +109 -0
- package/lib/lockfile.js +53 -0
- package/lib/manifest-validator.js +44 -0
- package/lib/notion-bootstrap.js +99 -0
- package/lib/notion-client.js +41 -0
- package/lib/notion-fallback-prompt.js +81 -0
- package/lib/placeholder-map.js +59 -0
- package/lib/settings-merger.js +61 -0
- package/lib/stack-filter.js +18 -0
- package/lib/template-renderer.js +69 -0
- package/lib/three-way-merge.js +33 -0
- package/lib/update-classifier.js +46 -0
- package/package.json +62 -0
- package/rules/.gitkeep +0 -0
- package/rules/common/design-principles.md +49 -0
- package/rules/common/feature-documentation.md +44 -0
- package/rules/common/git-workflow.md +30 -0
- package/rules/common/security.md +24 -0
- package/rules/common/testing.md +33 -0
- package/rules/kotlin/kotlin-style.md +48 -0
- package/schema/config.schema.json +85 -0
- package/schema/manifest.schema.json +91 -0
- package/scripts/validate-all-manifests.js +40 -0
- package/skills/architecture-designer/SKILL.md +112 -0
- package/skills/architecture-designer/manifest.yaml +17 -0
- package/skills/changelog-generator/SKILL.md +95 -0
- package/skills/changelog-generator/manifest.yaml +14 -0
- package/skills/ci-cd-pipeline-builder/SKILL.md +117 -0
- package/skills/ci-cd-pipeline-builder/manifest.yaml +18 -0
- package/skills/code-reviewer/SKILL.md +79 -0
- package/skills/code-reviewer/manifest.yaml +15 -0
- package/skills/commit/SKILL.md +43 -0
- package/skills/commit/manifest.yaml +15 -0
- package/skills/database-optimizer/SKILL.md +107 -0
- package/skills/database-optimizer/manifest.yaml +18 -0
- package/skills/debugging-wizard/SKILL.md +97 -0
- package/skills/debugging-wizard/manifest.yaml +16 -0
- package/skills/dependency-injection-architecture/SKILL.md +90 -0
- package/skills/dependency-injection-architecture/manifest.yaml +18 -0
- package/skills/diagram-design/SKILL.md +356 -0
- package/skills/diagram-design/manifest.yaml +19 -0
- package/skills/discover-patterns/SKILL.md +38 -0
- package/skills/discover-patterns/manifest.yaml +12 -0
- package/skills/docker-deployment/SKILL.md +150 -0
- package/skills/docker-deployment/manifest.yaml +28 -0
- package/skills/git-workflow-pr-writing/SKILL.md +71 -0
- package/skills/git-workflow-pr-writing/manifest.yaml +14 -0
- package/skills/grill-me/SKILL.md +20 -0
- package/skills/grill-me/manifest.yaml +15 -0
- package/skills/idea-brainstormer/SKILL.md +113 -0
- package/skills/idea-brainstormer/manifest.yaml +21 -0
- package/skills/kotlin-specialist/SKILL.md +53 -0
- package/skills/kotlin-specialist/manifest.yaml +21 -0
- package/skills/newepic/SKILL.md +105 -0
- package/skills/newepic/manifest.yaml +20 -0
- package/skills/newtask/SKILL.md +170 -0
- package/skills/newtask/manifest.yaml +27 -0
- package/skills/notion-content-reader/SKILL.md +59 -0
- package/skills/notion-content-reader/manifest.yaml +17 -0
- package/skills/notion-data-migrator/SKILL.md +55 -0
- package/skills/notion-data-migrator/manifest.yaml +17 -0
- package/skills/notion-database-manager/SKILL.md +71 -0
- package/skills/notion-database-manager/manifest.yaml +18 -0
- package/skills/notion-navigator/SKILL.md +66 -0
- package/skills/notion-navigator/manifest.yaml +38 -0
- package/skills/notion-page-builder/SKILL.md +47 -0
- package/skills/notion-page-builder/manifest.yaml +17 -0
- package/skills/notion-spovishun-task-manager/SKILL.md +136 -0
- package/skills/notion-spovishun-task-manager/manifest.yaml +28 -0
- package/skills/notion-task-board-manager/SKILL.md +80 -0
- package/skills/notion-task-board-manager/manifest.yaml +19 -0
- package/skills/notion-task-to-code/SKILL.md +87 -0
- package/skills/notion-task-to-code/manifest.yaml +23 -0
- package/skills/notion-workflow-spovishun/SKILL.md +71 -0
- package/skills/notion-workflow-spovishun/manifest.yaml +13 -0
- package/skills/notion-workspace-organizer/SKILL.md +58 -0
- package/skills/notion-workspace-organizer/manifest.yaml +17 -0
- package/skills/postgresql-exposed-orm/SKILL.md +57 -0
- package/skills/postgresql-exposed-orm/manifest.yaml +20 -0
- package/skills/reflect/SKILL.md +94 -0
- package/skills/reflect/manifest.yaml +14 -0
- package/skills/skill-security-auditor/SKILL.md +109 -0
- package/skills/skill-security-auditor/manifest.yaml +14 -0
- package/skills/solution-designer/SKILL.md +143 -0
- package/skills/solution-designer/manifest.yaml +21 -0
- package/skills/task-decomposer/SKILL.md +169 -0
- package/skills/task-decomposer/manifest.yaml +28 -0
- package/skills/technical-documentation-writer/SKILL.md +97 -0
- package/skills/technical-documentation-writer/manifest.yaml +15 -0
- package/skills/telegram-bot-development/SKILL.md +62 -0
- package/skills/telegram-bot-development/manifest.yaml +20 -0
- package/skills/unit-testing-kotlin/SKILL.md +74 -0
- package/skills/unit-testing-kotlin/manifest.yaml +18 -0
- package/skills/update-doc-full/SKILL.md +199 -0
- package/skills/update-doc-full/manifest.yaml +14 -0
- package/skills/write-a-skill/SKILL.md +165 -0
- package/skills/write-a-skill/manifest.yaml +20 -0
|
File without changes
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] — 2026-05-25
|
|
9
|
+
|
|
10
|
+
First public release on npm.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **CLI commands**
|
|
15
|
+
- `validate <skill-dir>` — validates a skill's `manifest.yaml` against the JSON Schema
|
|
16
|
+
- `init` — interactive wizard that generates `spovishun-skills.config.yaml`
|
|
17
|
+
- `install --target=<claude|codex|windsurf>` — installs filtered artefacts and writes the lockfile
|
|
18
|
+
- `sync` — re-applies the install from existing config + lockfile (no wizard)
|
|
19
|
+
- `update --upstream=<dir>` — three-way merge against an upstream copy (`--skill <id>`, `--dry-run`)
|
|
20
|
+
- `doctor` — validates installation integrity (config, lockfile, Notion ids, `.gitignore`, `settings.json`)
|
|
21
|
+
- **Target adapters**
|
|
22
|
+
- Claude Code (native plugin via `.claude-plugin/` + `.claude/`)
|
|
23
|
+
- Codex (single `AGENTS.md` ≤ 32 KiB)
|
|
24
|
+
- Windsurf (one file per artifact under `.windsurf/rules/`, auto-split at 6 000 chars)
|
|
25
|
+
- **Validation & schema**
|
|
26
|
+
- Manifest validation with Ajv 8 and JSON Schema 2020-12
|
|
27
|
+
- Consumer config schema (`schema/config.schema.json`)
|
|
28
|
+
- Conditional `requires:` rule (mandatory for `category: stack-specific`, forbidden for `universal`)
|
|
29
|
+
- **Placeholders** — Mustache `{{KEY}}` substitution with `UPPER_SNAKE_CASE` key validation; non-matching tokens (e.g. `${{ runner.os }}`) preserved verbatim
|
|
30
|
+
- **Stack filtering** — install only artifacts whose `requires:` flags are all enabled in the consumer config
|
|
31
|
+
- **Lockfile** (`spovishun-skills.lock.yaml`) — pinned versions and SHA-256 checksums for reproducible installs
|
|
32
|
+
|
|
33
|
+
### Bundled artifacts
|
|
34
|
+
|
|
35
|
+
- 37 skills · 9 agents · 6 hooks (Claude only) · 6 rules
|
|
36
|
+
|
|
37
|
+
### Known limitations
|
|
38
|
+
|
|
39
|
+
- **Cursor adapter** — planned for 1.1
|
|
40
|
+
- **`update --target=codex`** is a no-op. The monolithic `AGENTS.md` doesn't support per-artifact three-way merge; regenerate with `install --target=codex` instead.
|
|
41
|
+
|
|
42
|
+
[1.0.0]: https://github.com/Astrumon/spovishun-skills/releases/tag/v1.0.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Danylo Bidnyk
|
|
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
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# spovishun-skills
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/spovishun-skills)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
|
|
7
|
+
Portable [Claude Code](https://claude.com/claude-code) skills, agents, hooks and rules — extracted from the [Spovishun](https://github.com/Astrumon/SpovishunTelegramBotV2) project, packaged for reuse in any project.
|
|
8
|
+
|
|
9
|
+
One canonical source. Multiple AI assistants. Configurable per project.
|
|
10
|
+
|
|
11
|
+
## What you get
|
|
12
|
+
|
|
13
|
+
A single `npx` command installs a curated set of:
|
|
14
|
+
|
|
15
|
+
- **37 skills** — task decomposition, code review, Notion workflows, Kotlin/Postgres helpers, etc.
|
|
16
|
+
- **9 agents** — specialized reviewers (architecture, refactor, docs, …)
|
|
17
|
+
- **6 hooks** *(Claude only)* — session start/end, learning capture, Notion task injection
|
|
18
|
+
- **6 rules** — design principles, git workflow, security, testing, Kotlin style
|
|
19
|
+
|
|
20
|
+
Artifacts are filtered by your stack flags (kotlin / postgres / telegram / notion / docker) — you get only what's relevant to your project.
|
|
21
|
+
|
|
22
|
+
## Supported targets
|
|
23
|
+
|
|
24
|
+
| Target | Status | Notes |
|
|
25
|
+
|--------------|--------|--------------------------------------------------------------------|
|
|
26
|
+
| Claude Code | ✅ | Native plugin via `.claude-plugin/` + `.claude/` |
|
|
27
|
+
| Codex | ✅ | Single `AGENTS.md` at project root (≤ 32 KiB) |
|
|
28
|
+
| Windsurf | ✅ | One file per skill/rule under `.windsurf/rules/` (≤ 6 000 chars) |
|
|
29
|
+
| Cursor | 🚧 | Planned for 1.1 |
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# 1. Generate your project config (interactive wizard)
|
|
35
|
+
npx spovishun-skills@latest init
|
|
36
|
+
|
|
37
|
+
# 2. Install artifacts for your target AI assistant
|
|
38
|
+
npx spovishun-skills@latest install --target=claude
|
|
39
|
+
|
|
40
|
+
# 3. Verify installation integrity
|
|
41
|
+
npx spovishun-skills@latest doctor
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
After `install`, commit the generated `spovishun-skills.lock.yaml` to lock versions. Re-run `sync` on any machine to reproduce the exact same install.
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
`init` creates `spovishun-skills.config.yaml` in your project root:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
project:
|
|
52
|
+
name: "MyProject"
|
|
53
|
+
language: "uk" # uk | en
|
|
54
|
+
|
|
55
|
+
stack:
|
|
56
|
+
kotlin: true
|
|
57
|
+
postgres: false
|
|
58
|
+
telegram: true
|
|
59
|
+
notion: true
|
|
60
|
+
docker: false
|
|
61
|
+
|
|
62
|
+
git:
|
|
63
|
+
branch_prefix: "feature/myproject"
|
|
64
|
+
main_branch: "main"
|
|
65
|
+
dev_branch: "develop"
|
|
66
|
+
|
|
67
|
+
# Required only if stack.notion: true
|
|
68
|
+
notion:
|
|
69
|
+
token_env: "NOTION_TOKEN" # env var name, NOT the token value
|
|
70
|
+
database_id: "<32-hex-chars>"
|
|
71
|
+
epics_database_id: "<32-hex-chars>"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Schema is validated against [`schema/config.schema.json`](./schema/config.schema.json). Edit by hand or re-run `init` to overwrite.
|
|
75
|
+
|
|
76
|
+
## Commands
|
|
77
|
+
|
|
78
|
+
| Command | Purpose |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `init` | Interactive wizard → `spovishun-skills.config.yaml` |
|
|
81
|
+
| `install --target=<t>` | Generate target-specific files + write lockfile |
|
|
82
|
+
| `sync` | Re-apply install from existing config + lockfile (no wizard, no merge) |
|
|
83
|
+
| `update --upstream=<dir>` | Three-way merge against a fresh upstream copy of `spovishun-skills` |
|
|
84
|
+
| `doctor` | Validate installation integrity (config, lockfile, Notion ids, `.gitignore`, `settings.json`) |
|
|
85
|
+
| `validate <skill-dir>` | Validate a single skill's `manifest.yaml` against the JSON schema |
|
|
86
|
+
|
|
87
|
+
Run `npx spovishun-skills --help` for full options.
|
|
88
|
+
|
|
89
|
+
### `update` flags
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx spovishun-skills update --upstream=./node_modules/spovishun-skills
|
|
93
|
+
# [--skill <id>] limit to one artifact
|
|
94
|
+
# [--dry-run] print planned actions, write nothing
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
> **Codex limitation:** `update --target=codex` is a no-op (the monolithic `AGENTS.md` doesn't support per-artifact merge). Regenerate with `install --target=codex` instead.
|
|
98
|
+
|
|
99
|
+
## How stack filtering works
|
|
100
|
+
|
|
101
|
+
Each artifact's `manifest.yaml` declares which stack flags it needs:
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
id: postgresql-exposed-orm
|
|
105
|
+
category: stack-specific
|
|
106
|
+
requires:
|
|
107
|
+
- kotlin
|
|
108
|
+
- postgres
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
An artifact installs **iff all `requires:` flags are `true`** in your `spovishun-skills.config.yaml`. Universal artifacts (no `requires:`) install for everyone.
|
|
112
|
+
|
|
113
|
+
## Reproducible installs (lockfile)
|
|
114
|
+
|
|
115
|
+
After `install`, `spovishun-skills.lock.yaml` is written to your project root. It pins exact versions + checksums of every installed artifact. **Commit this file** — `sync` and `update` both rely on it.
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
version: 1
|
|
119
|
+
generated_at: "2026-05-25T17:00:00Z"
|
|
120
|
+
target: claude
|
|
121
|
+
artifacts:
|
|
122
|
+
- id: code-reviewer
|
|
123
|
+
type: skill
|
|
124
|
+
version: 1.2.0
|
|
125
|
+
checksum: "sha256:…"
|
|
126
|
+
- …
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Placeholders
|
|
130
|
+
|
|
131
|
+
Canonical artifact bodies use Mustache `{{KEY}}` placeholders for project-specific values (project name, language, Notion DB ids, etc.). Keys must be `UPPER_SNAKE_CASE`. Resolved at install time from your config.
|
|
132
|
+
|
|
133
|
+
Tokens that don't match the pattern (e.g. GitHub Actions `${{ runner.os }}`) are preserved verbatim.
|
|
134
|
+
|
|
135
|
+
## Requirements
|
|
136
|
+
|
|
137
|
+
- Node.js ≥ 18
|
|
138
|
+
- For Notion-tagged artifacts: a `NOTION_TOKEN` env var (or whatever name you set in `notion.token_env`)
|
|
139
|
+
|
|
140
|
+
## Project structure (after install into a Claude project)
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
your-project/
|
|
144
|
+
├── .claude-plugin/ ← native plugin entry
|
|
145
|
+
├── .claude/
|
|
146
|
+
│ ├── skills/
|
|
147
|
+
│ ├── agents/
|
|
148
|
+
│ ├── hooks/
|
|
149
|
+
│ ├── rules/
|
|
150
|
+
│ └── settings.json
|
|
151
|
+
├── spovishun-skills.config.yaml ← editable, commit it
|
|
152
|
+
└── spovishun-skills.lock.yaml ← generated, commit it
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
For Codex: a single `AGENTS.md` at project root. For Windsurf: `.windsurf/rules/*.md`.
|
|
156
|
+
|
|
157
|
+
## Contributing
|
|
158
|
+
|
|
159
|
+
PRs welcome. See [`CLAUDE.md`](./CLAUDE.md) for architecture, layer rules, manifest schema, and commit convention.
|
|
160
|
+
|
|
161
|
+
Branch: `feature/<short-slug>` · Commits: Conventional Commits (`feat:`, `fix:`, `refactor:`, …).
|
|
162
|
+
|
|
163
|
+
## Changelog
|
|
164
|
+
|
|
165
|
+
See [CHANGELOG.md](./CHANGELOG.md).
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
[MIT](./LICENSE) © Danylo Bidnyk
|
|
File without changes
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, copyFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { filterByStack } from '../../lib/stack-filter.js';
|
|
4
|
+
import { renderTemplate } from '../../lib/template-renderer.js';
|
|
5
|
+
import { buildPlaceholderMap } from '../../lib/placeholder-map.js';
|
|
6
|
+
import { sha256 } from '../../lib/checksum.js';
|
|
7
|
+
import { mergeSettings } from '../../lib/settings-merger.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Installs filtered artifacts into the consumer's .claude/ directory.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {string} opts.consumerCwd — absolute path to consumer project root
|
|
14
|
+
* @param {string} opts.pkgRoot — absolute path to this package's root (for hooks/ and rules/)
|
|
15
|
+
* @param {object} opts.config — validated consumer config object
|
|
16
|
+
* @param {Array} opts.artifacts — all loaded artifacts from loadArtifacts()
|
|
17
|
+
* @returns {Array<{kind, id, version, checksum}>} — lockfile entries for installed artifacts
|
|
18
|
+
*/
|
|
19
|
+
export async function installClaude({ consumerCwd, pkgRoot, config, artifacts }) {
|
|
20
|
+
const filtered = filterByStack(artifacts, config.stack ?? {});
|
|
21
|
+
const configMap = buildPlaceholderMap(config);
|
|
22
|
+
|
|
23
|
+
const claudeDir = join(consumerCwd, '.claude');
|
|
24
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
25
|
+
mkdirSync(join(claudeDir, 'skills'), { recursive: true });
|
|
26
|
+
mkdirSync(join(claudeDir, 'agents'), { recursive: true });
|
|
27
|
+
mkdirSync(join(claudeDir, 'hooks'), { recursive: true });
|
|
28
|
+
mkdirSync(join(claudeDir, 'rules'), { recursive: true });
|
|
29
|
+
|
|
30
|
+
const lockEntries = [];
|
|
31
|
+
|
|
32
|
+
for (const artifact of filtered) {
|
|
33
|
+
const manifestPlaceholders = (artifact.manifest?.placeholders ?? []).map((p) => p.key);
|
|
34
|
+
const rendered = renderTemplate(artifact.bodyText, { configMap, manifestPlaceholders });
|
|
35
|
+
const checksum = sha256(rendered);
|
|
36
|
+
|
|
37
|
+
if (artifact.kind === 'skill') {
|
|
38
|
+
const outPath = join(claudeDir, 'skills', `${artifact.id}.md`);
|
|
39
|
+
writeFileSync(outPath, rendered, 'utf8');
|
|
40
|
+
} else if (artifact.kind === 'agent') {
|
|
41
|
+
const outPath = join(claudeDir, 'agents', `${artifact.id}.md`);
|
|
42
|
+
writeFileSync(outPath, rendered, 'utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
lockEntries.push({ kind: artifact.kind, id: artifact.id, version: artifact.version, checksum });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Always ensure settings.json exists, even with no plugin hooks
|
|
49
|
+
patchSettings(claudeDir, {});
|
|
50
|
+
installHooks(pkgRoot, claudeDir);
|
|
51
|
+
installRules(pkgRoot, claudeDir);
|
|
52
|
+
|
|
53
|
+
return lockEntries;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Copies all hook scripts from hooks/ to .claude/hooks/ and merges hooks.json
|
|
58
|
+
* event mappings into .claude/settings.json.
|
|
59
|
+
*/
|
|
60
|
+
function installHooks(pkgRoot, claudeDir) {
|
|
61
|
+
const hooksDir = join(pkgRoot, 'hooks');
|
|
62
|
+
if (!existsSync(hooksDir)) return;
|
|
63
|
+
|
|
64
|
+
const hooksJsonPath = join(hooksDir, 'hooks.json');
|
|
65
|
+
if (!existsSync(hooksJsonPath)) return;
|
|
66
|
+
|
|
67
|
+
let hooksJson;
|
|
68
|
+
try {
|
|
69
|
+
hooksJson = JSON.parse(readFileSync(hooksJsonPath, 'utf8'));
|
|
70
|
+
} catch {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Copy all .js scripts verbatim
|
|
75
|
+
const scripts = readdirSync(hooksDir).filter((f) => f.endsWith('.js'));
|
|
76
|
+
for (const script of scripts) {
|
|
77
|
+
copyFileSync(join(hooksDir, script), join(claudeDir, 'hooks', script));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Merge event entries from hooks.json into settings.json
|
|
81
|
+
const pluginHooks = hooksJson.hooks ?? {};
|
|
82
|
+
patchSettings(claudeDir, pluginHooks);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Copies all .md rule files from rules/ into .claude/rules/, preserving subdirectory structure.
|
|
87
|
+
*/
|
|
88
|
+
function installRules(pkgRoot, claudeDir) {
|
|
89
|
+
const rulesDir = join(pkgRoot, 'rules');
|
|
90
|
+
if (!existsSync(rulesDir)) return;
|
|
91
|
+
|
|
92
|
+
copyRulesRecursive(rulesDir, rulesDir, claudeDir);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function copyRulesRecursive(baseDir, currentDir, claudeDir) {
|
|
96
|
+
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
97
|
+
if (entry.name.startsWith('.')) continue;
|
|
98
|
+
const srcPath = join(currentDir, entry.name);
|
|
99
|
+
if (entry.isDirectory()) {
|
|
100
|
+
copyRulesRecursive(baseDir, srcPath, claudeDir);
|
|
101
|
+
} else if (entry.name.endsWith('.md')) {
|
|
102
|
+
const rel = relative(baseDir, srcPath);
|
|
103
|
+
const destPath = join(claudeDir, 'rules', rel);
|
|
104
|
+
mkdirSync(join(destPath, '..'), { recursive: true });
|
|
105
|
+
copyFileSync(srcPath, destPath);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function patchSettings(claudeDir, pluginHooks) {
|
|
111
|
+
const settingsPath = join(claudeDir, 'settings.json');
|
|
112
|
+
let existing = {};
|
|
113
|
+
if (existsSync(settingsPath)) {
|
|
114
|
+
try {
|
|
115
|
+
existing = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
116
|
+
} catch {
|
|
117
|
+
existing = {};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const merged = mergeSettings(existing, pluginHooks);
|
|
122
|
+
writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
123
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { threeWayMerge } from '../../lib/three-way-merge.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Writes or merges a single artifact for the claude target.
|
|
7
|
+
*
|
|
8
|
+
* - AUTO_APPLY / NEW (conflict=false): overwrites <kind>s/<id>.md with upstream content
|
|
9
|
+
* - CONFLICT (conflict=true): writes conflict markers into the file
|
|
10
|
+
*
|
|
11
|
+
* @param {object} opts
|
|
12
|
+
* @param {string} opts.consumerCwd — consumer project root
|
|
13
|
+
* @param {object} opts.upstreamEntry — { artifact: {kind, id}, rendered: string }
|
|
14
|
+
* @param {object} [opts.installedEntry] — { content: string, paths: string[] } or null
|
|
15
|
+
* @param {boolean} opts.conflict
|
|
16
|
+
* @param {string} [opts.oursLabel]
|
|
17
|
+
* @param {string} [opts.theirsLabel]
|
|
18
|
+
*/
|
|
19
|
+
export async function updateClaude({
|
|
20
|
+
consumerCwd,
|
|
21
|
+
upstreamEntry,
|
|
22
|
+
installedEntry = null,
|
|
23
|
+
conflict,
|
|
24
|
+
oursLabel = 'ours',
|
|
25
|
+
theirsLabel = 'theirs',
|
|
26
|
+
}) {
|
|
27
|
+
const { artifact, rendered } = upstreamEntry;
|
|
28
|
+
const subdir = artifact.kind === 'skill' ? 'skills' : 'agents';
|
|
29
|
+
const outDir = join(consumerCwd, '.claude', subdir);
|
|
30
|
+
mkdirSync(outDir, { recursive: true });
|
|
31
|
+
const outPath = join(outDir, `${artifact.id}.md`);
|
|
32
|
+
|
|
33
|
+
if (!conflict) {
|
|
34
|
+
writeFileSync(outPath, rendered, 'utf8');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ours = installedEntry ? installedEntry.content : rendered;
|
|
39
|
+
const { content } = threeWayMerge({ ours, theirs: rendered, oursLabel, theirsLabel });
|
|
40
|
+
writeFileSync(outPath, content, 'utf8');
|
|
41
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { renderTemplate } from '../../lib/template-renderer.js';
|
|
2
|
+
|
|
3
|
+
const HEADING_DEMOTE = 2;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds the AGENTS.md content string for Codex from filtered artifacts.
|
|
7
|
+
* Pure function — no filesystem access.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} opts
|
|
10
|
+
* @param {Array} opts.artifacts — stack-filtered artifacts (skills, agents)
|
|
11
|
+
* @param {Array} [opts.rules] — rule files: { id, body }; sorted by id
|
|
12
|
+
* @param {object} opts.config — validated consumer config
|
|
13
|
+
* @param {Map} opts.configMap — placeholder map from buildPlaceholderMap()
|
|
14
|
+
* @param {string} opts.pluginVersion — version string for the header
|
|
15
|
+
* @returns {string} — full AGENTS.md text
|
|
16
|
+
*/
|
|
17
|
+
export function buildAgentsMd({ artifacts, rules = [], config, configMap, pluginVersion }) {
|
|
18
|
+
const skills = artifacts.filter((a) => a.kind === 'skill');
|
|
19
|
+
const agents = artifacts.filter((a) => a.kind === 'agent');
|
|
20
|
+
|
|
21
|
+
const lines = [];
|
|
22
|
+
lines.push(...renderHeader(config, pluginVersion));
|
|
23
|
+
lines.push(...renderProjectContext(config));
|
|
24
|
+
|
|
25
|
+
if (skills.length > 0) {
|
|
26
|
+
lines.push('## Skills', '');
|
|
27
|
+
for (const skill of skills) {
|
|
28
|
+
lines.push(...renderArtifact(skill, configMap));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (agents.length > 0) {
|
|
33
|
+
lines.push('## Agents', '');
|
|
34
|
+
for (const agent of agents) {
|
|
35
|
+
lines.push(...renderArtifact(agent, configMap));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (rules.length > 0) {
|
|
40
|
+
lines.push('## Rules', '');
|
|
41
|
+
for (const rule of rules) {
|
|
42
|
+
lines.push(...renderRule(rule, configMap));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderRule(rule, configMap) {
|
|
50
|
+
const rendered = renderTemplate(rule.body, { configMap });
|
|
51
|
+
const demoted = demoteHeadings(rendered, HEADING_DEMOTE);
|
|
52
|
+
return [`### ${rule.id}`, '', demoted.trimEnd(), ''];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderHeader(config, pluginVersion) {
|
|
56
|
+
const name = config.project?.name ?? 'Project';
|
|
57
|
+
return [
|
|
58
|
+
`# ${name} — Agent Instructions`,
|
|
59
|
+
'',
|
|
60
|
+
`> Generated by spovishun-skills v${pluginVersion}. Do not edit manually — re-run \`spovishun-skills install --target=codex\` instead.`,
|
|
61
|
+
'',
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderProjectContext(config) {
|
|
66
|
+
const lines = ['## Project context', ''];
|
|
67
|
+
if (config.project?.language) {
|
|
68
|
+
lines.push(`- Language: ${config.project.language}`);
|
|
69
|
+
}
|
|
70
|
+
const activeFlags = Object.entries(config.stack ?? {})
|
|
71
|
+
.filter(([, v]) => v === true)
|
|
72
|
+
.map(([k]) => k);
|
|
73
|
+
if (activeFlags.length > 0) {
|
|
74
|
+
lines.push(`- Tech stack: ${activeFlags.join(' | ')}`);
|
|
75
|
+
}
|
|
76
|
+
if (config.git?.branch_prefix) {
|
|
77
|
+
lines.push(`- Git branch prefix: \`${config.git.branch_prefix}\``);
|
|
78
|
+
}
|
|
79
|
+
lines.push('');
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function renderArtifact(artifact, configMap) {
|
|
84
|
+
const body = prepareBody(artifact);
|
|
85
|
+
const manifestPlaceholders = (artifact.manifest?.placeholders ?? []).map((p) => p.key);
|
|
86
|
+
const rendered = renderTemplate(body, { configMap, manifestPlaceholders });
|
|
87
|
+
const demoted = demoteHeadings(rendered, HEADING_DEMOTE);
|
|
88
|
+
return [
|
|
89
|
+
`### ${artifact.id} (v${artifact.version})`,
|
|
90
|
+
'',
|
|
91
|
+
demoted.trimEnd(),
|
|
92
|
+
'',
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Strips a leading YAML frontmatter block (--- ... ---) if present.
|
|
98
|
+
* AGENT.md files use Claude-specific frontmatter (tools, model, maxTurns) that
|
|
99
|
+
* Codex cannot interpret, so we drop it to keep the output clean.
|
|
100
|
+
*/
|
|
101
|
+
function prepareBody(artifact) {
|
|
102
|
+
const text = artifact.bodyText;
|
|
103
|
+
if (!text.startsWith('---\n')) return text;
|
|
104
|
+
const end = text.indexOf('\n---', 4);
|
|
105
|
+
if (end === -1) return text;
|
|
106
|
+
return text.slice(end + 4).replace(/^\r?\n/, '');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Adds `levels` extra `#` to each ATX heading so artifact bodies nest under
|
|
111
|
+
* the top-level AGENTS.md sections without collision. Caps at h6.
|
|
112
|
+
* Code fences are skipped so headings inside ```...``` blocks remain intact.
|
|
113
|
+
*/
|
|
114
|
+
function demoteHeadings(text, levels) {
|
|
115
|
+
const out = [];
|
|
116
|
+
let inFence = false;
|
|
117
|
+
for (const line of text.split('\n')) {
|
|
118
|
+
const fenceMatch = line.match(/^(\s*)(```|~~~)/);
|
|
119
|
+
if (fenceMatch) {
|
|
120
|
+
inFence = !inFence;
|
|
121
|
+
out.push(line);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!inFence) {
|
|
125
|
+
const m = line.match(/^(#{1,6})(\s)/);
|
|
126
|
+
if (m) {
|
|
127
|
+
const newLevel = Math.min(6, m[1].length + levels);
|
|
128
|
+
out.push('#'.repeat(newLevel) + m[2] + line.slice(m[0].length));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
out.push(line);
|
|
133
|
+
}
|
|
134
|
+
return out.join('\n');
|
|
135
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { writeFileSync, readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
4
|
+
import { filterByStack } from '../../lib/stack-filter.js';
|
|
5
|
+
import { buildPlaceholderMap } from '../../lib/placeholder-map.js';
|
|
6
|
+
import { sha256 } from '../../lib/checksum.js';
|
|
7
|
+
import { buildAgentsMd } from './build-agents-md.js';
|
|
8
|
+
|
|
9
|
+
export const AGENTS_MD_FILENAME = 'AGENTS.md';
|
|
10
|
+
export const SIZE_LIMIT_BYTES = 32 * 1024;
|
|
11
|
+
|
|
12
|
+
const CODEX_KINDS = new Set(['skill', 'agent']);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generates AGENTS.md for Codex at the consumer project root.
|
|
16
|
+
*
|
|
17
|
+
* Skips hooks and other Claude-only artifacts. Renders Mustache placeholders
|
|
18
|
+
* the same way the Claude adapter does. If the resulting file exceeds the
|
|
19
|
+
* 32 KiB AGENTS.md soft limit, emits a stderr warning but still writes the file.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} opts
|
|
22
|
+
* @param {string} opts.consumerCwd — absolute path to consumer project root
|
|
23
|
+
* @param {string} opts.pkgRoot — absolute path to the spovishun-skills package root (for rules/)
|
|
24
|
+
* @param {object} opts.config — validated consumer config
|
|
25
|
+
* @param {Array} opts.artifacts — all loaded artifacts from loadArtifacts()
|
|
26
|
+
* @param {string} opts.pluginVersion — version string for the AGENTS.md header
|
|
27
|
+
* @param {object} [opts.warn] — writable stream for warnings (default: process.stderr)
|
|
28
|
+
* @returns {Array<{kind, id, version, checksum}>} — lockfile entries for included artifacts
|
|
29
|
+
*/
|
|
30
|
+
export async function installCodex({
|
|
31
|
+
consumerCwd,
|
|
32
|
+
pkgRoot,
|
|
33
|
+
config,
|
|
34
|
+
artifacts,
|
|
35
|
+
pluginVersion,
|
|
36
|
+
warn = process.stderr,
|
|
37
|
+
}) {
|
|
38
|
+
const stackFiltered = filterByStack(artifacts, config.stack ?? {});
|
|
39
|
+
const included = stackFiltered.filter((a) => CODEX_KINDS.has(a.kind));
|
|
40
|
+
const rules = collectRules(pkgRoot);
|
|
41
|
+
const configMap = buildPlaceholderMap(config);
|
|
42
|
+
|
|
43
|
+
const content = buildAgentsMd({
|
|
44
|
+
artifacts: included,
|
|
45
|
+
rules,
|
|
46
|
+
config,
|
|
47
|
+
configMap,
|
|
48
|
+
pluginVersion,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const outPath = join(consumerCwd, AGENTS_MD_FILENAME);
|
|
52
|
+
writeFileSync(outPath, content, 'utf8');
|
|
53
|
+
|
|
54
|
+
const byteSize = Buffer.byteLength(content, 'utf8');
|
|
55
|
+
if (byteSize > SIZE_LIMIT_BYTES) {
|
|
56
|
+
const kib = (byteSize / 1024).toFixed(1);
|
|
57
|
+
warn.write(
|
|
58
|
+
`Warning: AGENTS.md is ${kib} KiB (>32 KiB Codex soft limit). Codex may truncate. ` +
|
|
59
|
+
`Consider splitting universal/global instructions into ~/.codex/AGENTS.md and ` +
|
|
60
|
+
`keeping project-specific content in ./AGENTS.md.\n`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const artifactEntries = included.map((artifact) => ({
|
|
65
|
+
kind: artifact.kind,
|
|
66
|
+
id: artifact.id,
|
|
67
|
+
version: artifact.version,
|
|
68
|
+
checksum: sha256(artifact.bodyText),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
const ruleEntries = rules.map((rule) => ({
|
|
72
|
+
kind: 'rule',
|
|
73
|
+
id: rule.id,
|
|
74
|
+
version: '0.0.0',
|
|
75
|
+
checksum: sha256(rule.body),
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
return [...artifactEntries, ...ruleEntries];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Walks pkgRoot/rules/ recursively and returns each .md file as a flat list.
|
|
83
|
+
* Rules in this repo are data files (not artifact-loader entries), so we walk
|
|
84
|
+
* them directly the same way installClaude does.
|
|
85
|
+
*/
|
|
86
|
+
function collectRules(pkgRoot) {
|
|
87
|
+
if (!pkgRoot) return [];
|
|
88
|
+
const rulesDir = join(pkgRoot, 'rules');
|
|
89
|
+
if (!existsSync(rulesDir)) return [];
|
|
90
|
+
|
|
91
|
+
const collected = [];
|
|
92
|
+
walk(rulesDir, rulesDir, collected);
|
|
93
|
+
collected.sort((a, b) => a.id.localeCompare(b.id));
|
|
94
|
+
return collected;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function walk(baseDir, currentDir, out) {
|
|
98
|
+
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
99
|
+
if (entry.name.startsWith('.')) continue;
|
|
100
|
+
const fullPath = join(currentDir, entry.name);
|
|
101
|
+
if (entry.isDirectory()) {
|
|
102
|
+
walk(baseDir, fullPath, out);
|
|
103
|
+
} else if (entry.name.endsWith('.md')) {
|
|
104
|
+
const rel = relative(baseDir, fullPath).split(/[\\/]/).join('/');
|
|
105
|
+
const id = rel.replace(/\.md$/, '');
|
|
106
|
+
out.push({ id, body: readFileSync(fullPath, 'utf8') });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|