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.
Files changed (146) hide show
  1. package/.claude-plugin/.gitkeep +0 -0
  2. package/CHANGELOG.md +42 -0
  3. package/LICENSE +21 -0
  4. package/README.md +169 -0
  5. package/adapters/.gitkeep +0 -0
  6. package/adapters/claude/index.js +123 -0
  7. package/adapters/claude/update.js +41 -0
  8. package/adapters/codex/build-agents-md.js +135 -0
  9. package/adapters/codex/index.js +109 -0
  10. package/adapters/windsurf/index.js +126 -0
  11. package/adapters/windsurf/update.js +46 -0
  12. package/agents/.gitkeep +0 -0
  13. package/agents/code-architecture-reviewer/AGENT.md +87 -0
  14. package/agents/code-architecture-reviewer/manifest.yaml +14 -0
  15. package/agents/code-refactor-master/AGENT.md +71 -0
  16. package/agents/code-refactor-master/manifest.yaml +17 -0
  17. package/agents/database-reviewer/AGENT.md +70 -0
  18. package/agents/database-reviewer/manifest.yaml +15 -0
  19. package/agents/doc-updater/AGENT.md +81 -0
  20. package/agents/doc-updater/manifest.yaml +26 -0
  21. package/agents/documentation-architect/AGENT.md +63 -0
  22. package/agents/documentation-architect/manifest.yaml +15 -0
  23. package/agents/kotlin-reviewer/AGENT.md +91 -0
  24. package/agents/kotlin-reviewer/manifest.yaml +15 -0
  25. package/agents/plan-reviewer/AGENT.md +58 -0
  26. package/agents/plan-reviewer/manifest.yaml +15 -0
  27. package/agents/refactor-planner/AGENT.md +77 -0
  28. package/agents/refactor-planner/manifest.yaml +15 -0
  29. package/agents/web-research-specialist/AGENT.md +70 -0
  30. package/agents/web-research-specialist/manifest.yaml +17 -0
  31. package/bin/doctor.js +422 -0
  32. package/bin/init.js +133 -0
  33. package/bin/install.js +66 -0
  34. package/bin/spovishun-skills.js +167 -0
  35. package/bin/sync.js +47 -0
  36. package/bin/update.js +232 -0
  37. package/hooks/.gitkeep +0 -0
  38. package/hooks/capture-learning.js +92 -0
  39. package/hooks/hooks.json +74 -0
  40. package/hooks/notion-task-inject.js +701 -0
  41. package/hooks/precompact-backup.js +26 -0
  42. package/hooks/session-end.js +67 -0
  43. package/hooks/session-start.js +42 -0
  44. package/lib/ajv.js +9 -0
  45. package/lib/artifact-loader.js +135 -0
  46. package/lib/checksum.js +10 -0
  47. package/lib/config-loader.js +37 -0
  48. package/lib/config-validator.js +77 -0
  49. package/lib/errors.js +13 -0
  50. package/lib/installed-files-loader.js +109 -0
  51. package/lib/lockfile.js +53 -0
  52. package/lib/manifest-validator.js +44 -0
  53. package/lib/notion-bootstrap.js +99 -0
  54. package/lib/notion-client.js +41 -0
  55. package/lib/notion-fallback-prompt.js +81 -0
  56. package/lib/placeholder-map.js +59 -0
  57. package/lib/settings-merger.js +61 -0
  58. package/lib/stack-filter.js +18 -0
  59. package/lib/template-renderer.js +69 -0
  60. package/lib/three-way-merge.js +33 -0
  61. package/lib/update-classifier.js +46 -0
  62. package/package.json +62 -0
  63. package/rules/.gitkeep +0 -0
  64. package/rules/common/design-principles.md +49 -0
  65. package/rules/common/feature-documentation.md +44 -0
  66. package/rules/common/git-workflow.md +30 -0
  67. package/rules/common/security.md +24 -0
  68. package/rules/common/testing.md +33 -0
  69. package/rules/kotlin/kotlin-style.md +48 -0
  70. package/schema/config.schema.json +85 -0
  71. package/schema/manifest.schema.json +91 -0
  72. package/scripts/validate-all-manifests.js +40 -0
  73. package/skills/architecture-designer/SKILL.md +112 -0
  74. package/skills/architecture-designer/manifest.yaml +17 -0
  75. package/skills/changelog-generator/SKILL.md +95 -0
  76. package/skills/changelog-generator/manifest.yaml +14 -0
  77. package/skills/ci-cd-pipeline-builder/SKILL.md +117 -0
  78. package/skills/ci-cd-pipeline-builder/manifest.yaml +18 -0
  79. package/skills/code-reviewer/SKILL.md +79 -0
  80. package/skills/code-reviewer/manifest.yaml +15 -0
  81. package/skills/commit/SKILL.md +43 -0
  82. package/skills/commit/manifest.yaml +15 -0
  83. package/skills/database-optimizer/SKILL.md +107 -0
  84. package/skills/database-optimizer/manifest.yaml +18 -0
  85. package/skills/debugging-wizard/SKILL.md +97 -0
  86. package/skills/debugging-wizard/manifest.yaml +16 -0
  87. package/skills/dependency-injection-architecture/SKILL.md +90 -0
  88. package/skills/dependency-injection-architecture/manifest.yaml +18 -0
  89. package/skills/diagram-design/SKILL.md +356 -0
  90. package/skills/diagram-design/manifest.yaml +19 -0
  91. package/skills/discover-patterns/SKILL.md +38 -0
  92. package/skills/discover-patterns/manifest.yaml +12 -0
  93. package/skills/docker-deployment/SKILL.md +150 -0
  94. package/skills/docker-deployment/manifest.yaml +28 -0
  95. package/skills/git-workflow-pr-writing/SKILL.md +71 -0
  96. package/skills/git-workflow-pr-writing/manifest.yaml +14 -0
  97. package/skills/grill-me/SKILL.md +20 -0
  98. package/skills/grill-me/manifest.yaml +15 -0
  99. package/skills/idea-brainstormer/SKILL.md +113 -0
  100. package/skills/idea-brainstormer/manifest.yaml +21 -0
  101. package/skills/kotlin-specialist/SKILL.md +53 -0
  102. package/skills/kotlin-specialist/manifest.yaml +21 -0
  103. package/skills/newepic/SKILL.md +105 -0
  104. package/skills/newepic/manifest.yaml +20 -0
  105. package/skills/newtask/SKILL.md +170 -0
  106. package/skills/newtask/manifest.yaml +27 -0
  107. package/skills/notion-content-reader/SKILL.md +59 -0
  108. package/skills/notion-content-reader/manifest.yaml +17 -0
  109. package/skills/notion-data-migrator/SKILL.md +55 -0
  110. package/skills/notion-data-migrator/manifest.yaml +17 -0
  111. package/skills/notion-database-manager/SKILL.md +71 -0
  112. package/skills/notion-database-manager/manifest.yaml +18 -0
  113. package/skills/notion-navigator/SKILL.md +66 -0
  114. package/skills/notion-navigator/manifest.yaml +38 -0
  115. package/skills/notion-page-builder/SKILL.md +47 -0
  116. package/skills/notion-page-builder/manifest.yaml +17 -0
  117. package/skills/notion-spovishun-task-manager/SKILL.md +136 -0
  118. package/skills/notion-spovishun-task-manager/manifest.yaml +28 -0
  119. package/skills/notion-task-board-manager/SKILL.md +80 -0
  120. package/skills/notion-task-board-manager/manifest.yaml +19 -0
  121. package/skills/notion-task-to-code/SKILL.md +87 -0
  122. package/skills/notion-task-to-code/manifest.yaml +23 -0
  123. package/skills/notion-workflow-spovishun/SKILL.md +71 -0
  124. package/skills/notion-workflow-spovishun/manifest.yaml +13 -0
  125. package/skills/notion-workspace-organizer/SKILL.md +58 -0
  126. package/skills/notion-workspace-organizer/manifest.yaml +17 -0
  127. package/skills/postgresql-exposed-orm/SKILL.md +57 -0
  128. package/skills/postgresql-exposed-orm/manifest.yaml +20 -0
  129. package/skills/reflect/SKILL.md +94 -0
  130. package/skills/reflect/manifest.yaml +14 -0
  131. package/skills/skill-security-auditor/SKILL.md +109 -0
  132. package/skills/skill-security-auditor/manifest.yaml +14 -0
  133. package/skills/solution-designer/SKILL.md +143 -0
  134. package/skills/solution-designer/manifest.yaml +21 -0
  135. package/skills/task-decomposer/SKILL.md +169 -0
  136. package/skills/task-decomposer/manifest.yaml +28 -0
  137. package/skills/technical-documentation-writer/SKILL.md +97 -0
  138. package/skills/technical-documentation-writer/manifest.yaml +15 -0
  139. package/skills/telegram-bot-development/SKILL.md +62 -0
  140. package/skills/telegram-bot-development/manifest.yaml +20 -0
  141. package/skills/unit-testing-kotlin/SKILL.md +74 -0
  142. package/skills/unit-testing-kotlin/manifest.yaml +18 -0
  143. package/skills/update-doc-full/SKILL.md +199 -0
  144. package/skills/update-doc-full/manifest.yaml +14 -0
  145. package/skills/write-a-skill/SKILL.md +165 -0
  146. 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
+ [![npm version](https://img.shields.io/npm/v/spovishun-skills.svg)](https://www.npmjs.com/package/spovishun-skills)
4
+ [![license](https://img.shields.io/npm/l/spovishun-skills.svg)](./LICENSE)
5
+ [![node](https://img.shields.io/node/v/spovishun-skills.svg)](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
+ }