@documentation-robotics/cli 0.2.0 → 0.3.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 (269) hide show
  1. package/README.md +36 -2
  2. package/dist/analysis/relationship-classifier.js +2 -9
  3. package/dist/analysis/relationship-classifier.js.map +2 -2
  4. package/dist/analyzers/call-graph-utils.js +2 -2
  5. package/dist/analyzers/call-graph-utils.js.map +2 -2
  6. package/dist/analyzers/cards/build.js +171 -0
  7. package/dist/analyzers/cards/build.js.map +7 -0
  8. package/dist/analyzers/cards/classify.js +42 -0
  9. package/dist/analyzers/cards/classify.js.map +7 -0
  10. package/dist/analyzers/cards/edge-index.js +44 -0
  11. package/dist/analyzers/cards/edge-index.js.map +7 -0
  12. package/dist/analyzers/cards/facts.js +46 -0
  13. package/dist/analyzers/cards/facts.js.map +7 -0
  14. package/dist/analyzers/cards/graph.js +42 -0
  15. package/dist/analyzers/cards/graph.js.map +7 -0
  16. package/dist/analyzers/cards/index.js +8 -0
  17. package/dist/analyzers/cards/index.js.map +7 -0
  18. package/dist/analyzers/cards/tier0.js +18 -0
  19. package/dist/analyzers/cards/tier0.js.map +7 -0
  20. package/dist/analyzers/cards/types.js +1 -0
  21. package/dist/analyzers/cards/types.js.map +7 -0
  22. package/dist/analyzers/cbm-analyzer.js +195 -171
  23. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  24. package/dist/analyzers/cbm-responses.js +336 -0
  25. package/dist/analyzers/cbm-responses.js.map +7 -0
  26. package/dist/analyzers/edge-relationships.js +45 -0
  27. package/dist/analyzers/edge-relationships.js.map +7 -0
  28. package/dist/analyzers/mapping-loader.js +9 -0
  29. package/dist/analyzers/mapping-loader.js.map +2 -2
  30. package/dist/analyzers/tier0-rules.js +153 -0
  31. package/dist/analyzers/tier0-rules.js.map +7 -0
  32. package/dist/analyzers/types.js.map +1 -1
  33. package/dist/audit/relationships/ai/prompts.js +10 -10
  34. package/dist/audit/relationships/ai/prompts.js.map +2 -2
  35. package/dist/audit/relationships/analysis/layer-templates.js +52 -52
  36. package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
  37. package/dist/cli.js +12 -2
  38. package/dist/cli.js.map +2 -2
  39. package/dist/coding-agents/default-permissions.js +31 -1
  40. package/dist/coding-agents/default-permissions.js.map +2 -2
  41. package/dist/coding-agents/drbot-mcp.js +167 -0
  42. package/dist/coding-agents/drbot-mcp.js.map +7 -0
  43. package/dist/commands/add.js +3 -2
  44. package/dist/commands/add.js.map +2 -2
  45. package/dist/commands/analyzer.js +117 -14
  46. package/dist/commands/analyzer.js.map +2 -2
  47. package/dist/commands/changeset-verify.js +55 -0
  48. package/dist/commands/changeset-verify.js.map +7 -0
  49. package/dist/commands/changeset.js +43 -22
  50. package/dist/commands/changeset.js.map +2 -2
  51. package/dist/commands/delete.js +10 -4
  52. package/dist/commands/delete.js.map +2 -2
  53. package/dist/commands/farm.js +176 -60
  54. package/dist/commands/farm.js.map +2 -2
  55. package/dist/commands/mcp.js +19 -4
  56. package/dist/commands/mcp.js.map +2 -2
  57. package/dist/commands/relationship.js +207 -26
  58. package/dist/commands/relationship.js.map +2 -2
  59. package/dist/commands/update.js +42 -23
  60. package/dist/commands/update.js.map +2 -2
  61. package/dist/commands/upgrade.js +99 -11
  62. package/dist/commands/upgrade.js.map +3 -3
  63. package/dist/commands/version.js +1 -1
  64. package/dist/commands/visualize.js +4 -3
  65. package/dist/commands/visualize.js.map +2 -2
  66. package/dist/core/catalog-migration.js +33 -0
  67. package/dist/core/catalog-migration.js.map +7 -0
  68. package/dist/core/changeset-analysis.js +421 -0
  69. package/dist/core/changeset-analysis.js.map +7 -0
  70. package/dist/core/changeset-base-index.js +153 -0
  71. package/dist/core/changeset-base-index.js.map +7 -0
  72. package/dist/core/changeset-brief-delta.js +102 -0
  73. package/dist/core/changeset-brief-delta.js.map +7 -0
  74. package/dist/core/changeset-graph.js +281 -0
  75. package/dist/core/changeset-graph.js.map +7 -0
  76. package/dist/core/changeset-only-guard.js +32 -0
  77. package/dist/core/changeset-only-guard.js.map +7 -0
  78. package/dist/core/changeset-summary.js +68 -0
  79. package/dist/core/changeset-summary.js.map +7 -0
  80. package/dist/core/changeset-validator.js +66 -20
  81. package/dist/core/changeset-validator.js.map +2 -2
  82. package/dist/core/changeset-verify.js +518 -0
  83. package/dist/core/changeset-verify.js.map +7 -0
  84. package/dist/core/changeset.js +56 -9
  85. package/dist/core/changeset.js.map +2 -2
  86. package/dist/core/element-type-migration.js +145 -0
  87. package/dist/core/element-type-migration.js.map +7 -0
  88. package/dist/core/farm-manifest.js +12 -6
  89. package/dist/core/farm-manifest.js.map +2 -2
  90. package/dist/core/farm-sync-engine.js +1 -1
  91. package/dist/core/farm-sync-engine.js.map +2 -2
  92. package/dist/core/farm-sync-state.js +10 -2
  93. package/dist/core/farm-sync-state.js.map +2 -2
  94. package/dist/core/migration-registry.js +75 -46
  95. package/dist/core/migration-registry.js.map +2 -2
  96. package/dist/core/model-upgrade.js +102 -0
  97. package/dist/core/model-upgrade.js.map +7 -0
  98. package/dist/core/model.js +82 -30
  99. package/dist/core/model.js.map +2 -2
  100. package/dist/core/mutation-handler.js +71 -6
  101. package/dist/core/mutation-handler.js.map +2 -2
  102. package/dist/core/relationship-migration-table.js +6639 -0
  103. package/dist/core/relationship-migration-table.js.map +7 -0
  104. package/dist/core/relationship-migration.js +272 -0
  105. package/dist/core/relationship-migration.js.map +7 -0
  106. package/dist/core/relationship-prune.js +49 -0
  107. package/dist/core/relationship-prune.js.map +7 -0
  108. package/dist/core/relationship-staging.js +68 -0
  109. package/dist/core/relationship-staging.js.map +7 -0
  110. package/dist/core/relationships.js +26 -4
  111. package/dist/core/relationships.js.map +2 -2
  112. package/dist/core/report-data-model.js +7 -13
  113. package/dist/core/report-data-model.js.map +2 -2
  114. package/dist/core/staged-changeset-storage.js +64 -17
  115. package/dist/core/staged-changeset-storage.js.map +2 -2
  116. package/dist/core/staged-source-reference.js +28 -0
  117. package/dist/core/staged-source-reference.js.map +7 -0
  118. package/dist/core/staging-area.js +83 -18
  119. package/dist/core/staging-area.js.map +2 -2
  120. package/dist/core/virtual-projection.js +13 -5
  121. package/dist/core/virtual-projection.js.map +2 -2
  122. package/dist/export/archimate-exporter.js +1 -0
  123. package/dist/export/archimate-exporter.js.map +2 -2
  124. package/dist/generated/compiled-validators.js +2 -2
  125. package/dist/generated/compiled-validators.js.map +2 -2
  126. package/dist/generated/layer-registry.js +16 -16
  127. package/dist/generated/layer-registry.js.map +2 -2
  128. package/dist/generated/node-types.js +186 -168
  129. package/dist/generated/node-types.js.map +2 -2
  130. package/dist/generated/relationship-index.js +2236 -7790
  131. package/dist/generated/relationship-index.js.map +2 -2
  132. package/dist/integrations/claude_code/agents/dr-architect.md +16 -3
  133. package/dist/integrations/claude_code/agents/dr-extractor.md +1 -1
  134. package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
  135. package/dist/integrations/claude_code/commands/dr-map.md +13 -0
  136. package/dist/integrations/claude_code/commands/dr-plan.md +40 -0
  137. package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
  138. package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
  139. package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
  140. package/dist/integrations/claude_code/docs/MCP_SERVER.md +26 -2
  141. package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
  142. package/dist/integrations/claude_code/docs/README.md +48 -1
  143. package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
  144. package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
  145. package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
  146. package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
  147. package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
  148. package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
  149. package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
  150. package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
  151. package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
  152. package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
  153. package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
  154. package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
  155. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +92 -15
  156. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +119 -0
  157. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +3 -0
  158. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +1 -1
  159. package/dist/integrations/github_copilot/agents/dr-architect.md +1 -1
  160. package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
  161. package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
  162. package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
  163. package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
  164. package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
  165. package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
  166. package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
  167. package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
  168. package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
  169. package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
  170. package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
  171. package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
  172. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +222 -102
  173. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +119 -0
  174. package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
  175. package/dist/integrations/hash-utils.js.map +2 -2
  176. package/dist/mcp/api-key-manager.js +14 -1
  177. package/dist/mcp/api-key-manager.js.map +2 -2
  178. package/dist/mcp/changeset-only.js +34 -0
  179. package/dist/mcp/changeset-only.js.map +7 -0
  180. package/dist/mcp/tool-registry.js +19 -3
  181. package/dist/mcp/tool-registry.js.map +2 -2
  182. package/dist/mcp/tools/changeset-analysis-tools.js +78 -0
  183. package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
  184. package/dist/mcp/tools/changeset-editing.js +184 -0
  185. package/dist/mcp/tools/changeset-editing.js.map +7 -0
  186. package/dist/mcp/tools/changeset-lifecycle.js +83 -0
  187. package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
  188. package/dist/mcp/tools/changeset-shared.js +31 -0
  189. package/dist/mcp/tools/changeset-shared.js.map +7 -0
  190. package/dist/mcp/tools/changeset-show.js +17 -1
  191. package/dist/mcp/tools/changeset-show.js.map +2 -2
  192. package/dist/mcp/tools/changeset-verify.js +43 -0
  193. package/dist/mcp/tools/changeset-verify.js.map +7 -0
  194. package/dist/mcp/tools/index.js +19 -1
  195. package/dist/mcp/tools/index.js.map +2 -2
  196. package/dist/mcp/tools/model-add.js +6 -5
  197. package/dist/mcp/tools/model-add.js.map +2 -2
  198. package/dist/mcp/tools/model-delete.js +2 -2
  199. package/dist/mcp/tools/model-delete.js.map +2 -2
  200. package/dist/mcp/tools/model-search.js +14 -0
  201. package/dist/mcp/tools/model-search.js.map +2 -2
  202. package/dist/mcp/tools/model-show.js +29 -2
  203. package/dist/mcp/tools/model-show.js.map +2 -2
  204. package/dist/mcp/tools/model-trace.js +93 -30
  205. package/dist/mcp/tools/model-trace.js.map +2 -2
  206. package/dist/mcp/tools/model-update.js +35 -19
  207. package/dist/mcp/tools/model-update.js.map +2 -2
  208. package/dist/mcp/tools/relationship-tools.js +98 -0
  209. package/dist/mcp/tools/relationship-tools.js.map +7 -0
  210. package/dist/mcp/tools/shared.js +25 -2
  211. package/dist/mcp/tools/shared.js.map +2 -2
  212. package/dist/mcp/tools/spec-type.js +90 -0
  213. package/dist/mcp/tools/spec-type.js.map +7 -0
  214. package/dist/schemas/bundled/analyzers/cbm.json +1186 -68
  215. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  216. package/dist/schemas/bundled/api.json +1029 -1658
  217. package/dist/schemas/bundled/apm.json +499 -734
  218. package/dist/schemas/bundled/application.json +735 -371
  219. package/dist/schemas/bundled/base.json +3 -220
  220. package/dist/schemas/bundled/business.json +452 -366
  221. package/dist/schemas/bundled/data-model.json +395 -411
  222. package/dist/schemas/bundled/data-store.json +334 -558
  223. package/dist/schemas/bundled/manifest.json +18 -18
  224. package/dist/schemas/bundled/motivation.json +349 -368
  225. package/dist/schemas/bundled/navigation.json +415 -731
  226. package/dist/schemas/bundled/product.json +325 -159
  227. package/dist/schemas/bundled/security.json +864 -1232
  228. package/dist/schemas/bundled/technology.json +574 -703
  229. package/dist/schemas/bundled/testing.json +718 -1643
  230. package/dist/schemas/bundled/ux.json +739 -1352
  231. package/dist/server/schemas.js +8 -0
  232. package/dist/server/schemas.js.map +2 -2
  233. package/dist/server/server.js +218 -34
  234. package/dist/server/server.js.map +2 -2
  235. package/dist/server-entry.js +2 -1
  236. package/dist/server-entry.js.map +2 -2
  237. package/dist/telemetry/index.js +1 -1
  238. package/dist/utils/ansi-suppressor.js +6 -8
  239. package/dist/utils/ansi-suppressor.js.map +2 -2
  240. package/dist/utils/attribute-patch.js +41 -0
  241. package/dist/utils/attribute-patch.js.map +7 -0
  242. package/dist/utils/error-hints.js +29 -0
  243. package/dist/utils/error-hints.js.map +7 -0
  244. package/dist/utils/errors.js +9 -3
  245. package/dist/utils/errors.js.map +2 -2
  246. package/dist/utils/project-paths.js +26 -1
  247. package/dist/utils/project-paths.js.map +2 -2
  248. package/dist/utils/source-reference.js +14 -12
  249. package/dist/utils/source-reference.js.map +2 -2
  250. package/dist/utils/spec-version.js +2 -2
  251. package/dist/utils/spec-version.js.map +1 -1
  252. package/dist/validators/composed-reference-validator.js +5 -27
  253. package/dist/validators/composed-reference-validator.js.map +2 -2
  254. package/dist/validators/reference-validator.js +0 -7
  255. package/dist/validators/reference-validator.js.map +2 -2
  256. package/dist/validators/relationship-schema-validator.js +3 -3
  257. package/dist/validators/relationship-schema-validator.js.map +2 -2
  258. package/dist/validators/schema-validator.js +27 -9
  259. package/dist/validators/schema-validator.js.map +2 -2
  260. package/dist/validators/types.js.map +1 -1
  261. package/dist/viewer/README.md +6 -6
  262. package/dist/viewer/assets/main-CxzcuU_o.js +84 -0
  263. package/dist/viewer/assets/main-j-7ULYpm.css +1 -0
  264. package/dist/viewer/index.html +2 -2
  265. package/dist/viewer/manifest.json +10 -10
  266. package/dist/viewer/version.json +1 -1
  267. package/package.json +2 -1
  268. package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
  269. package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/commands/delete.ts"],
4
- "sourcesContent": ["/**\n * Delete an element\n */\n\nimport { confirm } from \"@clack/prompts\";\nimport ansis from \"ansis\";\nimport { Model } from \"../core/model.js\";\nimport { MutationHandler } from \"../core/mutation-handler.js\";\nimport { ReferenceRegistry } from \"../core/reference-registry.js\";\nimport { DependencyTracker, TraceDirection } from \"../core/dependency-tracker.js\";\nimport { findElementLayer } from \"../utils/element-utils.js\";\nimport { CLIError, handleError, handleSuccess, handleInfo, ErrorCategory, ModelNotFoundError } from \"../utils/errors.js\";\nimport { startSpan, endSpan } from \"../telemetry/index.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\n\ndeclare const TELEMETRY_ENABLED: boolean | undefined;\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== \"undefined\" ? TELEMETRY_ENABLED : false;\n\nexport interface DeleteOptions {\n model?: string;\n force?: boolean;\n verbose?: boolean;\n debug?: boolean;\n cascade?: boolean;\n dryRun?: boolean;\n}\n\nexport async function deleteCommand(id: string, options: DeleteOptions): Promise<void> {\n const span = isTelemetryEnabled\n ? startSpan(\"element.delete\", {\n \"element.id\": id,\n \"element.force\": !!options.force,\n \"element.cascade\": !!options.cascade,\n \"element.dryRun\": !!options.dryRun,\n })\n : null;\n\n try {\n // Load model (with error handling for missing models)\n let model: Model;\n try {\n model = await Model.load(options.model || process.cwd());\n } catch (error) {\n const message = getErrorMessage(error);\n if (message.includes(\"No DR project\") || message.includes(\"Model not found\")) {\n throw new ModelNotFoundError();\n }\n throw error;\n }\n\n // Find element\n const layerName = await findElementLayer(model, id);\n if (!layerName) {\n throw new CLIError(`Element ${id} not found`, ErrorCategory.USER, [\n `Use \"dr search ${id}\" to find similar elements`,\n 'Use \"dr list <layer>\" to list all elements in a layer',\n ]);\n }\n\n const layer = (await model.getLayer(layerName))!;\n const element = layer.getElement(id);\n\n if (!element) {\n throw new CLIError(`Element ${id} not found`, ErrorCategory.USER, [\n `Use \"dr search ${id}\" to find similar elements`,\n 'Use \"dr list <layer>\" to list all elements in a layer',\n ]);\n }\n\n // Build reference registry to track dependencies\n const registry = new ReferenceRegistry();\n for (const layer of model.layers.values()) {\n for (const element of layer.listElements()) {\n registry.registerElement(element);\n }\n }\n\n // Create dependency tracker\n const tracker = new DependencyTracker(registry, model);\n\n // Find all elements that depend on this element (would be orphaned)\n const dependents = tracker.traceDependencies(id, TraceDirection.DOWN, null);\n\n // Display element information\n handleInfo(\"\");\n handleInfo(ansis.bold(`Removing element: ${ansis.yellow(id)}`), {\n Layer: layerName,\n Type: element.type || \"unknown\",\n ...(element.name ? { Name: element.name } : {}),\n });\n\n // Display dependencies if any exist\n if (dependents.length > 0) {\n handleInfo(\"\");\n handleInfo(\n ansis.yellow(`\u26A0 Warning: This element has ${dependents.length} dependent element(s):`)\n );\n\n const directDependents = tracker.traceDependencies(id, TraceDirection.DOWN, 1);\n\n // Show direct dependents\n for (const dep of directDependents.slice(0, 5)) {\n const depElement = model.getElementById(dep);\n const depLayer = depElement?.layer || \"unknown\";\n handleInfo(ansis.dim(` - ${dep} (${depLayer})`));\n }\n\n if (directDependents.length > 5) {\n handleInfo(ansis.dim(` ... and ${directDependents.length - 5} more direct dependents`));\n }\n\n if (dependents.length > directDependents.length) {\n handleInfo(\n ansis.dim(\n ` (${dependents.length - directDependents.length} transitive dependents not shown)`\n )\n );\n }\n\n // Check if cascade or force is enabled\n if (!options.cascade && !options.force) {\n handleInfo(\"\");\n handleInfo(ansis.red(\"\u2717 Cannot remove element with dependencies.\"));\n handleInfo(ansis.dim(\"Use --cascade to remove all dependent elements\"));\n handleInfo(ansis.dim(\"or --force to skip dependency checks\"));\n const suggestions = [\n `Use --dry-run with --cascade to preview what would be deleted`,\n 'Review dependencies before deletion using \"dr show <element>\"',\n \"Use --cascade to automatically remove all dependent elements\",\n \"Or use --force to remove only this element (dependencies will reference a non-existent element)\",\n ];\n throw new CLIError(\"Element has dependencies\", ErrorCategory.USER, suggestions, {\n operation: \"delete\",\n relatedElements: directDependents.slice(0, 10),\n context: `Element ${id} has ${dependents.length} dependent elements`,\n });\n }\n }\n\n // Calculate total elements to remove\n const elementsToRemove = options.cascade ? [id, ...dependents] : [id];\n\n // Display dry-run summary\n if (options.dryRun) {\n handleInfo(\"\");\n handleInfo(ansis.yellow(`Dry run - not removing`));\n handleInfo(ansis.dim(`Would remove ${elementsToRemove.length} element(s)`));\n\n if (options.cascade && dependents.length > 0) {\n handleInfo(\"\");\n handleInfo(ansis.dim(\"Elements that would be removed:\"));\n handleInfo(ansis.dim(` - ${id} (target)`));\n for (const dep of dependents.slice(0, 10)) {\n handleInfo(ansis.dim(` - ${dep} (dependent)`));\n }\n if (dependents.length > 10) {\n handleInfo(ansis.dim(` ... and ${dependents.length - 10} more`));\n }\n }\n\n handleSuccess(\n `Dry run: would remove ${elementsToRemove.length} element(s)`,\n {\n elementId: id,\n layer: layerName!,\n dryRun: true,\n elementsToRemoveCount: 1,\n dependentElementsToRemoveCount: dependents.length,\n },\n { verbose: options.verbose }\n );\n return;\n }\n\n // Confirm deletion unless --force\n if (!options.force) {\n const isInteractive = process.stdin.isTTY && process.stdout.isTTY;\n if (!isInteractive) {\n handleInfo(ansis.dim(\"Non-interactive environment detected - proceeding without confirmation\"));\n } else {\n const message =\n options.cascade && dependents.length > 0\n ? `Delete ${ansis.bold(id)} and ${ansis.bold(String(dependents.length))} dependent element(s)? This cannot be undone.`\n : `Delete element ${ansis.bold(id)}? This cannot be undone.`;\n\n const confirmed = await confirm({\n message,\n initialValue: false,\n });\n\n if (!confirmed) {\n handleSuccess(\n \"Deletion cancelled\",\n {\n status: \"cancelled\",\n elementId: id,\n layer: layerName!,\n },\n { verbose: options.verbose }\n );\n return;\n }\n }\n }\n\n // Execute deletion with error recovery guidance\n let deletedCount = 0;\n try {\n if (options.cascade && dependents.length > 0) {\n // Delete dependents first (reverse dependency order to avoid orphaned references)\n const sortedDependents = [...dependents].reverse();\n\n for (const depId of sortedDependents) {\n try {\n const depLayerName = await findElementLayer(model, depId);\n if (!depLayerName) continue;\n\n const depLayer = (await model.getLayer(depLayerName))!;\n const depElement = depLayer.getElement(depId)!;\n\n const handler = new MutationHandler(model, depId, depLayerName);\n await handler.executeDelete(depElement);\n deletedCount++;\n\n if (options.verbose) {\n handleInfo(ansis.dim(` Deleted dependent: ${depId}`));\n }\n } catch (depError) {\n // Track partial progress for error recovery\n const suggestions = [\n `${deletedCount} dependent element(s) were deleted before the error`,\n 'Check the model state with \"dr validate\" to identify any broken references',\n 'You may need to manually delete remaining elements or use \"dr show\" to inspect',\n ];\n throw new CLIError(\n `Failed to delete dependent element ${depId}`,\n ErrorCategory.SYSTEM,\n suggestions,\n {\n operation: \"delete (cascade)\",\n partialProgress: { completed: deletedCount, total: sortedDependents.length },\n relatedElements: [id, ...sortedDependents.slice(0, 5)],\n }\n );\n }\n }\n }\n } catch (error) {\n // If we're in the middle of cascade deletion, provide recovery info\n if (options.cascade && deletedCount > 0 && !(error instanceof CLIError)) {\n const suggestions = [\n `${deletedCount} dependent element(s) were successfully deleted`,\n 'Use \"dr validate\" to check for broken references',\n \"The target element was not deleted - you can retry the operation\",\n ];\n throw new CLIError(\n \"Cascade deletion was partially completed but encountered an error\",\n ErrorCategory.SYSTEM,\n suggestions,\n {\n operation: \"delete (cascade)\",\n partialProgress: { completed: deletedCount, total: dependents.length },\n }\n );\n }\n throw error;\n }\n\n // Delete the target element\n const handler = new MutationHandler(model, id, layerName);\n await handler.executeDelete(element);\n\n if (isTelemetryEnabled && span) {\n (span as any).setAttribute(\"layer.name\", layerName);\n (span as any).setAttribute(\"element.dependents\", dependents.length);\n (span as any).setAttribute(\"element.totalDeleted\", elementsToRemove.length);\n }\n\n // Check if operation was staged or applied to base model\n const beforeState = handler.getBeforeState();\n if (beforeState) {\n // Check if we went through staging path\n const stagingManager = handler.getStagingManager();\n const activeChangeset = await stagingManager.getActive();\n if (activeChangeset && activeChangeset.status === \"staged\") {\n // Staging path\n handleSuccess(\n `Staged deletion of element ${ansis.bold(id)} to ${ansis.bold(activeChangeset.name)}`,\n {\n elementId: id,\n layer: layerName!,\n changesetStatus: \"staged\",\n changeset: activeChangeset.name,\n totalElementsDeleted: elementsToRemove.length,\n },\n { verbose: options.verbose }\n );\n } else {\n // Base model path: purge any stale relationships MutationHandler may have missed\n const staleBase = elementsToRemove.reduce(\n (n, eid) => n + model.relationships.deleteForElement(eid), 0\n );\n if (staleBase > 0) await model.saveRelationships();\n\n // Report deletion with cascade confirmation\n showDeletionSuccess();\n }\n } else {\n // Fallback success message if before state is not available\n // This should rarely occur, but ensures user always gets feedback\n const staleFallback = elementsToRemove.reduce(\n (n, eid) => n + model.relationships.deleteForElement(eid), 0\n );\n if (staleFallback > 0) await model.saveRelationships();\n\n // Report deletion with cascade confirmation\n showDeletionSuccess();\n }\n\n function showDeletionSuccess(): void {\n const details: Record<string, unknown> = {\n elementId: id,\n layer: layerName!,\n totalElementsDeleted: elementsToRemove.length,\n };\n handleSuccess(`Deleted element ${ansis.bold(id)}`, details, { verbose: options.verbose });\n\n // Show cascade confirmation when cascade is active\n if (options.cascade && dependents.length > 0) {\n handleInfo(ansis.green(`\u2713 Deleted ${dependents.length} dependent element(s)`));\n }\n }\n } catch (error) {\n if (isTelemetryEnabled && span) {\n (span as any).recordException(error as Error);\n (span as any).setStatus({ code: 2, message: (error as Error).message });\n }\n handleError(error);\n } finally {\n if (isTelemetryEnabled) {\n endSpan(span);\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Delete an element\n */\n\nimport { confirm } from \"@clack/prompts\";\nimport ansis from \"ansis\";\nimport { Model } from \"../core/model.js\";\nimport { MutationHandler } from \"../core/mutation-handler.js\";\nimport { ReferenceRegistry } from \"../core/reference-registry.js\";\nimport { DependencyTracker, TraceDirection } from \"../core/dependency-tracker.js\";\nimport { findElementLayer } from \"../utils/element-utils.js\";\nimport { elementIdentifiers } from \"../core/relationships.js\";\nimport { CLIError, handleError, handleSuccess, handleInfo, ErrorCategory, ModelNotFoundError } from \"../utils/errors.js\";\nimport { startSpan, endSpan } from \"../telemetry/index.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\n\ndeclare const TELEMETRY_ENABLED: boolean | undefined;\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== \"undefined\" ? TELEMETRY_ENABLED : false;\n\nexport interface DeleteOptions {\n model?: string;\n force?: boolean;\n verbose?: boolean;\n debug?: boolean;\n cascade?: boolean;\n dryRun?: boolean;\n}\n\nexport async function deleteCommand(id: string, options: DeleteOptions): Promise<void> {\n const span = isTelemetryEnabled\n ? startSpan(\"element.delete\", {\n \"element.id\": id,\n \"element.force\": !!options.force,\n \"element.cascade\": !!options.cascade,\n \"element.dryRun\": !!options.dryRun,\n })\n : null;\n\n try {\n // Load model (with error handling for missing models)\n let model: Model;\n try {\n model = await Model.load(options.model);\n } catch (error) {\n const message = getErrorMessage(error);\n if (message.includes(\"No DR project\") || message.includes(\"Model not found\")) {\n throw new ModelNotFoundError();\n }\n throw error;\n }\n\n // Find element\n const layerName = await findElementLayer(model, id);\n if (!layerName) {\n throw new CLIError(`Element ${id} not found`, ErrorCategory.USER, [\n `Use \"dr search ${id}\" to find similar elements`,\n 'Use \"dr list <layer>\" to list all elements in a layer',\n ]);\n }\n\n const layer = (await model.getLayer(layerName))!;\n const element = layer.getElement(id);\n\n if (!element) {\n throw new CLIError(`Element ${id} not found`, ErrorCategory.USER, [\n `Use \"dr search ${id}\" to find similar elements`,\n 'Use \"dr list <layer>\" to list all elements in a layer',\n ]);\n }\n\n // Build reference registry to track dependencies\n const registry = new ReferenceRegistry();\n for (const layer of model.layers.values()) {\n for (const element of layer.listElements()) {\n registry.registerElement(element);\n }\n }\n\n // Create dependency tracker\n const tracker = new DependencyTracker(registry, model);\n\n // Find all elements that depend on this element (would be orphaned)\n const dependents = tracker.traceDependencies(id, TraceDirection.DOWN, null);\n\n // Display element information\n handleInfo(\"\");\n handleInfo(ansis.bold(`Removing element: ${ansis.yellow(id)}`), {\n Layer: layerName,\n Type: element.type || \"unknown\",\n ...(element.name ? { Name: element.name } : {}),\n });\n\n // Display dependencies if any exist\n if (dependents.length > 0) {\n handleInfo(\"\");\n handleInfo(\n ansis.yellow(`\u26A0 Warning: This element has ${dependents.length} dependent element(s):`)\n );\n\n const directDependents = tracker.traceDependencies(id, TraceDirection.DOWN, 1);\n\n // Show direct dependents\n for (const dep of directDependents.slice(0, 5)) {\n const depElement = model.getElementById(dep);\n const depLayer = depElement?.layer || \"unknown\";\n handleInfo(ansis.dim(` - ${dep} (${depLayer})`));\n }\n\n if (directDependents.length > 5) {\n handleInfo(ansis.dim(` ... and ${directDependents.length - 5} more direct dependents`));\n }\n\n if (dependents.length > directDependents.length) {\n handleInfo(\n ansis.dim(\n ` (${dependents.length - directDependents.length} transitive dependents not shown)`\n )\n );\n }\n\n // Check if cascade or force is enabled\n if (!options.cascade && !options.force) {\n handleInfo(\"\");\n handleInfo(ansis.red(\"\u2717 Cannot remove element with dependencies.\"));\n handleInfo(ansis.dim(\"Use --cascade to remove all dependent elements\"));\n handleInfo(ansis.dim(\"or --force to skip dependency checks\"));\n const suggestions = [\n `Use --dry-run with --cascade to preview what would be deleted`,\n 'Review dependencies before deletion using \"dr show <element>\"',\n \"Use --cascade to automatically remove all dependent elements\",\n \"Or use --force to remove only this element (dependencies will reference a non-existent element)\",\n ];\n throw new CLIError(\"Element has dependencies\", ErrorCategory.USER, suggestions, {\n operation: \"delete\",\n relatedElements: directDependents.slice(0, 10),\n context: `Element ${id} has ${dependents.length} dependent elements`,\n });\n }\n }\n\n // Calculate total elements to remove\n const elementsToRemove = options.cascade ? [id, ...dependents] : [id];\n\n // Every identifier relationships.yaml may use for the elements being removed\n // (UUID id and dotted path), captured before they are deleted from the model.\n const relationshipIdentifiers = new Set<string>();\n for (const eid of elementsToRemove) {\n const target = eid === id ? element : model.getElementById(eid);\n for (const identifier of elementIdentifiers(target ?? {}, eid)) {\n relationshipIdentifiers.add(identifier);\n }\n }\n\n // Display dry-run summary\n if (options.dryRun) {\n handleInfo(\"\");\n handleInfo(ansis.yellow(`Dry run - not removing`));\n handleInfo(ansis.dim(`Would remove ${elementsToRemove.length} element(s)`));\n\n if (options.cascade && dependents.length > 0) {\n handleInfo(\"\");\n handleInfo(ansis.dim(\"Elements that would be removed:\"));\n handleInfo(ansis.dim(` - ${id} (target)`));\n for (const dep of dependents.slice(0, 10)) {\n handleInfo(ansis.dim(` - ${dep} (dependent)`));\n }\n if (dependents.length > 10) {\n handleInfo(ansis.dim(` ... and ${dependents.length - 10} more`));\n }\n }\n\n handleSuccess(\n `Dry run: would remove ${elementsToRemove.length} element(s)`,\n {\n elementId: id,\n layer: layerName!,\n dryRun: true,\n elementsToRemoveCount: 1,\n dependentElementsToRemoveCount: dependents.length,\n },\n { verbose: options.verbose }\n );\n return;\n }\n\n // Confirm deletion unless --force\n if (!options.force) {\n const isInteractive = process.stdin.isTTY && process.stdout.isTTY;\n if (!isInteractive) {\n handleInfo(ansis.dim(\"Non-interactive environment detected - proceeding without confirmation\"));\n } else {\n const message =\n options.cascade && dependents.length > 0\n ? `Delete ${ansis.bold(id)} and ${ansis.bold(String(dependents.length))} dependent element(s)? This cannot be undone.`\n : `Delete element ${ansis.bold(id)}? This cannot be undone.`;\n\n const confirmed = await confirm({\n message,\n initialValue: false,\n });\n\n if (!confirmed) {\n handleSuccess(\n \"Deletion cancelled\",\n {\n status: \"cancelled\",\n elementId: id,\n layer: layerName!,\n },\n { verbose: options.verbose }\n );\n return;\n }\n }\n }\n\n // Execute deletion with error recovery guidance\n let deletedCount = 0;\n try {\n if (options.cascade && dependents.length > 0) {\n // Delete dependents first (reverse dependency order to avoid orphaned references)\n const sortedDependents = [...dependents].reverse();\n\n for (const depId of sortedDependents) {\n try {\n const depLayerName = await findElementLayer(model, depId);\n if (!depLayerName) continue;\n\n const depLayer = (await model.getLayer(depLayerName))!;\n const depElement = depLayer.getElement(depId)!;\n\n const handler = new MutationHandler(model, depId, depLayerName);\n await handler.executeDelete(depElement);\n deletedCount++;\n\n if (options.verbose) {\n handleInfo(ansis.dim(` Deleted dependent: ${depId}`));\n }\n } catch (depError) {\n // Track partial progress for error recovery\n const suggestions = [\n `${deletedCount} dependent element(s) were deleted before the error`,\n 'Check the model state with \"dr validate\" to identify any broken references',\n 'You may need to manually delete remaining elements or use \"dr show\" to inspect',\n ];\n throw new CLIError(\n `Failed to delete dependent element ${depId}`,\n ErrorCategory.SYSTEM,\n suggestions,\n {\n operation: \"delete (cascade)\",\n partialProgress: { completed: deletedCount, total: sortedDependents.length },\n relatedElements: [id, ...sortedDependents.slice(0, 5)],\n }\n );\n }\n }\n }\n } catch (error) {\n // If we're in the middle of cascade deletion, provide recovery info\n if (options.cascade && deletedCount > 0 && !(error instanceof CLIError)) {\n const suggestions = [\n `${deletedCount} dependent element(s) were successfully deleted`,\n 'Use \"dr validate\" to check for broken references',\n \"The target element was not deleted - you can retry the operation\",\n ];\n throw new CLIError(\n \"Cascade deletion was partially completed but encountered an error\",\n ErrorCategory.SYSTEM,\n suggestions,\n {\n operation: \"delete (cascade)\",\n partialProgress: { completed: deletedCount, total: dependents.length },\n }\n );\n }\n throw error;\n }\n\n // Delete the target element\n const handler = new MutationHandler(model, id, layerName);\n await handler.executeDelete(element);\n\n if (isTelemetryEnabled && span) {\n (span as any).setAttribute(\"layer.name\", layerName);\n (span as any).setAttribute(\"element.dependents\", dependents.length);\n (span as any).setAttribute(\"element.totalDeleted\", elementsToRemove.length);\n }\n\n // Check if operation was staged or applied to base model\n const beforeState = handler.getBeforeState();\n if (beforeState) {\n // Check if we went through staging path\n const stagingManager = handler.getStagingManager();\n const activeChangeset = await stagingManager.getActive();\n if (activeChangeset && activeChangeset.status === \"staged\") {\n // Staging path\n handleSuccess(\n `Staged deletion of element ${ansis.bold(id)} to ${ansis.bold(activeChangeset.name)}`,\n {\n elementId: id,\n layer: layerName!,\n changesetStatus: \"staged\",\n changeset: activeChangeset.name,\n totalElementsDeleted: elementsToRemove.length,\n },\n { verbose: options.verbose }\n );\n } else {\n // Base model path: purge any stale relationships MutationHandler may have missed\n const staleBase = model.relationships.deleteForElement([...relationshipIdentifiers]);\n if (staleBase > 0) await model.saveRelationships();\n\n // Report deletion with cascade confirmation\n showDeletionSuccess();\n }\n } else {\n // Fallback success message if before state is not available\n // This should rarely occur, but ensures user always gets feedback\n const staleFallback = model.relationships.deleteForElement([...relationshipIdentifiers]);\n if (staleFallback > 0) await model.saveRelationships();\n\n // Report deletion with cascade confirmation\n showDeletionSuccess();\n }\n\n function showDeletionSuccess(): void {\n const details: Record<string, unknown> = {\n elementId: id,\n layer: layerName!,\n totalElementsDeleted: elementsToRemove.length,\n };\n handleSuccess(`Deleted element ${ansis.bold(id)}`, details, { verbose: options.verbose });\n\n // Show cascade confirmation when cascade is active\n if (options.cascade && dependents.length > 0) {\n handleInfo(ansis.green(`\u2713 Deleted ${dependents.length} dependent element(s)`));\n }\n }\n } catch (error) {\n if (isTelemetryEnabled && span) {\n (span as any).recordException(error as Error);\n (span as any).setStatus({ code: 2, message: (error as Error).message });\n }\n handleError(error);\n } finally {\n if (isTelemetryEnabled) {\n endSpan(span);\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["showDeletionSuccess", "layer", "element"]
7
7
  }
@@ -2,25 +2,76 @@ import ansis from "ansis";
2
2
  import path from "path";
3
3
  import { rm } from "fs/promises";
4
4
  import { FarmManifest } from "../core/farm-manifest.js";
5
- import { findFarmRoot } from "../utils/project-paths.js";
5
+ import { findFarmRoot, resolveModelDirectory } from "../utils/project-paths.js";
6
6
  import { fileExists } from "../utils/file-io.js";
7
7
  import { execSync, execFileSync } from "child_process";
8
8
  import * as prompts from "@clack/prompts";
9
- import { getErrorMessage, handleSuccess, handleInfo } from "../utils/errors.js";
9
+ import { getErrorMessage, handleSuccess, handleInfo, handleWarning } from "../utils/errors.js";
10
10
  import { isJson } from "../utils/globals.js";
11
11
  import { isTelemetryEnabled, startSpan, endSpan } from "../telemetry/index.js";
12
12
  import { Model } from "../core/model.js";
13
13
  import { ComposedReferenceValidator } from "../validators/composed-reference-validator.js";
14
14
  import { ValidationFormatter } from "../validators/validation-formatter.js";
15
15
  import { FarmSyncEngine } from "../core/farm-sync-engine.js";
16
- function isGitRepository(cwd) {
16
+ import { FarmSyncState, FarmSyncStateNotFoundError } from "../core/farm-sync-state.js";
17
+ function checkGitRepository(cwd) {
17
18
  try {
18
- return execSync("git rev-parse --git-dir", { cwd, stdio: "pipe" }), !0;
19
+ return execSync("git rev-parse --git-dir", { cwd, stdio: "pipe" }), { isRepo: !0 };
19
20
  } catch (error) {
20
21
  const errorMsg = getErrorMessage(error);
21
- return errorMsg.toLowerCase().includes("not a git repository") || process.env.DEBUG && console.warn(`Warning: Unexpected error checking git repository at ${cwd}: ${errorMsg}`), !1;
22
+ return errorMsg.toLowerCase().includes("not a git repository") ? { isRepo: !1 } : { isRepo: !1, error: errorMsg.trim() };
22
23
  }
23
24
  }
25
+ function uncommittedChangeWarning(what, farmRoot, gitError) {
26
+ return `${what} was saved but not committed: could not check git repository at ${farmRoot}: ${gitError}`;
27
+ }
28
+ function gitTopLevel(cwd) {
29
+ try {
30
+ return execSync("git rev-parse --show-toplevel", { cwd, stdio: "pipe", encoding: "utf-8" }).trim() || null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ function isInSameGitRepository(farmRoot, target) {
36
+ const farmTop = gitTopLevel(farmRoot);
37
+ return farmTop !== null && farmTop === gitTopLevel(target);
38
+ }
39
+ function farmRelativePath(farmRoot, target) {
40
+ const rel = path.relative(farmRoot, target);
41
+ return rel === "" ? "." : rel.split(path.sep).join("/");
42
+ }
43
+ function isWithinPath(target, parent) {
44
+ return target === parent || target.startsWith(parent + path.sep);
45
+ }
46
+ async function resolveProjectModel(name, input) {
47
+ const { farmRoot, codebaseFullPath, explicitModel, forceNewModel } = input;
48
+ if (explicitModel) {
49
+ const explicitFullPath = path.resolve(farmRoot, explicitModel), resolved = await resolveModelDirectory(explicitFullPath, { requireManifest: !0 });
50
+ if (!resolved)
51
+ throw new Error(`No model found at '${explicitModel}': expected manifest.yaml in ${explicitFullPath}, ${path.join(explicitFullPath, "model")} or ${path.join(explicitFullPath, "documentation-robotics", "model")}. Omit --model to scaffold a new model at ${name}-model.`);
52
+ return {
53
+ modelFolder: farmRelativePath(farmRoot, resolved.modelDir),
54
+ modelFullPath: resolved.modelDir,
55
+ adopted: !0
56
+ };
57
+ }
58
+ const detected = codebaseFullPath === farmRoot ? null : await resolveModelDirectory(codebaseFullPath, { requireManifest: !0 });
59
+ if (detected && !forceNewModel)
60
+ return {
61
+ modelFolder: farmRelativePath(farmRoot, detected.modelDir),
62
+ modelFullPath: detected.modelDir,
63
+ adopted: !0
64
+ };
65
+ const modelFolder = `${name}-model`, modelFullPath = path.resolve(path.join(farmRoot, modelFolder));
66
+ if (!modelFullPath.startsWith(farmRoot + path.sep))
67
+ throw new Error(`Invalid model path: would escape farm root. Path: ${modelFolder}`);
68
+ return {
69
+ modelFolder,
70
+ modelFullPath,
71
+ adopted: !1,
72
+ ...detected ? { ignoredModelPath: farmRelativePath(farmRoot, detected.modelDir) } : {}
73
+ };
74
+ }
24
75
  async function farmInitCommand(options) {
25
76
  const span = isTelemetryEnabled ? startSpan("farm.init") : null;
26
77
  try {
@@ -73,6 +124,8 @@ async function farmInitCommand(options) {
73
124
  async function farmAddCommand(name, options) {
74
125
  const span = isTelemetryEnabled ? startSpan("farm.add", { "project.name": name }) : null, useJson = options.format === "json" || isJson();
75
126
  try {
127
+ if (options.model && options.newModel)
128
+ throw new Error("--model and --new-model cannot be combined: --model registers an existing model, --new-model scaffolds a fresh one");
76
129
  const farmRoot = await findFarmRoot();
77
130
  if (!farmRoot)
78
131
  throw new Error("Not in a farm directory. Run 'dr farm init' first.");
@@ -80,8 +133,10 @@ async function farmAddCommand(name, options) {
80
133
  if (manifest.getProject(name))
81
134
  throw new Error(`Project '${name}' already exists in this farm`);
82
135
  let codebasePath = options.codebase || name;
136
+ const codebaseFullPath = path.resolve(path.join(farmRoot, codebasePath)), farmRootResolved = path.resolve(farmRoot);
83
137
  if (options.remote) {
84
- const codebaseFullPath = path.join(farmRoot, codebasePath);
138
+ if (!codebaseFullPath.startsWith(farmRootResolved + path.sep))
139
+ throw new Error(`Invalid codebase path: would escape farm root. Path: ${codebasePath}`);
85
140
  if (await fileExists(codebaseFullPath))
86
141
  throw new Error(`Codebase directory '${codebasePath}' already exists`);
87
142
  useJson || handleInfo(`Cloning ${options.remote} to ${codebasePath}...`);
@@ -93,8 +148,17 @@ async function farmAddCommand(name, options) {
93
148
  throw new Error(`Failed to clone repository: ${getErrorMessage(error)}`);
94
149
  }
95
150
  }
96
- const modelFolder = `${name}-model`, modelFullPath = path.join(farmRoot, modelFolder);
97
- if (await fileExists(modelFullPath) || (await Model.init(modelFullPath, {
151
+ const registration = await resolveProjectModel(name, {
152
+ farmRoot: farmRootResolved,
153
+ codebaseFullPath,
154
+ explicitModel: options.model,
155
+ forceNewModel: options.newModel ?? !1
156
+ }), modelFolder = registration.modelFolder;
157
+ if (registration.ignoredModelPath) {
158
+ const ignoredWarning = `Codebase '${codebasePath}' already has a model at ${registration.ignoredModelPath}; ignoring it and scaffolding a new model at ${modelFolder} (--new-model)`;
159
+ registration.warning = ignoredWarning, useJson || handleWarning(ignoredWarning);
160
+ }
161
+ registration.adopted ? useJson || handleInfo(`Using existing model at ${registration.modelFullPath}; nothing was created`) : await fileExists(registration.modelFullPath) || (await Model.init(registration.modelFullPath, {
98
162
  name,
99
163
  version: "0.1.0",
100
164
  description: void 0,
@@ -106,25 +170,42 @@ async function farmAddCommand(name, options) {
106
170
  source: codebasePath,
107
171
  model: modelFolder,
108
172
  remote: options.remote
109
- }), await manifest.save(farmYamlPath), isGitRepository(farmRoot))
173
+ }), await manifest.save(farmYamlPath);
174
+ const gitCheck = checkGitRepository(farmRoot);
175
+ let gitCommitted = !1, gitWarning;
176
+ if (gitCheck.error)
177
+ gitWarning = uncommittedChangeWarning("farm.yaml", farmRoot, gitCheck.error), useJson || handleWarning(gitWarning);
178
+ else if (gitCheck.isRepo)
110
179
  try {
111
180
  execSync("git add farm.yaml", { cwd: farmRoot, stdio: "pipe" }), execFileSync("git", ["commit", "-m", `Add project: ${name}`], {
112
181
  cwd: farmRoot,
113
182
  stdio: useJson ? "pipe" : "inherit"
114
- });
183
+ }), gitCommitted = !0;
115
184
  } catch (commitError) {
116
185
  const errorMsg = getErrorMessage(commitError);
117
186
  if (!errorMsg.includes("nothing to commit"))
118
187
  throw new Error(`Failed to commit farm changes: ${errorMsg}`);
119
188
  }
120
- useJson ? console.log(JSON.stringify({
121
- status: "ok",
122
- project: name,
123
- source: codebasePath,
124
- model: modelFolder
125
- })) : handleSuccess(`Project added to farm: ${ansis.bold(name)}
189
+ if (useJson)
190
+ console.log(JSON.stringify({
191
+ status: "ok",
192
+ project: name,
193
+ source: codebasePath,
194
+ model: modelFolder,
195
+ model_path: registration.modelFullPath,
196
+ model_adopted: registration.adopted,
197
+ ...registration.ignoredModelPath ? { ignored_model: registration.ignoredModelPath } : {},
198
+ ...registration.warning ? { warning: registration.warning } : {},
199
+ git_committed: gitCommitted,
200
+ ...gitWarning ? { git_warning: gitWarning } : {}
201
+ }));
202
+ else {
203
+ const modelNote = registration.adopted ? " (existing model, adopted in place)" : "";
204
+ handleSuccess(`Project added to farm: ${ansis.bold(name)}
126
205
  Codebase: ${codebasePath}
127
- Model: ${modelFolder}`), span && endSpan(span);
206
+ Model: ${modelFolder}${modelNote}`);
207
+ }
208
+ span && endSpan(span);
128
209
  } catch (error) {
129
210
  useJson ? console.log(JSON.stringify({
130
211
  status: "error",
@@ -142,23 +223,29 @@ async function farmRemoveCommand(name, options) {
142
223
  const farmYamlPath = path.join(farmRoot, "farm.yaml"), manifest = await FarmManifest.load(farmYamlPath), project = manifest.getProject(name);
143
224
  if (!project)
144
225
  throw new Error(`Project '${name}' not found in farm`);
226
+ let modelDeleted = !1, modelKeptReason;
145
227
  if (options.deleteModel) {
146
- const modelFullPath = path.resolve(path.join(farmRoot, project.model)), farmRootResolved = path.resolve(farmRoot);
147
- if (!modelFullPath.startsWith(farmRootResolved + path.sep))
148
- throw new Error(`Invalid model path: would escape farm root. Path: ${project.model}`);
149
- if (await fileExists(modelFullPath))
228
+ const modelFullPath = path.resolve(path.join(farmRoot, project.model)), farmRootResolved = path.resolve(farmRoot), codebaseFullPath = path.resolve(path.join(farmRoot, project.source));
229
+ if (modelFullPath.startsWith(farmRootResolved + path.sep) ? isWithinPath(modelFullPath, codebaseFullPath) && (modelKeptReason = `model '${project.model}' is the codebase's own model (inside '${project.source}')`) : modelKeptReason = `model '${project.model}' lies outside the farm root and is not managed by the farm`, modelKeptReason)
230
+ useJson || handleWarning(`Model folder not deleted: ${modelKeptReason}. Remove it by hand if that is what you want.`);
231
+ else if (await fileExists(modelFullPath))
150
232
  try {
151
- await rm(modelFullPath, { recursive: !0, force: !1 }), useJson || handleInfo(`Deleted model folder: ${project.model}`);
233
+ await rm(modelFullPath, { recursive: !0, force: !1 }), modelDeleted = !0, useJson || handleInfo(`Deleted model folder: ${project.model}`);
152
234
  } catch (error) {
153
235
  throw new Error(`Failed to delete model folder: ${getErrorMessage(error)}`);
154
236
  }
155
237
  }
156
- if (manifest.removeProject(name), await manifest.save(farmYamlPath), isGitRepository(farmRoot))
238
+ manifest.removeProject(name), await manifest.save(farmYamlPath);
239
+ const gitCheck = checkGitRepository(farmRoot);
240
+ let gitCommitted = !1, gitWarning;
241
+ if (gitCheck.error)
242
+ gitWarning = uncommittedChangeWarning("farm.yaml", farmRoot, gitCheck.error), useJson || handleWarning(gitWarning);
243
+ else if (gitCheck.isRepo)
157
244
  try {
158
245
  execSync("git add farm.yaml", { cwd: farmRoot, stdio: "pipe" }), execFileSync("git", ["commit", "-m", `Remove project: ${name}`], {
159
246
  cwd: farmRoot,
160
247
  stdio: useJson ? "pipe" : "inherit"
161
- });
248
+ }), gitCommitted = !0;
162
249
  } catch (commitError) {
163
250
  const errorMsg = getErrorMessage(commitError);
164
251
  if (!errorMsg.includes("nothing to commit"))
@@ -167,7 +254,11 @@ async function farmRemoveCommand(name, options) {
167
254
  useJson ? console.log(JSON.stringify({
168
255
  status: "ok",
169
256
  project: name,
170
- modelDeleted: options.deleteModel ?? !1
257
+ model: project.model,
258
+ modelDeleted,
259
+ ...modelKeptReason ? { model_kept_reason: modelKeptReason } : {},
260
+ git_committed: gitCommitted,
261
+ ...gitWarning ? { git_warning: gitWarning } : {}
171
262
  })) : handleSuccess(`Project removed from farm: ${ansis.bold(name)}`), span && endSpan(span);
172
263
  } catch (error) {
173
264
  useJson ? console.log(JSON.stringify({
@@ -183,33 +274,34 @@ async function farmStatusCommand(options) {
183
274
  const farmRoot = await findFarmRoot();
184
275
  if (!farmRoot)
185
276
  throw new Error("Not in a farm directory.");
186
- const farmYamlPath = path.join(farmRoot, "farm.yaml"), manifest = await FarmManifest.load(farmYamlPath), projects = manifest.getAllProjects(), engine = new FarmSyncEngine(farmRoot), projectsWithStatus = await Promise.all(projects.map(async (p) => {
277
+ const farmYamlPath = path.join(farmRoot, "farm.yaml"), manifest = await FarmManifest.load(farmYamlPath), projects = manifest.getAllProjects(), engine = new FarmSyncEngine(farmRoot), useJson = options.format === "json" || isJson(), projectsWithStatus = await Promise.all(projects.map(async (p) => {
278
+ const result = {
279
+ name: p.name,
280
+ source: p.source,
281
+ model: p.model,
282
+ remote: p.remote,
283
+ last_sync_commit: void 0,
284
+ current_commit: void 0,
285
+ has_pending_changes: void 0
286
+ };
287
+ let lastSyncCommit;
187
288
  try {
188
- const syncStatePath = path.join(farmRoot, p.model, ".farm-sync.yaml"), syncState = await (await import("../core/farm-sync-state.js")).FarmSyncState.loadOrCreate(syncStatePath, p.name), currentCommit = await engine.getCurrentCommit(p.source), lastSyncCommit = syncState.lastSyncCommit, hasPendingChanges = !!(lastSyncCommit && lastSyncCommit !== currentCommit);
189
- return {
190
- name: p.name,
191
- source: p.source,
192
- model: p.model,
193
- remote: p.remote,
194
- last_sync_commit: lastSyncCommit,
195
- current_commit: currentCommit,
196
- has_pending_changes: hasPendingChanges
197
- };
289
+ const syncStatePath = path.join(farmRoot, p.model, ".farm-sync.yaml");
290
+ lastSyncCommit = (await FarmSyncState.loadOrCreate(syncStatePath, p.name)).lastSyncCommit, result.last_sync_commit = lastSyncCommit;
198
291
  } catch (error) {
199
- const errorMsg = getErrorMessage(error);
200
- return !errorMsg.includes("not found") && !errorMsg.includes("does not exist") && !errorMsg.includes("ENOENT") && console.error(`Warning: Error reading sync state for ${p.name}: ${errorMsg}`), {
201
- name: p.name,
202
- source: p.source,
203
- model: p.model,
204
- remote: p.remote,
205
- last_sync_commit: void 0,
206
- current_commit: void 0,
207
- has_pending_changes: !1
208
- };
292
+ return error instanceof FarmSyncStateNotFoundError ? (result.has_pending_changes = !1, result) : (result.sync_state_error = getErrorMessage(error), useJson || handleWarning(`${p.name}: could not read sync state: ${result.sync_state_error}`), result);
209
293
  }
210
- }));
211
- options.format === "json" || isJson() ? console.log(JSON.stringify({
212
- status: "ok",
294
+ try {
295
+ const currentCommit = await engine.getCurrentCommit(p.source);
296
+ result.current_commit = currentCommit, result.has_pending_changes = !!(lastSyncCommit && lastSyncCommit !== currentCommit);
297
+ } catch (error) {
298
+ result.codebase_error = getErrorMessage(error), useJson || handleWarning(`${p.name}: ${result.codebase_error}`);
299
+ }
300
+ return result;
301
+ })), projectsWithErrors = projectsWithStatus.filter((p) => p.sync_state_error !== void 0 || p.codebase_error !== void 0), allHealthy = projectsWithErrors.length === 0;
302
+ useJson ? console.log(JSON.stringify({
303
+ status: allHealthy ? "ok" : "error",
304
+ code: allHealthy ? 0 : 1,
213
305
  farm: {
214
306
  name: manifest.name,
215
307
  path: farmRoot,
@@ -221,8 +313,9 @@ async function farmStatusCommand(options) {
221
313
  project_count: projects.length
222
314
  })) : (console.log(ansis.bold(`Farm: ${manifest.name}`)), console.log(`Location: ${farmRoot}`), manifest.platform_view && console.log(`Platform-View: ${ansis.green("enabled")}`), console.log(`Projects: ${projects.length}
223
315
  `), projects.length === 0 ? console.log(ansis.dim("No projects registered yet.")) : projectsWithStatus.forEach((project) => {
224
- console.log(` ${ansis.cyan(project.name)}`), console.log(` Codebase: ${project.source}`), console.log(` Model: ${project.model}`), project.remote && console.log(` Remote: ${project.remote}`), project.has_pending_changes ? console.log(` Status: ${ansis.yellow("\u26A0 Pending changes")}`) : project.last_sync_commit ? console.log(` Status: ${ansis.green("\u2713 Up to date")}`) : console.log(` Status: ${ansis.dim("Not synced yet")}`);
225
- })), span && endSpan(span);
316
+ console.log(` ${ansis.cyan(project.name)}`), console.log(` Codebase: ${project.source}`), console.log(` Model: ${project.model}`), project.remote && console.log(` Remote: ${project.remote}`), project.sync_state_error ? (console.log(` Status: ${ansis.red("\u2717 Sync state corrupted")}`), console.log(` Error: ${project.sync_state_error}`)) : project.codebase_error ? (console.log(` Status: ${ansis.red("\u2717 Codebase unavailable")}`), console.log(` Error: ${project.codebase_error}`)) : project.has_pending_changes ? console.log(` Status: ${ansis.yellow("\u26A0 Pending changes")}`) : project.last_sync_commit ? console.log(` Status: ${ansis.green("\u2713 Up to date")}`) : console.log(` Status: ${ansis.dim("Not synced yet")}`);
317
+ }), allHealthy || console.error(ansis.red(`
318
+ ${projectsWithErrors.length} project(s) could not be checked (see errors above)`))), span && endSpan(span), allHealthy || process.exit(1);
226
319
  } catch (error) {
227
320
  options.format === "json" || isJson() ? console.log(JSON.stringify({
228
321
  status: "error",
@@ -465,6 +558,11 @@ async function farmSyncCommand(options) {
465
558
  throw new Error("Concurrency must be a positive number");
466
559
  if (options.autoCommit && concurrency > 1)
467
560
  throw new Error("Cannot use --auto-commit with --concurrency > 1: concurrent git operations would cause race conditions. Use --concurrency 1 for auto-commit, or remove --auto-commit for parallel processing.");
561
+ if (options.output) {
562
+ const outputExt = path.extname(options.output).toLowerCase();
563
+ if (outputExt !== ".json")
564
+ throw new Error(`Unsupported output format: ${outputExt || "(no extension)"}. Use .json`);
565
+ }
468
566
  const useJson = options.format === "json" || isJson();
469
567
  !useJson && !options.verbose && handleInfo(`Syncing ${projectsToSync.length} project(s) (concurrency: ${concurrency})...`);
470
568
  const allResults = [], processSyncProject = async (project) => {
@@ -492,17 +590,26 @@ async function farmSyncCommand(options) {
492
590
  };
493
591
  if (options.autoCommit && result.changesetId && result.changeCount > 0 && !options.dryRun)
494
592
  try {
495
- const projectModelPath = path.join(farmRoot, project.model), commitResult = await new (await import("../core/staging-area.js")).StagingAreaManager(projectModelPath, model).commit(model, result.changesetId);
496
- if (resultEntry.auto_committed = !0, resultEntry.committed_changes = commitResult.committed, isGitRepository(farmRoot))
593
+ const commitResult = await new (await import("../core/staging-area.js")).StagingAreaManager(model.rootPath, model).commit(model, result.changesetId);
594
+ resultEntry.auto_committed = !0, resultEntry.committed_changes = commitResult.committed;
595
+ const gitCheck = checkGitRepository(farmRoot);
596
+ if (gitCheck.error)
597
+ resultEntry.git_committed = !1, resultEntry.git_warning = uncommittedChangeWarning(`Model '${project.model}'`, farmRoot, gitCheck.error), useJson || handleWarning(`${project.name}: ${resultEntry.git_warning}`);
598
+ else if (gitCheck.isRepo && !isInSameGitRepository(farmRoot, modelPath))
599
+ resultEntry.git_committed = !1, resultEntry.git_warning = `Model '${project.model}' is outside the farm git repository at ${farmRoot}: the synced changes were saved but not committed. Commit them in the model's own repository.`, useJson || handleWarning(`${project.name}: ${resultEntry.git_warning}`);
600
+ else if (gitCheck.isRepo)
497
601
  try {
498
- execFileSync("git", ["add", project.model], { cwd: farmRoot, stdio: "pipe" }), execFileSync("git", ["commit", "-m", `Sync: ${result.changesetId} - ${commitResult.committed} change(s)`], { cwd: farmRoot, stdio: "pipe" });
602
+ execFileSync("git", ["add", project.model], { cwd: farmRoot, stdio: "pipe" }), execFileSync("git", ["commit", "-m", `Sync: ${result.changesetId} - ${commitResult.committed} change(s)`], { cwd: farmRoot, stdio: "pipe" }), resultEntry.git_committed = !0;
499
603
  } catch (gitCommitError) {
500
604
  if (!getErrorMessage(gitCommitError).includes("nothing to commit"))
501
605
  throw gitCommitError;
606
+ resultEntry.git_committed = !0;
502
607
  }
608
+ else
609
+ resultEntry.git_committed = !1;
503
610
  options.verbose && !useJson && handleInfo(` Auto-committed: ${commitResult.committed} change(s)`);
504
611
  } catch (commitError) {
505
- resultEntry.auto_committed = !1, resultEntry.commit_error = getErrorMessage(commitError), resultEntry.status = "error", useJson || handleInfo(` Auto-commit failed: ${getErrorMessage(commitError)}`);
612
+ resultEntry.auto_committed = !1, resultEntry.commit_error = getErrorMessage(commitError), resultEntry.status = "error", useJson || handleWarning(`${project.name}: auto-commit failed: ${getErrorMessage(commitError)}`);
506
613
  }
507
614
  if (allResults.push(resultEntry), options.verbose && !useJson) {
508
615
  handleInfo(`
@@ -559,11 +666,11 @@ Project: ${project.name}`);
559
666
  }
560
667
  if (options.output) {
561
668
  const { writeFile } = await import("../utils/file-io.js");
562
- path.extname(options.output).toLowerCase() === ".json" && await writeFile(options.output, JSON.stringify({
669
+ await writeFile(options.output, JSON.stringify({
563
670
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
564
671
  farm_root: farmRoot,
565
672
  projects: allResults
566
- }, null, 2));
673
+ }, null, 2)), useJson || handleInfo(`Report written to ${options.output}`);
567
674
  }
568
675
  span && endSpan(span), allSucceeded || process.exit(1);
569
676
  } catch (error) {
@@ -588,12 +695,21 @@ Notes:
588
695
  the aggregation hub. When validating this project with 'dr farm validate --project platform-view',
589
696
  the CLI will automatically resolve cross-model references from all farm projects.`).action(async (options) => {
590
697
  await farmInitCommand(options);
591
- }), farmGroup.command("add <name>").description("Add a project to the farm").option("--remote <url>", "Git remote URL to clone").option("--codebase <path>", "Path to codebase folder (defaults to project name)").option("--format <format>", "Output format: text, json", "text").addHelpText("after", `
698
+ }), farmGroup.command("add <name>").description("Add a project to the farm").option("--remote <url>", "Git remote URL to clone").option("--codebase <path>", "Path to codebase folder, relative to the farm root (defaults to project name; may point outside the farm root unless --remote is used)").option("--model <path>", "Register an existing model at this path (relative to the farm root) instead of detecting or scaffolding one").option("--new-model", "Always scaffold a fresh <name>-model, even when the codebase already carries a model").option("--format <format>", "Output format: text, json", "text").addHelpText("after", `
592
699
  Examples:
593
700
  $ dr farm add my-service
594
701
  $ dr farm add my-service --remote https://github.com/org/my-service.git
595
702
  $ dr farm add my-service --codebase ./services/my-service
596
- $ dr farm add my-service --format json`).action(async (name, options) => {
703
+ $ dr farm add my-service --codebase ../my-service # sibling checkout outside the farm root
704
+ $ dr farm add my-service --codebase ../my-service --new-model
705
+ $ dr farm add my-service --model ../shared-models/my-service
706
+ $ dr farm add my-service --format json
707
+
708
+ Model detection:
709
+ If the codebase already holds a model (documentation-robotics/model/manifest.yaml, model/manifest.yaml
710
+ or manifest.yaml at the codebase path itself), that model is adopted in place and nothing is
711
+ created; farm.yaml records its path (which may lie outside the farm root). Use --new-model to
712
+ scaffold a separate <name>-model regardless, or --model to point at a model explicitly.`).action(async (name, options) => {
597
713
  await farmAddCommand(name, options);
598
714
  }), farmGroup.command("remove <name>").description("Remove a project from the farm").option("--delete-model", "Also delete the model folder").option("--format <format>", "Output format: text, json", "text").addHelpText("after", `
599
715
  Examples:
@@ -626,7 +742,7 @@ Examples:
626
742
  $ dr farm pull --verbose
627
743
  $ dr farm pull --format json`).action(async (options) => {
628
744
  await farmPullCommand(options);
629
- }), farmGroup.command("sync").description("Sync farm projects with their models (generates staged changesets)").option("--project <name>", "Sync only a specific project (default: all)").option("--verbose", "Show verbose output").option("--dry-run", "Preview without creating changesets").option("--force", "Proceed despite ambiguous file-to-element mappings").option("--output <path>", "Export sync report to file (JSON)").option("--format <format>", "Output format: text, json", "text").option("--auto-commit", "Automatically commit changes (bypasses staged review)").option("--concurrency <n>", "Process multiple projects in parallel (default: 1)", "1").addHelpText("after", `
745
+ }), farmGroup.command("sync").description("Sync farm projects with their models (generates staged changesets)").option("--project <name>", "Sync only a specific project (default: all)").option("--verbose", "Show verbose output").option("--dry-run", "Preview without creating changesets").option("--force", "Proceed despite ambiguous file-to-element mappings").option("--output <path>", "Export sync report to file (.json only)").option("--format <format>", "Output format: text, json", "text").option("--auto-commit", "Automatically commit changes (bypasses staged review)").option("--concurrency <n>", "Process multiple projects in parallel (default: 1)", "1").addHelpText("after", `
630
746
  Examples:
631
747
  $ dr farm sync
632
748
  $ dr farm sync --project my-service