@booklib/core 2.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/.cursor/rules/booklib-standards.mdc +40 -0
- package/.gemini/context.md +372 -0
- package/AGENTS.md +166 -0
- package/CHANGELOG.md +226 -0
- package/CLAUDE.md +81 -0
- package/CODE_OF_CONDUCT.md +31 -0
- package/CONTRIBUTING.md +304 -0
- package/LICENSE +21 -0
- package/PLAN.md +28 -0
- package/README.ja.md +198 -0
- package/README.ko.md +198 -0
- package/README.md +503 -0
- package/README.pt-BR.md +198 -0
- package/README.uk.md +241 -0
- package/README.zh-CN.md +198 -0
- package/SECURITY.md +9 -0
- package/agents/architecture-reviewer.md +136 -0
- package/agents/booklib-reviewer.md +90 -0
- package/agents/data-reviewer.md +107 -0
- package/agents/jvm-reviewer.md +146 -0
- package/agents/python-reviewer.md +128 -0
- package/agents/rust-reviewer.md +115 -0
- package/agents/ts-reviewer.md +110 -0
- package/agents/ui-reviewer.md +117 -0
- package/assets/logo.svg +36 -0
- package/bin/booklib-mcp.js +304 -0
- package/bin/booklib.js +1705 -0
- package/bin/skills.cjs +1292 -0
- package/booklib-router.mdc +36 -0
- package/booklib.config.json +19 -0
- package/commands/animation-at-work.md +10 -0
- package/commands/clean-code-reviewer.md +10 -0
- package/commands/data-intensive-patterns.md +10 -0
- package/commands/data-pipelines.md +10 -0
- package/commands/design-patterns.md +10 -0
- package/commands/domain-driven-design.md +10 -0
- package/commands/effective-java.md +10 -0
- package/commands/effective-kotlin.md +10 -0
- package/commands/effective-python.md +10 -0
- package/commands/effective-typescript.md +10 -0
- package/commands/kotlin-in-action.md +10 -0
- package/commands/lean-startup.md +10 -0
- package/commands/microservices-patterns.md +10 -0
- package/commands/programming-with-rust.md +10 -0
- package/commands/refactoring-ui.md +10 -0
- package/commands/rust-in-action.md +10 -0
- package/commands/skill-router.md +10 -0
- package/commands/spring-boot-in-action.md +10 -0
- package/commands/storytelling-with-data.md +10 -0
- package/commands/system-design-interview.md +10 -0
- package/commands/using-asyncio-python.md +10 -0
- package/commands/web-scraping-python.md +10 -0
- package/community/registry.json +1616 -0
- package/hooks/hooks.json +23 -0
- package/hooks/posttooluse-capture.mjs +67 -0
- package/hooks/suggest.js +153 -0
- package/lib/agent-behaviors.js +40 -0
- package/lib/agent-detector.js +96 -0
- package/lib/config-loader.js +39 -0
- package/lib/conflict-resolver.js +148 -0
- package/lib/context-builder.js +574 -0
- package/lib/discovery-engine.js +298 -0
- package/lib/doctor/hook-installer.js +83 -0
- package/lib/doctor/usage-tracker.js +87 -0
- package/lib/engine/ai-features.js +253 -0
- package/lib/engine/auditor.js +103 -0
- package/lib/engine/bm25-index.js +178 -0
- package/lib/engine/capture.js +120 -0
- package/lib/engine/corrections.js +198 -0
- package/lib/engine/doctor.js +195 -0
- package/lib/engine/graph-injector.js +137 -0
- package/lib/engine/graph.js +161 -0
- package/lib/engine/handoff.js +405 -0
- package/lib/engine/indexer.js +242 -0
- package/lib/engine/parser.js +53 -0
- package/lib/engine/query-expander.js +42 -0
- package/lib/engine/reranker.js +40 -0
- package/lib/engine/rrf.js +59 -0
- package/lib/engine/scanner.js +151 -0
- package/lib/engine/searcher.js +139 -0
- package/lib/engine/session-coordinator.js +306 -0
- package/lib/engine/session-manager.js +429 -0
- package/lib/engine/synthesizer.js +70 -0
- package/lib/installer.js +70 -0
- package/lib/instinct-block.js +33 -0
- package/lib/mcp-config-writer.js +88 -0
- package/lib/paths.js +57 -0
- package/lib/profiles/design.md +19 -0
- package/lib/profiles/general.md +16 -0
- package/lib/profiles/research-analysis.md +22 -0
- package/lib/profiles/software-development.md +23 -0
- package/lib/profiles/writing-content.md +19 -0
- package/lib/project-initializer.js +916 -0
- package/lib/registry/skills.js +102 -0
- package/lib/registry-searcher.js +99 -0
- package/lib/rules/rules-manager.js +169 -0
- package/lib/skill-fetcher.js +333 -0
- package/lib/well-known-builder.js +70 -0
- package/lib/wizard/index.js +404 -0
- package/lib/wizard/integration-detector.js +41 -0
- package/lib/wizard/project-detector.js +100 -0
- package/lib/wizard/prompt.js +156 -0
- package/lib/wizard/registry-embeddings.js +107 -0
- package/lib/wizard/skill-recommender.js +69 -0
- package/llms-full.txt +254 -0
- package/llms.txt +70 -0
- package/package.json +45 -0
- package/research-reports/2026-04-01-current-architecture.md +160 -0
- package/research-reports/IDEAS.md +93 -0
- package/rules/common/clean-code.md +42 -0
- package/rules/java/effective-java.md +42 -0
- package/rules/kotlin/effective-kotlin.md +37 -0
- package/rules/python/effective-python.md +38 -0
- package/rules/rust/rust.md +37 -0
- package/rules/typescript/effective-typescript.md +42 -0
- package/scripts/gen-llms-full.mjs +36 -0
- package/scripts/gen-og.mjs +142 -0
- package/scripts/validate-frontmatter.js +25 -0
- package/skills/animation-at-work/SKILL.md +270 -0
- package/skills/animation-at-work/assets/example_asset.txt +1 -0
- package/skills/animation-at-work/evals/evals.json +44 -0
- package/skills/animation-at-work/evals/results.json +13 -0
- package/skills/animation-at-work/examples/after.md +64 -0
- package/skills/animation-at-work/examples/before.md +35 -0
- package/skills/animation-at-work/references/api_reference.md +369 -0
- package/skills/animation-at-work/references/review-checklist.md +79 -0
- package/skills/animation-at-work/scripts/audit_animations.py +295 -0
- package/skills/animation-at-work/scripts/example.py +1 -0
- package/skills/clean-code-reviewer/SKILL.md +444 -0
- package/skills/clean-code-reviewer/audit.json +35 -0
- package/skills/clean-code-reviewer/evals/evals.json +185 -0
- package/skills/clean-code-reviewer/evals/results.json +13 -0
- package/skills/clean-code-reviewer/examples/after.md +48 -0
- package/skills/clean-code-reviewer/examples/before.md +33 -0
- package/skills/clean-code-reviewer/references/api_reference.md +158 -0
- package/skills/clean-code-reviewer/references/practices-catalog.md +282 -0
- package/skills/clean-code-reviewer/references/review-checklist.md +254 -0
- package/skills/clean-code-reviewer/scripts/pre-review.py +206 -0
- package/skills/data-intensive-patterns/SKILL.md +267 -0
- package/skills/data-intensive-patterns/assets/example_asset.txt +1 -0
- package/skills/data-intensive-patterns/evals/evals.json +54 -0
- package/skills/data-intensive-patterns/evals/results.json +13 -0
- package/skills/data-intensive-patterns/examples/after.md +61 -0
- package/skills/data-intensive-patterns/examples/before.md +38 -0
- package/skills/data-intensive-patterns/references/api_reference.md +34 -0
- package/skills/data-intensive-patterns/references/patterns-catalog.md +551 -0
- package/skills/data-intensive-patterns/references/review-checklist.md +193 -0
- package/skills/data-intensive-patterns/scripts/adr.py +213 -0
- package/skills/data-intensive-patterns/scripts/example.py +1 -0
- package/skills/data-pipelines/SKILL.md +259 -0
- package/skills/data-pipelines/assets/example_asset.txt +1 -0
- package/skills/data-pipelines/evals/evals.json +45 -0
- package/skills/data-pipelines/evals/results.json +13 -0
- package/skills/data-pipelines/examples/after.md +97 -0
- package/skills/data-pipelines/examples/before.md +37 -0
- package/skills/data-pipelines/references/api_reference.md +301 -0
- package/skills/data-pipelines/references/review-checklist.md +181 -0
- package/skills/data-pipelines/scripts/example.py +1 -0
- package/skills/data-pipelines/scripts/new_pipeline.py +444 -0
- package/skills/design-patterns/SKILL.md +271 -0
- package/skills/design-patterns/assets/example_asset.txt +1 -0
- package/skills/design-patterns/evals/evals.json +46 -0
- package/skills/design-patterns/evals/results.json +13 -0
- package/skills/design-patterns/examples/after.md +52 -0
- package/skills/design-patterns/examples/before.md +29 -0
- package/skills/design-patterns/references/api_reference.md +1 -0
- package/skills/design-patterns/references/patterns-catalog.md +726 -0
- package/skills/design-patterns/references/review-checklist.md +173 -0
- package/skills/design-patterns/scripts/example.py +1 -0
- package/skills/design-patterns/scripts/scaffold.py +807 -0
- package/skills/domain-driven-design/SKILL.md +142 -0
- package/skills/domain-driven-design/assets/example_asset.txt +1 -0
- package/skills/domain-driven-design/evals/evals.json +48 -0
- package/skills/domain-driven-design/evals/results.json +13 -0
- package/skills/domain-driven-design/examples/after.md +80 -0
- package/skills/domain-driven-design/examples/before.md +43 -0
- package/skills/domain-driven-design/references/api_reference.md +1 -0
- package/skills/domain-driven-design/references/patterns-catalog.md +545 -0
- package/skills/domain-driven-design/references/review-checklist.md +158 -0
- package/skills/domain-driven-design/scripts/example.py +1 -0
- package/skills/domain-driven-design/scripts/scaffold.py +421 -0
- package/skills/effective-java/SKILL.md +227 -0
- package/skills/effective-java/assets/example_asset.txt +1 -0
- package/skills/effective-java/evals/evals.json +46 -0
- package/skills/effective-java/evals/results.json +13 -0
- package/skills/effective-java/examples/after.md +83 -0
- package/skills/effective-java/examples/before.md +37 -0
- package/skills/effective-java/references/api_reference.md +1 -0
- package/skills/effective-java/references/items-catalog.md +955 -0
- package/skills/effective-java/references/review-checklist.md +216 -0
- package/skills/effective-java/scripts/checkstyle_setup.py +211 -0
- package/skills/effective-java/scripts/example.py +1 -0
- package/skills/effective-kotlin/SKILL.md +271 -0
- package/skills/effective-kotlin/assets/example_asset.txt +1 -0
- package/skills/effective-kotlin/audit.json +29 -0
- package/skills/effective-kotlin/evals/evals.json +45 -0
- package/skills/effective-kotlin/evals/results.json +13 -0
- package/skills/effective-kotlin/examples/after.md +36 -0
- package/skills/effective-kotlin/examples/before.md +38 -0
- package/skills/effective-kotlin/references/api_reference.md +1 -0
- package/skills/effective-kotlin/references/practices-catalog.md +1228 -0
- package/skills/effective-kotlin/references/review-checklist.md +126 -0
- package/skills/effective-kotlin/scripts/example.py +1 -0
- package/skills/effective-python/SKILL.md +441 -0
- package/skills/effective-python/evals/evals.json +44 -0
- package/skills/effective-python/evals/results.json +13 -0
- package/skills/effective-python/examples/after.md +56 -0
- package/skills/effective-python/examples/before.md +40 -0
- package/skills/effective-python/ref-01-pythonic-thinking.md +202 -0
- package/skills/effective-python/ref-02-lists-and-dicts.md +146 -0
- package/skills/effective-python/ref-03-functions.md +186 -0
- package/skills/effective-python/ref-04-comprehensions-generators.md +211 -0
- package/skills/effective-python/ref-05-classes-interfaces.md +188 -0
- package/skills/effective-python/ref-06-metaclasses-attributes.md +209 -0
- package/skills/effective-python/ref-07-concurrency.md +213 -0
- package/skills/effective-python/ref-08-robustness-performance.md +248 -0
- package/skills/effective-python/ref-09-testing-debugging.md +253 -0
- package/skills/effective-python/ref-10-collaboration.md +175 -0
- package/skills/effective-python/references/api_reference.md +218 -0
- package/skills/effective-python/references/practices-catalog.md +483 -0
- package/skills/effective-python/references/review-checklist.md +190 -0
- package/skills/effective-python/scripts/lint.py +173 -0
- package/skills/effective-typescript/SKILL.md +262 -0
- package/skills/effective-typescript/audit.json +29 -0
- package/skills/effective-typescript/evals/evals.json +37 -0
- package/skills/effective-typescript/evals/results.json +13 -0
- package/skills/effective-typescript/examples/after.md +70 -0
- package/skills/effective-typescript/examples/before.md +47 -0
- package/skills/effective-typescript/references/api_reference.md +118 -0
- package/skills/effective-typescript/references/practices-catalog.md +371 -0
- package/skills/effective-typescript/scripts/review.py +169 -0
- package/skills/kotlin-in-action/SKILL.md +261 -0
- package/skills/kotlin-in-action/assets/example_asset.txt +1 -0
- package/skills/kotlin-in-action/evals/evals.json +43 -0
- package/skills/kotlin-in-action/evals/results.json +13 -0
- package/skills/kotlin-in-action/examples/after.md +53 -0
- package/skills/kotlin-in-action/examples/before.md +39 -0
- package/skills/kotlin-in-action/references/api_reference.md +1 -0
- package/skills/kotlin-in-action/references/practices-catalog.md +436 -0
- package/skills/kotlin-in-action/references/review-checklist.md +204 -0
- package/skills/kotlin-in-action/scripts/example.py +1 -0
- package/skills/kotlin-in-action/scripts/setup_detekt.py +224 -0
- package/skills/lean-startup/SKILL.md +160 -0
- package/skills/lean-startup/assets/example_asset.txt +1 -0
- package/skills/lean-startup/evals/evals.json +43 -0
- package/skills/lean-startup/evals/results.json +13 -0
- package/skills/lean-startup/examples/after.md +80 -0
- package/skills/lean-startup/examples/before.md +34 -0
- package/skills/lean-startup/references/api_reference.md +319 -0
- package/skills/lean-startup/references/review-checklist.md +137 -0
- package/skills/lean-startup/scripts/example.py +1 -0
- package/skills/lean-startup/scripts/new_experiment.py +286 -0
- package/skills/microservices-patterns/SKILL.md +384 -0
- package/skills/microservices-patterns/evals/evals.json +45 -0
- package/skills/microservices-patterns/evals/results.json +13 -0
- package/skills/microservices-patterns/examples/after.md +69 -0
- package/skills/microservices-patterns/examples/before.md +40 -0
- package/skills/microservices-patterns/references/patterns-catalog.md +391 -0
- package/skills/microservices-patterns/references/review-checklist.md +169 -0
- package/skills/microservices-patterns/scripts/new_service.py +583 -0
- package/skills/programming-with-rust/SKILL.md +209 -0
- package/skills/programming-with-rust/evals/evals.json +37 -0
- package/skills/programming-with-rust/evals/results.json +13 -0
- package/skills/programming-with-rust/examples/after.md +107 -0
- package/skills/programming-with-rust/examples/before.md +59 -0
- package/skills/programming-with-rust/references/api_reference.md +152 -0
- package/skills/programming-with-rust/references/practices-catalog.md +335 -0
- package/skills/programming-with-rust/scripts/review.py +142 -0
- package/skills/refactoring-ui/SKILL.md +362 -0
- package/skills/refactoring-ui/assets/example_asset.txt +1 -0
- package/skills/refactoring-ui/evals/evals.json +45 -0
- package/skills/refactoring-ui/evals/results.json +13 -0
- package/skills/refactoring-ui/examples/after.md +85 -0
- package/skills/refactoring-ui/examples/before.md +58 -0
- package/skills/refactoring-ui/references/api_reference.md +355 -0
- package/skills/refactoring-ui/references/review-checklist.md +114 -0
- package/skills/refactoring-ui/scripts/audit_css.py +250 -0
- package/skills/refactoring-ui/scripts/example.py +1 -0
- package/skills/rust-in-action/SKILL.md +350 -0
- package/skills/rust-in-action/evals/evals.json +38 -0
- package/skills/rust-in-action/evals/results.json +13 -0
- package/skills/rust-in-action/examples/after.md +156 -0
- package/skills/rust-in-action/examples/before.md +56 -0
- package/skills/rust-in-action/references/practices-catalog.md +346 -0
- package/skills/rust-in-action/scripts/review.py +147 -0
- package/skills/skill-router/SKILL.md +186 -0
- package/skills/skill-router/evals/evals.json +38 -0
- package/skills/skill-router/evals/results.json +13 -0
- package/skills/skill-router/examples/after.md +63 -0
- package/skills/skill-router/examples/before.md +39 -0
- package/skills/skill-router/references/api_reference.md +24 -0
- package/skills/skill-router/references/routing-heuristics.md +89 -0
- package/skills/skill-router/references/skill-catalog.md +174 -0
- package/skills/skill-router/scripts/route.py +266 -0
- package/skills/spring-boot-in-action/SKILL.md +340 -0
- package/skills/spring-boot-in-action/evals/evals.json +39 -0
- package/skills/spring-boot-in-action/evals/results.json +13 -0
- package/skills/spring-boot-in-action/examples/after.md +185 -0
- package/skills/spring-boot-in-action/examples/before.md +84 -0
- package/skills/spring-boot-in-action/references/practices-catalog.md +403 -0
- package/skills/spring-boot-in-action/scripts/review.py +184 -0
- package/skills/storytelling-with-data/SKILL.md +241 -0
- package/skills/storytelling-with-data/assets/example_asset.txt +1 -0
- package/skills/storytelling-with-data/evals/evals.json +47 -0
- package/skills/storytelling-with-data/evals/results.json +13 -0
- package/skills/storytelling-with-data/examples/after.md +50 -0
- package/skills/storytelling-with-data/examples/before.md +33 -0
- package/skills/storytelling-with-data/references/api_reference.md +379 -0
- package/skills/storytelling-with-data/references/review-checklist.md +111 -0
- package/skills/storytelling-with-data/scripts/chart_review.py +301 -0
- package/skills/storytelling-with-data/scripts/example.py +1 -0
- package/skills/system-design-interview/SKILL.md +233 -0
- package/skills/system-design-interview/assets/example_asset.txt +1 -0
- package/skills/system-design-interview/evals/evals.json +46 -0
- package/skills/system-design-interview/evals/results.json +13 -0
- package/skills/system-design-interview/examples/after.md +94 -0
- package/skills/system-design-interview/examples/before.md +27 -0
- package/skills/system-design-interview/references/api_reference.md +582 -0
- package/skills/system-design-interview/references/review-checklist.md +201 -0
- package/skills/system-design-interview/scripts/example.py +1 -0
- package/skills/system-design-interview/scripts/new_design.py +421 -0
- package/skills/using-asyncio-python/SKILL.md +290 -0
- package/skills/using-asyncio-python/assets/example_asset.txt +1 -0
- package/skills/using-asyncio-python/evals/evals.json +43 -0
- package/skills/using-asyncio-python/evals/results.json +13 -0
- package/skills/using-asyncio-python/examples/after.md +68 -0
- package/skills/using-asyncio-python/examples/before.md +39 -0
- package/skills/using-asyncio-python/references/api_reference.md +267 -0
- package/skills/using-asyncio-python/references/review-checklist.md +149 -0
- package/skills/using-asyncio-python/scripts/check_blocking.py +270 -0
- package/skills/using-asyncio-python/scripts/example.py +1 -0
- package/skills/web-scraping-python/SKILL.md +280 -0
- package/skills/web-scraping-python/assets/example_asset.txt +1 -0
- package/skills/web-scraping-python/evals/evals.json +46 -0
- package/skills/web-scraping-python/evals/results.json +13 -0
- package/skills/web-scraping-python/examples/after.md +109 -0
- package/skills/web-scraping-python/examples/before.md +40 -0
- package/skills/web-scraping-python/references/api_reference.md +393 -0
- package/skills/web-scraping-python/references/review-checklist.md +163 -0
- package/skills/web-scraping-python/scripts/example.py +1 -0
- package/skills/web-scraping-python/scripts/new_scraper.py +231 -0
- package/skills/writing-plans/audit.json +34 -0
- package/tests/agent-detector.test.js +83 -0
- package/tests/corrections.test.js +245 -0
- package/tests/doctor/hook-installer.test.js +72 -0
- package/tests/doctor/usage-tracker.test.js +140 -0
- package/tests/engine/benchmark-eval.test.js +31 -0
- package/tests/engine/bm25-index.test.js +85 -0
- package/tests/engine/capture-command.test.js +35 -0
- package/tests/engine/capture.test.js +17 -0
- package/tests/engine/graph-augmented-search.test.js +107 -0
- package/tests/engine/graph-injector.test.js +44 -0
- package/tests/engine/graph.test.js +216 -0
- package/tests/engine/hybrid-searcher.test.js +74 -0
- package/tests/engine/indexer-bm25.test.js +37 -0
- package/tests/engine/mcp-tools.test.js +73 -0
- package/tests/engine/project-initializer-mcp.test.js +99 -0
- package/tests/engine/query-expander.test.js +36 -0
- package/tests/engine/reranker.test.js +51 -0
- package/tests/engine/rrf.test.js +49 -0
- package/tests/engine/srag-prefix.test.js +47 -0
- package/tests/instinct-block.test.js +23 -0
- package/tests/mcp-config-writer.test.js +60 -0
- package/tests/project-initializer-new-agents.test.js +48 -0
- package/tests/rules/rules-manager.test.js +230 -0
- package/tests/well-known-builder.test.js +40 -0
- package/tests/wizard/integration-detector.test.js +31 -0
- package/tests/wizard/project-detector.test.js +51 -0
- package/tests/wizard/prompt-session.test.js +61 -0
- package/tests/wizard/prompt.test.js +16 -0
- package/tests/wizard/registry-embeddings.test.js +35 -0
- package/tests/wizard/skill-recommender.test.js +34 -0
- package/tests/wizard/slot-count.test.js +25 -0
- package/vercel.json +21 -0
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import matter from 'gray-matter';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { parseSkillFile } from './engine/parser.js';
|
|
8
|
+
import { BookLibScanner } from './engine/scanner.js';
|
|
9
|
+
import { resolveBookLibPaths } from './paths.js';
|
|
10
|
+
import { loadConfig } from './config-loader.js';
|
|
11
|
+
import { AgentDetector } from './agent-detector.js';
|
|
12
|
+
import { renderBehaviors } from './agent-behaviors.js';
|
|
13
|
+
import { renderInstinctBlock } from './instinct-block.js';
|
|
14
|
+
|
|
15
|
+
const PACKAGE_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..');
|
|
16
|
+
|
|
17
|
+
const TOOL_FILE_MAP = {
|
|
18
|
+
claude: { filePath: 'CLAUDE.md', fileHeader: '' },
|
|
19
|
+
cursor: { filePath: '.cursor/rules/booklib-standards.mdc', fileHeader: null },
|
|
20
|
+
copilot: { filePath: '.github/copilot-instructions.md', fileHeader: '# Copilot Instructions\n\n' },
|
|
21
|
+
gemini: { filePath: '.gemini/context.md', fileHeader: '# Project Context\n\n' },
|
|
22
|
+
codex: { filePath: 'AGENTS.md', fileHeader: '# Agent Instructions\n\n' },
|
|
23
|
+
windsurf: { filePath: '.windsurfrules', fileHeader: '' },
|
|
24
|
+
'roo-code': { filePath: '.roo/rules/booklib-standards.md', fileHeader: null },
|
|
25
|
+
openhands: { filePath: '.openhands/instructions.md', fileHeader: '# OpenHands Instructions\n\n' },
|
|
26
|
+
junie: { filePath: '.junie/guidelines.md', fileHeader: '# Junie Guidelines\n\n' },
|
|
27
|
+
goose: { filePath: '.goose/context.md', fileHeader: '# Goose Context\n\n' },
|
|
28
|
+
opencode: { filePath: '.opencode/instructions.md', fileHeader: '# OpenCode Instructions\n\n' },
|
|
29
|
+
letta: { filePath: '.letta/skills/booklib.md', fileHeader: null },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const TOOL_DOCS = {
|
|
33
|
+
claude: 'https://docs.anthropic.com/en/docs/claude-code/claude-md',
|
|
34
|
+
cursor: 'https://docs.cursor.com/context/rules-for-ai',
|
|
35
|
+
copilot: 'https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions',
|
|
36
|
+
gemini: 'https://github.com/google-gemini/gemini-cli#configuration',
|
|
37
|
+
codex: 'https://github.com/openai/codex#agents-md',
|
|
38
|
+
windsurf: 'https://docs.windsurf.com/windsurf/customize',
|
|
39
|
+
'roo-code': 'https://docs.roocode.com/features/custom-rules',
|
|
40
|
+
openhands: 'https://docs.all-hands.dev/usage/configuration',
|
|
41
|
+
junie: 'https://www.jetbrains.com/help/junie/guidelines',
|
|
42
|
+
goose: 'https://block.github.io/goose/docs/configuration',
|
|
43
|
+
opencode: 'https://github.com/opencode-ai/opencode#configuration',
|
|
44
|
+
letta: 'https://docs.letta.com/agents/custom-instructions',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generates tool-specific context files from BookLib skills.
|
|
49
|
+
*
|
|
50
|
+
* Supported targets:
|
|
51
|
+
* cursor → .cursor/rules/booklib-standards.mdc
|
|
52
|
+
* claude → CLAUDE.md (appends a standards section)
|
|
53
|
+
* copilot → .github/copilot-instructions.md
|
|
54
|
+
* gemini → .gemini/context.md
|
|
55
|
+
* all → all of the above
|
|
56
|
+
*/
|
|
57
|
+
export class ProjectInitializer {
|
|
58
|
+
constructor(options = {}) {
|
|
59
|
+
this.paths = resolveBookLibPaths(options.projectCwd);
|
|
60
|
+
this.projectCwd = options.projectCwd ?? process.cwd();
|
|
61
|
+
this.config = loadConfig(options.projectCwd);
|
|
62
|
+
this.scanner = new BookLibScanner();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Detects which skills are relevant to the project via scan, returns skill names.
|
|
67
|
+
*/
|
|
68
|
+
detectRelevantSkills() {
|
|
69
|
+
const files = this.scanner.getFiles(this.projectCwd);
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
const skill = this.scanner.detectSkill(file);
|
|
73
|
+
if (skill) seen.add(skill);
|
|
74
|
+
}
|
|
75
|
+
return [...seen];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Main entry point. Detects or uses provided skills, then writes context files.
|
|
80
|
+
*
|
|
81
|
+
* @param {object} opts
|
|
82
|
+
* @param {string[]} [opts.skills] - explicit skill names; auto-detected if omitted
|
|
83
|
+
* @param {string} opts.target - 'cursor' | 'claude' | 'copilot' | 'gemini' | 'all' | 'auto'
|
|
84
|
+
* @param {boolean} [opts.dryRun] - print what would be written, don't write
|
|
85
|
+
* @param {boolean} [opts.quiet] - suppress informational output (e.g. skipped files)
|
|
86
|
+
* @param {function} [opts.onFileConflict] - async callback invoked when a target file already
|
|
87
|
+
* exists. Receives { filePath, lineCount, hasMarkers } and should return 'skip' to leave the
|
|
88
|
+
* file untouched, or any other value ('append'|'update') to proceed with the default behavior.
|
|
89
|
+
* When omitted, existing files are always appended/updated without prompting.
|
|
90
|
+
* @param {string} [opts.profile] - activity-based profile name (e.g. 'software-development')
|
|
91
|
+
* @param {string} [opts.stack] - stack description to fill into profile template
|
|
92
|
+
* @param {boolean} [opts.legacy] - use old _render() with full block extraction (default: false)
|
|
93
|
+
* @returns {string[]} list of files written
|
|
94
|
+
*/
|
|
95
|
+
async init({ skills, target = 'all', dryRun = false, quiet = false, onFileConflict, profile, stack, legacy = false } = {}) {
|
|
96
|
+
const skillNames = skills?.length ? skills : this.detectRelevantSkills();
|
|
97
|
+
if (skillNames.length === 0) {
|
|
98
|
+
throw new Error('No relevant skills detected. Pass --skills explicitly or run booklib index first.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ALL_TARGETS = [
|
|
102
|
+
'claude', 'cursor', 'copilot', 'gemini', 'codex', 'windsurf',
|
|
103
|
+
'roo-code', 'openhands', 'junie', 'goose', 'opencode', 'letta',
|
|
104
|
+
];
|
|
105
|
+
const targets = target === 'all'
|
|
106
|
+
? ALL_TARGETS
|
|
107
|
+
: target === 'auto'
|
|
108
|
+
? new AgentDetector({ cwd: this.projectCwd }).detect()
|
|
109
|
+
: target.split(',').map(t => t.trim());
|
|
110
|
+
|
|
111
|
+
const MARKER_START = '<!-- booklib-standards-start -->';
|
|
112
|
+
const MARKER_RE = /<!-- booklib-standards-start -->[\s\S]*?<!-- booklib-standards-end -->/;
|
|
113
|
+
const MARKER_END = '<!-- booklib-standards-end -->';
|
|
114
|
+
|
|
115
|
+
// Only extract blocks if legacy mode is requested
|
|
116
|
+
const blocks = legacy ? this._extractBlocks(skillNames) : [];
|
|
117
|
+
|
|
118
|
+
const written = [];
|
|
119
|
+
for (const t of targets) {
|
|
120
|
+
let filePath, content, fileHeader;
|
|
121
|
+
|
|
122
|
+
if (legacy) {
|
|
123
|
+
({ filePath, content, fileHeader } = this._render(t, blocks, skillNames));
|
|
124
|
+
} else {
|
|
125
|
+
const mapping = TOOL_FILE_MAP[t];
|
|
126
|
+
if (!mapping) continue;
|
|
127
|
+
filePath = mapping.filePath;
|
|
128
|
+
fileHeader = mapping.fileHeader;
|
|
129
|
+
const effectiveProfile = profile || 'software-development';
|
|
130
|
+
const profileContent = this._renderFromProfile(t, effectiveProfile, skillNames, stack);
|
|
131
|
+
content = `${MARKER_START}\n${profileContent}\n${MARKER_END}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const absPath = path.join(this.projectCwd, filePath);
|
|
135
|
+
if (dryRun) {
|
|
136
|
+
console.log(`\n[dry-run] Would write: ${filePath}\n${'─'.repeat(60)}\n${content.slice(0, 400)}…`);
|
|
137
|
+
} else {
|
|
138
|
+
// Conflict detection: when file exists and a callback is provided, let the caller decide
|
|
139
|
+
if (fs.existsSync(absPath) && onFileConflict) {
|
|
140
|
+
const existing = fs.readFileSync(absPath, 'utf8');
|
|
141
|
+
const hasMarkers = existing.includes(MARKER_START);
|
|
142
|
+
const lineCount = existing.split('\n').length;
|
|
143
|
+
const action = await onFileConflict({ filePath, lineCount, hasMarkers });
|
|
144
|
+
if (action === 'skip') {
|
|
145
|
+
if (!quiet) console.log(` · ${filePath} (skipped)`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// 'append' or 'update' — fall through to existing write logic
|
|
149
|
+
}
|
|
150
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
151
|
+
if (fileHeader === null) {
|
|
152
|
+
// booklib owns this file entirely (cursor) — always overwrite
|
|
153
|
+
fs.writeFileSync(absPath, content);
|
|
154
|
+
} else if (fs.existsSync(absPath)) {
|
|
155
|
+
const existing = fs.readFileSync(absPath, 'utf8');
|
|
156
|
+
if (existing.includes(MARKER_START)) {
|
|
157
|
+
// Update only the booklib section, preserve everything else
|
|
158
|
+
fs.writeFileSync(absPath, existing.replace(MARKER_RE, content));
|
|
159
|
+
} else {
|
|
160
|
+
// File exists with no booklib section — append it
|
|
161
|
+
fs.appendFileSync(absPath, `\n\n${content}`);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
// New file — write header + booklib section
|
|
165
|
+
fs.writeFileSync(absPath, `${fileHeader}${content}`);
|
|
166
|
+
}
|
|
167
|
+
written.push(filePath);
|
|
168
|
+
if (!quiet) console.log(` ✅ ${filePath}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return written;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Reads each skill's SKILL.md and extracts framework + pitfall blocks.
|
|
178
|
+
*/
|
|
179
|
+
_extractBlocks(skillNames) {
|
|
180
|
+
const blocks = [];
|
|
181
|
+
for (const name of skillNames) {
|
|
182
|
+
const skillPath = path.join(this.paths.skillsPath, name, 'SKILL.md');
|
|
183
|
+
const cachePath = path.join(this.paths.cachePath, 'skills', name, 'SKILL.md');
|
|
184
|
+
const bundledPath = path.join(PACKAGE_ROOT, 'skills', name, 'SKILL.md');
|
|
185
|
+
const mdPath = [skillPath, cachePath, bundledPath].find(p => fs.existsSync(p)) ?? null;
|
|
186
|
+
if (!mdPath) continue;
|
|
187
|
+
|
|
188
|
+
const content = fs.readFileSync(mdPath, 'utf8');
|
|
189
|
+
const chunks = parseSkillFile(content, mdPath);
|
|
190
|
+
|
|
191
|
+
const framework = chunks.find(c =>
|
|
192
|
+
c.metadata.type === 'framework' || c.metadata.type === 'core_principles'
|
|
193
|
+
)?.text ?? null;
|
|
194
|
+
|
|
195
|
+
const pitfalls = chunks.find(c =>
|
|
196
|
+
c.metadata.type === 'pitfalls' || c.metadata.type === 'anti_patterns'
|
|
197
|
+
)?.text ?? null;
|
|
198
|
+
|
|
199
|
+
if (framework || pitfalls) {
|
|
200
|
+
blocks.push({ skill: name, framework, pitfalls });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return blocks;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resolves the path to a skill's SKILL.md file by checking four locations
|
|
208
|
+
* in priority order: project-local, cached community, bundled, and Claude Code.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} name - skill name (e.g. 'effective-kotlin')
|
|
211
|
+
* @returns {string|null} absolute path to SKILL.md, or null if not found
|
|
212
|
+
*/
|
|
213
|
+
_findSkillFile(name) {
|
|
214
|
+
const candidates = [
|
|
215
|
+
path.join(this.paths.skillsPath, name, 'SKILL.md'),
|
|
216
|
+
path.join(this.paths.cachePath, 'skills', name, 'SKILL.md'),
|
|
217
|
+
path.join(PACKAGE_ROOT, 'skills', name, 'SKILL.md'),
|
|
218
|
+
path.join(os.homedir(), '.claude', 'skills', name, 'SKILL.md'),
|
|
219
|
+
];
|
|
220
|
+
return candidates.find(p => fs.existsSync(p)) ?? null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Reads ONLY frontmatter from each skill's SKILL.md and returns a concise
|
|
225
|
+
* markdown table. Used by Config Profiles (Spec A) to replace the verbose
|
|
226
|
+
* raw-block output of _extractBlocks().
|
|
227
|
+
*
|
|
228
|
+
* @param {string[]} skillNames - skill names to include
|
|
229
|
+
* @returns {string} markdown table or fallback message
|
|
230
|
+
*/
|
|
231
|
+
_buildSkillTable(skillNames) {
|
|
232
|
+
const rows = [];
|
|
233
|
+
for (const name of skillNames) {
|
|
234
|
+
const skillPath = this._findSkillFile(name);
|
|
235
|
+
if (!skillPath) continue;
|
|
236
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
237
|
+
const { data } = matter(content);
|
|
238
|
+
const desc = (data.description ?? '').replace(/\n/g, ' ').slice(0, 60);
|
|
239
|
+
const tags = Array.isArray(data.tags) ? data.tags.join(', ') : '';
|
|
240
|
+
rows.push({ name: data.name ?? name, description: desc, tags });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (rows.length === 0) return '_No skills matched._';
|
|
244
|
+
|
|
245
|
+
let table = '| Skill | Focus | Tags |\n|-------|-------|------|\n';
|
|
246
|
+
for (const r of rows) {
|
|
247
|
+
table += `| ${r.name} | ${r.description} | ${r.tags} |\n`;
|
|
248
|
+
}
|
|
249
|
+
return table;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Loads a profile template from lib/profiles/<name>.md.
|
|
254
|
+
* Falls back to 'general' if the requested profile does not exist.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} profileName - profile identifier
|
|
257
|
+
* @returns {string} raw template content
|
|
258
|
+
*/
|
|
259
|
+
_loadProfile(profileName) {
|
|
260
|
+
const profilePath = path.join(PACKAGE_ROOT, 'lib', 'profiles', `${profileName}.md`);
|
|
261
|
+
if (!fs.existsSync(profilePath)) {
|
|
262
|
+
return this._loadProfile('general'); // fallback
|
|
263
|
+
}
|
|
264
|
+
return fs.readFileSync(profilePath, 'utf8');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Renders a profile template by substituting {{variables}} with generated content.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} target - tool target (e.g. 'claude', 'cursor')
|
|
271
|
+
* @param {string} profileName - profile identifier
|
|
272
|
+
* @param {string[]} skillNames - active skill names
|
|
273
|
+
* @param {string} [stackDescription] - user-provided stack description
|
|
274
|
+
* @returns {string} rendered markdown content
|
|
275
|
+
*/
|
|
276
|
+
_renderFromProfile(target, profileName, skillNames, stackDescription) {
|
|
277
|
+
let template = this._loadProfile(profileName);
|
|
278
|
+
|
|
279
|
+
template = template.replace('{{stack}}', stackDescription || '<!-- Not specified -->');
|
|
280
|
+
template = template.replace('{{skills_table}}', this._buildSkillTable(skillNames));
|
|
281
|
+
template = template.replace('{{agent_behaviors}}', this._getAgentBehaviors(target));
|
|
282
|
+
template = template.replace('{{references}}', this._getReferences(target));
|
|
283
|
+
|
|
284
|
+
return template;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Returns the agent behavior instructions block for a given target tool.
|
|
289
|
+
* Uses renderInstinctBlock() for compact, action-oriented triggers.
|
|
290
|
+
*
|
|
291
|
+
* @param {string} target - tool target (e.g. 'claude', 'cursor', 'copilot')
|
|
292
|
+
* @returns {string} markdown block with agent behavior instructions
|
|
293
|
+
*/
|
|
294
|
+
_getAgentBehaviors(target) {
|
|
295
|
+
return renderInstinctBlock(target);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Returns a References section with tool-specific documentation links.
|
|
300
|
+
*
|
|
301
|
+
* @param {string} target - tool target
|
|
302
|
+
* @returns {string} markdown references section
|
|
303
|
+
*/
|
|
304
|
+
_getReferences(target) {
|
|
305
|
+
const url = TOOL_DOCS[target] ?? '';
|
|
306
|
+
const lines = [];
|
|
307
|
+
if (url) lines.push(`- [How to customize this file](${url})`);
|
|
308
|
+
lines.push('- [BookLib documentation](https://booklib-ai.github.io/booklib/)');
|
|
309
|
+
lines.push('- [BookLib skills catalog](https://github.com/booklib-ai/booklib)');
|
|
310
|
+
return '## References\n\n' + lines.join('\n');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Renders extracted blocks into a tool-specific file.
|
|
315
|
+
*/
|
|
316
|
+
_render(target, blocks, skillNames) {
|
|
317
|
+
const principles = blocks
|
|
318
|
+
.filter(b => b.framework)
|
|
319
|
+
.map(b => `### From ${b.skill}\n\n${b.framework.trim()}`)
|
|
320
|
+
.join('\n\n---\n\n');
|
|
321
|
+
|
|
322
|
+
const antiPatterns = blocks
|
|
323
|
+
.filter(b => b.pitfalls)
|
|
324
|
+
.map(b => `### From ${b.skill}\n\n${b.pitfalls.trim()}`)
|
|
325
|
+
.join('\n\n---\n\n');
|
|
326
|
+
|
|
327
|
+
const sources = skillNames.join(', ');
|
|
328
|
+
const generated = `Generated by BookLib from: ${sources}`;
|
|
329
|
+
|
|
330
|
+
const toolDocsUrl = TOOL_DOCS[target] ?? '';
|
|
331
|
+
const referencesSection = `
|
|
332
|
+
### References
|
|
333
|
+
|
|
334
|
+
- ${toolDocsUrl ? `[How to customize this file](${toolDocsUrl})` : 'Customize this file for your project'}
|
|
335
|
+
- [BookLib documentation](https://booklib-ai.github.io/booklib/)
|
|
336
|
+
- [BookLib skills catalog](https://github.com/booklib-ai/booklib)
|
|
337
|
+
`;
|
|
338
|
+
|
|
339
|
+
switch (target) {
|
|
340
|
+
case 'cursor':
|
|
341
|
+
// booklib owns .cursor/rules/booklib-standards.mdc — full overwrite, fileHeader: null signals that
|
|
342
|
+
return {
|
|
343
|
+
filePath: '.cursor/rules/booklib-standards.mdc',
|
|
344
|
+
fileHeader: null,
|
|
345
|
+
content: `---
|
|
346
|
+
description: Coding standards synthesized from ${sources}
|
|
347
|
+
alwaysApply: true
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
<!-- ${generated} -->
|
|
351
|
+
|
|
352
|
+
## Core Principles
|
|
353
|
+
|
|
354
|
+
${principles || '_No structured principles found in selected skills._'}
|
|
355
|
+
|
|
356
|
+
## Anti-Patterns to Avoid
|
|
357
|
+
|
|
358
|
+
${antiPatterns || '_No anti-patterns found in selected skills._'}
|
|
359
|
+
${referencesSection}`,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
case 'claude':
|
|
363
|
+
return {
|
|
364
|
+
filePath: 'CLAUDE.md',
|
|
365
|
+
fileHeader: '',
|
|
366
|
+
content: `<!-- booklib-standards-start -->
|
|
367
|
+
## Coding Standards
|
|
368
|
+
|
|
369
|
+
> ${generated}
|
|
370
|
+
|
|
371
|
+
### Principles
|
|
372
|
+
|
|
373
|
+
${principles || '_No structured principles found._'}
|
|
374
|
+
|
|
375
|
+
### Anti-Patterns
|
|
376
|
+
|
|
377
|
+
${antiPatterns || '_No anti-patterns found._'}
|
|
378
|
+
${referencesSection}<!-- booklib-standards-end -->`,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
case 'copilot':
|
|
382
|
+
return {
|
|
383
|
+
filePath: '.github/copilot-instructions.md',
|
|
384
|
+
fileHeader: '# Copilot Instructions\n\n',
|
|
385
|
+
content: `<!-- booklib-standards-start -->
|
|
386
|
+
<!-- ${generated} -->
|
|
387
|
+
|
|
388
|
+
## What to follow
|
|
389
|
+
|
|
390
|
+
${principles || '_No structured principles found._'}
|
|
391
|
+
|
|
392
|
+
## What to avoid
|
|
393
|
+
|
|
394
|
+
${antiPatterns || '_No anti-patterns found._'}
|
|
395
|
+
${referencesSection}<!-- booklib-standards-end -->
|
|
396
|
+
`,
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
case 'gemini':
|
|
400
|
+
return {
|
|
401
|
+
filePath: '.gemini/context.md',
|
|
402
|
+
fileHeader: '# Project Context\n\n',
|
|
403
|
+
content: `<!-- booklib-standards-start -->
|
|
404
|
+
<!-- ${generated} -->
|
|
405
|
+
|
|
406
|
+
## Coding Standards
|
|
407
|
+
|
|
408
|
+
${principles || '_No structured principles found._'}
|
|
409
|
+
|
|
410
|
+
## Anti-Patterns
|
|
411
|
+
|
|
412
|
+
${antiPatterns || '_No anti-patterns found._'}
|
|
413
|
+
${referencesSection}<!-- booklib-standards-end -->
|
|
414
|
+
`,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
case 'codex':
|
|
418
|
+
return {
|
|
419
|
+
filePath: 'AGENTS.md',
|
|
420
|
+
fileHeader: '# Agent Instructions\n\n',
|
|
421
|
+
content: `<!-- booklib-standards-start -->
|
|
422
|
+
<!-- ${generated} -->
|
|
423
|
+
|
|
424
|
+
## Coding Standards
|
|
425
|
+
|
|
426
|
+
${principles || '_No structured principles found in selected skills._'}
|
|
427
|
+
|
|
428
|
+
## Anti-Patterns to Avoid
|
|
429
|
+
|
|
430
|
+
${antiPatterns || '_No anti-patterns found in selected skills._'}
|
|
431
|
+
${referencesSection}<!-- booklib-standards-end -->
|
|
432
|
+
`,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
case 'windsurf':
|
|
436
|
+
return {
|
|
437
|
+
filePath: '.windsurfrules',
|
|
438
|
+
fileHeader: '',
|
|
439
|
+
content: `<!-- booklib-standards-start -->
|
|
440
|
+
<!-- ${generated} -->
|
|
441
|
+
|
|
442
|
+
## Core Principles
|
|
443
|
+
|
|
444
|
+
${principles || '_No structured principles found in selected skills._'}
|
|
445
|
+
|
|
446
|
+
## Anti-Patterns to Avoid
|
|
447
|
+
|
|
448
|
+
${antiPatterns || '_No anti-patterns found in selected skills._'}
|
|
449
|
+
${referencesSection}<!-- booklib-standards-end -->
|
|
450
|
+
`,
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
case 'roo-code':
|
|
454
|
+
return {
|
|
455
|
+
filePath: '.roo/rules/booklib-standards.md',
|
|
456
|
+
fileHeader: null,
|
|
457
|
+
content: `<!-- booklib-standards-start -->
|
|
458
|
+
<!-- ${generated} -->
|
|
459
|
+
|
|
460
|
+
## Core Principles
|
|
461
|
+
|
|
462
|
+
${principles || '_No structured principles found in selected skills._'}
|
|
463
|
+
|
|
464
|
+
## Anti-Patterns
|
|
465
|
+
|
|
466
|
+
${antiPatterns || '_No anti-patterns found._'}
|
|
467
|
+
${referencesSection}<!-- booklib-standards-end -->
|
|
468
|
+
`,
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
case 'openhands':
|
|
472
|
+
return {
|
|
473
|
+
filePath: '.openhands/instructions.md',
|
|
474
|
+
fileHeader: '# OpenHands Instructions\n\n',
|
|
475
|
+
content: `<!-- booklib-standards-start -->
|
|
476
|
+
<!-- ${generated} -->
|
|
477
|
+
|
|
478
|
+
## Coding Standards
|
|
479
|
+
|
|
480
|
+
${principles || '_No structured principles found._'}
|
|
481
|
+
|
|
482
|
+
## What to Avoid
|
|
483
|
+
|
|
484
|
+
${antiPatterns || '_No anti-patterns found._'}
|
|
485
|
+
${referencesSection}<!-- booklib-standards-end -->
|
|
486
|
+
`,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
case 'junie':
|
|
490
|
+
return {
|
|
491
|
+
filePath: '.junie/guidelines.md',
|
|
492
|
+
fileHeader: '# Junie Guidelines\n\n',
|
|
493
|
+
content: `<!-- booklib-standards-start -->
|
|
494
|
+
<!-- ${generated} -->
|
|
495
|
+
|
|
496
|
+
## Principles
|
|
497
|
+
|
|
498
|
+
${principles || '_No structured principles found._'}
|
|
499
|
+
|
|
500
|
+
## Anti-Patterns
|
|
501
|
+
|
|
502
|
+
${antiPatterns || '_No anti-patterns found._'}
|
|
503
|
+
${referencesSection}<!-- booklib-standards-end -->
|
|
504
|
+
`,
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
case 'goose':
|
|
508
|
+
return {
|
|
509
|
+
filePath: '.goose/context.md',
|
|
510
|
+
fileHeader: '# Goose Context\n\n',
|
|
511
|
+
content: `<!-- booklib-standards-start -->
|
|
512
|
+
<!-- ${generated} -->
|
|
513
|
+
|
|
514
|
+
## Standards
|
|
515
|
+
|
|
516
|
+
${principles || '_No structured principles found._'}
|
|
517
|
+
|
|
518
|
+
## Avoid
|
|
519
|
+
|
|
520
|
+
${antiPatterns || '_No anti-patterns found._'}
|
|
521
|
+
${referencesSection}<!-- booklib-standards-end -->
|
|
522
|
+
`,
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
case 'opencode':
|
|
526
|
+
return {
|
|
527
|
+
filePath: '.opencode/instructions.md',
|
|
528
|
+
fileHeader: '# OpenCode Instructions\n\n',
|
|
529
|
+
content: `<!-- booklib-standards-start -->
|
|
530
|
+
<!-- ${generated} -->
|
|
531
|
+
|
|
532
|
+
## Coding Standards
|
|
533
|
+
|
|
534
|
+
${principles || '_No structured principles found._'}
|
|
535
|
+
|
|
536
|
+
## Anti-Patterns
|
|
537
|
+
|
|
538
|
+
${antiPatterns || '_No anti-patterns found._'}
|
|
539
|
+
${referencesSection}<!-- booklib-standards-end -->
|
|
540
|
+
`,
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
case 'letta':
|
|
544
|
+
return {
|
|
545
|
+
filePath: '.letta/skills/booklib.md',
|
|
546
|
+
fileHeader: null,
|
|
547
|
+
content: `<!-- booklib-standards-start -->
|
|
548
|
+
<!-- ${generated} -->
|
|
549
|
+
|
|
550
|
+
# BookLib Knowledge
|
|
551
|
+
|
|
552
|
+
## Principles
|
|
553
|
+
|
|
554
|
+
${principles || '_No structured principles found._'}
|
|
555
|
+
|
|
556
|
+
## Anti-Patterns
|
|
557
|
+
|
|
558
|
+
${antiPatterns || '_No anti-patterns found._'}
|
|
559
|
+
${referencesSection}<!-- booklib-standards-end -->
|
|
560
|
+
`,
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
default:
|
|
564
|
+
throw new Error(
|
|
565
|
+
`Unknown target: ${target}. Valid values: claude, cursor, copilot, gemini, codex, windsurf, roo-code, openhands, junie, goose, opencode, letta, all, auto`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Returns additional skill names worth knowing about, based on detected skills
|
|
572
|
+
* and project characteristics. Used for discovery hints — NOT injected into CLAUDE.md.
|
|
573
|
+
*/
|
|
574
|
+
suggestRelatedSkills(detectedSkills, projectCwd) {
|
|
575
|
+
const suggestions = new Set();
|
|
576
|
+
const packageJsonPath = path.join(projectCwd, 'package.json');
|
|
577
|
+
let pkg = {};
|
|
578
|
+
try { pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); } catch { /* no package.json */ }
|
|
579
|
+
const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
|
|
580
|
+
|
|
581
|
+
for (const skill of detectedSkills) {
|
|
582
|
+
if (skill === 'clean-code-reviewer') {
|
|
583
|
+
suggestions.add('node-error-handling');
|
|
584
|
+
if (deps.some(d => ['express', 'fastify', 'koa', 'hono'].includes(d))) {
|
|
585
|
+
suggestions.add('owasp-input-validation');
|
|
586
|
+
suggestions.add('node-security-validation');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (skill === 'effective-typescript') {
|
|
590
|
+
suggestions.add('clean-code-reviewer');
|
|
591
|
+
suggestions.add('node-error-handling');
|
|
592
|
+
}
|
|
593
|
+
if (skill === 'effective-python') {
|
|
594
|
+
suggestions.add('django-security');
|
|
595
|
+
}
|
|
596
|
+
if (skill === 'effective-java') {
|
|
597
|
+
suggestions.add('springboot-patterns');
|
|
598
|
+
}
|
|
599
|
+
if (skill === 'effective-kotlin') {
|
|
600
|
+
suggestions.add('kotlin-testing');
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Remove already-detected skills from suggestions
|
|
605
|
+
for (const s of detectedSkills) suggestions.delete(s);
|
|
606
|
+
return [...suggestions].slice(0, 3);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ── ECC Artifact fetching ──────────────────────────────────────────────────
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Pulls rules/, agents/, and commands/ from all configured github-skills-dir sources.
|
|
613
|
+
*
|
|
614
|
+
* Sources opt in by including an "artifacts" array in booklib.config.json:
|
|
615
|
+
* { "type": "github-skills-dir", "repo": "...", "artifacts": ["rules", "agents", "commands"] }
|
|
616
|
+
*
|
|
617
|
+
* When multiple sources export the same artifact type, files are prefixed with the repo
|
|
618
|
+
* slug (last path segment of owner/repo) to avoid collisions.
|
|
619
|
+
*
|
|
620
|
+
* @param {object} opts
|
|
621
|
+
* @param {string[]|null} [opts.languages] - language folders for rules/ (null = all)
|
|
622
|
+
* @param {boolean} [opts.includeAgents] - pull agents/ → .claude/agents/
|
|
623
|
+
* @param {boolean} [opts.includeCommands] - pull commands/ → .claude/commands/
|
|
624
|
+
* @param {boolean} [opts.dryRun] - print what would be written without writing
|
|
625
|
+
* @returns {string[]} list of files written
|
|
626
|
+
*/
|
|
627
|
+
async fetchEccArtifacts({ languages = null, includeAgents = true, includeCommands = true, dryRun = false } = {}) {
|
|
628
|
+
const artifactSources = this.config.sources.filter(
|
|
629
|
+
s => s.type === 'github-skills-dir' && Array.isArray(s.artifacts) && s.artifacts.length > 0
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
if (artifactSources.length === 0) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
'No artifact-capable sources found. Add "artifacts": ["rules","agents","commands"] to a ' +
|
|
635
|
+
'github-skills-dir entry in booklib.config.json.'
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Determine which artifact types appear in more than one source — those need a prefix.
|
|
640
|
+
const typeCounts = { rules: 0, agents: 0, commands: 0 };
|
|
641
|
+
for (const src of artifactSources) {
|
|
642
|
+
if (src.artifacts.includes('rules')) typeCounts.rules++;
|
|
643
|
+
if (src.artifacts.includes('agents')) typeCounts.agents++;
|
|
644
|
+
if (src.artifacts.includes('commands')) typeCounts.commands++;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const written = [];
|
|
648
|
+
|
|
649
|
+
for (const source of artifactSources) {
|
|
650
|
+
const { repo, branch = 'main', artifacts: artifactList } = source;
|
|
651
|
+
// Derive a short slug from the repo name (owner/repo → repo segment)
|
|
652
|
+
const slug = repo.split('/').pop().replace(/[^a-z0-9]/gi, '-').toLowerCase();
|
|
653
|
+
|
|
654
|
+
if (languages !== false && artifactList.includes('rules')) {
|
|
655
|
+
const prefix = typeCounts.rules > 1 ? `${slug}-` : '';
|
|
656
|
+
written.push(...await this._pullRules(repo, branch, languages, dryRun, prefix));
|
|
657
|
+
}
|
|
658
|
+
if (includeAgents && artifactList.includes('agents')) {
|
|
659
|
+
const prefix = typeCounts.agents > 1 ? `${slug}-` : '';
|
|
660
|
+
written.push(...await this._pullDir(repo, branch, 'agents', '.claude/agents', dryRun, prefix));
|
|
661
|
+
}
|
|
662
|
+
if (includeCommands && artifactList.includes('commands')) {
|
|
663
|
+
const prefix = typeCounts.commands > 1 ? `${slug}-` : '';
|
|
664
|
+
written.push(...await this._pullDir(repo, branch, 'commands', '.claude/commands', dryRun, prefix));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return written;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Pulls rules/<language>/*.md → .cursor/rules/[prefix]<language>-<file>.mdc */
|
|
672
|
+
async _pullRules(repo, branch, languages, dryRun, prefix = '') {
|
|
673
|
+
const written = [];
|
|
674
|
+
let langDirs;
|
|
675
|
+
try {
|
|
676
|
+
const entries = await this._fetchJson(`https://api.github.com/repos/${repo}/contents/rules`);
|
|
677
|
+
if (!Array.isArray(entries)) return [];
|
|
678
|
+
langDirs = entries.filter(e => e.type === 'dir').map(e => e.name);
|
|
679
|
+
} catch {
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (languages && languages.length > 0) {
|
|
684
|
+
langDirs = langDirs.filter(d => languages.includes(d));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
for (const lang of langDirs) {
|
|
688
|
+
let files;
|
|
689
|
+
try {
|
|
690
|
+
const entries = await this._fetchJson(`https://api.github.com/repos/${repo}/contents/rules/${lang}`);
|
|
691
|
+
files = Array.isArray(entries) ? entries.filter(e => e.type === 'file' && e.name.endsWith('.md')) : [];
|
|
692
|
+
} catch {
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
for (const file of files) {
|
|
697
|
+
const rawUrl = `https://raw.githubusercontent.com/${repo}/${branch}/rules/${lang}/${file.name}`;
|
|
698
|
+
const destName = `${prefix}${lang}-${file.name.replace(/\.md$/, '.mdc')}`;
|
|
699
|
+
const destPath = `.cursor/rules/${destName}`;
|
|
700
|
+
const absPath = path.join(this.projectCwd, destPath);
|
|
701
|
+
|
|
702
|
+
if (dryRun) {
|
|
703
|
+
console.log(`[dry-run] Would write: ${destPath} (from ${rawUrl})`);
|
|
704
|
+
written.push(destPath);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
let content;
|
|
709
|
+
try { content = await this._fetchText(rawUrl); } catch { continue; }
|
|
710
|
+
|
|
711
|
+
if (!content.trimStart().startsWith('---')) {
|
|
712
|
+
content = `---\ndescription: ${lang} ${file.name.replace('.md', '')} rules\nalwaysApply: false\n---\n\n${content}`;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
716
|
+
fs.writeFileSync(absPath, content);
|
|
717
|
+
console.log(` ✅ ${destPath}`);
|
|
718
|
+
written.push(destPath);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return written;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** Pulls <srcDir>/*.md → <destDir>/[prefix]<file>.md */
|
|
726
|
+
async _pullDir(repo, branch, srcDir, destDir, dryRun, prefix = '') {
|
|
727
|
+
const written = [];
|
|
728
|
+
let files;
|
|
729
|
+
try {
|
|
730
|
+
const entries = await this._fetchJson(`https://api.github.com/repos/${repo}/contents/${srcDir}`);
|
|
731
|
+
files = Array.isArray(entries) ? entries.filter(e => e.type === 'file' && e.name.endsWith('.md')) : [];
|
|
732
|
+
} catch {
|
|
733
|
+
return [];
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
for (const file of files) {
|
|
737
|
+
const rawUrl = `https://raw.githubusercontent.com/${repo}/${branch}/${srcDir}/${file.name}`;
|
|
738
|
+
const destPath = `${destDir}/${prefix}${file.name}`;
|
|
739
|
+
const absPath = path.join(this.projectCwd, destPath);
|
|
740
|
+
|
|
741
|
+
if (dryRun) {
|
|
742
|
+
console.log(`[dry-run] Would write: ${destPath} (from ${rawUrl})`);
|
|
743
|
+
written.push(destPath);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
let content;
|
|
748
|
+
try { content = await this._fetchText(rawUrl); } catch { continue; }
|
|
749
|
+
|
|
750
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
751
|
+
fs.writeFileSync(absPath, content);
|
|
752
|
+
console.log(` ✅ ${destPath}`);
|
|
753
|
+
written.push(destPath);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return written;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Writes MCP server config files for the selected tools.
|
|
761
|
+
*
|
|
762
|
+
* @param {object} opts
|
|
763
|
+
* @param {string[]} opts.tools - tool names: 'claude'|'cursor'|'copilot'|'gemini'|'codex'|'roo-code'|'windsurf'|'goose'|'zed'|'continue'
|
|
764
|
+
* @param {boolean} [opts.dryRun]
|
|
765
|
+
* @returns {string[]} list of files written
|
|
766
|
+
*/
|
|
767
|
+
async generateMcpConfigs({ tools = [], dryRun = false } = {}) {
|
|
768
|
+
const written = [];
|
|
769
|
+
for (const tool of tools) {
|
|
770
|
+
const config = this._renderMcpConfig(tool);
|
|
771
|
+
if (!config) continue;
|
|
772
|
+
const { filePath, mode } = config;
|
|
773
|
+
const absPath = config.global ? filePath : path.join(this.projectCwd, filePath);
|
|
774
|
+
if (dryRun) {
|
|
775
|
+
console.log(`[dry-run] Would write MCP config: ${filePath}`);
|
|
776
|
+
written.push(filePath);
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
780
|
+
if (mode === 'json-merge') {
|
|
781
|
+
this._mergeJsonMcpServer(absPath, filePath, config.mcpKey, config.mcpValue);
|
|
782
|
+
} else if (mode === 'toml-merge') {
|
|
783
|
+
this._mergeTomlMcpSection(absPath);
|
|
784
|
+
} else if (mode === 'yaml-merge') {
|
|
785
|
+
this._mergeGooseYaml(absPath);
|
|
786
|
+
} else {
|
|
787
|
+
fs.writeFileSync(absPath, config.content);
|
|
788
|
+
}
|
|
789
|
+
console.log(` ✅ ${filePath}`);
|
|
790
|
+
written.push(filePath);
|
|
791
|
+
}
|
|
792
|
+
return written;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** Returns a descriptor for writing the MCP config for a given tool. */
|
|
796
|
+
_renderMcpConfig(tool) {
|
|
797
|
+
const BOOKLIB_ENTRY = { command: 'booklib-mcp', args: [] };
|
|
798
|
+
switch (tool) {
|
|
799
|
+
case 'claude':
|
|
800
|
+
return { filePath: '.claude/settings.json', mode: 'json-merge', mcpKey: ['mcpServers', 'booklib'], mcpValue: BOOKLIB_ENTRY };
|
|
801
|
+
case 'cursor':
|
|
802
|
+
return { filePath: '.cursor/mcp.json', mode: 'json-merge', mcpKey: ['mcpServers', 'booklib'], mcpValue: BOOKLIB_ENTRY };
|
|
803
|
+
case 'copilot':
|
|
804
|
+
return { filePath: '.vscode/mcp.json', mode: 'json-merge', mcpKey: ['servers', 'booklib'], mcpValue: BOOKLIB_ENTRY };
|
|
805
|
+
case 'gemini':
|
|
806
|
+
return { filePath: '.gemini/settings.json', mode: 'json-merge', mcpKey: ['mcpServers', 'booklib'], mcpValue: BOOKLIB_ENTRY };
|
|
807
|
+
case 'codex':
|
|
808
|
+
return { filePath: '.codex/config.toml', mode: 'toml-merge' };
|
|
809
|
+
case 'roo-code':
|
|
810
|
+
return { filePath: '.roo/mcp.json', mode: 'json-merge', mcpKey: ['mcpServers', 'booklib'], mcpValue: BOOKLIB_ENTRY };
|
|
811
|
+
case 'windsurf': {
|
|
812
|
+
const windsurfPath = path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
|
|
813
|
+
return { filePath: windsurfPath, mode: 'json-merge', mcpKey: ['mcpServers', 'booklib'], mcpValue: BOOKLIB_ENTRY, global: true };
|
|
814
|
+
}
|
|
815
|
+
case 'goose':
|
|
816
|
+
return { filePath: '.goose/config.yaml', mode: 'yaml-merge' };
|
|
817
|
+
case 'zed':
|
|
818
|
+
return { filePath: '.zed/settings.json', mode: 'json-merge', mcpKey: ['context_servers', 'booklib-mcp'], mcpValue: { command: { path: 'booklib-mcp', args: [] } } };
|
|
819
|
+
case 'continue':
|
|
820
|
+
return { filePath: '.continue/mcpServers/booklib.yaml', mode: 'overwrite', content: 'name: booklib\ncommand: booklib-mcp\nargs: []\n' };
|
|
821
|
+
default:
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** Reads an existing JSON config (if any), sets keyPath to value, writes back. */
|
|
827
|
+
_mergeJsonMcpServer(absPath, filePath, keyPath, value) {
|
|
828
|
+
let root = {};
|
|
829
|
+
if (fs.existsSync(absPath)) {
|
|
830
|
+
try {
|
|
831
|
+
root = JSON.parse(fs.readFileSync(absPath, 'utf8'));
|
|
832
|
+
} catch {
|
|
833
|
+
console.warn(` ⚠️ Could not parse ${filePath} — writing fresh`);
|
|
834
|
+
root = {};
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
let node = root;
|
|
838
|
+
for (let i = 0; i < keyPath.length - 1; i++) {
|
|
839
|
+
if (!node[keyPath[i]] || typeof node[keyPath[i]] !== 'object') node[keyPath[i]] = {};
|
|
840
|
+
node = node[keyPath[i]];
|
|
841
|
+
}
|
|
842
|
+
node[keyPath[keyPath.length - 1]] = value;
|
|
843
|
+
fs.writeFileSync(absPath, JSON.stringify(root, null, 2) + '\n');
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/** Appends or replaces the [mcp_servers.booklib] section in a TOML file. */
|
|
847
|
+
_mergeTomlMcpSection(absPath) {
|
|
848
|
+
const BOOKLIB_BLOCK = '[mcp_servers.booklib]\ncommand = "booklib-mcp"\nargs = []\n';
|
|
849
|
+
// Match from the section header to the next section header or end of string
|
|
850
|
+
const SECTION_RE = /\[mcp_servers\.booklib\][\s\S]*?(?=\n\[|$)/;
|
|
851
|
+
|
|
852
|
+
let existing = '';
|
|
853
|
+
if (fs.existsSync(absPath)) {
|
|
854
|
+
existing = fs.readFileSync(absPath, 'utf8');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (SECTION_RE.test(existing)) {
|
|
858
|
+
fs.writeFileSync(absPath, existing.replace(SECTION_RE, BOOKLIB_BLOCK.trimEnd()));
|
|
859
|
+
} else {
|
|
860
|
+
fs.writeFileSync(absPath, existing + (existing.endsWith('\n') ? '' : '\n') + '\n' + BOOKLIB_BLOCK);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/** Appends or merges the booklib entry in a Goose YAML config. */
|
|
865
|
+
_mergeGooseYaml(absPath) {
|
|
866
|
+
const entry = '\nmcp_servers:\n booklib:\n command: booklib-mcp\n args: []\n';
|
|
867
|
+
if (fs.existsSync(absPath)) {
|
|
868
|
+
const content = fs.readFileSync(absPath, 'utf8');
|
|
869
|
+
if (content.includes('booklib:')) return; // already exists
|
|
870
|
+
if (content.includes('mcp_servers:')) {
|
|
871
|
+
// Append under existing mcp_servers section
|
|
872
|
+
const updated = content.replace('mcp_servers:', 'mcp_servers:\n booklib:\n command: booklib-mcp\n args: []');
|
|
873
|
+
fs.writeFileSync(absPath, updated);
|
|
874
|
+
} else {
|
|
875
|
+
fs.appendFileSync(absPath, entry);
|
|
876
|
+
}
|
|
877
|
+
} else {
|
|
878
|
+
fs.writeFileSync(absPath, entry.trim() + '\n');
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ── HTTP helpers ───────────────────────────────────────────────────────────
|
|
883
|
+
|
|
884
|
+
_fetchJson(url) {
|
|
885
|
+
return new Promise((resolve, reject) => {
|
|
886
|
+
https.get(url, { headers: { 'User-Agent': 'booklib-init/1.0' } }, res => {
|
|
887
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
888
|
+
return resolve(this._fetchJson(res.headers.location));
|
|
889
|
+
}
|
|
890
|
+
let data = '';
|
|
891
|
+
res.on('data', c => (data += c));
|
|
892
|
+
res.on('end', () => {
|
|
893
|
+
try { resolve(JSON.parse(data)); } catch { reject(new Error('Invalid JSON')); }
|
|
894
|
+
});
|
|
895
|
+
}).on('error', reject);
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
_fetchText(url) {
|
|
900
|
+
return new Promise((resolve, reject) => {
|
|
901
|
+
https.get(url, { headers: { 'User-Agent': 'booklib-init/1.0' } }, res => {
|
|
902
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
903
|
+
return resolve(this._fetchText(res.headers.location));
|
|
904
|
+
}
|
|
905
|
+
if (res.statusCode !== 200) {
|
|
906
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
907
|
+
res.resume();
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
let data = '';
|
|
911
|
+
res.on('data', c => (data += c));
|
|
912
|
+
res.on('end', () => resolve(data));
|
|
913
|
+
}).on('error', reject);
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|