@documentation-robotics/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/README.md +46 -16
  2. package/dist/analysis/relationship-classifier.js +2 -9
  3. package/dist/analysis/relationship-classifier.js.map +2 -2
  4. package/dist/analyzers/active-analyzer.js +62 -0
  5. package/dist/analyzers/active-analyzer.js.map +7 -0
  6. package/dist/analyzers/call-graph-utils.js +2 -2
  7. package/dist/analyzers/call-graph-utils.js.map +2 -2
  8. package/dist/analyzers/cards/build.js +191 -0
  9. package/dist/analyzers/cards/build.js.map +7 -0
  10. package/dist/analyzers/cards/classify.js +42 -0
  11. package/dist/analyzers/cards/classify.js.map +7 -0
  12. package/dist/analyzers/cards/edge-index.js +44 -0
  13. package/dist/analyzers/cards/edge-index.js.map +7 -0
  14. package/dist/analyzers/cards/extract.js +138 -0
  15. package/dist/analyzers/cards/extract.js.map +7 -0
  16. package/dist/analyzers/cards/facts.js +46 -0
  17. package/dist/analyzers/cards/facts.js.map +7 -0
  18. package/dist/analyzers/cards/graph.js +42 -0
  19. package/dist/analyzers/cards/graph.js.map +7 -0
  20. package/dist/analyzers/cards/index.js +8 -0
  21. package/dist/analyzers/cards/index.js.map +7 -0
  22. package/dist/analyzers/cards/routes.js +96 -0
  23. package/dist/analyzers/cards/routes.js.map +7 -0
  24. package/dist/analyzers/cards/tier0.js +18 -0
  25. package/dist/analyzers/cards/tier0.js.map +7 -0
  26. package/dist/analyzers/cards/types.js +1 -0
  27. package/dist/analyzers/cards/types.js.map +7 -0
  28. package/dist/analyzers/cbm-analyzer.js +270 -174
  29. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  30. package/dist/analyzers/cbm-responses.js +342 -0
  31. package/dist/analyzers/cbm-responses.js.map +7 -0
  32. package/dist/analyzers/code-changes.js +159 -0
  33. package/dist/analyzers/code-changes.js.map +7 -0
  34. package/dist/analyzers/edge-relationships.js +49 -0
  35. package/dist/analyzers/edge-relationships.js.map +7 -0
  36. package/dist/analyzers/extraction-plan.js +541 -0
  37. package/dist/analyzers/extraction-plan.js.map +7 -0
  38. package/dist/analyzers/index-job.js +229 -0
  39. package/dist/analyzers/index-job.js.map +7 -0
  40. package/dist/analyzers/mapping-loader.js +9 -0
  41. package/dist/analyzers/mapping-loader.js.map +2 -2
  42. package/dist/analyzers/tier0-rules.js +153 -0
  43. package/dist/analyzers/tier0-rules.js.map +7 -0
  44. package/dist/analyzers/types.js.map +1 -1
  45. package/dist/audit/relationships/ai/prompts.js +10 -10
  46. package/dist/audit/relationships/ai/prompts.js.map +2 -2
  47. package/dist/audit/relationships/analysis/layer-templates.js +52 -52
  48. package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
  49. package/dist/cli.js +23 -11
  50. package/dist/cli.js.map +2 -2
  51. package/dist/coding-agents/default-permissions.js +31 -1
  52. package/dist/coding-agents/default-permissions.js.map +2 -2
  53. package/dist/coding-agents/drbot-mcp.js +269 -0
  54. package/dist/coding-agents/drbot-mcp.js.map +7 -0
  55. package/dist/commands/add.js +7 -4
  56. package/dist/commands/add.js.map +2 -2
  57. package/dist/commands/analyzer.js +143 -40
  58. package/dist/commands/analyzer.js.map +2 -2
  59. package/dist/commands/changeset-verify.js +63 -0
  60. package/dist/commands/changeset-verify.js.map +7 -0
  61. package/dist/commands/changeset.js +80 -29
  62. package/dist/commands/changeset.js.map +2 -2
  63. package/dist/commands/delete.js +10 -4
  64. package/dist/commands/delete.js.map +2 -2
  65. package/dist/commands/farm.js +176 -60
  66. package/dist/commands/farm.js.map +2 -2
  67. package/dist/commands/init.js +9 -26
  68. package/dist/commands/init.js.map +2 -2
  69. package/dist/commands/mcp.js +40 -21
  70. package/dist/commands/mcp.js.map +3 -3
  71. package/dist/commands/relationship.js +208 -37
  72. package/dist/commands/relationship.js.map +2 -2
  73. package/dist/commands/reports.js +3 -3
  74. package/dist/commands/reports.js.map +2 -2
  75. package/dist/commands/update.js +52 -32
  76. package/dist/commands/update.js.map +2 -2
  77. package/dist/commands/upgrade.js +99 -103
  78. package/dist/commands/upgrade.js.map +3 -3
  79. package/dist/commands/validate.js +19 -3
  80. package/dist/commands/validate.js.map +2 -2
  81. package/dist/commands/version.js +1 -1
  82. package/dist/commands/visualize.js +34 -15
  83. package/dist/commands/visualize.js.map +2 -2
  84. package/dist/core/annotation-store.js +15 -5
  85. package/dist/core/annotation-store.js.map +2 -2
  86. package/dist/core/catalog-migration.js +34 -0
  87. package/dist/core/catalog-migration.js.map +7 -0
  88. package/dist/core/changeset-analysis.js +421 -0
  89. package/dist/core/changeset-analysis.js.map +7 -0
  90. package/dist/core/changeset-base-index.js +160 -0
  91. package/dist/core/changeset-base-index.js.map +7 -0
  92. package/dist/core/changeset-brief-compact.js +256 -0
  93. package/dist/core/changeset-brief-compact.js.map +7 -0
  94. package/dist/core/changeset-brief-delta.js +102 -0
  95. package/dist/core/changeset-brief-delta.js.map +7 -0
  96. package/dist/core/changeset-graph.js +281 -0
  97. package/dist/core/changeset-graph.js.map +7 -0
  98. package/dist/core/changeset-only-guard.js +32 -0
  99. package/dist/core/changeset-only-guard.js.map +7 -0
  100. package/dist/core/changeset-summary.js +68 -0
  101. package/dist/core/changeset-summary.js.map +7 -0
  102. package/dist/core/changeset-validator.js +66 -20
  103. package/dist/core/changeset-validator.js.map +2 -2
  104. package/dist/core/changeset-verify.js +848 -0
  105. package/dist/core/changeset-verify.js.map +7 -0
  106. package/dist/core/changeset.js +56 -9
  107. package/dist/core/changeset.js.map +2 -2
  108. package/dist/core/element-type-migration.js +145 -0
  109. package/dist/core/element-type-migration.js.map +7 -0
  110. package/dist/core/extraction-observability.js +161 -0
  111. package/dist/core/extraction-observability.js.map +7 -0
  112. package/dist/core/farm-manifest.js +12 -6
  113. package/dist/core/farm-manifest.js.map +2 -2
  114. package/dist/core/farm-sync-engine.js +1 -1
  115. package/dist/core/farm-sync-engine.js.map +2 -2
  116. package/dist/core/farm-sync-state.js +10 -2
  117. package/dist/core/farm-sync-state.js.map +2 -2
  118. package/dist/core/migration-registry.js +88 -47
  119. package/dist/core/migration-registry.js.map +2 -2
  120. package/dist/core/model-upgrade.js +102 -0
  121. package/dist/core/model-upgrade.js.map +7 -0
  122. package/dist/core/model.js +82 -30
  123. package/dist/core/model.js.map +2 -2
  124. package/dist/core/mutation-handler.js +71 -6
  125. package/dist/core/mutation-handler.js.map +2 -2
  126. package/dist/core/project-init.js +53 -0
  127. package/dist/core/project-init.js.map +7 -0
  128. package/dist/core/relationship-migration-table.js +6639 -0
  129. package/dist/core/relationship-migration-table.js.map +7 -0
  130. package/dist/core/relationship-migration.js +272 -0
  131. package/dist/core/relationship-migration.js.map +7 -0
  132. package/dist/core/relationship-prune.js +49 -0
  133. package/dist/core/relationship-prune.js.map +7 -0
  134. package/dist/core/relationship-staging.js +161 -0
  135. package/dist/core/relationship-staging.js.map +7 -0
  136. package/dist/core/relationships.js +26 -4
  137. package/dist/core/relationships.js.map +2 -2
  138. package/dist/core/report-data-model.js +7 -13
  139. package/dist/core/report-data-model.js.map +2 -2
  140. package/dist/core/staged-changeset-storage.js +145 -18
  141. package/dist/core/staged-changeset-storage.js.map +2 -2
  142. package/dist/core/staged-source-reference.js +28 -0
  143. package/dist/core/staged-source-reference.js.map +7 -0
  144. package/dist/core/staging-area.js +166 -24
  145. package/dist/core/staging-area.js.map +2 -2
  146. package/dist/core/upgrade-plan.js +117 -0
  147. package/dist/core/upgrade-plan.js.map +7 -0
  148. package/dist/core/virtual-projection.js +13 -5
  149. package/dist/core/virtual-projection.js.map +2 -2
  150. package/dist/export/archimate-exporter.js +1 -0
  151. package/dist/export/archimate-exporter.js.map +2 -2
  152. package/dist/generated/compiled-validators.js +2 -2
  153. package/dist/generated/compiled-validators.js.map +2 -2
  154. package/dist/generated/layer-registry.js +16 -16
  155. package/dist/generated/layer-registry.js.map +2 -2
  156. package/dist/generated/node-types.js +186 -168
  157. package/dist/generated/node-types.js.map +2 -2
  158. package/dist/generated/relationship-index.js +2468 -7913
  159. package/dist/generated/relationship-index.js.map +2 -2
  160. package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
  161. package/dist/integrations/claude_code/agents/dr-architect.md +30 -3
  162. package/dist/integrations/claude_code/agents/dr-extractor.md +8 -2
  163. package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
  164. package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
  165. package/dist/integrations/claude_code/commands/dr-map.md +20 -1
  166. package/dist/integrations/claude_code/commands/dr-plan.md +66 -0
  167. package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
  168. package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
  169. package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
  170. package/dist/integrations/claude_code/docs/MCP_SERVER.md +47 -26
  171. package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
  172. package/dist/integrations/claude_code/docs/README.md +68 -2
  173. package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
  174. package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
  175. package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
  176. package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
  177. package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
  178. package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
  179. package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
  180. package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
  181. package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
  182. package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
  183. package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
  184. package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
  185. package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
  186. package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
  187. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +95 -15
  188. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +178 -0
  189. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +8 -0
  190. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +6 -1
  191. package/dist/integrations/github_copilot/agents/dr-architect.md +3 -1
  192. package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
  193. package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
  194. package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
  195. package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
  196. package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
  197. package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
  198. package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
  199. package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
  200. package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
  201. package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
  202. package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
  203. package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
  204. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +225 -102
  205. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +177 -0
  206. package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
  207. package/dist/integrations/hash-utils.js.map +2 -2
  208. package/dist/mcp/api-key-manager.js +14 -1
  209. package/dist/mcp/api-key-manager.js.map +2 -2
  210. package/dist/mcp/changeset-only.js +45 -0
  211. package/dist/mcp/changeset-only.js.map +7 -0
  212. package/dist/mcp/project-root.js +85 -0
  213. package/dist/mcp/project-root.js.map +7 -0
  214. package/dist/mcp/server-instructions.js +27 -0
  215. package/dist/mcp/server-instructions.js.map +7 -0
  216. package/dist/mcp/strict-schema.js +52 -0
  217. package/dist/mcp/strict-schema.js.map +7 -0
  218. package/dist/mcp/tool-registry.js +55 -5
  219. package/dist/mcp/tool-registry.js.map +2 -2
  220. package/dist/mcp/tools/changeset-analysis-tools.js +98 -0
  221. package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
  222. package/dist/mcp/tools/changeset-commit.js +224 -0
  223. package/dist/mcp/tools/changeset-commit.js.map +7 -0
  224. package/dist/mcp/tools/changeset-editing.js +260 -0
  225. package/dist/mcp/tools/changeset-editing.js.map +7 -0
  226. package/dist/mcp/tools/changeset-lifecycle.js +83 -0
  227. package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
  228. package/dist/mcp/tools/changeset-shared.js +31 -0
  229. package/dist/mcp/tools/changeset-shared.js.map +7 -0
  230. package/dist/mcp/tools/changeset-show.js +17 -1
  231. package/dist/mcp/tools/changeset-show.js.map +2 -2
  232. package/dist/mcp/tools/changeset-verify.js +49 -0
  233. package/dist/mcp/tools/changeset-verify.js.map +7 -0
  234. package/dist/mcp/tools/codebase-extract.js +836 -0
  235. package/dist/mcp/tools/codebase-extract.js.map +7 -0
  236. package/dist/mcp/tools/codebase-index.js +125 -0
  237. package/dist/mcp/tools/codebase-index.js.map +7 -0
  238. package/dist/mcp/tools/codebase-shared.js +265 -0
  239. package/dist/mcp/tools/codebase-shared.js.map +7 -0
  240. package/dist/mcp/tools/codebase-verify.js +48 -0
  241. package/dist/mcp/tools/codebase-verify.js.map +7 -0
  242. package/dist/mcp/tools/index.js +34 -1
  243. package/dist/mcp/tools/index.js.map +2 -2
  244. package/dist/mcp/tools/model-add.js +191 -74
  245. package/dist/mcp/tools/model-add.js.map +2 -2
  246. package/dist/mcp/tools/model-delete.js +2 -2
  247. package/dist/mcp/tools/model-delete.js.map +2 -2
  248. package/dist/mcp/tools/model-search.js +14 -0
  249. package/dist/mcp/tools/model-search.js.map +2 -2
  250. package/dist/mcp/tools/model-show.js +29 -2
  251. package/dist/mcp/tools/model-show.js.map +2 -2
  252. package/dist/mcp/tools/model-trace.js +93 -30
  253. package/dist/mcp/tools/model-trace.js.map +2 -2
  254. package/dist/mcp/tools/model-update.js +46 -26
  255. package/dist/mcp/tools/model-update.js.map +2 -2
  256. package/dist/mcp/tools/model-validate.js +46 -4
  257. package/dist/mcp/tools/model-validate.js.map +2 -2
  258. package/dist/mcp/tools/project-status-collectors.js +381 -0
  259. package/dist/mcp/tools/project-status-collectors.js.map +7 -0
  260. package/dist/mcp/tools/project-status.js +237 -0
  261. package/dist/mcp/tools/project-status.js.map +7 -0
  262. package/dist/mcp/tools/project-tools.js +505 -0
  263. package/dist/mcp/tools/project-tools.js.map +7 -0
  264. package/dist/mcp/tools/relationship-tools.js +167 -0
  265. package/dist/mcp/tools/relationship-tools.js.map +7 -0
  266. package/dist/mcp/tools/review-open.js +210 -0
  267. package/dist/mcp/tools/review-open.js.map +7 -0
  268. package/dist/mcp/tools/shared.js +35 -4
  269. package/dist/mcp/tools/shared.js.map +2 -2
  270. package/dist/mcp/tools/spec-type.js +90 -0
  271. package/dist/mcp/tools/spec-type.js.map +7 -0
  272. package/dist/reports/model-report-orchestrator.js +31 -2
  273. package/dist/reports/model-report-orchestrator.js.map +2 -2
  274. package/dist/schemas/bundled/analyzers/cbm.json +1200 -70
  275. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  276. package/dist/schemas/bundled/api.json +1029 -1658
  277. package/dist/schemas/bundled/apm.json +499 -734
  278. package/dist/schemas/bundled/application.json +735 -371
  279. package/dist/schemas/bundled/base.json +3 -220
  280. package/dist/schemas/bundled/business.json +452 -366
  281. package/dist/schemas/bundled/data-model.json +395 -411
  282. package/dist/schemas/bundled/data-store.json +334 -558
  283. package/dist/schemas/bundled/manifest.json +18 -18
  284. package/dist/schemas/bundled/motivation.json +349 -368
  285. package/dist/schemas/bundled/navigation.json +415 -731
  286. package/dist/schemas/bundled/product.json +325 -159
  287. package/dist/schemas/bundled/security.json +864 -1232
  288. package/dist/schemas/bundled/technology.json +574 -703
  289. package/dist/schemas/bundled/testing.json +865 -1646
  290. package/dist/schemas/bundled/ux.json +739 -1352
  291. package/dist/server/schemas.js +8 -0
  292. package/dist/server/schemas.js.map +2 -2
  293. package/dist/server/server.js +686 -231
  294. package/dist/server/server.js.map +3 -3
  295. package/dist/server/viewer-registry.js +264 -0
  296. package/dist/server/viewer-registry.js.map +7 -0
  297. package/dist/server-entry.js +2 -1
  298. package/dist/server-entry.js.map +2 -2
  299. package/dist/telemetry/index.js +1 -1
  300. package/dist/utils/ansi-suppressor.js +6 -8
  301. package/dist/utils/ansi-suppressor.js.map +2 -2
  302. package/dist/utils/attribute-patch.js +41 -0
  303. package/dist/utils/attribute-patch.js.map +7 -0
  304. package/dist/utils/error-hints.js +29 -0
  305. package/dist/utils/error-hints.js.map +7 -0
  306. package/dist/utils/errors.js +9 -3
  307. package/dist/utils/errors.js.map +2 -2
  308. package/dist/utils/id-generator.js +5 -2
  309. package/dist/utils/id-generator.js.map +2 -2
  310. package/dist/utils/project-paths.js +26 -1
  311. package/dist/utils/project-paths.js.map +2 -2
  312. package/dist/utils/source-reference.js +64 -31
  313. package/dist/utils/source-reference.js.map +2 -2
  314. package/dist/utils/spec-version.js +2 -2
  315. package/dist/utils/spec-version.js.map +1 -1
  316. package/dist/validators/composed-reference-validator.js +5 -27
  317. package/dist/validators/composed-reference-validator.js.map +2 -2
  318. package/dist/validators/model-warnings.js +28 -0
  319. package/dist/validators/model-warnings.js.map +7 -0
  320. package/dist/validators/reference-validator.js +0 -7
  321. package/dist/validators/reference-validator.js.map +2 -2
  322. package/dist/validators/relationship-schema-validator.js +3 -3
  323. package/dist/validators/relationship-schema-validator.js.map +2 -2
  324. package/dist/validators/schema-validator.js +27 -9
  325. package/dist/validators/schema-validator.js.map +2 -2
  326. package/dist/validators/types.js.map +1 -1
  327. package/dist/viewer/README.md +6 -6
  328. package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
  329. package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
  330. package/dist/viewer/index.html +2 -2
  331. package/dist/viewer/manifest.json +10 -10
  332. package/dist/viewer/version.json +1 -1
  333. package/package.json +2 -1
  334. package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
  335. package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
@@ -0,0 +1,264 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, readdirSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
4
+ import { chmod, mkdir, readdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
5
+ import { homedir } from "node:os";
6
+ import { basename, dirname, join, resolve } from "node:path";
7
+ const DR_STATE_DIR_ENV = "DR_STATE_DIR";
8
+ function stateDir(env = process.env) {
9
+ const override = env[DR_STATE_DIR_ENV];
10
+ if (override)
11
+ return resolve(override);
12
+ const xdg = env.XDG_STATE_HOME;
13
+ return xdg ? join(resolve(xdg), "dr") : join(homedir(), ".local", "state", "dr");
14
+ }
15
+ function viewerRecordDir(env = process.env) {
16
+ return join(stateDir(env), "viewers");
17
+ }
18
+ function canonicalProjectRoot(projectRoot) {
19
+ try {
20
+ return realpathSync(projectRoot);
21
+ } catch {
22
+ return resolve(projectRoot);
23
+ }
24
+ }
25
+ function recordKey(projectRoot) {
26
+ return createHash("sha256").update(canonicalProjectRoot(projectRoot)).digest("hex").slice(0, 24);
27
+ }
28
+ function viewerRecordPath(projectRoot, env = process.env) {
29
+ return join(viewerRecordDir(env), `${recordKey(projectRoot)}.json`);
30
+ }
31
+ function viewerLogPath(projectRoot, env = process.env) {
32
+ return join(viewerRecordDir(env), `${recordKey(projectRoot)}.log`);
33
+ }
34
+ function viewerStartingPath(projectRoot, env = process.env) {
35
+ return join(viewerRecordDir(env), `${recordKey(projectRoot)}.starting`);
36
+ }
37
+ let tempSeq = 0;
38
+ function tempPathFor(path, pid) {
39
+ return `${path}.${pid}.${++tempSeq}.tmp`;
40
+ }
41
+ function removeTempFilesSync(path, pid) {
42
+ const prefix = `${basename(path)}.${pid}.`;
43
+ try {
44
+ for (const name of readdirSync(dirname(path)))
45
+ name.startsWith(prefix) && name.endsWith(".tmp") && unlinkSync(join(dirname(path), name));
46
+ } catch {
47
+ }
48
+ }
49
+ const STALE_TEMP_MS = 6e4;
50
+ async function ensureViewerRecordDir(env = process.env) {
51
+ const dir = viewerRecordDir(env);
52
+ return await mkdir(dir, { recursive: !0, mode: 448 }), await chmod(dir, 448), dir;
53
+ }
54
+ async function removeStaleTempFiles(env = process.env, now = Date.now()) {
55
+ const dir = viewerRecordDir(env), removed = [];
56
+ let names;
57
+ try {
58
+ names = await readdir(dir);
59
+ } catch {
60
+ return removed;
61
+ }
62
+ for (const name of names) {
63
+ if (!name.endsWith(".tmp"))
64
+ continue;
65
+ const path = join(dir, name);
66
+ try {
67
+ now - (await stat(path)).mtimeMs > STALE_TEMP_MS && (await unlink(path), removed.push(path));
68
+ } catch {
69
+ }
70
+ }
71
+ return removed;
72
+ }
73
+ async function writeViewerRecord(record, env = process.env) {
74
+ await ensureViewerRecordDir(env), await removeStaleTempFiles(env);
75
+ const path = viewerRecordPath(record.projectRoot, env), stored = { ...record, projectRoot: canonicalProjectRoot(record.projectRoot) }, temp = tempPathFor(path, process.pid);
76
+ try {
77
+ await writeFile(temp, JSON.stringify(stored, null, 2) + `
78
+ `, { mode: 384 }), await chmod(temp, 384), await rename(temp, path);
79
+ } catch (error) {
80
+ throw await unlink(temp).catch(() => {
81
+ }), error;
82
+ }
83
+ return path;
84
+ }
85
+ function parseRecord(text) {
86
+ try {
87
+ const data = JSON.parse(text);
88
+ if (data && typeof data == "object" && Number.isInteger(data.pid) && Number.isInteger(data.port) && (typeof data.token == "string" || data.token === null) && typeof data.projectRoot == "string")
89
+ return {
90
+ pid: data.pid,
91
+ port: data.port,
92
+ token: data.token,
93
+ projectRoot: data.projectRoot,
94
+ startedAt: typeof data.startedAt == "string" ? data.startedAt : "",
95
+ withChangesets: data.withChangesets === !0
96
+ };
97
+ } catch {
98
+ }
99
+ return null;
100
+ }
101
+ async function readViewerRecord(projectRoot, env = process.env) {
102
+ let text;
103
+ try {
104
+ text = await readFile(viewerRecordPath(projectRoot, env), "utf8");
105
+ } catch (error) {
106
+ if (error.code === "ENOENT")
107
+ return;
108
+ throw error;
109
+ }
110
+ return parseRecord(text);
111
+ }
112
+ async function removeViewerRecord(projectRoot, options = {}) {
113
+ const env = options.env ?? process.env, path = viewerRecordPath(projectRoot, env);
114
+ if (options.pid !== void 0) {
115
+ const current = await readViewerRecord(projectRoot, env).catch(() => {
116
+ });
117
+ if (current && current.pid !== options.pid)
118
+ return;
119
+ }
120
+ try {
121
+ await unlink(path);
122
+ } catch (error) {
123
+ if (error.code !== "ENOENT")
124
+ throw error;
125
+ }
126
+ }
127
+ function removeViewerRecordSync(projectRoot, options = {}) {
128
+ const env = options.env ?? process.env, path = viewerRecordPath(projectRoot, env);
129
+ removeTempFilesSync(path, options.pid ?? process.pid);
130
+ try {
131
+ if (!existsSync(path))
132
+ return;
133
+ if (options.pid !== void 0) {
134
+ const current = parseRecord(readFileSync(path, "utf8"));
135
+ if (current && current.pid !== options.pid)
136
+ return;
137
+ }
138
+ unlinkSync(path);
139
+ } catch {
140
+ }
141
+ }
142
+ function isProcessAlive(pid) {
143
+ if (!Number.isInteger(pid) || pid <= 0)
144
+ return !1;
145
+ try {
146
+ return process.kill(pid, 0), !0;
147
+ } catch (error) {
148
+ return error.code === "EPERM";
149
+ }
150
+ }
151
+ async function viewerAnswers(port, timeoutMs = 1500) {
152
+ try {
153
+ const response = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(timeoutMs) });
154
+ if (!response.ok)
155
+ return !1;
156
+ const body = await response.json();
157
+ return body?.status === "ok" || body?.status === "degraded";
158
+ } catch {
159
+ return !1;
160
+ }
161
+ }
162
+ async function probeViewer(projectRoot, options = {}) {
163
+ const env = options.env ?? process.env, isAlive = options.isAlive ?? isProcessAlive, answers = options.answers ?? viewerAnswers, record = await readViewerRecord(projectRoot, env);
164
+ return record === void 0 ? { state: "none" } : record === null ? (await removeViewerRecord(projectRoot, { env }), { state: "none" }) : record.projectRoot !== canonicalProjectRoot(projectRoot) ? { state: "none" } : isAlive(record.pid) ? await answers(record.port, options.probeTimeoutMs ?? 1500) ? { state: "running", record } : await answers(record.port, options.retryTimeoutMs ?? 5e3) ? { state: "running", record } : { state: "unreachable", record } : (await removeViewerRecord(projectRoot, { env, pid: record.pid }), { state: "none" });
165
+ }
166
+ async function findRunningViewer(projectRoot, options = {}) {
167
+ const probe = await probeViewer(projectRoot, options);
168
+ return probe.state === "running" ? probe.record : null;
169
+ }
170
+ async function writeViewerStarting(projectRoot, starting, env = process.env) {
171
+ await ensureViewerRecordDir(env);
172
+ const path = viewerStartingPath(projectRoot, env), temp = tempPathFor(path, process.pid);
173
+ await writeFile(temp, JSON.stringify(starting) + `
174
+ `, { mode: 384 }), await rename(temp, path);
175
+ }
176
+ async function readViewerStarting(projectRoot, env = process.env, isAlive = isProcessAlive) {
177
+ const path = viewerStartingPath(projectRoot, env);
178
+ let data;
179
+ try {
180
+ data = JSON.parse(await readFile(path, "utf8"));
181
+ } catch (error) {
182
+ return error.code !== "ENOENT" && await unlink(path).catch(() => {
183
+ }), null;
184
+ }
185
+ return !Number.isInteger(data?.pid) || !isAlive(data.pid) ? (await unlink(path).catch(() => {
186
+ }), null) : data;
187
+ }
188
+ async function removeViewerStarting(projectRoot, options = {}) {
189
+ const env = options.env ?? process.env, path = viewerStartingPath(projectRoot, env);
190
+ if (options.pid !== void 0)
191
+ try {
192
+ if (JSON.parse(await readFile(path, "utf8")).pid !== options.pid)
193
+ return;
194
+ } catch {
195
+ return;
196
+ }
197
+ await unlink(path).catch(() => {
198
+ });
199
+ }
200
+ async function viewersOnPort(port, env = process.env, isAlive = isProcessAlive) {
201
+ const dir = viewerRecordDir(env);
202
+ let names;
203
+ try {
204
+ names = await readdir(dir);
205
+ } catch {
206
+ return [];
207
+ }
208
+ const found = [];
209
+ for (const name of names) {
210
+ if (!name.endsWith(".json"))
211
+ continue;
212
+ const record = parseRecord(await readFile(join(dir, name), "utf8").catch(() => ""));
213
+ record && record.port === port && isAlive(record.pid) && found.push(record);
214
+ }
215
+ return found;
216
+ }
217
+ async function describePortHolder(port, env = process.env) {
218
+ const viewers = await viewersOnPort(port, env).catch(() => []);
219
+ if (viewers.length > 0) {
220
+ const viewer = viewers[0];
221
+ return `the dr viewer for ${viewer.projectRoot} (pid ${viewer.pid})`;
222
+ }
223
+ try {
224
+ const lines = (spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fpc"], {
225
+ encoding: "utf8",
226
+ timeout: 2e3
227
+ }).stdout ?? "").split(`
228
+ `), pid = lines.find((line) => line.startsWith("p"))?.slice(1), command = lines.find((line) => line.startsWith("c"))?.slice(1);
229
+ if (pid)
230
+ return `${command ? `${command} ` : "a process "}(pid ${pid})`;
231
+ } catch {
232
+ }
233
+ return "another program";
234
+ }
235
+ function viewerUrl(record, options = {}) {
236
+ const query = record.token ? `?token=${encodeURIComponent(record.token)}` : "", route = options.changesetId ? `#/changesets/list?changeset=${encodeURIComponent(options.changesetId)}` : "";
237
+ return `http://localhost:${record.port}/${query}${route}`;
238
+ }
239
+ export {
240
+ DR_STATE_DIR_ENV,
241
+ canonicalProjectRoot,
242
+ describePortHolder,
243
+ ensureViewerRecordDir,
244
+ findRunningViewer,
245
+ isProcessAlive,
246
+ probeViewer,
247
+ readViewerRecord,
248
+ readViewerStarting,
249
+ removeStaleTempFiles,
250
+ removeViewerRecord,
251
+ removeViewerRecordSync,
252
+ removeViewerStarting,
253
+ stateDir,
254
+ viewerAnswers,
255
+ viewerLogPath,
256
+ viewerRecordDir,
257
+ viewerRecordPath,
258
+ viewerStartingPath,
259
+ viewerUrl,
260
+ viewersOnPort,
261
+ writeViewerRecord,
262
+ writeViewerStarting
263
+ };
264
+ //# sourceMappingURL=viewer-registry.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/viewer-registry.ts"],
4
+ "sourcesContent": ["/**\n * Viewer registry: which `dr visualize` is serving which project.\n *\n * `dr visualize` writes one record per project once its server is listening, and removes it on\n * exit. Tools that hand the human a link to the viewer (`review_open`, `project_status`) read it\n * with `findRunningViewer` and reuse a running viewer instead of starting another.\n *\n * The record holds the viewer's auth token, so it lives in the user's state directory, outside\n * any repository, where it can't be committed:\n *\n * $DR_STATE_DIR/viewers/<hash>.json (DR_STATE_DIR overrides, for tests)\n * $XDG_STATE_HOME/dr/viewers/<hash>.json (when XDG_STATE_HOME is set)\n * ~/.local/state/dr/viewers/<hash>.json (otherwise)\n *\n * `<hash>` is a hash of the project root's real path. Each file is mode 0600 and the viewers\n * directory 0700.\n *\n * Stable API (imported by MCP tools): ViewerRecord, writeViewerRecord, readViewerRecord,\n * removeViewerRecord, removeViewerRecordSync, findRunningViewer, probeViewer, viewerUrl,\n * describePortHolder.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { existsSync, readdirSync, readFileSync, realpathSync, unlinkSync } from \"node:fs\";\nimport { chmod, mkdir, readdir, readFile, rename, stat, unlink, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, join, resolve } from \"node:path\";\n\n/** Env var that points the user state directory somewhere else (tests). */\nexport const DR_STATE_DIR_ENV = \"DR_STATE_DIR\";\n\n/** A running `dr visualize` for one project. */\nexport interface ViewerRecord {\n /** Pid of the `dr visualize` process. */\n pid: number;\n port: number;\n /** The auth token, or null when the viewer runs with --no-auth. */\n token: string | null;\n /** Real path of the project root the viewer serves. */\n projectRoot: string;\n /** ISO timestamp of when the viewer started listening. */\n startedAt: string;\n /** Whether the viewer runs with --with-changesets (DrBot may stage changes). */\n withChangesets: boolean;\n}\n\n/** The user's dr state directory (not the project's .dr/). */\nexport function stateDir(env: NodeJS.ProcessEnv = process.env): string {\n const override = env[DR_STATE_DIR_ENV];\n if (override) return resolve(override);\n const xdg = env.XDG_STATE_HOME;\n if (xdg) return join(resolve(xdg), \"dr\");\n return join(homedir(), \".local\", \"state\", \"dr\");\n}\n\n/** Directory holding the viewer records. */\nexport function viewerRecordDir(env: NodeJS.ProcessEnv = process.env): string {\n return join(stateDir(env), \"viewers\");\n}\n\n/** The project root's real path, or its resolved path when it doesn't exist. */\nexport function canonicalProjectRoot(projectRoot: string): string {\n try {\n return realpathSync(projectRoot);\n } catch {\n return resolve(projectRoot);\n }\n}\n\nfunction recordKey(projectRoot: string): string {\n return createHash(\"sha256\").update(canonicalProjectRoot(projectRoot)).digest(\"hex\").slice(0, 24);\n}\n\n/** Path of a project's viewer record. */\nexport function viewerRecordPath(projectRoot: string, env: NodeJS.ProcessEnv = process.env): string {\n return join(viewerRecordDir(env), `${recordKey(projectRoot)}.json`);\n}\n\n/** Path of the log a viewer started by `review_open` writes its output to. */\nexport function viewerLogPath(projectRoot: string, env: NodeJS.ProcessEnv = process.env): string {\n return join(viewerRecordDir(env), `${recordKey(projectRoot)}.log`);\n}\n\n/** Path of the \"a viewer is starting\" marker review_open writes while a viewer it spawned loads. */\nexport function viewerStartingPath(projectRoot: string, env: NodeJS.ProcessEnv = process.env): string {\n return join(viewerRecordDir(env), `${recordKey(projectRoot)}.starting`);\n}\n\nlet tempSeq = 0;\n\n/** Temp file for one write before it is renamed into place: `<file>.<pid>.<n>.tmp`. */\nfunction tempPathFor(path: string, pid: number): string {\n return `${path}.${pid}.${++tempSeq}.tmp`;\n}\n\n/** Removes this pid's temp files for `path` (writes it had in flight when it exited). */\nfunction removeTempFilesSync(path: string, pid: number): void {\n const prefix = `${basename(path)}.${pid}.`;\n try {\n for (const name of readdirSync(dirname(path))) {\n if (name.startsWith(prefix) && name.endsWith(\".tmp\")) unlinkSync(join(dirname(path), name));\n }\n } catch {\n // No directory, or nothing to remove.\n }\n}\n\n/** Temp files older than this are left over from a writer that died mid-write. */\nconst STALE_TEMP_MS = 60_000;\n\n/** Creates the viewers directory (0700) and returns it. */\nexport async function ensureViewerRecordDir(env: NodeJS.ProcessEnv = process.env): Promise<string> {\n const dir = viewerRecordDir(env);\n await mkdir(dir, { recursive: true, mode: 0o700 });\n await chmod(dir, 0o700);\n return dir;\n}\n\n/**\n * Removes `.tmp` files older than a minute from the viewers directory: half-written records\n * (which may hold a token) left by a writer that was killed mid-write. Never throws.\n */\nexport async function removeStaleTempFiles(\n env: NodeJS.ProcessEnv = process.env,\n now: number = Date.now()\n): Promise<string[]> {\n const dir = viewerRecordDir(env);\n const removed: string[] = [];\n let names: string[];\n try {\n names = await readdir(dir);\n } catch {\n return removed;\n }\n for (const name of names) {\n if (!name.endsWith(\".tmp\")) continue;\n const path = join(dir, name);\n try {\n if (now - (await stat(path)).mtimeMs > STALE_TEMP_MS) {\n await unlink(path);\n removed.push(path);\n }\n } catch {\n // Gone already, or not ours to remove.\n }\n }\n return removed;\n}\n\n/**\n * Writes a project's viewer record (0600), replacing any earlier one. The write is atomic (a\n * temp file renamed into place), so a reader never sees half a record; stale temp files from\n * earlier writers are cleaned up first. Returns the record's path.\n */\nexport async function writeViewerRecord(\n record: ViewerRecord,\n env: NodeJS.ProcessEnv = process.env\n): Promise<string> {\n await ensureViewerRecordDir(env);\n await removeStaleTempFiles(env);\n const path = viewerRecordPath(record.projectRoot, env);\n const stored: ViewerRecord = { ...record, projectRoot: canonicalProjectRoot(record.projectRoot) };\n const temp = tempPathFor(path, process.pid);\n try {\n await writeFile(temp, JSON.stringify(stored, null, 2) + \"\\n\", { mode: 0o600 });\n await chmod(temp, 0o600);\n await rename(temp, path);\n } catch (error) {\n await unlink(temp).catch(() => {});\n throw error;\n }\n return path;\n}\n\nfunction parseRecord(text: string): ViewerRecord | null {\n try {\n const data = JSON.parse(text);\n if (\n data &&\n typeof data === \"object\" &&\n Number.isInteger(data.pid) &&\n Number.isInteger(data.port) &&\n (typeof data.token === \"string\" || data.token === null) &&\n typeof data.projectRoot === \"string\"\n ) {\n return {\n pid: data.pid,\n port: data.port,\n token: data.token,\n projectRoot: data.projectRoot,\n startedAt: typeof data.startedAt === \"string\" ? data.startedAt : \"\",\n withChangesets: data.withChangesets === true,\n };\n }\n } catch {\n // Unparseable: treated as no record.\n }\n return null;\n}\n\n/**\n * A project's viewer record as written, without checking that the viewer still runs. Undefined\n * when there is no record; null when the file is there but unreadable as a record.\n */\nexport async function readViewerRecord(\n projectRoot: string,\n env: NodeJS.ProcessEnv = process.env\n): Promise<ViewerRecord | null | undefined> {\n let text: string;\n try {\n text = await readFile(viewerRecordPath(projectRoot, env), \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return undefined;\n throw error;\n }\n return parseRecord(text);\n}\n\nexport interface RemoveViewerRecordOptions {\n /** Remove only if the record belongs to this pid (so a viewer never removes a newer one's). */\n pid?: number;\n env?: NodeJS.ProcessEnv;\n}\n\n/** Removes a project's viewer record. Missing is fine. */\nexport async function removeViewerRecord(\n projectRoot: string,\n options: RemoveViewerRecordOptions = {}\n): Promise<void> {\n const env = options.env ?? process.env;\n const path = viewerRecordPath(projectRoot, env);\n if (options.pid !== undefined) {\n const current = await readViewerRecord(projectRoot, env).catch(() => undefined);\n if (current && current.pid !== options.pid) return;\n }\n try {\n await unlink(path);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n }\n}\n\n/** Synchronous removeViewerRecord, for a process \"exit\" handler. Never throws. */\nexport function removeViewerRecordSync(projectRoot: string, options: RemoveViewerRecordOptions = {}): void {\n const env = options.env ?? process.env;\n const path = viewerRecordPath(projectRoot, env);\n // A write this process had in flight when it exited.\n removeTempFilesSync(path, options.pid ?? process.pid);\n try {\n if (!existsSync(path)) return;\n if (options.pid !== undefined) {\n const current = parseRecord(readFileSync(path, \"utf8\"));\n if (current && current.pid !== options.pid) return;\n }\n unlinkSync(path);\n } catch {\n // Best effort at exit; findRunningViewer clears stale records anyway.\n }\n}\n\n/** Whether a process with this pid exists (EPERM means it exists but belongs to someone else). */\nexport function isProcessAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/** Whether a dr viewer answers its health check on this port. */\nexport async function viewerAnswers(port: number, timeoutMs = 1500): Promise<boolean> {\n try {\n const response = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(timeoutMs) });\n if (!response.ok) return false;\n const body = (await response.json()) as { status?: unknown };\n return body?.status === \"ok\" || body?.status === \"degraded\";\n } catch {\n return false;\n }\n}\n\nexport interface FindRunningViewerOptions {\n env?: NodeJS.ProcessEnv;\n /** Injected for tests; defaults to isProcessAlive. */\n isAlive?: (pid: number) => boolean;\n /** Injected for tests; defaults to viewerAnswers. Called with the probe's timeout. */\n answers?: (port: number, timeoutMs: number) => Promise<boolean>;\n /** First health probe's timeout (default 1.5s). */\n probeTimeoutMs?: number;\n /** The retry's timeout when the first probe fails and the pid is alive (default 5s). */\n retryTimeoutMs?: number;\n}\n\n/**\n * What the registry knows about a project's viewer:\n * - `none`: no record, or a stale one (its pid is dead), which is removed.\n * - `running`: the pid is alive and the server answers its health check.\n * - `unreachable`: the pid is alive but the server did not answer, twice. The record is kept: the\n * viewer may only be busy, and a second viewer for the project would be wrong.\n */\nexport type ViewerProbe =\n | { state: \"none\" }\n | { state: \"running\"; record: ViewerRecord }\n | { state: \"unreachable\"; record: ViewerRecord };\n\n/** Checks a project's viewer record against its process and its health check. */\nexport async function probeViewer(projectRoot: string, options: FindRunningViewerOptions = {}): Promise<ViewerProbe> {\n const env = options.env ?? process.env;\n const isAlive = options.isAlive ?? isProcessAlive;\n const answers = options.answers ?? viewerAnswers;\n\n const record = await readViewerRecord(projectRoot, env);\n if (record === undefined) return { state: \"none\" };\n if (record === null) {\n await removeViewerRecord(projectRoot, { env });\n return { state: \"none\" };\n }\n if (record.projectRoot !== canonicalProjectRoot(projectRoot)) return { state: \"none\" };\n if (!isAlive(record.pid)) {\n await removeViewerRecord(projectRoot, { env, pid: record.pid });\n return { state: \"none\" };\n }\n if (await answers(record.port, options.probeTimeoutMs ?? 1500)) return { state: \"running\", record };\n if (await answers(record.port, options.retryTimeoutMs ?? 5000)) return { state: \"running\", record };\n return { state: \"unreachable\", record };\n}\n\n/**\n * The viewer serving this project, or null. A record counts only when its pid is alive and the\n * server answers its health check. A record whose pid is dead is removed; one whose pid is alive\n * but doesn't answer is kept (probeViewer tells the two apart).\n */\nexport async function findRunningViewer(\n projectRoot: string,\n options: FindRunningViewerOptions = {}\n): Promise<ViewerRecord | null> {\n const probe = await probeViewer(projectRoot, options);\n return probe.state === \"running\" ? probe.record : null;\n}\n\n/** A viewer review_open spawned that has not recorded itself yet. */\nexport interface ViewerStarting {\n pid: number;\n startedAt: string;\n logPath?: string;\n}\n\n/** Marks that a viewer is starting for the project (0600), so other callers wait for it. */\nexport async function writeViewerStarting(\n projectRoot: string,\n starting: ViewerStarting,\n env: NodeJS.ProcessEnv = process.env\n): Promise<void> {\n await ensureViewerRecordDir(env);\n const path = viewerStartingPath(projectRoot, env);\n const temp = tempPathFor(path, process.pid);\n await writeFile(temp, JSON.stringify(starting) + \"\\n\", { mode: 0o600 });\n await rename(temp, path);\n}\n\n/** The viewer starting for the project while its pid is alive; a dead one's marker is removed. */\nexport async function readViewerStarting(\n projectRoot: string,\n env: NodeJS.ProcessEnv = process.env,\n isAlive: (pid: number) => boolean = isProcessAlive\n): Promise<ViewerStarting | null> {\n const path = viewerStartingPath(projectRoot, env);\n let data: ViewerStarting;\n try {\n data = JSON.parse(await readFile(path, \"utf8\"));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") await unlink(path).catch(() => {});\n return null;\n }\n if (!Number.isInteger(data?.pid) || !isAlive(data.pid)) {\n await unlink(path).catch(() => {});\n return null;\n }\n return data;\n}\n\n/** Removes the starting marker (only this pid's, when given). */\nexport async function removeViewerStarting(\n projectRoot: string,\n options: { pid?: number; env?: NodeJS.ProcessEnv } = {}\n): Promise<void> {\n const env = options.env ?? process.env;\n const path = viewerStartingPath(projectRoot, env);\n if (options.pid !== undefined) {\n try {\n const data = JSON.parse(await readFile(path, \"utf8\")) as ViewerStarting;\n if (data.pid !== options.pid) return;\n } catch {\n return;\n }\n }\n await unlink(path).catch(() => {});\n}\n\n/** The live dr viewers the registry knows of on this port (any project). */\nexport async function viewersOnPort(\n port: number,\n env: NodeJS.ProcessEnv = process.env,\n isAlive: (pid: number) => boolean = isProcessAlive\n): Promise<ViewerRecord[]> {\n const dir = viewerRecordDir(env);\n let names: string[];\n try {\n names = await readdir(dir);\n } catch {\n return [];\n }\n const found: ViewerRecord[] = [];\n for (const name of names) {\n if (!name.endsWith(\".json\")) continue;\n const record = parseRecord(await readFile(join(dir, name), \"utf8\").catch(() => \"\"));\n if (record && record.port === port && isAlive(record.pid)) found.push(record);\n }\n return found;\n}\n\n/**\n * Says what holds a port, for an error a person reads: another project's dr viewer (from the\n * registry), else the listening process `lsof` names, else \"another program\". Never throws.\n */\nexport async function describePortHolder(port: number, env: NodeJS.ProcessEnv = process.env): Promise<string> {\n const viewers = await viewersOnPort(port, env).catch(() => []);\n if (viewers.length > 0) {\n const viewer = viewers[0];\n return `the dr viewer for ${viewer.projectRoot} (pid ${viewer.pid})`;\n }\n try {\n const result = spawnSync(\"lsof\", [\"-nP\", `-iTCP:${port}`, \"-sTCP:LISTEN\", \"-Fpc\"], {\n encoding: \"utf8\",\n timeout: 2000,\n });\n const lines = (result.stdout ?? \"\").split(\"\\n\");\n const pid = lines.find((line) => line.startsWith(\"p\"))?.slice(1);\n const command = lines.find((line) => line.startsWith(\"c\"))?.slice(1);\n if (pid) return `${command ? `${command} ` : \"a process \"}(pid ${pid})`;\n } catch {\n // No lsof: fall through.\n }\n return \"another program\";\n}\n\nexport interface ViewerUrlOptions {\n /** Open this changeset in the viewer's changeset overlay. */\n changesetId?: string;\n}\n\n/**\n * Link into a running viewer. The viewer reads the token from the query string before the hash\n * (`/?token=...#/route`), stores it and drops it from the address bar; the route and its search\n * params go in the hash. Without auth there is no token.\n */\nexport function viewerUrl(record: Pick<ViewerRecord, \"port\" | \"token\">, options: ViewerUrlOptions = {}): string {\n const query = record.token ? `?token=${encodeURIComponent(record.token)}` : \"\";\n const route = options.changesetId\n ? `#/changesets/list?changeset=${encodeURIComponent(options.changesetId)}`\n : \"\";\n return `http://localhost:${record.port}/${query}${route}`;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  import { Model } from "./core/model.js";
2
2
  import { VisualizationServer } from "./server/server.js";
3
3
  import { getErrorMessage } from "./utils/errors.js";
4
- const port = parseInt(process.env.DR_VISUALIZE_PORT || "8080", 10), projectPath = process.env.DR_PROJECT_PATH || process.cwd(), authEnabled = process.env.DR_AUTH_ENABLED !== "false", authToken = process.env.DR_AUTH_TOKEN && process.env.DR_AUTH_TOKEN.length > 0 ? process.env.DR_AUTH_TOKEN : void 0, withDanger = process.env.DR_WITH_DANGER === "true", viewerPath = process.env.DR_VIEWER_PATH, isTelemetryEnabled = !1;
4
+ const port = parseInt(process.env.DR_VISUALIZE_PORT || "8080", 10), projectPath = process.env.DR_PROJECT_PATH || process.cwd(), authEnabled = process.env.DR_AUTH_ENABLED !== "false", authToken = process.env.DR_AUTH_TOKEN && process.env.DR_AUTH_TOKEN.length > 0 ? process.env.DR_AUTH_TOKEN : void 0, withDanger = process.env.DR_WITH_DANGER === "true", withChangesets = process.env.DR_WITH_CHANGESETS === "true", viewerPath = process.env.DR_VIEWER_PATH, isTelemetryEnabled = !1;
5
5
  let serverInstance = null, isShuttingDown = !1;
6
6
  async function main() {
7
7
  try {
@@ -23,6 +23,7 @@ async function main() {
23
23
  authEnabled,
24
24
  authToken,
25
25
  withDanger,
26
+ withChangesets,
26
27
  viewerPath
27
28
  });
28
29
  serverInstance = server, await server.start(port), authEnabled && console.log(`TOKEN:${server.getAuthToken()}`), setupShutdownHandlers();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/server-entry.ts"],
4
- "sourcesContent": ["/**\n * Visualization Server Entry Point\n * This is spawned as a subprocess by `dr visualize` (see cli/src/commands/visualize.ts),\n * using whichever runtime (Node.js or Bun) is already running the parent CLI process.\n * It receives configuration via environment variables and starts the server.\n */\n\nimport { Model } from \"./core/model.js\";\nimport { VisualizationServer } from \"./server/server.js\";\nimport { getErrorMessage } from \"./utils/errors.js\";\n\n// Get configuration from environment variables\nconst port = parseInt(process.env.DR_VISUALIZE_PORT || \"8080\", 10);\nconst projectPath = process.env.DR_PROJECT_PATH || process.cwd();\nconst authEnabled = process.env.DR_AUTH_ENABLED !== \"false\";\n// Only pass authToken if it's a non-empty string, otherwise let server generate one\nconst authToken =\n process.env.DR_AUTH_TOKEN && process.env.DR_AUTH_TOKEN.length > 0\n ? process.env.DR_AUTH_TOKEN\n : undefined;\nconst withDanger = process.env.DR_WITH_DANGER === \"true\";\nconst viewerPath = process.env.DR_VIEWER_PATH;\n\n// Conditional telemetry import based on compile-time flag\ndeclare const TELEMETRY_ENABLED: boolean | undefined;\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== \"undefined\" ? TELEMETRY_ENABLED : false;\n\nlet serverInstance: VisualizationServer | null = null;\nlet isShuttingDown = false;\n\nasync function main() {\n try {\n // CRITICAL: Initialize telemetry SDK in subprocess\n // Without this, no spans will be created or exported even though TELEMETRY_ENABLED=true\n if (isTelemetryEnabled) {\n try {\n const { initTelemetry } = await import(\"./telemetry/index.js\");\n await initTelemetry();\n\n // Load and display actual OTLP configuration\n const { loadOTLPConfig } = await import(\"./telemetry/config.js\");\n const otlpConfig = await loadOTLPConfig();\n\n if (process.env.DEBUG || process.env.VERBOSE) {\n console.log(`[Telemetry] SDK initialized in server subprocess`);\n console.log(`[Telemetry] OTLP traces endpoint: ${otlpConfig.endpoint}`);\n console.log(`[Telemetry] OTLP logs endpoint: ${otlpConfig.logsEndpoint}`);\n console.log(`[Telemetry] Service name: ${otlpConfig.serviceName}`);\n\n // Runtime detection and compatibility info\n const isBun = typeof (globalThis as any).Bun !== \"undefined\";\n if (isBun) {\n console.log(`[Telemetry] Runtime: Bun (using fetch-based OTLP exporter)`);\n } else {\n console.log(`[Telemetry] Runtime: Node.js (using http-based OTLP exporter)`);\n }\n }\n } catch (error) {\n // Log telemetry initialization errors but don't fail the server\n console.error(\"[Telemetry] Failed to initialize telemetry:\", error);\n console.error(\"[Telemetry] Server will continue without telemetry\");\n }\n } else if (process.env.DEBUG || process.env.VERBOSE) {\n console.log(`[Telemetry] TELEMETRY_ENABLED is false - telemetry disabled`);\n }\n\n // Load model with full content\n const model = await Model.load(projectPath, { lazyLoad: false });\n\n // Create and start server\n const server = new VisualizationServer(model, {\n authEnabled,\n authToken,\n withDanger,\n viewerPath,\n });\n serverInstance = server; // Store for signal handlers\n await server.start(port);\n\n // Output token for parent process to read\n if (authEnabled) {\n console.log(`TOKEN:${server.getAuthToken()}`);\n }\n\n // Setup graceful shutdown handlers\n setupShutdownHandlers();\n } catch (error) {\n const message = getErrorMessage(error);\n console.error(`Error: ${message}`);\n if (error instanceof Error) {\n console.error(`Stack: ${error.stack}`);\n }\n await gracefulShutdown(1);\n }\n}\n\n/**\n * Gracefully shutdown the server and flush telemetry\n */\nasync function gracefulShutdown(exitCode: number = 0): Promise<void> {\n if (isShuttingDown) {\n return; // Prevent multiple shutdown attempts\n }\n isShuttingDown = true;\n\n try {\n // Stop the visualization server\n if (serverInstance) {\n serverInstance.stop();\n serverInstance = null;\n }\n\n // Flush telemetry spans before exit\n if (isTelemetryEnabled) {\n console.log(\"[Telemetry] Shutting down telemetry and flushing spans...\");\n const { shutdownTelemetry } = await import(\"./telemetry/index.js\");\n await shutdownTelemetry();\n console.log(\"[Telemetry] Telemetry shutdown complete\");\n } else {\n if (process.env.DEBUG) {\n console.log(\"[Telemetry] Telemetry is disabled (TELEMETRY_ENABLED=false at build time)\");\n }\n }\n } catch (error) {\n // Don't block shutdown on errors\n if (process.env.DEBUG) {\n console.error(\"Error during shutdown:\", error);\n }\n } finally {\n process.exit(exitCode);\n }\n}\n\n/**\n * Setup handlers for graceful shutdown on signals\n */\nfunction setupShutdownHandlers(): void {\n // Handle Ctrl-C (SIGINT)\n process.on(\"SIGINT\", async () => {\n console.log(\"\\n[SIGINT] Shutting down visualization server...\");\n await gracefulShutdown(0);\n });\n\n // Handle termination signal (SIGTERM)\n process.on(\"SIGTERM\", async () => {\n console.log(\"\\n[SIGTERM] Shutting down visualization server...\");\n await gracefulShutdown(0);\n });\n\n // Handle uncaught exceptions\n process.on(\"uncaughtException\", async (error) => {\n console.error(\"Uncaught exception:\", error);\n await gracefulShutdown(1);\n });\n\n // Handle unhandled promise rejections\n process.on(\"unhandledRejection\", async (reason) => {\n console.error(\"Unhandled rejection:\", reason);\n await gracefulShutdown(1);\n });\n}\n\nmain();\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Visualization Server Entry Point\n * This is spawned as a subprocess by `dr visualize` (see cli/src/commands/visualize.ts),\n * using whichever runtime (Node.js or Bun) is already running the parent CLI process.\n * It receives configuration via environment variables and starts the server.\n */\n\nimport { Model } from \"./core/model.js\";\nimport { VisualizationServer } from \"./server/server.js\";\nimport { getErrorMessage } from \"./utils/errors.js\";\n\n// Get configuration from environment variables\nconst port = parseInt(process.env.DR_VISUALIZE_PORT || \"8080\", 10);\nconst projectPath = process.env.DR_PROJECT_PATH || process.cwd();\nconst authEnabled = process.env.DR_AUTH_ENABLED !== \"false\";\n// Only pass authToken if it's a non-empty string, otherwise let server generate one\nconst authToken =\n process.env.DR_AUTH_TOKEN && process.env.DR_AUTH_TOKEN.length > 0\n ? process.env.DR_AUTH_TOKEN\n : undefined;\nconst withDanger = process.env.DR_WITH_DANGER === \"true\";\nconst withChangesets = process.env.DR_WITH_CHANGESETS === \"true\";\nconst viewerPath = process.env.DR_VIEWER_PATH;\n\n// Conditional telemetry import based on compile-time flag\ndeclare const TELEMETRY_ENABLED: boolean | undefined;\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== \"undefined\" ? TELEMETRY_ENABLED : false;\n\nlet serverInstance: VisualizationServer | null = null;\nlet isShuttingDown = false;\n\nasync function main() {\n try {\n // CRITICAL: Initialize telemetry SDK in subprocess\n // Without this, no spans will be created or exported even though TELEMETRY_ENABLED=true\n if (isTelemetryEnabled) {\n try {\n const { initTelemetry } = await import(\"./telemetry/index.js\");\n await initTelemetry();\n\n // Load and display actual OTLP configuration\n const { loadOTLPConfig } = await import(\"./telemetry/config.js\");\n const otlpConfig = await loadOTLPConfig();\n\n if (process.env.DEBUG || process.env.VERBOSE) {\n console.log(`[Telemetry] SDK initialized in server subprocess`);\n console.log(`[Telemetry] OTLP traces endpoint: ${otlpConfig.endpoint}`);\n console.log(`[Telemetry] OTLP logs endpoint: ${otlpConfig.logsEndpoint}`);\n console.log(`[Telemetry] Service name: ${otlpConfig.serviceName}`);\n\n // Runtime detection and compatibility info\n const isBun = typeof (globalThis as any).Bun !== \"undefined\";\n if (isBun) {\n console.log(`[Telemetry] Runtime: Bun (using fetch-based OTLP exporter)`);\n } else {\n console.log(`[Telemetry] Runtime: Node.js (using http-based OTLP exporter)`);\n }\n }\n } catch (error) {\n // Log telemetry initialization errors but don't fail the server\n console.error(\"[Telemetry] Failed to initialize telemetry:\", error);\n console.error(\"[Telemetry] Server will continue without telemetry\");\n }\n } else if (process.env.DEBUG || process.env.VERBOSE) {\n console.log(`[Telemetry] TELEMETRY_ENABLED is false - telemetry disabled`);\n }\n\n // Load model with full content\n const model = await Model.load(projectPath, { lazyLoad: false });\n\n // Create and start server\n const server = new VisualizationServer(model, {\n authEnabled,\n authToken,\n withDanger,\n withChangesets,\n viewerPath,\n });\n serverInstance = server; // Store for signal handlers\n await server.start(port);\n\n // Output token for parent process to read\n if (authEnabled) {\n console.log(`TOKEN:${server.getAuthToken()}`);\n }\n\n // Setup graceful shutdown handlers\n setupShutdownHandlers();\n } catch (error) {\n const message = getErrorMessage(error);\n console.error(`Error: ${message}`);\n if (error instanceof Error) {\n console.error(`Stack: ${error.stack}`);\n }\n await gracefulShutdown(1);\n }\n}\n\n/**\n * Gracefully shutdown the server and flush telemetry\n */\nasync function gracefulShutdown(exitCode: number = 0): Promise<void> {\n if (isShuttingDown) {\n return; // Prevent multiple shutdown attempts\n }\n isShuttingDown = true;\n\n try {\n // Stop the visualization server\n if (serverInstance) {\n serverInstance.stop();\n serverInstance = null;\n }\n\n // Flush telemetry spans before exit\n if (isTelemetryEnabled) {\n console.log(\"[Telemetry] Shutting down telemetry and flushing spans...\");\n const { shutdownTelemetry } = await import(\"./telemetry/index.js\");\n await shutdownTelemetry();\n console.log(\"[Telemetry] Telemetry shutdown complete\");\n } else {\n if (process.env.DEBUG) {\n console.log(\"[Telemetry] Telemetry is disabled (TELEMETRY_ENABLED=false at build time)\");\n }\n }\n } catch (error) {\n // Don't block shutdown on errors\n if (process.env.DEBUG) {\n console.error(\"Error during shutdown:\", error);\n }\n } finally {\n process.exit(exitCode);\n }\n}\n\n/**\n * Setup handlers for graceful shutdown on signals\n */\nfunction setupShutdownHandlers(): void {\n // Handle Ctrl-C (SIGINT)\n process.on(\"SIGINT\", async () => {\n console.log(\"\\n[SIGINT] Shutting down visualization server...\");\n await gracefulShutdown(0);\n });\n\n // Handle termination signal (SIGTERM)\n process.on(\"SIGTERM\", async () => {\n console.log(\"\\n[SIGTERM] Shutting down visualization server...\");\n await gracefulShutdown(0);\n });\n\n // Handle uncaught exceptions\n process.on(\"uncaughtException\", async (error) => {\n console.error(\"Uncaught exception:\", error);\n await gracefulShutdown(1);\n });\n\n // Handle unhandled promise rejections\n process.on(\"unhandledRejection\", async (reason) => {\n console.error(\"Unhandled rejection:\", reason);\n await gracefulShutdown(1);\n });\n}\n\nmain();\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -21,7 +21,7 @@ async function initTelemetry() {
21
21
  `), process.stderr.write(` - Logs endpoint: ${otlpConfig.logsEndpoint}
22
22
  `), process.stderr.write(` - Service name: ${otlpConfig.serviceName}
23
23
  `));
24
- const cliVersion = "0.2.0";
24
+ const cliVersion = "0.4.0";
25
25
  let projectName = "unknown";
26
26
  try {
27
27
  const [{ readFileSync, existsSync }, { parse }] = await Promise.all([
@@ -2,22 +2,20 @@ function stripAnsi(text) {
2
2
  return text.replace(/\x1b\[[0-9;]*m/g, "");
3
3
  }
4
4
  function installAnsiSuppressor() {
5
- if (process.stdout.isTTY === !0)
6
- return;
7
5
  const originalLog = console.log, originalError = console.error, originalWarn = console.warn, originalInfo = console.info;
8
- console.log = function(...args) {
6
+ process.stdout.isTTY !== !0 && (console.log = function(...args) {
9
7
  const stripped = args.map((arg) => typeof arg == "string" ? stripAnsi(arg) : arg);
10
8
  return originalLog.apply(console, stripped);
11
- }, console.error = function(...args) {
9
+ }, console.info = function(...args) {
10
+ const stripped = args.map((arg) => typeof arg == "string" ? stripAnsi(arg) : arg);
11
+ return originalInfo.apply(console, stripped);
12
+ }), process.stderr.isTTY !== !0 && (console.error = function(...args) {
12
13
  const stripped = args.map((arg) => typeof arg == "string" ? stripAnsi(arg) : arg);
13
14
  return originalError.apply(console, stripped);
14
15
  }, console.warn = function(...args) {
15
16
  const stripped = args.map((arg) => typeof arg == "string" ? stripAnsi(arg) : arg);
16
17
  return originalWarn.apply(console, stripped);
17
- }, console.info = function(...args) {
18
- const stripped = args.map((arg) => typeof arg == "string" ? stripAnsi(arg) : arg);
19
- return originalInfo.apply(console, stripped);
20
- };
18
+ });
21
19
  }
22
20
  export {
23
21
  installAnsiSuppressor
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/ansi-suppressor.ts"],
4
- "sourcesContent": ["/**\n * Global ANSI code suppression for non-TTY output\n * Intercepts console output and strips ANSI escape sequences when stdout is not a TTY\n */\n\nfunction stripAnsi(text: string): string {\n // eslint-disable-next-line no-control-regex\n return text.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n}\n\nexport function installAnsiSuppressor(): void {\n // Only suppress if stdout is not a TTY\n if (process.stdout.isTTY === true) {\n return;\n }\n\n const originalLog = console.log;\n const originalError = console.error;\n const originalWarn = console.warn;\n const originalInfo = console.info;\n\n console.log = function (...args: any[]) {\n const stripped = args.map((arg) => {\n if (typeof arg === \"string\") {\n return stripAnsi(arg);\n }\n return arg;\n });\n return originalLog.apply(console, stripped);\n };\n\n console.error = function (...args: any[]) {\n const stripped = args.map((arg) => {\n if (typeof arg === \"string\") {\n return stripAnsi(arg);\n }\n return arg;\n });\n return originalError.apply(console, stripped);\n };\n\n console.warn = function (...args: any[]) {\n const stripped = args.map((arg) => {\n if (typeof arg === \"string\") {\n return stripAnsi(arg);\n }\n return arg;\n });\n return originalWarn.apply(console, stripped);\n };\n\n console.info = function (...args: any[]) {\n const stripped = args.map((arg) => {\n if (typeof arg === \"string\") {\n return stripAnsi(arg);\n }\n return arg;\n });\n return originalInfo.apply(console, stripped);\n };\n}\n"],
5
- "mappings": "AAKA,SAAS,UAAU,MAAY;AAE7B,SAAO,KAAK,QAAQ,mBAAmB,EAAE;AAC3C;AAEM,SAAU,wBAAqB;AAEnC,MAAI,QAAQ,OAAO,UAAU;AAC3B;AAGF,QAAM,cAAc,QAAQ,KACtB,gBAAgB,QAAQ,OACxB,eAAe,QAAQ,MACvB,eAAe,QAAQ;AAE7B,UAAQ,MAAM,YAAa,MAAW;AACpC,UAAM,WAAW,KAAK,IAAI,CAAC,QACrB,OAAO,OAAQ,WACV,UAAU,GAAG,IAEf,GACR;AACD,WAAO,YAAY,MAAM,SAAS,QAAQ;EAC5C,GAEA,QAAQ,QAAQ,YAAa,MAAW;AACtC,UAAM,WAAW,KAAK,IAAI,CAAC,QACrB,OAAO,OAAQ,WACV,UAAU,GAAG,IAEf,GACR;AACD,WAAO,cAAc,MAAM,SAAS,QAAQ;EAC9C,GAEA,QAAQ,OAAO,YAAa,MAAW;AACrC,UAAM,WAAW,KAAK,IAAI,CAAC,QACrB,OAAO,OAAQ,WACV,UAAU,GAAG,IAEf,GACR;AACD,WAAO,aAAa,MAAM,SAAS,QAAQ;EAC7C,GAEA,QAAQ,OAAO,YAAa,MAAW;AACrC,UAAM,WAAW,KAAK,IAAI,CAAC,QACrB,OAAO,OAAQ,WACV,UAAU,GAAG,IAEf,GACR;AACD,WAAO,aAAa,MAAM,SAAS,QAAQ;EAC7C;AACF;",
4
+ "sourcesContent": ["/**\n * Global ANSI code suppression for non-TTY output\n * Intercepts console output and strips ANSI escape sequences when the target stream is not a TTY\n * Handles stdout (log/info) and stderr (error/warn) independently based on their respective TTY status\n */\n\nfunction stripAnsi(text: string): string {\n // eslint-disable-next-line no-control-regex\n return text.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n}\n\nexport function installAnsiSuppressor(): void {\n const originalLog = console.log;\n const originalError = console.error;\n const originalWarn = console.warn;\n const originalInfo = console.info;\n\n // Suppress ANSI codes from stdout (log/info) if stdout is not a TTY\n const shouldSuppressStdout = process.stdout.isTTY !== true;\n if (shouldSuppressStdout) {\n console.log = function (...args: any[]) {\n const stripped = args.map((arg) => {\n if (typeof arg === \"string\") {\n return stripAnsi(arg);\n }\n return arg;\n });\n return originalLog.apply(console, stripped);\n };\n\n console.info = function (...args: any[]) {\n const stripped = args.map((arg) => {\n if (typeof arg === \"string\") {\n return stripAnsi(arg);\n }\n return arg;\n });\n return originalInfo.apply(console, stripped);\n };\n }\n\n // Suppress ANSI codes from stderr (error/warn) if stderr is not a TTY\n const shouldSuppressStderr = process.stderr.isTTY !== true;\n if (shouldSuppressStderr) {\n console.error = function (...args: any[]) {\n const stripped = args.map((arg) => {\n if (typeof arg === \"string\") {\n return stripAnsi(arg);\n }\n return arg;\n });\n return originalError.apply(console, stripped);\n };\n\n console.warn = function (...args: any[]) {\n const stripped = args.map((arg) => {\n if (typeof arg === \"string\") {\n return stripAnsi(arg);\n }\n return arg;\n });\n return originalWarn.apply(console, stripped);\n };\n }\n}\n"],
5
+ "mappings": "AAMA,SAAS,UAAU,MAAY;AAE7B,SAAO,KAAK,QAAQ,mBAAmB,EAAE;AAC3C;AAEM,SAAU,wBAAqB;AACnC,QAAM,cAAc,QAAQ,KACtB,gBAAgB,QAAQ,OACxB,eAAe,QAAQ,MACvB,eAAe,QAAQ;AAI7B,EAD6B,QAAQ,OAAO,UAAU,OAEpD,QAAQ,MAAM,YAAa,MAAW;AACpC,UAAM,WAAW,KAAK,IAAI,CAAC,QACrB,OAAO,OAAQ,WACV,UAAU,GAAG,IAEf,GACR;AACD,WAAO,YAAY,MAAM,SAAS,QAAQ;EAC5C,GAEA,QAAQ,OAAO,YAAa,MAAW;AACrC,UAAM,WAAW,KAAK,IAAI,CAAC,QACrB,OAAO,OAAQ,WACV,UAAU,GAAG,IAEf,GACR;AACD,WAAO,aAAa,MAAM,SAAS,QAAQ;EAC7C,IAI2B,QAAQ,OAAO,UAAU,OAEpD,QAAQ,QAAQ,YAAa,MAAW;AACtC,UAAM,WAAW,KAAK,IAAI,CAAC,QACrB,OAAO,OAAQ,WACV,UAAU,GAAG,IAEf,GACR;AACD,WAAO,cAAc,MAAM,SAAS,QAAQ;EAC9C,GAEA,QAAQ,OAAO,YAAa,MAAW;AACrC,UAAM,WAAW,KAAK,IAAI,CAAC,QACrB,OAAO,OAAQ,WACV,UAAU,GAAG,IAEf,GACR;AACD,WAAO,aAAa,MAAM,SAAS,QAAQ;EAC7C;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,41 @@
1
+ import { CLIError, ErrorCategory } from "./errors.js";
2
+ function isPlainObject(value) {
3
+ return !!value && typeof value == "object" && !Array.isArray(value);
4
+ }
5
+ function applyAttributePatch(current, patch) {
6
+ const merged = { ...current ?? {} };
7
+ for (const [key, value] of Object.entries(patch))
8
+ value === null ? delete merged[key] : value !== void 0 && (merged[key] = value);
9
+ return merged;
10
+ }
11
+ function effectiveAttributePatch(patch) {
12
+ return patch !== void 0 && Object.keys(patch).length > 0 ? patch : void 0;
13
+ }
14
+ function resolveAttributeUpdate(current, update, flags = { attributes: "attributes", attributePatch: "attributePatch" }) {
15
+ if (update.attributes !== void 0 && update.attributePatch !== void 0)
16
+ throw new CLIError(`Cannot use ${flags.attributes} and ${flags.attributePatch} together`, ErrorCategory.USER, [
17
+ `Use ${flags.attributes} to replace all attributes, or ${flags.attributePatch} to change only the keys it names`
18
+ ]);
19
+ if (update.attributes !== void 0)
20
+ return update.attributes;
21
+ if (update.attributePatch !== void 0)
22
+ return applyAttributePatch(current, update.attributePatch);
23
+ }
24
+ function parseJsonObjectOption(raw, flag) {
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(raw);
28
+ } catch {
29
+ throw new CLIError(`Invalid JSON in ${flag}`, ErrorCategory.USER, ["Ensure your JSON is valid and properly formatted"]);
30
+ }
31
+ if (!isPlainObject(parsed))
32
+ throw new CLIError(`${flag} must be a JSON object`, ErrorCategory.USER, [`Example: ${flag} '{"summary":"New summary"}'`]);
33
+ return parsed;
34
+ }
35
+ export {
36
+ applyAttributePatch,
37
+ effectiveAttributePatch,
38
+ parseJsonObjectOption,
39
+ resolveAttributeUpdate
40
+ };
41
+ //# sourceMappingURL=attribute-patch.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/utils/attribute-patch.ts"],
4
+ "sourcesContent": ["/**\n * Attribute updates for `dr update` and the MCP `model_update` tool.\n *\n * `attributes` replaces an element's attributes; `attributePatch` merges into them, so changing one\n * attribute does not mean repeating every required one. A `null` value in a patch removes that key.\n * Both callers resolve the new attributes here so they behave the same; schema validation then runs\n * on the result.\n */\n\nimport { CLIError, ErrorCategory } from \"./errors.js\";\n\nexport type AttributePatch = Record<string, unknown>;\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Merges `patch` over `current` (shallow: a patched key's value replaces the old one), removing keys\n * whose patch value is `null`. Returns a new object; `current` is not modified (staged `before`\n * snapshots share the element's attributes object).\n */\nexport function applyAttributePatch(\n current: Record<string, unknown> | undefined,\n patch: AttributePatch\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...(current ?? {}) };\n for (const [key, value] of Object.entries(patch)) {\n if (value === null) {\n delete merged[key];\n } else if (value !== undefined) {\n merged[key] = value;\n }\n }\n return merged;\n}\n\n/**\n * The patch as an update: an empty patch names no key, so it changes nothing and is not an update\n * (callers then report \"No fields specified\" and stage nothing).\n */\nexport function effectiveAttributePatch(patch: AttributePatch | undefined): AttributePatch | undefined {\n return patch !== undefined && Object.keys(patch).length > 0 ? patch : undefined;\n}\n\n/**\n * The attributes an update asks for: `attributes` as given (replace), or `attributePatch` merged\n * over `current`, or undefined when the update leaves attributes alone. Passing both is an error.\n *\n * @param flags how the two options are spelled to the caller, for the error message\n */\nexport function resolveAttributeUpdate(\n current: Record<string, unknown> | undefined,\n update: { attributes?: Record<string, unknown>; attributePatch?: AttributePatch },\n flags: { attributes: string; attributePatch: string } = { attributes: \"attributes\", attributePatch: \"attributePatch\" }\n): Record<string, unknown> | undefined {\n if (update.attributes !== undefined && update.attributePatch !== undefined) {\n throw new CLIError(`Cannot use ${flags.attributes} and ${flags.attributePatch} together`, ErrorCategory.USER, [\n `Use ${flags.attributes} to replace all attributes, or ${flags.attributePatch} to change only the keys it names`,\n ]);\n }\n if (update.attributes !== undefined) return update.attributes;\n if (update.attributePatch !== undefined) return applyAttributePatch(current, update.attributePatch);\n return undefined;\n}\n\n/** Parses a CLI JSON option that must be an object (`--attributes`, `--attribute-patch`). */\nexport function parseJsonObjectOption(raw: string, flag: string): Record<string, unknown> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new CLIError(`Invalid JSON in ${flag}`, ErrorCategory.USER, [\"Ensure your JSON is valid and properly formatted\"]);\n }\n if (!isPlainObject(parsed)) {\n throw new CLIError(`${flag} must be a JSON object`, ErrorCategory.USER, [`Example: ${flag} '{\"summary\":\"New summary\"}'`]);\n }\n return parsed;\n}\n"],
5
+ "mappings": "AASA,SAAS,UAAU,qBAAqB;AAIxC,SAAS,cAAc,OAAc;AACnC,SAAO,CAAC,CAAC,SAAS,OAAO,SAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AACrE;AAOM,SAAU,oBACd,SACA,OAAqB;AAErB,QAAM,SAAkC,EAAE,GAAI,WAAW,CAAA,EAAG;AAC5D,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK;AAC7C,IAAI,UAAU,OACZ,OAAO,OAAO,GAAG,IACR,UAAU,WACnB,OAAO,GAAG,IAAI;AAGlB,SAAO;AACT;AAMM,SAAU,wBAAwB,OAAiC;AACvE,SAAO,UAAU,UAAa,OAAO,KAAK,KAAK,EAAE,SAAS,IAAI,QAAQ;AACxE;AAQM,SAAU,uBACd,SACA,QACA,QAAwD,EAAE,YAAY,cAAc,gBAAgB,iBAAgB,GAAE;AAEtH,MAAI,OAAO,eAAe,UAAa,OAAO,mBAAmB;AAC/D,UAAM,IAAI,SAAS,cAAc,MAAM,UAAU,QAAQ,MAAM,cAAc,aAAa,cAAc,MAAM;MAC5G,OAAO,MAAM,UAAU,kCAAkC,MAAM,cAAc;KAC9E;AAEH,MAAI,OAAO,eAAe;AAAW,WAAO,OAAO;AACnD,MAAI,OAAO,mBAAmB;AAAW,WAAO,oBAAoB,SAAS,OAAO,cAAc;AAEpG;AAGM,SAAU,sBAAsB,KAAa,MAAY;AAC7D,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,GAAG;EACzB,QAAQ;AACN,UAAM,IAAI,SAAS,mBAAmB,IAAI,IAAI,cAAc,MAAM,CAAC,kDAAkD,CAAC;EACxH;AACA,MAAI,CAAC,cAAc,MAAM;AACvB,UAAM,IAAI,SAAS,GAAG,IAAI,0BAA0B,cAAc,MAAM,CAAC,YAAY,IAAI,8BAA8B,CAAC;AAE1H,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,29 @@
1
+ const CLI_FLAGS = {
2
+ sourceFile: "--source-file",
3
+ sourceSymbol: "--source-symbol",
4
+ sourceProvenance: "--source-provenance",
5
+ sourceRepoRemote: "--source-repo-remote",
6
+ sourceRepoCommit: "--source-repo-commit",
7
+ clearSourceReference: "--clear-source-reference"
8
+ };
9
+ function optionName(surface, option) {
10
+ return surface === "mcp" ? option : CLI_FLAGS[option];
11
+ }
12
+ function optionExample(surface, option, value) {
13
+ return surface === "mcp" ? `${option}: "${value}"` : `${CLI_FLAGS[option]} "${value}"`;
14
+ }
15
+ function schemaLookupHint(surface, layer, type) {
16
+ return surface === "mcp" ? `Call "spec_type" with layer "${layer}" and type "${type}" to see the required attributes` : `Run "dr schema node ${layer}.${type}" to see the required attributes`;
17
+ }
18
+ function undefinedTypeWarning(surface, layer, elementId, type, replacements) {
19
+ const message = `Element '${elementId}' has type '${type}', which the spec does not define for the ${layer} layer, so its attributes were not checked against a type schema`, replacement = replacements[0];
20
+ let hint;
21
+ return surface === "mcp" ? hint = replacement ? `Pass type "${replacement}" to model_update to move the element to a type the spec defines` : `Call "spec_type" with layer "${layer}" and no type to list the layer's types, then pass one as type to model_update` : hint = replacement ? `Run "dr update ${elementId} --type ${replacement}" to move the element to a type the spec defines` : `Run "dr schema types ${layer}" to list the layer's types, then "dr update ${elementId} --type <type>"`, { message, hint };
22
+ }
23
+ export {
24
+ optionExample,
25
+ optionName,
26
+ schemaLookupHint,
27
+ undefinedTypeWarning
28
+ };
29
+ //# sourceMappingURL=error-hints.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/utils/error-hints.ts"],
4
+ "sourcesContent": ["/**\n * Surface-aware error hints.\n *\n * The CLI and the MCP server share validation code, but a user at the terminal passes\n * `--source-provenance` and runs `dr schema`, while an agent over MCP passes the\n * `sourceProvenance` parameter and calls the `spec_type` tool. Errors built from shared code\n * take a surface and name things the way that caller can act on them.\n */\n\nexport type HintSurface = \"cli\" | \"mcp\";\n\n/** Option keys that shared validators name in their messages. */\nexport type HintOption =\n | \"sourceFile\"\n | \"sourceSymbol\"\n | \"sourceProvenance\"\n | \"sourceRepoRemote\"\n | \"sourceRepoCommit\"\n | \"clearSourceReference\";\n\nconst CLI_FLAGS: Record<HintOption, string> = {\n sourceFile: \"--source-file\",\n sourceSymbol: \"--source-symbol\",\n sourceProvenance: \"--source-provenance\",\n sourceRepoRemote: \"--source-repo-remote\",\n sourceRepoCommit: \"--source-repo-commit\",\n clearSourceReference: \"--clear-source-reference\",\n};\n\n/** How the caller names an option: `--source-file` on the CLI, `sourceFile` over MCP. */\nexport function optionName(surface: HintSurface, option: HintOption): string {\n return surface === \"mcp\" ? option : CLI_FLAGS[option];\n}\n\n/** How the caller passes an option with a value, for examples in hints. */\nexport function optionExample(surface: HintSurface, option: HintOption, value: string): string {\n return surface === \"mcp\" ? `${option}: \"${value}\"` : `${CLI_FLAGS[option]} \"${value}\"`;\n}\n\n/** Where to look up a type's required attributes: `dr schema` on the CLI, `spec_type` over MCP. */\nexport function schemaLookupHint(surface: HintSurface, layer: string, type: string): string {\n return surface === \"mcp\"\n ? `Call \"spec_type\" with layer \"${layer}\" and type \"${type}\" to see the required attributes`\n : `Run \"dr schema node ${layer}.${type}\" to see the required attributes`;\n}\n\n/**\n * Warning for an update to an element whose current type the spec does not define (an obsolete\n * `product.capability`). With no type schema there is nothing to check its attributes against, so\n * the update goes through; the hint names the type change that fixes it, because looking up the\n * obsolete type with `spec_type` or `dr schema node` cannot work.\n */\nexport function undefinedTypeWarning(\n surface: HintSurface,\n layer: string,\n elementId: string,\n type: string,\n replacements: string[]\n): { message: string; hint: string } {\n const message =\n `Element '${elementId}' has type '${type}', which the spec does not define for the ${layer} layer, ` +\n `so its attributes were not checked against a type schema`;\n const replacement = replacements[0];\n let hint: string;\n if (surface === \"mcp\") {\n hint = replacement\n ? `Pass type \"${replacement}\" to model_update to move the element to a type the spec defines`\n : `Call \"spec_type\" with layer \"${layer}\" and no type to list the layer's types, then pass one as type to model_update`;\n } else {\n hint = replacement\n ? `Run \"dr update ${elementId} --type ${replacement}\" to move the element to a type the spec defines`\n : `Run \"dr schema types ${layer}\" to list the layer's types, then \"dr update ${elementId} --type <type>\"`;\n }\n return { message, hint };\n}\n"],
5
+ "mappings": "AAoBA,MAAM,YAAwC;EAC5C,YAAY;EACZ,cAAc;EACd,kBAAkB;EAClB,kBAAkB;EAClB,kBAAkB;EAClB,sBAAsB;;AAIlB,SAAU,WAAW,SAAsB,QAAkB;AACjE,SAAO,YAAY,QAAQ,SAAS,UAAU,MAAM;AACtD;AAGM,SAAU,cAAc,SAAsB,QAAoB,OAAa;AACnF,SAAO,YAAY,QAAQ,GAAG,MAAM,MAAM,KAAK,MAAM,GAAG,UAAU,MAAM,CAAC,KAAK,KAAK;AACrF;AAGM,SAAU,iBAAiB,SAAsB,OAAe,MAAY;AAChF,SAAO,YAAY,QACf,gCAAgC,KAAK,eAAe,IAAI,qCACxD,uBAAuB,KAAK,IAAI,IAAI;AAC1C;AAQM,SAAU,qBACd,SACA,OACA,WACA,MACA,cAAsB;AAEtB,QAAM,UACJ,YAAY,SAAS,eAAe,IAAI,6CAA6C,KAAK,oEAEtF,cAAc,aAAa,CAAC;AAClC,MAAI;AACJ,SAAI,YAAY,QACd,OAAO,cACH,cAAc,WAAW,qEACzB,gCAAgC,KAAK,mFAEzC,OAAO,cACH,kBAAkB,SAAS,WAAW,WAAW,qDACjD,wBAAwB,KAAK,gDAAgD,SAAS,mBAErF,EAAE,SAAS,KAAI;AACxB;",
6
+ "names": []
7
+ }
@@ -162,8 +162,8 @@ function handleWarning(message, suggestions) {
162
162
  function handleSuccess(message, details, options) {
163
163
  if (isJson()) {
164
164
  const output = {
165
- status: "ok",
166
- ...details
165
+ ...details,
166
+ status: "ok"
167
167
  };
168
168
  console.log(JSON.stringify(output));
169
169
  } else if (console.log(ansis.green(`\u2713 ${message}`)), details && (options?.verbose || details.changesetStatus))
@@ -176,7 +176,13 @@ function keyToDisplayName(key) {
176
176
  return key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
177
177
  }
178
178
  function handleInfo(message, details) {
179
- if (!isJson() && (console.log(message), details))
179
+ if (isJson()) {
180
+ const output = {
181
+ level: "info",
182
+ message
183
+ };
184
+ details && (output.details = details), console.error(JSON.stringify(output));
185
+ } else if (console.log(message), details)
180
186
  for (const [key, value] of Object.entries(details))
181
187
  console.log(ansis.dim(` ${key}: ${value}`));
182
188
  }