superlocalmemory 3.6.23 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +271 -71
  3. package/bin/slm-npm +43 -89
  4. package/ide/configs/antigravity-mcp.json +2 -2
  5. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  6. package/ide/configs/claude-desktop-mcp.json +2 -2
  7. package/ide/configs/windsurf-mcp.json +2 -2
  8. package/ide/hooks/context-hook.js +6 -2
  9. package/ide/hooks/post-recall-hook.js +7 -3
  10. package/ide/hooks/tool-event-hook.sh +2 -1
  11. package/package.json +17 -9
  12. package/plugin/.claude-plugin/plugin.json +1 -1
  13. package/plugin/_GENERATED.md +1 -1
  14. package/plugin/agents/slm-memory-advisor.md +1 -1
  15. package/plugin/requirements.txt +1 -1
  16. package/plugin/skills/slm-session/SKILL.md +1 -1
  17. package/plugin-src/agents/slm-memory-advisor.md +1 -1
  18. package/plugin-src/manifest.json +1 -1
  19. package/plugin-src/requirements.txt +1 -1
  20. package/plugin-src/rules/AGENTS.md +1 -1
  21. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  22. package/pyproject.toml +40 -8
  23. package/scripts/postinstall-interactive.js +17 -94
  24. package/scripts/postinstall.js +185 -258
  25. package/scripts/preuninstall.js +9 -50
  26. package/src/superlocalmemory/__init__.py +2 -2
  27. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  28. package/src/superlocalmemory/attribution/signer.py +34 -19
  29. package/src/superlocalmemory/attribution/watermark.py +1 -1
  30. package/src/superlocalmemory/cli/_lazy_init.py +7 -7
  31. package/src/superlocalmemory/cli/commands.py +453 -191
  32. package/src/superlocalmemory/cli/context_commands.py +5 -4
  33. package/src/superlocalmemory/cli/daemon.py +282 -187
  34. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  35. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  36. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  37. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  38. package/src/superlocalmemory/cli/main.py +128 -31
  39. package/src/superlocalmemory/cli/pending_store.py +54 -38
  40. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  41. package/src/superlocalmemory/cli/service_installer.py +57 -52
  42. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  43. package/src/superlocalmemory/cli/version_banner.py +2 -1
  44. package/src/superlocalmemory/code_graph/config.py +3 -1
  45. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  46. package/src/superlocalmemory/core/config.py +65 -20
  47. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  48. package/src/superlocalmemory/core/context_cache.py +56 -8
  49. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  50. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  51. package/src/superlocalmemory/core/embeddings.py +54 -18
  52. package/src/superlocalmemory/core/engine.py +150 -104
  53. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  54. package/src/superlocalmemory/core/engine_wiring.py +32 -23
  55. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  56. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  57. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  58. package/src/superlocalmemory/core/health_monitor.py +4 -2
  59. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  60. package/src/superlocalmemory/core/injection.py +69 -18
  61. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  62. package/src/superlocalmemory/core/maintenance.py +1 -1
  63. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  64. package/src/superlocalmemory/core/mutations.py +143 -0
  65. package/src/superlocalmemory/core/platform_utils.py +7 -4
  66. package/src/superlocalmemory/core/ram_lock.py +16 -5
  67. package/src/superlocalmemory/core/rate_limit.py +1 -1
  68. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  69. package/src/superlocalmemory/core/recall_worker.py +76 -59
  70. package/src/superlocalmemory/core/registry.py +1 -1
  71. package/src/superlocalmemory/core/scale_engine.py +293 -0
  72. package/src/superlocalmemory/core/score_contract.py +62 -0
  73. package/src/superlocalmemory/core/security_primitives.py +3 -1
  74. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  75. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  76. package/src/superlocalmemory/core/tier_manager.py +32 -20
  77. package/src/superlocalmemory/core/worker_pool.py +13 -4
  78. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  79. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  80. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  81. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  82. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  83. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  84. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  85. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  86. package/src/superlocalmemory/encoding/foresight.py +2 -0
  87. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  88. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  89. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  90. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  91. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  92. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  93. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  94. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  95. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  96. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  97. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  98. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  99. package/src/superlocalmemory/hooks/claude_code_hooks.py +123 -35
  100. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  101. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  102. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  103. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  104. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  105. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  106. package/src/superlocalmemory/hooks/hook_handlers.py +219 -32
  107. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  108. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  109. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  110. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  111. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  112. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  113. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  114. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  115. package/src/superlocalmemory/infra/backup.py +7 -5
  116. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  117. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  118. package/src/superlocalmemory/infra/data_root.py +199 -0
  119. package/src/superlocalmemory/infra/event_bus.py +3 -1
  120. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  121. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  122. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  123. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  124. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  125. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  126. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  127. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  128. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  129. package/src/superlocalmemory/learning/ensemble.py +11 -0
  130. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  131. package/src/superlocalmemory/learning/feedback.py +1 -1
  132. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  133. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  134. package/src/superlocalmemory/learning/ranker.py +4 -1
  135. package/src/superlocalmemory/learning/source_quality.py +1 -1
  136. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  137. package/src/superlocalmemory/llm/backbone.py +1 -1
  138. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  139. package/src/superlocalmemory/math/fisher.py +1 -1
  140. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  141. package/src/superlocalmemory/math/hopfield.py +1 -1
  142. package/src/superlocalmemory/math/langevin.py +1 -1
  143. package/src/superlocalmemory/math/polar_quant.py +3 -4
  144. package/src/superlocalmemory/math/qjl.py +1 -1
  145. package/src/superlocalmemory/math/sheaf.py +1 -1
  146. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  147. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  148. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  149. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  150. package/src/superlocalmemory/mcp/server.py +39 -13
  151. package/src/superlocalmemory/mcp/shared.py +69 -3
  152. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  153. package/src/superlocalmemory/mcp/tools_core.py +130 -31
  154. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  155. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  156. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  157. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  158. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  159. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  160. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  161. package/src/superlocalmemory/mesh/broker.py +2 -0
  162. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  163. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  164. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  165. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  166. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  167. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  168. package/src/superlocalmemory/optimize/config/store.py +6 -4
  169. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  170. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  171. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  172. package/src/superlocalmemory/optimize/storage/db.py +12 -12
  173. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  174. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  175. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  176. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  177. package/src/superlocalmemory/retrieval/engine.py +169 -79
  178. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  179. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  180. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  181. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  182. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  183. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  184. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  185. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  186. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  187. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  188. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  189. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  190. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  191. package/src/superlocalmemory/server/api.py +10 -7
  192. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  193. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  194. package/src/superlocalmemory/server/route_mutations.py +84 -0
  195. package/src/superlocalmemory/server/routes/agents.py +8 -6
  196. package/src/superlocalmemory/server/routes/brain.py +14 -12
  197. package/src/superlocalmemory/server/routes/chat.py +29 -12
  198. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  199. package/src/superlocalmemory/server/routes/helpers.py +8 -63
  200. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  201. package/src/superlocalmemory/server/routes/memories.py +104 -43
  202. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  203. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  204. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  205. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  206. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  207. package/src/superlocalmemory/server/security_middleware.py +1 -1
  208. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  209. package/src/superlocalmemory/server/write_identity.py +147 -0
  210. package/src/superlocalmemory/storage/access_log.py +4 -3
  211. package/src/superlocalmemory/storage/database.py +118 -25
  212. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  213. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  214. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  215. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  216. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  217. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  218. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  219. package/src/superlocalmemory/storage/models.py +16 -0
  220. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  221. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  222. package/src/superlocalmemory/ui/favicon.svg +5 -0
  223. package/src/superlocalmemory/ui/index.html +1 -0
  224. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  225. package/src/superlocalmemory/ui/js/core.js +49 -8
  226. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  227. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  228. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  229. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  230. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  231. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  232. package/src/superlocalmemory/ui/js/settings.js +4 -2
  233. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  234. package/bin/slm +0 -59
  235. package/bin/slm.bat +0 -77
  236. package/bin/slm.cmd +0 -5
  237. package/ide/integrations/langchain/README.md +0 -106
  238. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  239. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  240. package/ide/integrations/langchain/pyproject.toml +0 -38
  241. package/ide/integrations/langchain/tests/__init__.py +0 -3
  242. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  243. package/ide/integrations/langchain/tests/test_security.py +0 -117
  244. package/ide/integrations/llamaindex/README.md +0 -81
  245. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  246. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  247. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  248. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  249. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  250. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  251. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  252. package/scripts/_savings_math.py +0 -270
  253. package/scripts/build-dmg.sh +0 -417
  254. package/scripts/build-plugin.js +0 -742
  255. package/scripts/build-slm-hook.ps1 +0 -40
  256. package/scripts/build-slm-hook.sh +0 -45
  257. package/scripts/build_entry.py +0 -452
  258. package/scripts/ci/stage5b_gate.sh +0 -50
  259. package/scripts/dogfood_savings.py +0 -490
  260. package/scripts/generate-thumbnails.py +0 -218
  261. package/scripts/install-skills.ps1 +0 -4
  262. package/scripts/install-skills.sh +0 -5
  263. package/scripts/install.ps1 +0 -701
  264. package/scripts/install.sh +0 -1015
  265. package/scripts/postinstall_binary.js +0 -287
  266. package/scripts/prepack.js +0 -33
  267. package/scripts/release_manifest.py +0 -273
  268. package/scripts/slm-hook.spec +0 -56
  269. package/scripts/start-dashboard.ps1 +0 -52
  270. package/scripts/start-dashboard.sh +0 -41
  271. package/scripts/sync-wiki.ps1 +0 -127
  272. package/scripts/sync-wiki.sh +0 -82
  273. package/scripts/test-dmg.sh +0 -161
  274. package/scripts/test-npm-package.ps1 +0 -252
  275. package/scripts/test-npm-package.sh +0 -207
  276. package/scripts/verify-install.ps1 +0 -294
  277. package/scripts/verify-install.sh +0 -266
  278. package/scripts/verify-v27.ps1 +0 -301
  279. package/scripts/verify-v27.sh +0 -233
  280. package/src/superlocalmemory.egg-info/PKG-INFO +0 -516
  281. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  282. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  283. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  284. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  285. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -1,742 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * build-plugin.js — SLM v3.6.14 plugin-src single-source build generator
4
- *
5
- * DOC-CORRECT layout (verified from official Claude Code plugin docs):
6
- * .claude-plugin/marketplace.json (repo root; source:"./plugin")
7
- * plugin/ (PLUGIN ROOT)
8
- * .claude-plugin/plugin.json (manifest ONLY here; version ONLY here)
9
- * skills/<7>/SKILL.md
10
- * agents/<2>.md
11
- * .mcp.json (SLM_MCP_PROFILE=code)
12
- * hooks/hooks.json
13
- * scripts/ensure-venv.sh (+x)
14
- * settings.json · requirements.txt · CLAUDE.md
15
- *
16
- * Node >= 18. Zero external dependencies (only node:fs/path/crypto/process).
17
- * Exit codes: 0 = success/in-sync 1 = fatal error 2 = --check drift detected
18
- *
19
- * Usage:
20
- * node scripts/build-plugin.js [--check] [--quiet] [--help]
21
- */
22
-
23
- 'use strict';
24
-
25
- import fs from 'node:fs';
26
- import path from 'node:path';
27
- import crypto from 'node:crypto';
28
- import process from 'node:process';
29
-
30
- // ---------------------------------------------------------------------------
31
- // Constants
32
- // ---------------------------------------------------------------------------
33
- const VERSION = '3.6.19';
34
- const MANIFEST_REL = 'plugin-src/manifest.json';
35
- const GENERATED_BANNER = `# _GENERATED — DO NOT HAND-EDIT
36
-
37
- This directory is generated by \`scripts/build-plugin.js\` from \`plugin-src/\`.
38
- Run \`npm run build:plugin\` to regenerate.
39
- Run \`npm run check:plugin\` (or \`--check\`) in CI to verify in-sync.
40
- Source of truth: \`plugin-src/skills/\`, \`plugin-src/manifest.json\`.
41
- `;
42
-
43
- const _SKIP_DIRS = new Set(['.backup', '.git', 'node_modules', 'build', 'venv', '__pycache__']);
44
-
45
- // ---------------------------------------------------------------------------
46
- // Pure utilities
47
- // ---------------------------------------------------------------------------
48
-
49
- /**
50
- * Normalize line endings to LF, ensure exactly one trailing newline.
51
- * @param {string} s
52
- * @returns {string}
53
- */
54
- export function normalizeNewlines(s) {
55
- const normalized = s.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
56
- return normalized.replace(/\n*$/, '\n');
57
- }
58
-
59
- /**
60
- * Stable JSON serializer (recursive sorted keys, 2-space indent, LF, trailing newline).
61
- * Prevents the array-replacer bug that silently drops nested keys.
62
- * @param {unknown} obj
63
- * @returns {string}
64
- */
65
- export function stableStringify(obj) {
66
- function sort(val) {
67
- if (Array.isArray(val)) return val.map(sort);
68
- if (val !== null && typeof val === 'object') {
69
- const out = {};
70
- for (const k of Object.keys(val).sort()) {
71
- out[k] = sort(val[k]);
72
- }
73
- return out;
74
- }
75
- return val;
76
- }
77
- return JSON.stringify(sort(obj), null, 2) + '\n';
78
- }
79
-
80
- /**
81
- * Replace the frontmatter `version:` line with the given version string.
82
- * ONLY touches the YAML frontmatter block (between leading `---`).
83
- * Throws if no version line found in frontmatter.
84
- * @param {string} md
85
- * @param {string} ver
86
- * @returns {string}
87
- */
88
- export function stampVersion(md, ver) {
89
- // Match frontmatter: opening ---, content, closing ---
90
- const fmMatch = md.match(/^(---\n)([\s\S]*?)(^---\n)/m);
91
- if (!fmMatch) {
92
- throw new Error('stampVersion: no YAML frontmatter found');
93
- }
94
- const [fullMatch, open, body, close] = fmMatch;
95
- if (!/^version:/m.test(body)) {
96
- throw new Error('stampVersion: no version: line in frontmatter');
97
- }
98
- const newBody = body.replace(/^version:.*$/m, `version: "${ver}"`);
99
- const newFm = open + newBody + close;
100
- return md.replace(fullMatch, newFm);
101
- }
102
-
103
- /**
104
- * OQ-2 attribution normalization — ANCHORED to attribution footer patterns only.
105
- * Does NOT rewrite prose like "API v2" or "the MIT license".
106
- *
107
- * Patterns fixed (in footer block):
108
- * **Project:** SuperLocalMemory V2 => **Project:** SuperLocalMemory v3.6.14
109
- * **License:** MIT (see ...) => **License:** AGPL-3.0-or-later
110
- * **Repository:** https://github.com/varun369/SuperLocalMemoryV2
111
- * => **Repository:** https://github.com/qualixar/superlocalmemory
112
- *
113
- * Also fixes the corrupted L87 table row in slm-recall.
114
- * @param {string} md
115
- * @param {string} ver
116
- * @returns {string}
117
- */
118
- function normalizeAttribution(md, ver) {
119
- let out = md;
120
-
121
- // Fix attribution footer lines (prefixed with **Project:**, **License:**, **Repository:**)
122
- // These are anchored to line-start with **...: patterns.
123
- out = out.replace(
124
- /^(\*\*Project:\*\*\s+SuperLocalMemory\s+)V2/mg,
125
- `$1v${ver}`
126
- );
127
- out = out.replace(
128
- /^(\*\*License:\*\*\s+)MIT(\s*\(see[^)]*\))?/mg,
129
- `$1AGPL-3.0-or-later`
130
- );
131
- out = out.replace(
132
- /^(\*\*Repository:\*\*\s+)https:\/\/github\.com\/varun369\/SuperLocalMemoryV2/mg,
133
- `$1https://github.com/qualixar/superlocalmemory`
134
- );
135
-
136
- // Fix corrupted L87 table row in slm-recall
137
- // Original: | `--min-score` | float | No | 0.3 | Minimum relevance (0.0-1-tags` | string | No | None | Filter by tags (comma-separated) |
138
- // Fix: split into two correct rows
139
- out = out.replace(
140
- /\| `--min-score` \| float \| No \| 0\.3 \| Minimum relevance \(0\.0-1-tags` \| string \| No \| None \| Filter by tags \(comma-separated\) \|/g,
141
- '| `--min-score` | float | No | 0.3 | Minimum relevance (0.0-1.0) |\n| `--tags` | string | No | None | Filter by tags (comma-separated) |'
142
- );
143
-
144
- return out;
145
- }
146
-
147
- /**
148
- * Full skill file rendering: normalizeNewlines → stampVersion → normalizeAttribution → normalizeNewlines.
149
- * Skills without a `version:` line in frontmatter are copied verbatim (no stamp).
150
- * @param {string} src raw source content
151
- * @param {string} ver version to stamp
152
- * @returns {string}
153
- */
154
- export function renderSkillFile(src, ver) {
155
- let out = normalizeNewlines(src);
156
- // Only stamp if there is a version: line in frontmatter — new skills may not have one
157
- const fmMatch = out.match(/^(---\n)([\s\S]*?)(^---\n)/m);
158
- if (fmMatch && /^version:/m.test(fmMatch[2])) {
159
- out = stampVersion(out, ver);
160
- }
161
- out = normalizeAttribution(out, ver);
162
- out = normalizeNewlines(out);
163
- return out;
164
- }
165
-
166
- /**
167
- * Render plugin.json — full §5 schema. version ONLY here.
168
- * Paths are relative to the plugin root (= plugin/).
169
- * Uses stableStringify — no nested key dropping.
170
- * @param {object} manifest
171
- * @returns {string}
172
- */
173
- export function renderPluginJson(manifest) {
174
- const obj = {
175
- author: {
176
- name: 'Qualixar',
177
- url: manifest.repository || 'https://github.com/qualixar/superlocalmemory',
178
- },
179
- description: manifest.marketplace.description,
180
- displayName: manifest.displayName || manifest.pluginName,
181
- hooks: './.mcp.json' === manifest.targets.plugin ? './hooks/hooks.json' : './hooks/hooks.json',
182
- keywords: manifest.keywords || [],
183
- mcpServers: './.mcp.json',
184
- name: manifest.pluginName,
185
- repository: manifest.repository || '',
186
- version: manifest.version,
187
- };
188
- // hooks pointer is always ./hooks/hooks.json relative to plugin root
189
- obj.hooks = './hooks/hooks.json';
190
- obj.mcpServers = './.mcp.json';
191
- return stableStringify(obj);
192
- }
193
-
194
- /**
195
- * Render marketplace.json — at repo root .claude-plugin/marketplace.json.
196
- * NO version key in plugin entry. source = "./plugin".
197
- * @param {object} manifest
198
- * @returns {string}
199
- */
200
- export function renderMarketplaceJson(manifest) {
201
- const ownerObj = { name: 'Qualixar' };
202
- if (manifest.marketplace.ownerEmail) {
203
- ownerObj.email = manifest.marketplace.ownerEmail;
204
- }
205
- const obj = {
206
- name: 'qualixar',
207
- owner: ownerObj,
208
- plugins: [
209
- {
210
- author: { name: 'Qualixar' },
211
- description: manifest.marketplace.description,
212
- name: manifest.pluginName,
213
- source: './plugin',
214
- },
215
- ],
216
- };
217
- return stableStringify(obj);
218
- }
219
-
220
- // ---------------------------------------------------------------------------
221
- // IO: loadManifest
222
- // ---------------------------------------------------------------------------
223
-
224
- /**
225
- * Load and validate plugin-src/manifest.json.
226
- * Throws (→ exit 1) on any IO or validation error.
227
- * @param {string} root repository root
228
- * @returns {object} parsed manifest
229
- */
230
- export function loadManifest(root) {
231
- const p = path.join(root, MANIFEST_REL);
232
- let raw;
233
- try {
234
- raw = fs.readFileSync(p, 'utf8');
235
- } catch (err) {
236
- throw new Error(`loadManifest: cannot read ${p}: ${err.message}`);
237
- }
238
-
239
- let manifest;
240
- try {
241
- manifest = JSON.parse(raw);
242
- } catch (err) {
243
- throw new Error(`loadManifest: invalid JSON in ${p}: ${err.message}`);
244
- }
245
-
246
- // Validate required fields
247
- if (!manifest.version) throw new Error('loadManifest: manifest.version is required');
248
- if (!manifest.pluginName) throw new Error('loadManifest: manifest.pluginName is required');
249
- if (!Array.isArray(manifest.skills) || manifest.skills.length === 0) {
250
- throw new Error('loadManifest: manifest.skills must be a non-empty array');
251
- }
252
- if (!manifest.targets || typeof manifest.targets !== 'object') {
253
- throw new Error('loadManifest: manifest.targets must be an object');
254
- }
255
- if (!manifest.targets.plugin || typeof manifest.targets.plugin !== 'string') {
256
- throw new Error('loadManifest: manifest.targets.plugin is required and must be a string');
257
- }
258
- if (path.isAbsolute(manifest.targets.plugin)) {
259
- throw new Error('loadManifest: manifest.targets.plugin must be relative (absolute paths forbidden)');
260
- }
261
-
262
- return manifest;
263
- }
264
-
265
- // ---------------------------------------------------------------------------
266
- // Core: derivePluginRoot — the plugin/ subdir from targets.plugin
267
- // ---------------------------------------------------------------------------
268
-
269
- /**
270
- * Derive the plugin root directory (plugin/) from manifest.targets.plugin.
271
- * targets.plugin = "plugin/.claude-plugin/plugin.json"
272
- * pluginRoot = path.resolve(root, "plugin")
273
- * @param {string} root
274
- * @param {object} manifest
275
- * @returns {string} absolute path to plugin root
276
- */
277
- export function derivePluginRoot(root, manifest) {
278
- // targets.plugin = "plugin/.claude-plugin/plugin.json"
279
- // dirname twice: "plugin/.claude-plugin" -> "plugin"
280
- const pluginJsonRel = manifest.targets.plugin; // "plugin/.claude-plugin/plugin.json"
281
- const pluginClaudeDir = path.dirname(pluginJsonRel); // "plugin/.claude-plugin"
282
- const pluginRootRel = path.dirname(pluginClaudeDir); // "plugin"
283
- return path.resolve(root, pluginRootRel);
284
- }
285
-
286
- /**
287
- * Return the set of absolute managed root directories.
288
- * Now only manages plugin/ dir.
289
- * @param {string} root
290
- * @param {object} manifest
291
- * @returns {Set<string>}
292
- */
293
- export function deriveManagedRoots(root, manifest) {
294
- const roots = new Set();
295
- roots.add(derivePluginRoot(root, manifest));
296
- return roots;
297
- }
298
-
299
- // ---------------------------------------------------------------------------
300
- // Core: buildPlan — pure, returns Map<absPath, content>
301
- // ---------------------------------------------------------------------------
302
-
303
- /**
304
- * Build the full plan as a Map from absolute path to file content.
305
- * Pure: no IO side effects (reads plugin-src source files).
306
- * @param {string} root repository root
307
- * @param {object} manifest parsed manifest
308
- * @returns {Map<string, string>}
309
- */
310
- export function buildPlan(root, manifest) {
311
- const plan = new Map();
312
-
313
- const pluginRoot = derivePluginRoot(root, manifest);
314
- const pluginClaudeDir = path.join(pluginRoot, '.claude-plugin');
315
-
316
- const sortedSkills = [...manifest.skills].sort((a, b) => a.name.localeCompare(b.name));
317
-
318
- // ---------------------------------------------------------------------------
319
- // Skills → plugin/skills/<name>/SKILL.md
320
- // ---------------------------------------------------------------------------
321
- for (const skill of sortedSkills) {
322
- const srcSkillDir = path.join(root, 'plugin-src', 'skills', skill.name);
323
- const srcSkillPath = path.join(srcSkillDir, 'SKILL.md');
324
-
325
- let srcContent;
326
- try {
327
- srcContent = fs.readFileSync(srcSkillPath, 'utf8');
328
- } catch (err) {
329
- throw new Error(`buildPlan: cannot read source skill ${srcSkillPath}: ${err.message}`);
330
- }
331
-
332
- const rendered = renderSkillFile(srcContent, manifest.version);
333
- plan.set(path.join(pluginRoot, 'skills', skill.name, 'SKILL.md'), rendered);
334
-
335
- // README verbatim (no stamp) if present
336
- if (skill.hasReadme) {
337
- const srcReadme = path.join(srcSkillDir, 'README.md');
338
- let readmeContent;
339
- try {
340
- readmeContent = fs.readFileSync(srcReadme, 'utf8');
341
- } catch (err) {
342
- throw new Error(`buildPlan: cannot read README ${srcReadme}: ${err.message}`);
343
- }
344
- plan.set(path.join(pluginRoot, 'skills', skill.name, 'README.md'), readmeContent);
345
- }
346
- }
347
-
348
- // ---------------------------------------------------------------------------
349
- // _GENERATED.md banner in plugin root
350
- // ---------------------------------------------------------------------------
351
- plan.set(path.join(pluginRoot, '_GENERATED.md'), GENERATED_BANNER);
352
-
353
- // ---------------------------------------------------------------------------
354
- // plugin/.claude-plugin/plugin.json
355
- // ---------------------------------------------------------------------------
356
- plan.set(path.join(pluginClaudeDir, 'plugin.json'), renderPluginJson(manifest));
357
-
358
- // ---------------------------------------------------------------------------
359
- // .claude-plugin/marketplace.json at REPO ROOT (not plugin root)
360
- // ---------------------------------------------------------------------------
361
- plan.set(path.join(root, '.claude-plugin', 'marketplace.json'), renderMarketplaceJson(manifest));
362
-
363
- // ---------------------------------------------------------------------------
364
- // Agents (verbatim, no commands/)
365
- // ---------------------------------------------------------------------------
366
- const agentsSrcPath = path.join(root, 'plugin-src', 'agents');
367
- if (fs.existsSync(agentsSrcPath)) {
368
- for (const fname of fs.readdirSync(agentsSrcPath)) {
369
- if (fname === '.gitkeep') continue;
370
- const srcFile = path.join(agentsSrcPath, fname);
371
- if (!fs.statSync(srcFile).isFile()) continue;
372
- const content = fs.readFileSync(srcFile, 'utf8');
373
- plan.set(path.join(pluginRoot, 'agents', fname), content);
374
- }
375
- }
376
-
377
- // ---------------------------------------------------------------------------
378
- // hooks: plugin/hooks/
379
- // ---------------------------------------------------------------------------
380
- const hooksPath = path.join(root, 'plugin-src', 'hooks');
381
- if (fs.existsSync(hooksPath)) {
382
- for (const fname of fs.readdirSync(hooksPath)) {
383
- if (fname === '.gitkeep') continue;
384
- const srcFile = path.join(hooksPath, fname);
385
- if (!fs.statSync(srcFile).isFile()) continue;
386
- const content = fs.readFileSync(srcFile, 'utf8');
387
- plan.set(path.join(pluginRoot, 'hooks', fname), content);
388
- }
389
- }
390
-
391
- // ---------------------------------------------------------------------------
392
- // Root-level plugin-src files → plugin/ (.mcp.json, settings.json, requirements.txt)
393
- // ---------------------------------------------------------------------------
394
- const rootFiles = ['.mcp.json', 'settings.json', 'requirements.txt'];
395
- for (const fname of rootFiles) {
396
- const srcFile = path.join(root, 'plugin-src', fname);
397
- if (fs.existsSync(srcFile)) {
398
- const content = fs.readFileSync(srcFile, 'utf8');
399
- plan.set(path.join(pluginRoot, fname), content);
400
- }
401
- }
402
-
403
- // ---------------------------------------------------------------------------
404
- // scripts/ → plugin/scripts/
405
- // ---------------------------------------------------------------------------
406
- const scriptsSrcPath = path.join(root, 'plugin-src', 'scripts');
407
- if (fs.existsSync(scriptsSrcPath)) {
408
- for (const fname of fs.readdirSync(scriptsSrcPath)) {
409
- if (fname === '.gitkeep') continue;
410
- const srcFile = path.join(scriptsSrcPath, fname);
411
- if (!fs.statSync(srcFile).isFile()) continue;
412
- const content = fs.readFileSync(srcFile, 'utf8');
413
- plan.set(path.join(pluginRoot, 'scripts', fname), content);
414
- }
415
- }
416
-
417
- // ---------------------------------------------------------------------------
418
- // CLAUDE.md from rules/CLAUDE.md.fragment → plugin/CLAUDE.md
419
- // ---------------------------------------------------------------------------
420
- const claudeFragmentPath = path.join(root, 'plugin-src', 'rules', 'CLAUDE.md.fragment');
421
- if (fs.existsSync(claudeFragmentPath)) {
422
- const fragmentContent = fs.readFileSync(claudeFragmentPath, 'utf8');
423
- plan.set(path.join(pluginRoot, 'CLAUDE.md'), fragmentContent);
424
- }
425
-
426
- return plan;
427
- }
428
-
429
- // ---------------------------------------------------------------------------
430
- // Pure utility: _isExecutable — determines if a file should have +x bit set.
431
- // Shell scripts (.sh) and extensionless launcher scripts (e.g. slm-launch)
432
- // in scripts/ directories need the executable bit.
433
- // ---------------------------------------------------------------------------
434
-
435
- /**
436
- * Return true if the file at absPath should receive the executable (+x) bit.
437
- * Rules:
438
- * - Any file ending in .sh
439
- * - Any extensionless file inside a scripts/ directory (cross-platform launchers)
440
- * @param {string} absPath
441
- * @returns {boolean}
442
- */
443
- function _isExecutable(absPath) {
444
- if (absPath.endsWith('.sh')) return true;
445
- // Extensionless files in a scripts/ directory (e.g. slm-launch)
446
- const base = path.basename(absPath);
447
- if (!base.includes('.') && absPath.includes(`${path.sep}scripts${path.sep}`)) {
448
- return true;
449
- }
450
- return false;
451
- }
452
-
453
- // ---------------------------------------------------------------------------
454
- // IO: applyPlan — write-only-if-changed via sha256
455
- // ---------------------------------------------------------------------------
456
-
457
- /**
458
- * Apply the plan: write files only when content has changed (by sha256).
459
- * Creates parent directories as needed.
460
- * IMPORTANT: If the target path is a symlink, unlink it first before writing.
461
- * This prevents writes from propagating through symlinks to legacy source files.
462
- * @param {Map<string, string>} plan
463
- * @returns {number} number of files written
464
- */
465
- export function applyPlan(plan) {
466
- let written = 0;
467
- for (const [absPath, content] of plan) {
468
- const newHash = sha256(content);
469
- let existingHash = null;
470
-
471
- // Check using lstat (not stat) to detect symlinks without following them
472
- if (fs.existsSync(absPath)) {
473
- const stat = fs.lstatSync(absPath);
474
- if (stat.isSymbolicLink()) {
475
- // Unlink the symlink before writing — prevents write-through to legacy source
476
- fs.unlinkSync(absPath);
477
- // existingHash stays null → will write
478
- } else {
479
- existingHash = sha256(fs.readFileSync(absPath, 'utf8'));
480
- }
481
- }
482
-
483
- if (existingHash !== newHash) {
484
- fs.mkdirSync(path.dirname(absPath), { recursive: true });
485
- fs.writeFileSync(absPath, content, 'utf8');
486
- // Preserve executable bit for shell scripts and extensionless launchers
487
- if (_isExecutable(absPath)) {
488
- const mode = fs.statSync(absPath).mode;
489
- fs.chmodSync(absPath, mode | 0o111);
490
- }
491
- written++;
492
- } else if (_isExecutable(absPath)) {
493
- // Ensure executable bit even if content unchanged
494
- const mode = fs.statSync(absPath).mode;
495
- if (!(mode & 0o100)) {
496
- fs.chmodSync(absPath, mode | 0o111);
497
- }
498
- }
499
- }
500
- return written;
501
- }
502
-
503
- function sha256(content) {
504
- return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
505
- }
506
-
507
- // ---------------------------------------------------------------------------
508
- // IO: checkPlan — compare plan vs disk, return { stale, missing, extra }
509
- // ---------------------------------------------------------------------------
510
-
511
- /**
512
- * Compare plan against disk. Returns arrays of stale/missing/extra paths.
513
- * @param {Map<string, string>} plan
514
- * @param {string} root
515
- * @param {object} manifest
516
- * @returns {{ stale: string[], missing: string[], extra: string[] }}
517
- */
518
- export function checkPlan(plan, root, manifest) {
519
- const stale = [];
520
- const missing = [];
521
- const extra = [];
522
-
523
- for (const [absPath, content] of plan) {
524
- if (!fs.existsSync(absPath)) {
525
- missing.push(absPath);
526
- } else {
527
- const disk = sha256(fs.readFileSync(absPath, 'utf8'));
528
- const expected = sha256(content);
529
- if (disk !== expected) stale.push(absPath);
530
- }
531
- }
532
-
533
- // Find extra files in managed roots (only plugin/)
534
- const managedRoots = deriveManagedRoots(root, manifest);
535
- for (const managedRoot of managedRoots) {
536
- if (!fs.existsSync(managedRoot)) continue;
537
- for (const absPath of walkDir(managedRoot)) {
538
- if (!plan.has(absPath)) {
539
- extra.push(absPath);
540
- }
541
- }
542
- }
543
-
544
- return { stale, missing, extra };
545
- }
546
-
547
- // ---------------------------------------------------------------------------
548
- // IO: pruneOrphans — GUARDED: plan.size>0, path in managed root
549
- // ---------------------------------------------------------------------------
550
-
551
- /**
552
- * Remove files in managed roots that are NOT in the plan (orphan pruning).
553
- * Guards:
554
- * 1. plan.size > 0 (CRIT-1: refuse if plan empty)
555
- * 2. fullPath must be exactly managedRoot or start with managedRoot + sep (escape guard)
556
- * 3. Only files (not dirs) removed; dirs cleaned up if empty
557
- * @param {string} root
558
- * @param {Map<string, string>} plan
559
- * @param {Set<string>} managedRoots
560
- */
561
- export function pruneOrphans(root, plan, managedRoots) {
562
- // CRIT-1: refuse if plan is empty
563
- if (plan.size === 0) {
564
- throw new Error('pruneOrphans: refusing to prune — plan is empty (safety guard)');
565
- }
566
-
567
- for (const managedRoot of managedRoots) {
568
- if (!fs.existsSync(managedRoot)) continue;
569
- for (const absPath of walkDir(managedRoot)) {
570
- // Escape guard: path must be within managedRoot
571
- if (
572
- absPath !== managedRoot &&
573
- !absPath.startsWith(managedRoot + path.sep)
574
- ) {
575
- throw new Error(
576
- `pruneOrphans: escape guard — ${absPath} is not under ${managedRoot}`
577
- );
578
- }
579
-
580
- if (!plan.has(absPath)) {
581
- fs.unlinkSync(absPath);
582
- // Remove empty parent dirs (up to managedRoot)
583
- cleanEmptyDirs(path.dirname(absPath), managedRoot);
584
- }
585
- }
586
- }
587
- }
588
-
589
- /**
590
- * Walk a directory recursively, skipping _SKIP_DIRS.
591
- * Yields absolute paths of FILES only.
592
- * @param {string} dir
593
- * @returns {Generator<string>}
594
- */
595
- export function* walkDir(dir) {
596
- if (!fs.existsSync(dir)) return;
597
- const entries = fs.readdirSync(dir, { withFileTypes: true });
598
- for (const entry of entries) {
599
- if (_SKIP_DIRS.has(entry.name)) continue;
600
- const full = path.join(dir, entry.name);
601
- if (entry.isDirectory()) {
602
- yield* walkDir(full);
603
- } else if (entry.isFile()) {
604
- yield full;
605
- }
606
- }
607
- }
608
-
609
- function cleanEmptyDirs(dir, stopAt) {
610
- if (dir === stopAt || !dir.startsWith(stopAt + path.sep)) return;
611
- try {
612
- const entries = fs.readdirSync(dir);
613
- if (entries.length === 0) {
614
- fs.rmdirSync(dir);
615
- cleanEmptyDirs(path.dirname(dir), stopAt);
616
- }
617
- } catch {
618
- // ignore — dir may not exist
619
- }
620
- }
621
-
622
- // ---------------------------------------------------------------------------
623
- // IO: listTargetFiles — for --check extra detection
624
- // ---------------------------------------------------------------------------
625
-
626
- /**
627
- * List all files in managed roots.
628
- * @param {string} root
629
- * @param {object} manifest
630
- * @returns {string[]}
631
- */
632
- export function listTargetFiles(root, manifest) {
633
- const result = [];
634
- for (const managedRoot of deriveManagedRoots(root, manifest)) {
635
- for (const f of walkDir(managedRoot)) {
636
- result.push(f);
637
- }
638
- }
639
- return result;
640
- }
641
-
642
- // ---------------------------------------------------------------------------
643
- // CLI: main
644
- // ---------------------------------------------------------------------------
645
-
646
- /**
647
- * Main entry point.
648
- * @param {string[]} argv
649
- */
650
- export function main(argv) {
651
- const args = argv.slice(2);
652
-
653
- if (args.includes('--help') || args.includes('-h')) {
654
- console.log(`
655
- Usage: node scripts/build-plugin.js [options]
656
-
657
- Options:
658
- --check Check if generated files are in-sync (exit 2 if not)
659
- --quiet Suppress non-error output
660
- --help, -h Show this help
661
-
662
- Output topology:
663
- .claude-plugin/marketplace.json (repo root; source:"./plugin")
664
- plugin/ (plugin root)
665
- .claude-plugin/plugin.json (manifest; version only here)
666
- skills/<7>/SKILL.md
667
- agents/*.md
668
- .mcp.json hooks/ scripts/ settings.json requirements.txt CLAUDE.md
669
-
670
- Exit codes: 0=success/in-sync 1=fatal 2=check-drift
671
- `.trimStart());
672
- process.exit(0);
673
- }
674
-
675
- const isCheck = args.includes('--check');
676
- const isQuiet = args.includes('--quiet');
677
-
678
- const root = process.cwd();
679
-
680
- let manifest;
681
- try {
682
- manifest = loadManifest(root);
683
- } catch (err) {
684
- console.error(`build-plugin: ${err.message}`);
685
- process.exit(1);
686
- }
687
-
688
- let plan;
689
- try {
690
- plan = buildPlan(root, manifest);
691
- } catch (err) {
692
- console.error(`build-plugin: ${err.message}`);
693
- process.exit(1);
694
- }
695
-
696
- if (isCheck) {
697
- // Never prune in --check mode
698
- const { stale, missing, extra } = checkPlan(plan, root, manifest);
699
- if (stale.length > 0 || missing.length > 0 || extra.length > 0) {
700
- if (!isQuiet) {
701
- if (stale.length) console.log('STALE:\n' + stale.map(f => ` ${f}`).join('\n'));
702
- if (missing.length) console.log('MISSING:\n' + missing.map(f => ` ${f}`).join('\n'));
703
- if (extra.length) console.log('EXTRA:\n' + extra.map(f => ` ${f}`).join('\n'));
704
- }
705
- process.exit(2);
706
- }
707
- if (!isQuiet) console.log('build-plugin --check: in-sync');
708
- process.exit(0);
709
- }
710
-
711
- // Build mode
712
- let written;
713
- try {
714
- written = applyPlan(plan);
715
- } catch (err) {
716
- console.error(`build-plugin: applyPlan failed: ${err.message}`);
717
- process.exit(1);
718
- }
719
-
720
- // Prune orphans in plugin/ (CRIT-1: guarded inside pruneOrphans)
721
- try {
722
- const managedRoots = deriveManagedRoots(root, manifest);
723
- pruneOrphans(root, plan, managedRoots);
724
- } catch (err) {
725
- console.error(`build-plugin: pruneOrphans failed: ${err.message}`);
726
- process.exit(1);
727
- }
728
-
729
- if (!isQuiet) {
730
- console.log(`build-plugin: done (${written} file(s) written)`);
731
- }
732
- process.exit(0);
733
- }
734
-
735
- // ---------------------------------------------------------------------------
736
- // Entry point
737
- // ---------------------------------------------------------------------------
738
- // Check if this is the main module (ESM way)
739
- const isMain = process.argv[1] === new URL(import.meta.url).pathname;
740
- if (isMain) {
741
- main(process.argv);
742
- }