orbital-command 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 (380) hide show
  1. package/bin/orbital.js +640 -37
  2. package/dist/assets/PrimitivesConfig-CrmQXYh4.js +32 -0
  3. package/dist/assets/QualityGates-BbasOsF3.js +21 -0
  4. package/dist/assets/SessionTimeline-CGeJsVvy.js +1 -0
  5. package/dist/assets/Settings-oiM496mc.js +12 -0
  6. package/dist/assets/SourceControl-B1fP2nJL.js +41 -0
  7. package/dist/assets/WorkflowVisualizer-CWLYf-f0.js +74 -0
  8. package/dist/assets/arrow-down-CPy85_J6.js +6 -0
  9. package/dist/assets/charts-DbDg0Psc.js +68 -0
  10. package/dist/assets/circle-x-Cwz6ZQDV.js +6 -0
  11. package/dist/assets/file-text-C46Xr65c.js +6 -0
  12. package/dist/assets/formatDistanceToNow-BMqsSP44.js +1 -0
  13. package/dist/assets/globe-Cn2yNZUD.js +6 -0
  14. package/dist/assets/index-Aj4sV8Al.css +1 -0
  15. package/dist/assets/index-Bc9dK3MW.js +354 -0
  16. package/dist/assets/key-OPaNTWJ5.js +6 -0
  17. package/dist/assets/minus-GMsbpKym.js +6 -0
  18. package/dist/assets/shield-DwAFkDYI.js +6 -0
  19. package/dist/assets/ui-BmsSg9jU.js +53 -0
  20. package/dist/assets/useWorkflowEditor-BJkTX_NR.js +16 -0
  21. package/dist/assets/{vendor-Dzv9lrRc.js → vendor-Bqt8AJn2.js} +1 -1
  22. package/dist/assets/zap-DfbUoOty.js +11 -0
  23. package/dist/favicon.svg +1 -0
  24. package/dist/index.html +6 -5
  25. package/dist/server/server/__tests__/data-routes.test.js +124 -0
  26. package/dist/server/server/__tests__/helpers/db.js +17 -0
  27. package/dist/server/server/__tests__/helpers/mock-emitter.js +8 -0
  28. package/dist/server/server/__tests__/scope-routes.test.js +137 -0
  29. package/dist/server/server/__tests__/sprint-routes.test.js +102 -0
  30. package/dist/server/server/__tests__/workflow-routes.test.js +107 -0
  31. package/dist/server/server/config-migrator.js +138 -0
  32. package/dist/server/server/config.js +17 -2
  33. package/dist/server/server/database.js +27 -12
  34. package/dist/server/server/global-config.js +143 -0
  35. package/dist/server/server/index.js +882 -252
  36. package/dist/server/server/init.js +579 -194
  37. package/dist/server/server/launch.js +29 -0
  38. package/dist/server/server/manifest-types.js +8 -0
  39. package/dist/server/server/manifest.js +454 -0
  40. package/dist/server/server/migrate-legacy.js +229 -0
  41. package/dist/server/server/parsers/event-parser.test.js +117 -0
  42. package/dist/server/server/parsers/scope-parser.js +74 -28
  43. package/dist/server/server/parsers/scope-parser.test.js +230 -0
  44. package/dist/server/server/project-context.js +255 -0
  45. package/dist/server/server/project-emitter.js +41 -0
  46. package/dist/server/server/project-manager.js +297 -0
  47. package/dist/server/server/routes/config-routes.js +1 -3
  48. package/dist/server/server/routes/data-routes.js +22 -110
  49. package/dist/server/server/routes/dispatch-routes.js +15 -9
  50. package/dist/server/server/routes/git-routes.js +74 -0
  51. package/dist/server/server/routes/manifest-routes.js +319 -0
  52. package/dist/server/server/routes/scope-routes.js +37 -23
  53. package/dist/server/server/routes/sync-routes.js +134 -0
  54. package/dist/server/server/routes/version-routes.js +1 -15
  55. package/dist/server/server/routes/workflow-routes.js +9 -3
  56. package/dist/server/server/schema.js +2 -0
  57. package/dist/server/server/services/batch-orchestrator.js +26 -16
  58. package/dist/server/server/services/claude-session-service.js +17 -14
  59. package/dist/server/server/services/deploy-service.test.js +119 -0
  60. package/dist/server/server/services/event-service.js +64 -1
  61. package/dist/server/server/services/event-service.test.js +191 -0
  62. package/dist/server/server/services/gate-service.test.js +105 -0
  63. package/dist/server/server/services/git-service.js +108 -4
  64. package/dist/server/server/services/github-service.js +110 -2
  65. package/dist/server/server/services/readiness-service.test.js +190 -0
  66. package/dist/server/server/services/scope-cache.js +5 -1
  67. package/dist/server/server/services/scope-cache.test.js +142 -0
  68. package/dist/server/server/services/scope-service.js +217 -126
  69. package/dist/server/server/services/scope-service.test.js +137 -0
  70. package/dist/server/server/services/sprint-orchestrator.js +7 -6
  71. package/dist/server/server/services/sprint-service.js +21 -1
  72. package/dist/server/server/services/sprint-service.test.js +238 -0
  73. package/dist/server/server/services/sync-service.js +434 -0
  74. package/dist/server/server/services/sync-types.js +2 -0
  75. package/dist/server/server/services/telemetry-service.js +143 -0
  76. package/dist/server/server/services/workflow-service.js +26 -5
  77. package/dist/server/server/services/workflow-service.test.js +159 -0
  78. package/dist/server/server/settings-sync.js +284 -0
  79. package/dist/server/server/update-planner.js +279 -0
  80. package/dist/server/server/utils/cc-hooks-parser.js +3 -0
  81. package/dist/server/server/utils/cc-hooks-parser.test.js +86 -0
  82. package/dist/server/server/utils/dispatch-utils.js +77 -20
  83. package/dist/server/server/utils/dispatch-utils.test.js +182 -0
  84. package/dist/server/server/utils/logger.js +37 -3
  85. package/dist/server/server/utils/package-info.js +30 -0
  86. package/dist/server/server/utils/route-helpers.js +10 -0
  87. package/dist/server/server/utils/terminal-launcher.js +79 -25
  88. package/dist/server/server/utils/worktree-manager.js +13 -4
  89. package/dist/server/server/validator.js +230 -0
  90. package/dist/server/server/watchers/global-watcher.js +63 -0
  91. package/dist/server/server/watchers/scope-watcher.js +27 -12
  92. package/dist/server/server/wizard/config-editor.js +237 -0
  93. package/dist/server/server/wizard/detect.js +96 -0
  94. package/dist/server/server/wizard/doctor.js +115 -0
  95. package/dist/server/server/wizard/index.js +155 -0
  96. package/dist/server/server/wizard/phases/confirm.js +39 -0
  97. package/dist/server/server/wizard/phases/project-setup.js +90 -0
  98. package/dist/server/server/wizard/phases/setup-wizard.js +66 -0
  99. package/dist/server/server/wizard/phases/welcome.js +35 -0
  100. package/dist/server/server/wizard/phases/workflow-setup.js +22 -0
  101. package/dist/server/server/wizard/types.js +29 -0
  102. package/dist/server/server/wizard/ui.js +74 -0
  103. package/dist/server/shared/__fixtures__/workflow-configs.js +75 -0
  104. package/dist/server/shared/default-workflow.json +65 -0
  105. package/dist/server/shared/onboarding-tour.test.js +81 -0
  106. package/dist/server/shared/project-colors.js +24 -0
  107. package/dist/server/shared/workflow-config.test.js +84 -0
  108. package/dist/server/shared/workflow-engine.test.js +302 -0
  109. package/dist/server/shared/workflow-normalizer.js +101 -0
  110. package/dist/server/shared/workflow-normalizer.test.js +100 -0
  111. package/dist/server/src/components/onboarding/tour-steps.js +84 -0
  112. package/package.json +20 -15
  113. package/schemas/orbital.config.schema.json +16 -1
  114. package/scripts/postinstall.js +55 -7
  115. package/server/__tests__/data-routes.test.ts +149 -0
  116. package/server/__tests__/helpers/db.ts +19 -0
  117. package/server/__tests__/helpers/mock-emitter.ts +10 -0
  118. package/server/__tests__/scope-routes.test.ts +157 -0
  119. package/server/__tests__/sprint-routes.test.ts +118 -0
  120. package/server/__tests__/workflow-routes.test.ts +120 -0
  121. package/server/config-migrator.ts +163 -0
  122. package/server/config.ts +26 -2
  123. package/server/database.ts +35 -18
  124. package/server/global-config.ts +200 -0
  125. package/server/index.ts +975 -287
  126. package/server/init.ts +625 -182
  127. package/server/launch.ts +32 -0
  128. package/server/manifest-types.ts +145 -0
  129. package/server/manifest.ts +494 -0
  130. package/server/migrate-legacy.ts +290 -0
  131. package/server/parsers/event-parser.test.ts +135 -0
  132. package/server/parsers/scope-parser.test.ts +270 -0
  133. package/server/parsers/scope-parser.ts +79 -31
  134. package/server/project-context.ts +309 -0
  135. package/server/project-emitter.ts +50 -0
  136. package/server/project-manager.ts +369 -0
  137. package/server/routes/config-routes.ts +3 -5
  138. package/server/routes/data-routes.ts +28 -141
  139. package/server/routes/dispatch-routes.ts +19 -11
  140. package/server/routes/git-routes.ts +77 -0
  141. package/server/routes/manifest-routes.ts +388 -0
  142. package/server/routes/scope-routes.ts +29 -25
  143. package/server/routes/sync-routes.ts +175 -0
  144. package/server/routes/version-routes.ts +1 -16
  145. package/server/routes/workflow-routes.ts +9 -3
  146. package/server/schema.ts +2 -0
  147. package/server/services/batch-orchestrator.ts +24 -16
  148. package/server/services/claude-session-service.ts +16 -14
  149. package/server/services/deploy-service.test.ts +145 -0
  150. package/server/services/deploy-service.ts +2 -2
  151. package/server/services/event-service.test.ts +242 -0
  152. package/server/services/event-service.ts +92 -3
  153. package/server/services/gate-service.test.ts +131 -0
  154. package/server/services/gate-service.ts +2 -2
  155. package/server/services/git-service.ts +137 -4
  156. package/server/services/github-service.ts +120 -2
  157. package/server/services/readiness-service.test.ts +217 -0
  158. package/server/services/scope-cache.test.ts +167 -0
  159. package/server/services/scope-cache.ts +4 -1
  160. package/server/services/scope-service.test.ts +169 -0
  161. package/server/services/scope-service.ts +220 -126
  162. package/server/services/sprint-orchestrator.ts +7 -7
  163. package/server/services/sprint-service.test.ts +271 -0
  164. package/server/services/sprint-service.ts +27 -3
  165. package/server/services/sync-service.ts +482 -0
  166. package/server/services/sync-types.ts +77 -0
  167. package/server/services/telemetry-service.ts +195 -0
  168. package/server/services/workflow-service.test.ts +190 -0
  169. package/server/services/workflow-service.ts +29 -9
  170. package/server/settings-sync.ts +359 -0
  171. package/server/update-planner.ts +346 -0
  172. package/server/utils/cc-hooks-parser.test.ts +96 -0
  173. package/server/utils/cc-hooks-parser.ts +4 -0
  174. package/server/utils/dispatch-utils.test.ts +245 -0
  175. package/server/utils/dispatch-utils.ts +97 -27
  176. package/server/utils/logger.ts +40 -3
  177. package/server/utils/package-info.ts +32 -0
  178. package/server/utils/route-helpers.ts +12 -0
  179. package/server/utils/terminal-launcher.ts +85 -25
  180. package/server/utils/worktree-manager.ts +9 -4
  181. package/server/validator.ts +270 -0
  182. package/server/watchers/global-watcher.ts +77 -0
  183. package/server/watchers/scope-watcher.ts +21 -9
  184. package/server/wizard/config-editor.ts +248 -0
  185. package/server/wizard/detect.ts +104 -0
  186. package/server/wizard/doctor.ts +114 -0
  187. package/server/wizard/index.ts +187 -0
  188. package/server/wizard/phases/confirm.ts +45 -0
  189. package/server/wizard/phases/project-setup.ts +106 -0
  190. package/server/wizard/phases/setup-wizard.ts +78 -0
  191. package/server/wizard/phases/welcome.ts +43 -0
  192. package/server/wizard/phases/workflow-setup.ts +28 -0
  193. package/server/wizard/types.ts +56 -0
  194. package/server/wizard/ui.ts +93 -0
  195. package/shared/__fixtures__/workflow-configs.ts +80 -0
  196. package/shared/default-workflow.json +65 -0
  197. package/shared/onboarding-tour.test.ts +94 -0
  198. package/shared/project-colors.ts +24 -0
  199. package/shared/workflow-config.test.ts +111 -0
  200. package/shared/workflow-config.ts +7 -0
  201. package/shared/workflow-engine.test.ts +388 -0
  202. package/shared/workflow-normalizer.test.ts +119 -0
  203. package/shared/workflow-normalizer.ts +118 -0
  204. package/templates/hooks/end-session.sh +3 -1
  205. package/templates/hooks/orbital-emit.sh +2 -2
  206. package/templates/hooks/orbital-report-deploy.sh +4 -4
  207. package/templates/hooks/orbital-report-gates.sh +4 -4
  208. package/templates/hooks/orbital-scope-update.sh +1 -1
  209. package/templates/hooks/scope-create-cleanup.sh +2 -2
  210. package/templates/hooks/scope-create-gate.sh +0 -1
  211. package/templates/hooks/scope-helpers.sh +18 -0
  212. package/templates/hooks/scope-prepare.sh +66 -11
  213. package/templates/migrations/renames.json +1 -0
  214. package/templates/orbital.config.json +7 -2
  215. package/templates/settings-hooks.json +1 -1
  216. package/templates/skills/git-commit/SKILL.md +9 -4
  217. package/templates/skills/git-dev/SKILL.md +8 -3
  218. package/templates/skills/git-main/SKILL.md +8 -2
  219. package/templates/skills/git-production/SKILL.md +6 -2
  220. package/templates/skills/git-staging/SKILL.md +8 -3
  221. package/templates/skills/scope-create/SKILL.md +17 -3
  222. package/templates/skills/scope-fix-review/SKILL.md +6 -3
  223. package/templates/skills/scope-implement/SKILL.md +4 -1
  224. package/templates/skills/scope-post-review/SKILL.md +63 -5
  225. package/templates/skills/scope-pre-review/SKILL.md +5 -2
  226. package/templates/skills/scope-verify/SKILL.md +5 -3
  227. package/templates/skills/test-code-review/SKILL.md +41 -33
  228. package/templates/skills/test-scaffold/SKILL.md +222 -0
  229. package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +0 -84
  230. package/dist/assets/charts-D__PA1zp.js +0 -72
  231. package/dist/assets/index-D1G6i0nS.css +0 -1
  232. package/dist/assets/index-DpItvKpf.js +0 -419
  233. package/dist/assets/ui-BvF022GT.js +0 -53
  234. package/index.html +0 -15
  235. package/postcss.config.js +0 -6
  236. package/src/App.tsx +0 -33
  237. package/src/components/AgentBadge.tsx +0 -40
  238. package/src/components/BatchPreflightModal.tsx +0 -115
  239. package/src/components/CardDisplayToggle.tsx +0 -74
  240. package/src/components/ColumnHeaderActions.tsx +0 -55
  241. package/src/components/ColumnMenu.tsx +0 -99
  242. package/src/components/DeployHistory.tsx +0 -141
  243. package/src/components/DispatchModal.tsx +0 -164
  244. package/src/components/DispatchPopover.tsx +0 -139
  245. package/src/components/DragOverlay.tsx +0 -25
  246. package/src/components/DriftSidebar.tsx +0 -140
  247. package/src/components/EnvironmentStrip.tsx +0 -88
  248. package/src/components/ErrorBoundary.tsx +0 -62
  249. package/src/components/FilterChip.tsx +0 -105
  250. package/src/components/GateIndicator.tsx +0 -33
  251. package/src/components/IdeaDetailModal.tsx +0 -190
  252. package/src/components/IdeaFormDialog.tsx +0 -113
  253. package/src/components/KanbanColumn.tsx +0 -201
  254. package/src/components/MarkdownRenderer.tsx +0 -114
  255. package/src/components/NeonGrid.tsx +0 -128
  256. package/src/components/PromotionQueue.tsx +0 -89
  257. package/src/components/ScopeCard.tsx +0 -234
  258. package/src/components/ScopeDetailModal.tsx +0 -255
  259. package/src/components/ScopeFilterBar.tsx +0 -152
  260. package/src/components/SearchInput.tsx +0 -102
  261. package/src/components/SessionPanel.tsx +0 -335
  262. package/src/components/SprintContainer.tsx +0 -303
  263. package/src/components/SprintDependencyDialog.tsx +0 -78
  264. package/src/components/SprintPreflightModal.tsx +0 -138
  265. package/src/components/StatusBar.tsx +0 -168
  266. package/src/components/SwimCell.tsx +0 -67
  267. package/src/components/SwimLaneRow.tsx +0 -94
  268. package/src/components/SwimlaneBoardView.tsx +0 -108
  269. package/src/components/VersionBadge.tsx +0 -139
  270. package/src/components/ViewModeSelector.tsx +0 -114
  271. package/src/components/config/AgentChip.tsx +0 -53
  272. package/src/components/config/AgentCreateDialog.tsx +0 -321
  273. package/src/components/config/AgentEditor.tsx +0 -175
  274. package/src/components/config/DirectoryTree.tsx +0 -582
  275. package/src/components/config/FileEditor.tsx +0 -550
  276. package/src/components/config/HookChip.tsx +0 -50
  277. package/src/components/config/StageCard.tsx +0 -198
  278. package/src/components/config/TransitionZone.tsx +0 -173
  279. package/src/components/config/UnifiedWorkflowPipeline.tsx +0 -216
  280. package/src/components/config/WorkflowPipeline.tsx +0 -161
  281. package/src/components/source-control/BranchList.tsx +0 -93
  282. package/src/components/source-control/BranchPanel.tsx +0 -105
  283. package/src/components/source-control/CommitLog.tsx +0 -100
  284. package/src/components/source-control/CommitRow.tsx +0 -47
  285. package/src/components/source-control/GitHubPanel.tsx +0 -110
  286. package/src/components/source-control/GitHubSetupGuide.tsx +0 -52
  287. package/src/components/source-control/GitOverviewBar.tsx +0 -101
  288. package/src/components/source-control/PullRequestList.tsx +0 -69
  289. package/src/components/source-control/WorktreeList.tsx +0 -80
  290. package/src/components/ui/badge.tsx +0 -41
  291. package/src/components/ui/button.tsx +0 -55
  292. package/src/components/ui/card.tsx +0 -78
  293. package/src/components/ui/dialog.tsx +0 -94
  294. package/src/components/ui/popover.tsx +0 -33
  295. package/src/components/ui/scroll-area.tsx +0 -54
  296. package/src/components/ui/separator.tsx +0 -28
  297. package/src/components/ui/tabs.tsx +0 -52
  298. package/src/components/ui/toggle-switch.tsx +0 -35
  299. package/src/components/ui/tooltip.tsx +0 -27
  300. package/src/components/workflow/AddEdgeDialog.tsx +0 -217
  301. package/src/components/workflow/AddListDialog.tsx +0 -201
  302. package/src/components/workflow/ChecklistEditor.tsx +0 -239
  303. package/src/components/workflow/CommandPrefixManager.tsx +0 -118
  304. package/src/components/workflow/ConfigSettingsPanel.tsx +0 -189
  305. package/src/components/workflow/DirectionSelector.tsx +0 -133
  306. package/src/components/workflow/DispatchConfigPanel.tsx +0 -180
  307. package/src/components/workflow/EdgeDetailPanel.tsx +0 -236
  308. package/src/components/workflow/EdgePropertyEditor.tsx +0 -251
  309. package/src/components/workflow/EditToolbar.tsx +0 -138
  310. package/src/components/workflow/HookDetailPanel.tsx +0 -250
  311. package/src/components/workflow/HookExecutionLog.tsx +0 -24
  312. package/src/components/workflow/HookSourceModal.tsx +0 -129
  313. package/src/components/workflow/HooksDashboard.tsx +0 -363
  314. package/src/components/workflow/ListPropertyEditor.tsx +0 -251
  315. package/src/components/workflow/MigrationPreviewDialog.tsx +0 -237
  316. package/src/components/workflow/MovementRulesPanel.tsx +0 -188
  317. package/src/components/workflow/NodeDetailPanel.tsx +0 -245
  318. package/src/components/workflow/PresetSelector.tsx +0 -414
  319. package/src/components/workflow/SkillCommandBuilder.tsx +0 -174
  320. package/src/components/workflow/WorkflowEdgeComponent.tsx +0 -145
  321. package/src/components/workflow/WorkflowNode.tsx +0 -147
  322. package/src/components/workflow/graphLayout.ts +0 -186
  323. package/src/components/workflow/mergeHooks.ts +0 -85
  324. package/src/components/workflow/useEditHistory.ts +0 -88
  325. package/src/components/workflow/useWorkflowEditor.ts +0 -262
  326. package/src/components/workflow/validateConfig.ts +0 -70
  327. package/src/hooks/useActiveDispatches.ts +0 -198
  328. package/src/hooks/useBoardSettings.ts +0 -170
  329. package/src/hooks/useCardDisplay.ts +0 -57
  330. package/src/hooks/useCcHooks.ts +0 -24
  331. package/src/hooks/useConfigTree.ts +0 -51
  332. package/src/hooks/useEnforcementRules.ts +0 -46
  333. package/src/hooks/useEvents.ts +0 -59
  334. package/src/hooks/useFileEditor.ts +0 -165
  335. package/src/hooks/useGates.ts +0 -57
  336. package/src/hooks/useIdeaActions.ts +0 -53
  337. package/src/hooks/useKanbanDnd.ts +0 -410
  338. package/src/hooks/useOrbitalConfig.ts +0 -54
  339. package/src/hooks/usePipeline.ts +0 -47
  340. package/src/hooks/usePipelineData.ts +0 -338
  341. package/src/hooks/useReconnect.ts +0 -25
  342. package/src/hooks/useScopeFilters.ts +0 -125
  343. package/src/hooks/useScopeSessions.ts +0 -44
  344. package/src/hooks/useScopes.ts +0 -67
  345. package/src/hooks/useSearch.ts +0 -67
  346. package/src/hooks/useSettings.tsx +0 -187
  347. package/src/hooks/useSocket.ts +0 -25
  348. package/src/hooks/useSourceControl.ts +0 -105
  349. package/src/hooks/useSprintPreflight.ts +0 -55
  350. package/src/hooks/useSprints.ts +0 -154
  351. package/src/hooks/useStatusBarHighlight.ts +0 -18
  352. package/src/hooks/useSwimlaneBoardSettings.ts +0 -104
  353. package/src/hooks/useTheme.ts +0 -9
  354. package/src/hooks/useTransitionReadiness.ts +0 -53
  355. package/src/hooks/useVersion.ts +0 -155
  356. package/src/hooks/useViolations.ts +0 -65
  357. package/src/hooks/useWorkflow.tsx +0 -125
  358. package/src/hooks/useZoomModifier.ts +0 -19
  359. package/src/index.css +0 -797
  360. package/src/layouts/DashboardLayout.tsx +0 -113
  361. package/src/lib/collisionDetection.ts +0 -20
  362. package/src/lib/scope-fields.ts +0 -61
  363. package/src/lib/swimlane.ts +0 -146
  364. package/src/lib/utils.ts +0 -15
  365. package/src/main.tsx +0 -19
  366. package/src/socket.ts +0 -11
  367. package/src/types/index.ts +0 -497
  368. package/src/views/AgentFeed.tsx +0 -339
  369. package/src/views/DeployPipeline.tsx +0 -59
  370. package/src/views/EnforcementView.tsx +0 -378
  371. package/src/views/PrimitivesConfig.tsx +0 -500
  372. package/src/views/QualityGates.tsx +0 -1012
  373. package/src/views/ScopeBoard.tsx +0 -454
  374. package/src/views/SessionTimeline.tsx +0 -516
  375. package/src/views/Settings.tsx +0 -183
  376. package/src/views/SourceControl.tsx +0 -95
  377. package/src/views/WorkflowVisualizer.tsx +0 -382
  378. package/tailwind.config.js +0 -161
  379. package/tsconfig.json +0 -25
  380. package/vite.config.ts +0 -38
package/server/init.ts CHANGED
@@ -6,6 +6,30 @@
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
8
  import { fileURLToPath } from 'url';
9
+ import { GLOBAL_PRIMITIVES_DIR, ensureOrbitalHome, unregisterProject } from './global-config.js';
10
+ import {
11
+ loadManifest,
12
+ saveManifest,
13
+ createManifest,
14
+ hashFile,
15
+ buildTemplateInventory,
16
+ templateFileRecord,
17
+ userFileRecord,
18
+ isSelfHosting,
19
+ getSymlinkTarget,
20
+ createBackup,
21
+ refreshFileStatuses,
22
+ reverseRemapPath,
23
+ safeBackupFile,
24
+ safeCopyTemplate,
25
+ } from './manifest.js';
26
+ import type { OrbitalManifest } from './manifest-types.js';
27
+ import { needsLegacyMigration, migrateFromLegacy } from './migrate-legacy.js';
28
+ import { computeUpdatePlan, loadRenameMap, formatPlan, getFilesToBackup } from './update-planner.js';
29
+ import { syncSettingsHooks, removeAllOrbitalHooks, getTemplateChecksum } from './settings-sync.js';
30
+ import { migrateConfig } from './config-migrator.js';
31
+ import { validate, formatValidationReport } from './validator.js';
32
+ import { getPackageVersion as _getPackageVersionUtil } from './utils/package-info.js';
9
33
 
10
34
  const __filename = fileURLToPath(import.meta.url);
11
35
  const __dirname = path.dirname(__filename);
@@ -489,20 +513,147 @@ function cleanEmptyDirs(dir: string): void {
489
513
  }
490
514
  }
491
515
 
516
+ // ─── Manifest Building ──────────────────────────────────────
517
+
518
+ /** Build a manifest by scanning .claude/ and classifying files against templates. */
519
+ function buildInitManifest(
520
+ projectRoot: string,
521
+ claudeDir: string,
522
+ preset: string,
523
+ ): OrbitalManifest {
524
+ const selfHosting = isSelfHosting(projectRoot);
525
+ const templateInventory = buildTemplateInventory(TEMPLATES_DIR);
526
+ const pkg = getPackageVersion();
527
+
528
+ const manifest = createManifest(pkg, preset);
529
+
530
+ // Classify all template files
531
+ for (const [relPath, templateHash] of templateInventory) {
532
+ const absPath = path.join(claudeDir, relPath);
533
+
534
+ if (selfHosting) {
535
+ const symlinkTarget = getSymlinkTarget(claudeDir, relPath);
536
+ if (symlinkTarget) {
537
+ manifest.files[relPath] = templateFileRecord(templateHash, symlinkTarget);
538
+ continue;
539
+ }
540
+ }
541
+
542
+ if (fs.existsSync(absPath)) {
543
+ const fileHash = hashFile(absPath);
544
+ if (fileHash === templateHash) {
545
+ manifest.files[relPath] = templateFileRecord(templateHash);
546
+ } else {
547
+ // File exists but doesn't match template — classify as outdated
548
+ // (no prior install to compare against, so we can't know if user edited it)
549
+ manifest.files[relPath] = {
550
+ origin: 'template',
551
+ status: 'outdated',
552
+ templateHash,
553
+ installedHash: fileHash,
554
+ };
555
+ }
556
+ }
557
+ // File doesn't exist — it may not have been copied (e.g. skipped on non-force init)
558
+ }
559
+
560
+ // Scan managed directories for user-created files
561
+ for (const dir of ['hooks', 'skills', 'agents']) {
562
+ const dirPath = path.join(claudeDir, dir);
563
+ if (!fs.existsSync(dirPath)) continue;
564
+
565
+ walkDirForManifest(dirPath, dir, (relPath, absPath) => {
566
+ if (manifest.files[relPath]) return; // Already classified
567
+ const fileHash = hashFile(absPath);
568
+ manifest.files[relPath] = userFileRecord(fileHash);
569
+ });
570
+ }
571
+
572
+ // Record settings hooks checksum
573
+ const settingsHooksPath = path.join(TEMPLATES_DIR, 'settings-hooks.json');
574
+ if (fs.existsSync(settingsHooksPath)) {
575
+ manifest.settingsHooksChecksum = getTemplateChecksum(settingsHooksPath);
576
+ }
577
+
578
+ // Record gitignore entries
579
+ manifest.gitignoreEntries = [
580
+ 'scopes/',
581
+ '.claude/orbital/',
582
+ '.claude/orbital-events/',
583
+ '.claude/config/workflow-manifest.sh',
584
+ ];
585
+
586
+ return manifest;
587
+ }
588
+
589
+ function getPackageVersion(): string {
590
+ return _getPackageVersionUtil(PACKAGE_ROOT);
591
+ }
592
+
593
+ /** Recursively walk a directory for manifest building. */
594
+ function walkDirForManifest(
595
+ dirPath: string,
596
+ prefix: string,
597
+ fn: (relPath: string, absPath: string) => void,
598
+ ): void {
599
+ if (!fs.existsSync(dirPath)) return;
600
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
601
+ if (entry.name.startsWith('.')) continue;
602
+ const absPath = path.join(dirPath, entry.name);
603
+ const relPath = `${prefix}/${entry.name}`;
604
+ // Follow symlinks: use stat() to check if target is a directory
605
+ const stat = fs.statSync(absPath);
606
+ if (stat.isDirectory()) {
607
+ walkDirForManifest(absPath, relPath, fn);
608
+ } else {
609
+ fn(relPath, absPath);
610
+ }
611
+ }
612
+ }
613
+
492
614
  // ─── Exports ─────────────────────────────────────────────────
493
615
 
616
+ /** Seed ~/.orbital/primitives/ from package templates. Called by init, update, and server startup. */
617
+ export function seedGlobalPrimitives(): void {
618
+ ensureOrbitalHome();
619
+ for (const type of ['hooks', 'skills', 'agents'] as const) {
620
+ const src = path.join(TEMPLATES_DIR, type);
621
+ const dest = path.join(GLOBAL_PRIMITIVES_DIR, type);
622
+ if (fs.existsSync(src)) {
623
+ copyDirSync(src, dest, { overwrite: true });
624
+ }
625
+ }
626
+ }
627
+
494
628
  export { TEMPLATES_DIR, ensureDir };
495
629
 
630
+ // Re-export manifest utilities for CLI access via loadSharedModule()
631
+ export { loadManifest, saveManifest, hashFile, buildTemplateInventory, refreshFileStatuses, summarizeManifest } from './manifest.js';
632
+ export { validate, formatValidationReport } from './validator.js';
633
+
496
634
  export interface InitOptions {
497
635
  force?: boolean;
636
+ quiet?: boolean; // suppress console output (used by wizard)
637
+ preset?: string; // workflow preset name (default: 'default')
638
+ projectName?: string; // override auto-detected project name
639
+ serverPort?: number; // override default server port
640
+ clientPort?: number; // override default client port
641
+ commands?: Partial<Record<string, string | null>>; // override detected commands
498
642
  }
499
643
 
500
644
  export function runInit(projectRoot: string, options: InitOptions = {}): void {
501
645
  const force = options.force ?? false;
646
+ const quiet = options.quiet ?? false;
647
+ const selectedPreset = options.preset || 'default';
502
648
  const claudeDir = path.join(projectRoot, '.claude');
503
649
 
504
- console.log(`\nOrbital Command — init`);
505
- console.log(`Project root: ${projectRoot}\n`);
650
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
651
+ const log = quiet ? (..._args: unknown[]) => {} : console.log.bind(console);
652
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
653
+ const warn = quiet ? (..._args: unknown[]) => {} : console.warn.bind(console);
654
+
655
+ log(`\nOrbital Command — init`);
656
+ log(`Project root: ${projectRoot}\n`);
506
657
 
507
658
  // 1. Create directories
508
659
  const dirs = [
@@ -513,24 +664,26 @@ export function runInit(projectRoot: string, options: InitOptions = {}): void {
513
664
  ];
514
665
  for (const dir of dirs) {
515
666
  const wasCreated = ensureDir(dir);
516
- console.log(` ${wasCreated ? 'Created' : 'Exists '} ${path.relative(projectRoot, dir)}/`);
667
+ log(` ${wasCreated ? 'Created' : 'Exists '} ${path.relative(projectRoot, dir)}/`);
517
668
  }
518
669
 
519
- // 1b. Create scopes/ subdirectories from the default workflow preset
670
+ // 1b. Create scopes/ subdirectories from the selected workflow preset
671
+ const selectedPresetPath = path.join(TEMPLATES_DIR, 'presets', `${selectedPreset}.json`);
520
672
  const defaultPresetPath = path.join(TEMPLATES_DIR, 'presets', 'default.json');
673
+ const presetPath = fs.existsSync(selectedPresetPath) ? selectedPresetPath : defaultPresetPath;
521
674
  let scopeDirs = ['icebox'];
522
675
  try {
523
- const preset = JSON.parse(fs.readFileSync(defaultPresetPath, 'utf8'));
676
+ const preset = JSON.parse(fs.readFileSync(presetPath, 'utf8'));
524
677
  if (preset.lists && Array.isArray(preset.lists)) {
525
678
  scopeDirs = preset.lists.filter((l: Record<string, unknown>) => l.hasDirectory).map((l: Record<string, unknown>) => l.id as string);
526
679
  }
527
680
  } catch {
528
- console.warn(' Warning: could not load default preset, creating scopes/icebox/ only');
681
+ warn(' Warning: could not load preset, creating scopes/icebox/ only');
529
682
  }
530
683
  for (const dirId of scopeDirs) {
531
684
  const scopeDir = path.join(projectRoot, 'scopes', dirId);
532
685
  const wasCreated = ensureDir(scopeDir);
533
- console.log(` ${wasCreated ? 'Created' : 'Exists '} scopes/${dirId}/`);
686
+ log(` ${wasCreated ? 'Created' : 'Exists '} scopes/${dirId}/`);
534
687
  }
535
688
 
536
689
  // 1c. Copy scope template
@@ -539,9 +692,9 @@ export function runInit(projectRoot: string, options: InitOptions = {}): void {
539
692
  if (fs.existsSync(scopeTemplateSrc)) {
540
693
  if (force || !fs.existsSync(scopeTemplateDest)) {
541
694
  fs.copyFileSync(scopeTemplateSrc, scopeTemplateDest);
542
- console.log(` ${force ? 'Reset ' : 'Created'} scopes/_template.md`);
695
+ log(` ${force ? 'Reset ' : 'Created'} scopes/_template.md`);
543
696
  } else {
544
- console.log(` Exists scopes/_template.md`);
697
+ log(` Exists scopes/_template.md`);
545
698
  }
546
699
  }
547
700
 
@@ -552,75 +705,97 @@ export function runInit(projectRoot: string, options: InitOptions = {}): void {
552
705
  if (configIsNew) {
553
706
  if (fs.existsSync(configSrc)) {
554
707
  fs.copyFileSync(configSrc, configDest);
555
- console.log(` Created .claude/orbital.config.json`);
708
+ // Apply wizard-collected or auto-detected values
709
+ try {
710
+ const config = JSON.parse(fs.readFileSync(configDest, 'utf8'));
711
+ config.projectName = options.projectName || path.basename(projectRoot)
712
+ .replace(/[-_]+/g, ' ')
713
+ .replace(/\b\w/g, c => c.toUpperCase());
714
+ if (options.serverPort) config.serverPort = options.serverPort;
715
+ if (options.clientPort) config.clientPort = options.clientPort;
716
+ fs.writeFileSync(configDest, JSON.stringify(config, null, 2) + '\n', 'utf8');
717
+ } catch { /* leave template default */ }
718
+ log(` Created .claude/orbital.config.json`);
556
719
  } else {
557
720
  const defaultConfig = {
558
- serverPort: 4444,
559
- clientPort: 4445,
560
- projectName: path.basename(projectRoot),
721
+ serverPort: options.serverPort || 4444,
722
+ clientPort: options.clientPort || 4445,
723
+ projectName: options.projectName || path.basename(projectRoot),
561
724
  };
562
725
  fs.writeFileSync(configDest, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf8');
563
- console.log(` Created .claude/orbital.config.json (default)`);
726
+ log(` Created .claude/orbital.config.json (default)`);
564
727
  }
565
728
 
566
- // Auto-detect project commands from package.json
567
- const pkgJsonPath = path.join(projectRoot, 'package.json');
568
- if (fs.existsSync(pkgJsonPath)) {
729
+ // Apply wizard-provided commands or auto-detect from package.json
730
+ if (options.commands) {
569
731
  try {
570
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
571
- const scripts = pkg.scripts || {};
572
732
  const config = JSON.parse(fs.readFileSync(configDest, 'utf8'));
573
733
  if (!config.commands) config.commands = {};
574
- let detected = 0;
575
-
576
- if (scripts.typecheck || scripts['type-check']) {
577
- config.commands.typeCheck = `npm run ${scripts.typecheck ? 'typecheck' : 'type-check'}`;
578
- detected++;
579
- }
580
- if (scripts.lint) { config.commands.lint = 'npm run lint'; detected++; }
581
- if (scripts.build) { config.commands.build = 'npm run build'; detected++; }
582
- if (scripts.test) { config.commands.test = 'npm run test'; detected++; }
583
-
584
- if (detected > 0) {
585
- fs.writeFileSync(configDest, JSON.stringify(config, null, 2) + '\n', 'utf8');
586
- console.log(` Detected ${detected} project command(s) from package.json`);
587
- }
734
+ Object.assign(config.commands, options.commands);
735
+ const commandCount = Object.values(options.commands).filter(v => v !== null).length;
736
+ fs.writeFileSync(configDest, JSON.stringify(config, null, 2) + '\n', 'utf8');
737
+ if (commandCount > 0) log(` Applied ${commandCount} project command(s)`);
588
738
  } catch { /* leave defaults */ }
739
+ } else {
740
+ // Auto-detect project commands from package.json
741
+ const pkgJsonPath = path.join(projectRoot, 'package.json');
742
+ if (fs.existsSync(pkgJsonPath)) {
743
+ try {
744
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
745
+ const scripts = pkg.scripts || {};
746
+ const config = JSON.parse(fs.readFileSync(configDest, 'utf8'));
747
+ if (!config.commands) config.commands = {};
748
+ let detected = 0;
749
+
750
+ if (scripts.typecheck || scripts['type-check']) {
751
+ config.commands.typeCheck = `npm run ${scripts.typecheck ? 'typecheck' : 'type-check'}`;
752
+ detected++;
753
+ }
754
+ if (scripts.lint) { config.commands.lint = 'npm run lint'; detected++; }
755
+ if (scripts.build) { config.commands.build = 'npm run build'; detected++; }
756
+ if (scripts.test) { config.commands.test = 'npm run test'; detected++; }
757
+
758
+ if (detected > 0) {
759
+ fs.writeFileSync(configDest, JSON.stringify(config, null, 2) + '\n', 'utf8');
760
+ log(` Detected ${detected} project command(s) from package.json`);
761
+ }
762
+ } catch { /* leave defaults */ }
763
+ }
589
764
  }
590
765
  } else {
591
- console.log(` Exists .claude/orbital.config.json`);
766
+ log(` Exists .claude/orbital.config.json`);
592
767
  }
593
768
 
594
769
  // 3. Copy hooks
595
- console.log('');
770
+ log('');
596
771
  const hooksSrc = path.join(TEMPLATES_DIR, 'hooks');
597
772
  const hooksDest = path.join(claudeDir, 'hooks');
598
773
  if (force) {
599
774
  const pruned = pruneStaleEntries(hooksSrc, hooksDest);
600
- if (pruned > 0) console.log(` Pruned ${pruned} stale hook entries`);
775
+ if (pruned > 0) log(` Pruned ${pruned} stale hook entries`);
601
776
  }
602
777
  const hooksResult = copyDirSync(hooksSrc, hooksDest, { overwrite: force });
603
- console.log(` Hooks ${hooksResult.created.length} copied, ${hooksResult.skipped.length} skipped`);
778
+ log(` Hooks ${hooksResult.created.length} copied, ${hooksResult.skipped.length} skipped`);
604
779
 
605
780
  // 4. Copy skills
606
781
  const skillsSrc = path.join(TEMPLATES_DIR, 'skills');
607
782
  const skillsDest = path.join(claudeDir, 'skills');
608
783
  if (force) {
609
784
  const pruned = pruneStaleEntries(skillsSrc, skillsDest);
610
- if (pruned > 0) console.log(` Pruned ${pruned} stale skill entries`);
785
+ if (pruned > 0) log(` Pruned ${pruned} stale skill entries`);
611
786
  }
612
787
  const skillsResult = copyDirSync(skillsSrc, skillsDest, { overwrite: force });
613
- console.log(` Skills ${skillsResult.created.length} copied, ${skillsResult.skipped.length} skipped`);
788
+ log(` Skills ${skillsResult.created.length} copied, ${skillsResult.skipped.length} skipped`);
614
789
 
615
790
  // 5. Copy agents
616
791
  const agentsSrc = path.join(TEMPLATES_DIR, 'agents');
617
792
  const agentsDest = path.join(claudeDir, 'agents');
618
793
  if (force) {
619
794
  const pruned = pruneStaleEntries(agentsSrc, agentsDest);
620
- if (pruned > 0) console.log(` Pruned ${pruned} stale agent entries`);
795
+ if (pruned > 0) log(` Pruned ${pruned} stale agent entries`);
621
796
  }
622
797
  const agentsResult = copyDirSync(agentsSrc, agentsDest, { overwrite: force });
623
- console.log(` Agents ${agentsResult.created.length} copied, ${agentsResult.skipped.length} skipped`);
798
+ log(` Agents ${agentsResult.created.length} copied, ${agentsResult.skipped.length} skipped`);
624
799
 
625
800
  // 6. Copy workflow presets
626
801
  const presetsSrc = path.join(TEMPLATES_DIR, 'presets');
@@ -628,22 +803,22 @@ export function runInit(projectRoot: string, options: InitOptions = {}): void {
628
803
  if (fs.existsSync(presetsSrc) && fs.readdirSync(presetsSrc).length > 0) {
629
804
  if (force) {
630
805
  const pruned = pruneStaleEntries(presetsSrc, presetsDest);
631
- if (pruned > 0) console.log(` Pruned ${pruned} stale preset entries`);
806
+ if (pruned > 0) log(` Pruned ${pruned} stale preset entries`);
632
807
  }
633
808
  const presetsResult = copyDirSync(presetsSrc, presetsDest, { overwrite: force });
634
- console.log(` Presets ${presetsResult.created.length} copied, ${presetsResult.skipped.length} skipped`);
809
+ log(` Presets ${presetsResult.created.length} copied, ${presetsResult.skipped.length} skipped`);
635
810
  }
636
811
 
637
812
  // 6b. Reset active workflow config when --force, or create if missing
638
813
  const activeWorkflowDest = path.join(claudeDir, 'config', 'workflow.json');
639
814
  if (force) {
640
- fs.copyFileSync(defaultPresetPath, activeWorkflowDest);
641
- console.log(` Reset .claude/config/workflow.json (default workflow)`);
815
+ fs.copyFileSync(presetPath, activeWorkflowDest);
816
+ log(` Reset .claude/config/workflow.json (${selectedPreset} workflow)`);
642
817
  } else if (!fs.existsSync(activeWorkflowDest)) {
643
- fs.copyFileSync(defaultPresetPath, activeWorkflowDest);
644
- console.log(` Created .claude/config/workflow.json`);
818
+ fs.copyFileSync(presetPath, activeWorkflowDest);
819
+ log(` Created .claude/config/workflow.json (${selectedPreset})`);
645
820
  } else {
646
- console.log(` Exists .claude/config/workflow.json`);
821
+ log(` Exists .claude/config/workflow.json`);
647
822
  }
648
823
 
649
824
  // 7. Copy agent-triggers.json
@@ -652,9 +827,9 @@ export function runInit(projectRoot: string, options: InitOptions = {}): void {
652
827
  if (fs.existsSync(triggersSrc)) {
653
828
  if (force || !fs.existsSync(triggersDest)) {
654
829
  fs.copyFileSync(triggersSrc, triggersDest);
655
- console.log(` Created .claude/config/agent-triggers.json`);
830
+ log(` Created .claude/config/agent-triggers.json`);
656
831
  } else {
657
- console.log(` Exists .claude/config/agent-triggers.json`);
832
+ log(` Exists .claude/config/agent-triggers.json`);
658
833
  }
659
834
  }
660
835
 
@@ -663,7 +838,7 @@ export function runInit(projectRoot: string, options: InitOptions = {}): void {
663
838
  const quickDest = path.join(claudeDir, 'quick');
664
839
  if (fs.existsSync(quickSrc)) {
665
840
  const quickResult = copyDirSync(quickSrc, quickDest, { overwrite: force });
666
- console.log(` Quick ${quickResult.created.length} copied, ${quickResult.skipped.length} skipped`);
841
+ log(` Quick ${quickResult.created.length} copied, ${quickResult.skipped.length} skipped`);
667
842
  }
668
843
 
669
844
  // 7c. Copy anti-patterns/ templates
@@ -671,7 +846,7 @@ export function runInit(projectRoot: string, options: InitOptions = {}): void {
671
846
  const antiDest = path.join(claudeDir, 'anti-patterns');
672
847
  if (fs.existsSync(antiSrc)) {
673
848
  const antiResult = copyDirSync(antiSrc, antiDest, { overwrite: force });
674
- console.log(` Anti-pat ${antiResult.created.length} copied, ${antiResult.skipped.length} skipped`);
849
+ log(` Anti-pat ${antiResult.created.length} copied, ${antiResult.skipped.length} skipped`);
675
850
  }
676
851
 
677
852
  // 7d. Copy lessons-learned.md
@@ -680,212 +855,480 @@ export function runInit(projectRoot: string, options: InitOptions = {}): void {
680
855
  if (fs.existsSync(lessonsSrc)) {
681
856
  if (force || !fs.existsSync(lessonsDest)) {
682
857
  fs.copyFileSync(lessonsSrc, lessonsDest);
683
- console.log(` Created .claude/lessons-learned.md`);
858
+ log(` Created .claude/lessons-learned.md`);
684
859
  } else {
685
- console.log(` Exists .claude/lessons-learned.md`);
860
+ log(` Exists .claude/lessons-learned.md`);
686
861
  }
687
862
  }
688
863
 
689
864
  // 7e. Generate workflow manifest
690
865
  const manifestOk = writeManifest(claudeDir);
691
- console.log(` ${manifestOk ? 'Created' : 'Skipped'} .claude/config/workflow-manifest.sh`);
866
+ log(` ${manifestOk ? 'Created' : 'Skipped'} .claude/config/workflow-manifest.sh`);
692
867
 
693
868
  // 7f. Generate INDEX.md
694
869
  const indexDest = path.join(claudeDir, 'INDEX.md');
695
870
  if (force || !fs.existsSync(indexDest)) {
696
871
  const indexContent = generateIndexMd(projectRoot, claudeDir);
697
872
  fs.writeFileSync(indexDest, indexContent, 'utf8');
698
- console.log(` ${force ? 'Reset ' : 'Created'} .claude/INDEX.md`);
873
+ log(` ${force ? 'Reset ' : 'Created'} .claude/INDEX.md`);
699
874
  } else {
700
- console.log(` Exists .claude/INDEX.md`);
875
+ log(` Exists .claude/INDEX.md`);
701
876
  }
702
877
 
703
878
  // 8. Merge hook registrations into settings.local.json
704
- console.log('');
879
+ log('');
705
880
  const settingsTarget = path.join(claudeDir, 'settings.local.json');
706
881
  const settingsSrc = path.join(TEMPLATES_DIR, 'settings-hooks.json');
707
882
  mergeSettingsHooks(settingsTarget, settingsSrc);
708
- console.log(` Merged hook registrations into .claude/settings.local.json`);
883
+ log(` Merged hook registrations into .claude/settings.local.json`);
709
884
 
710
885
  // 9. Update .gitignore
711
886
  const gitignoreUpdated = updateGitignore(projectRoot);
712
- console.log(` ${gitignoreUpdated ? 'Updated' : 'Exists '} .gitignore (Orbital patterns)`);
887
+ log(` ${gitignoreUpdated ? 'Updated' : 'Exists '} .gitignore (Orbital patterns)`);
713
888
 
714
889
  // 10. Make hook scripts executable
715
890
  chmodScripts(hooksDest);
716
- console.log(` chmod hook scripts set to executable`);
891
+ log(` chmod hook scripts set to executable`);
892
+
893
+ // 11. Seed global primitives from package templates
894
+ seedGlobalPrimitives();
895
+ log(` Seeded global primitives (~/.orbital/primitives/)`);
896
+
897
+ // 12. Build and save manifest
898
+ const manifest = buildInitManifest(projectRoot, claudeDir, selectedPreset);
899
+ saveManifest(projectRoot, manifest);
900
+ log(` Created .claude/orbital-manifest.json (${Object.keys(manifest.files).length} files tracked)`);
717
901
 
718
902
  // Summary
719
903
  const totalCreated = hooksResult.created.length + skillsResult.created.length + agentsResult.created.length;
720
904
  const totalSkipped = hooksResult.skipped.length + skillsResult.skipped.length + agentsResult.skipped.length;
721
- console.log(`\nDone. ${totalCreated} files installed, ${totalSkipped} skipped (use --force to overwrite).`);
905
+ log(`\nDone. ${totalCreated} files installed, ${totalSkipped} skipped (use --force to overwrite).`);
906
+ }
907
+
908
+ export interface UpdateOptions {
909
+ dryRun?: boolean;
910
+ force?: boolean;
722
911
  }
723
912
 
724
- export function runUpdate(projectRoot: string): void {
913
+ export function runUpdate(projectRoot: string, options: UpdateOptions = {}): void {
914
+ const { dryRun = false } = options;
725
915
  const claudeDir = path.join(projectRoot, '.claude');
916
+ const newVersion = getPackageVersion();
726
917
 
727
- console.log(`\nOrbital Command — update`);
918
+ console.log(`\nOrbital Command — update${dryRun ? ' (dry run)' : ''}`);
728
919
  console.log(`Project root: ${projectRoot}\n`);
729
920
 
730
- // 1. Copy hooks (overwrite) prune stale entries first
731
- const hooksSrc = path.join(TEMPLATES_DIR, 'hooks');
732
- const hooksDest = path.join(claudeDir, 'hooks');
733
- const hooksPruned = pruneStaleEntries(hooksSrc, hooksDest);
734
- if (hooksPruned > 0) console.log(` Pruned ${hooksPruned} stale hook entries`);
735
- const hooksResult = copyDirSync(hooksSrc, hooksDest, { overwrite: true });
736
- console.log(` Hooks ${hooksResult.created.length} updated`);
921
+ // 1. Load or create manifest (auto-migrate legacy installs)
922
+ let manifest = loadManifest(projectRoot);
923
+ if (!manifest) {
924
+ if (needsLegacyMigration(projectRoot)) {
925
+ console.log(' Migrating from legacy install...');
926
+ const result = migrateFromLegacy(projectRoot, TEMPLATES_DIR, newVersion);
927
+ console.log(` Migrated ${result.synced} synced, ${result.modified} modified, ${result.userOwned} user-owned files`);
928
+ if (result.importedPins > 0) console.log(` Imported ${result.importedPins} pinned files from orbital-sync.json`);
929
+ manifest = loadManifest(projectRoot);
930
+ }
931
+ if (!manifest) {
932
+ console.log(' No manifest found. Run `orbital init` first.');
933
+ return;
934
+ }
935
+ }
737
936
 
738
- // 2. Copy skills (overwrite) — prune stale entries first
739
- const skillsSrc = path.join(TEMPLATES_DIR, 'skills');
740
- const skillsDest = path.join(claudeDir, 'skills');
741
- const skillsPruned = pruneStaleEntries(skillsSrc, skillsDest);
742
- if (skillsPruned > 0) console.log(` Pruned ${skillsPruned} stale skill entries`);
743
- const skillsResult = copyDirSync(skillsSrc, skillsDest, { overwrite: true });
744
- console.log(` Skills ${skillsResult.created.length} updated`);
937
+ const oldVersion = manifest.packageVersion;
745
938
 
746
- // 3. Copy agents (overwrite) prune stale entries first
747
- const agentsSrc = path.join(TEMPLATES_DIR, 'agents');
748
- const agentsDest = path.join(claudeDir, 'agents');
749
- const agentsPruned = pruneStaleEntries(agentsSrc, agentsDest);
750
- if (agentsPruned > 0) console.log(` Pruned ${agentsPruned} stale agent entries`);
751
- const agentsResult = copyDirSync(agentsSrc, agentsDest, { overwrite: true });
752
- console.log(` Agents ${agentsResult.created.length} updated`);
939
+ // 1b. Refresh file statuses so outdated vs modified is accurate
940
+ refreshFileStatuses(manifest, claudeDir);
753
941
 
754
- // 4. Update workflow presets — prune stale entries first
755
- const presetsSrc = path.join(TEMPLATES_DIR, 'presets');
756
- const presetsDest = path.join(claudeDir, 'config', 'workflows');
757
- if (fs.existsSync(presetsSrc) && fs.readdirSync(presetsSrc).length > 0) {
758
- const presetsPruned = pruneStaleEntries(presetsSrc, presetsDest);
759
- if (presetsPruned > 0) console.log(` Pruned ${presetsPruned} stale preset entries`);
760
- const presetsResult = copyDirSync(presetsSrc, presetsDest, { overwrite: true });
761
- console.log(` Presets ${presetsResult.created.length} updated`);
942
+ // 2. Compute update plan
943
+ const renameMap = loadRenameMap(TEMPLATES_DIR, oldVersion, newVersion);
944
+ const plan = computeUpdatePlan({
945
+ templatesDir: TEMPLATES_DIR,
946
+ claudeDir,
947
+ manifest,
948
+ newVersion,
949
+ renameMap,
950
+ });
951
+
952
+ // 3. Dry-run mode — print plan and exit
953
+ if (dryRun) {
954
+ console.log(formatPlan(plan, oldVersion, newVersion));
955
+ return;
762
956
  }
763
957
 
764
- // 5. Update quick/, anti-patterns/, lessons-learned, scope template
765
- const quickSrc = path.join(TEMPLATES_DIR, 'quick');
766
- const quickDest = path.join(claudeDir, 'quick');
767
- if (fs.existsSync(quickSrc)) {
768
- const quickResult = copyDirSync(quickSrc, quickDest, { overwrite: true });
769
- console.log(` Quick ${quickResult.created.length} updated`);
958
+ if (plan.isEmpty && oldVersion === newVersion) {
959
+ console.log(' Everything up to date. No changes needed.');
770
960
  }
771
961
 
772
- const antiSrc = path.join(TEMPLATES_DIR, 'anti-patterns');
773
- const antiDest = path.join(claudeDir, 'anti-patterns');
774
- if (fs.existsSync(antiSrc)) {
775
- const antiResult = copyDirSync(antiSrc, antiDest, { overwrite: true });
776
- console.log(` Anti-pat ${antiResult.created.length} updated`);
962
+ // 4. Create backup of files that will be modified
963
+ const filesToBackup = getFilesToBackup(plan);
964
+ if (filesToBackup.length > 0) {
965
+ const backupDir = createBackup(claudeDir, filesToBackup);
966
+ if (backupDir) {
967
+ console.log(` Backup ${filesToBackup.length} files → ${path.relative(claudeDir, backupDir)}/`);
968
+ }
777
969
  }
778
970
 
779
- const lessonsSrc = path.join(TEMPLATES_DIR, 'lessons-learned.md');
780
- const lessonsDest = path.join(claudeDir, 'lessons-learned.md');
781
- if (fs.existsSync(lessonsSrc) && !fs.existsSync(lessonsDest)) {
782
- fs.copyFileSync(lessonsSrc, lessonsDest);
783
- console.log(` Created .claude/lessons-learned.md`);
971
+ // 5. Execute plan
972
+ const templateInventory = buildTemplateInventory(TEMPLATES_DIR);
973
+
974
+ // 5a. Handle renames
975
+ for (const { from, to } of plan.toRename) {
976
+ const fromPath = path.join(claudeDir, from);
977
+ const toPath = path.join(claudeDir, to);
978
+ const toDir = path.dirname(toPath);
979
+ if (!fs.existsSync(toDir)) fs.mkdirSync(toDir, { recursive: true });
980
+
981
+ if (fs.existsSync(fromPath)) {
982
+ safeBackupFile(fromPath);
983
+ const stat = fs.lstatSync(fromPath);
984
+ if (stat.isSymbolicLink()) {
985
+ // Recreate symlink at new path pointing to remapped template
986
+ const target = fs.readlinkSync(fromPath);
987
+ fs.unlinkSync(fromPath);
988
+ fs.symlinkSync(target, toPath);
989
+ } else {
990
+ fs.renameSync(fromPath, toPath);
991
+ }
992
+ }
993
+
994
+ // Transfer manifest record
995
+ const record = manifest.files[from];
996
+ if (record) {
997
+ delete manifest.files[from];
998
+ const newHash = templateInventory.get(to);
999
+ manifest.files[to] = { ...record, templateHash: newHash, installedHash: newHash || record.installedHash };
1000
+ }
1001
+ console.log(` RENAME ${from} → ${to}`);
1002
+ }
1003
+
1004
+ // 5b. Add new files
1005
+ for (const filePath of plan.toAdd) {
1006
+ const templateHash = templateInventory.get(filePath);
1007
+ if (!templateHash) continue;
1008
+
1009
+ const destPath = path.join(claudeDir, filePath);
1010
+ const destDir = path.dirname(destPath);
1011
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
1012
+
1013
+ copyTemplateFile(TEMPLATES_DIR, filePath, destPath);
1014
+ manifest.files[filePath] = templateFileRecord(templateHash);
1015
+ console.log(` ADD ${filePath}`);
1016
+ }
1017
+
1018
+ // 5c. Update changed synced/outdated files
1019
+ for (const filePath of plan.toUpdate) {
1020
+ const templateHash = templateInventory.get(filePath);
1021
+ if (!templateHash) continue;
1022
+
1023
+ const destPath = path.join(claudeDir, filePath);
1024
+ safeBackupFile(destPath);
1025
+ copyTemplateFile(TEMPLATES_DIR, filePath, destPath);
1026
+ manifest.files[filePath] = templateFileRecord(templateHash);
1027
+ console.log(` UPDATE ${filePath}`);
784
1028
  }
785
1029
 
1030
+ // 5d. Remove deleted files
1031
+ for (const filePath of plan.toRemove) {
1032
+ const absPath = path.join(claudeDir, filePath);
1033
+ if (fs.existsSync(absPath)) {
1034
+ safeBackupFile(absPath);
1035
+ fs.unlinkSync(absPath);
1036
+ }
1037
+ delete manifest.files[filePath];
1038
+ console.log(` REMOVE ${filePath}`);
1039
+ }
1040
+
1041
+ // 5e. Update pinned/modified file records (record new template hash without touching file)
1042
+ for (const { file, reason, newTemplateHash } of plan.toSkip) {
1043
+ if (manifest.files[file]) {
1044
+ manifest.files[file].templateHash = newTemplateHash;
1045
+ }
1046
+ if (reason === 'modified') {
1047
+ console.log(` SKIP ${file} (user modified)`);
1048
+ } else {
1049
+ console.log(` SKIP ${file} (pinned)`);
1050
+ }
1051
+ }
1052
+
1053
+ // 5f. Clean up empty directories
1054
+ for (const dir of ['hooks', 'skills', 'agents', 'config/workflows']) {
1055
+ const dirPath = path.join(claudeDir, dir);
1056
+ if (fs.existsSync(dirPath)) cleanEmptyDirs(dirPath);
1057
+ }
1058
+
1059
+ // 6. Bidirectional settings hook sync
1060
+ const settingsTarget = path.join(claudeDir, 'settings.local.json');
1061
+ const settingsSrc = path.join(TEMPLATES_DIR, 'settings-hooks.json');
1062
+ const syncResult = syncSettingsHooks(settingsTarget, settingsSrc, manifest.settingsHooksChecksum, renameMap);
1063
+ if (!syncResult.skipped) {
1064
+ console.log(` Settings +${syncResult.added} -${syncResult.removed} hooks (${syncResult.updated} renamed)`);
1065
+ manifest.settingsHooksChecksum = getTemplateChecksum(settingsSrc);
1066
+ }
1067
+
1068
+ // 7. Config migrations
1069
+ const configPath = path.join(claudeDir, 'orbital.config.json');
1070
+ const migrationResult = migrateConfig(configPath, manifest.appliedMigrations);
1071
+ if (migrationResult.applied.length > 0) {
1072
+ manifest.appliedMigrations.push(...migrationResult.applied);
1073
+ console.log(` Config ${migrationResult.applied.length} migration(s) applied`);
1074
+ }
1075
+ if (migrationResult.defaultsFilled.length > 0) {
1076
+ console.log(` Config ${migrationResult.defaultsFilled.length} default(s) filled`);
1077
+ }
1078
+
1079
+ // 8. Regenerate derived artifacts (always)
1080
+ const workflowManifestOk = writeManifest(claudeDir);
1081
+ console.log(` ${workflowManifestOk ? 'Updated' : 'Skipped'} .claude/config/workflow-manifest.sh`);
1082
+
1083
+ const indexContent = generateIndexMd(projectRoot, claudeDir);
1084
+ fs.writeFileSync(path.join(claudeDir, 'INDEX.md'), indexContent, 'utf8');
1085
+ console.log(` Updated .claude/INDEX.md`);
1086
+
1087
+ // 9. Update agent-triggers.json (template-managed)
1088
+ const triggersSrc = path.join(TEMPLATES_DIR, 'config', 'agent-triggers.json');
1089
+ const triggersDest = path.join(claudeDir, 'config', 'agent-triggers.json');
1090
+ if (fs.existsSync(triggersSrc)) {
1091
+ fs.copyFileSync(triggersSrc, triggersDest);
1092
+ console.log(` Updated .claude/config/agent-triggers.json`);
1093
+ }
1094
+
1095
+ // 10. Update scope template
786
1096
  const scopeTemplateSrc = path.join(TEMPLATES_DIR, 'scopes', '_template.md');
787
1097
  const scopeTemplateDest = path.join(projectRoot, 'scopes', '_template.md');
788
1098
  if (fs.existsSync(scopeTemplateSrc)) {
789
1099
  ensureDir(path.join(projectRoot, 'scopes'));
790
1100
  fs.copyFileSync(scopeTemplateSrc, scopeTemplateDest);
791
- console.log(` Updated scopes/_template.md`);
792
1101
  }
793
1102
 
794
- // 5b. Regenerate workflow manifest
795
- const manifestOk = writeManifest(claudeDir);
796
- console.log(` ${manifestOk ? 'Updated' : 'Skipped'} .claude/config/workflow-manifest.sh`);
1103
+ // 11. Make hook scripts executable
1104
+ chmodScripts(path.join(claudeDir, 'hooks'));
797
1105
 
798
- // 6. Re-merge settings hooks
799
- const settingsTarget = path.join(claudeDir, 'settings.local.json');
800
- const settingsSrc = path.join(TEMPLATES_DIR, 'settings-hooks.json');
801
- mergeSettingsHooks(settingsTarget, settingsSrc);
802
- console.log(` Merged hook registrations into .claude/settings.local.json`);
1106
+ // 12. Refresh global primitives
1107
+ seedGlobalPrimitives();
803
1108
 
804
- // 7. Make hook scripts executable
805
- chmodScripts(hooksDest);
806
- console.log(` chmod hook scripts set to executable`);
1109
+ // 13. Update manifest metadata
1110
+ manifest.previousPackageVersion = oldVersion;
1111
+ manifest.packageVersion = newVersion;
1112
+ manifest.updatedAt = new Date().toISOString();
1113
+ saveManifest(projectRoot, manifest);
1114
+
1115
+ // 14. Validate
1116
+ const report = validate(projectRoot, newVersion);
1117
+ if (report.errors > 0) {
1118
+ console.log(`\n Validation: ${report.errors} errors found`);
1119
+ console.log(formatValidationReport(report));
1120
+ }
1121
+
1122
+ const totalChanges = plan.toAdd.length + plan.toUpdate.length + plan.toRemove.length + plan.toRename.length;
1123
+ console.log(`\nUpdate complete. ${totalChanges} file changes, ${plan.toSkip.length} skipped.\n`);
1124
+ }
807
1125
 
808
- console.log(`\nUpdate complete.\n`);
1126
+ export interface UninstallOptions {
1127
+ dryRun?: boolean;
1128
+ keepConfig?: boolean;
809
1129
  }
810
1130
 
811
- export function runUninstall(projectRoot: string): void {
1131
+ export function runUninstall(projectRoot: string, options: UninstallOptions = {}): void {
1132
+ const { dryRun = false, keepConfig = false } = options;
812
1133
  const claudeDir = path.join(projectRoot, '.claude');
813
1134
 
814
- console.log(`\nOrbital Command — uninstall`);
1135
+ console.log(`\nOrbital Command — uninstall${dryRun ? ' (dry run)' : ''}`);
815
1136
  console.log(`Project root: ${projectRoot}\n`);
816
1137
 
817
- let removedCount = 0;
1138
+ const manifest = loadManifest(projectRoot);
1139
+
1140
+ // Fall back to legacy uninstall if no manifest
1141
+ if (!manifest) {
1142
+ console.log(' No manifest found — falling back to legacy uninstall.');
1143
+ runLegacyUninstall(projectRoot);
1144
+ return;
1145
+ }
1146
+
1147
+ // Compute what to remove vs preserve
1148
+ const toRemove: string[] = [];
1149
+ const toPreserve: string[] = [];
1150
+
1151
+ for (const [filePath, record] of Object.entries(manifest.files)) {
1152
+ if (record.origin === 'user') {
1153
+ toPreserve.push(filePath);
1154
+ } else if (record.status === 'modified' || record.status === 'outdated') {
1155
+ toPreserve.push(filePath);
1156
+ } else {
1157
+ toRemove.push(filePath);
1158
+ }
1159
+ }
1160
+
1161
+ if (dryRun) {
1162
+ console.log(' Files to REMOVE:');
1163
+ for (const f of toRemove) console.log(` ${f}`);
1164
+ if (toPreserve.length > 0) {
1165
+ console.log(' Files to PRESERVE:');
1166
+ for (const f of toPreserve) console.log(` ${f} (${manifest.files[f].origin}/${manifest.files[f].status})`);
1167
+ }
1168
+ console.log(`\n Would also remove: settings hooks, generated artifacts, config files, gitignore entries`);
1169
+ console.log(` No changes made. Run without --dry-run to apply.`);
1170
+ return;
1171
+ }
818
1172
 
819
- // 1. Remove orbital hooks from settings.local.json
1173
+ // 1. Remove _orbital hooks from settings.local.json
820
1174
  const settingsPath = path.join(claudeDir, 'settings.local.json');
821
- if (fs.existsSync(settingsPath)) {
822
- try {
823
- const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
824
- if (settings.hooks) {
825
- for (const [event] of Object.entries(settings.hooks)) {
826
- for (const group of settings.hooks[event]) {
827
- if (group.hooks) {
828
- const before = group.hooks.length;
829
- group.hooks = group.hooks.filter((h: HookEntry) => !h._orbital);
830
- removedCount += before - group.hooks.length;
831
- }
832
- }
833
- settings.hooks[event] = settings.hooks[event].filter(
834
- (g: HookGroup) => g.hooks && g.hooks.length > 0
835
- );
836
- if (settings.hooks[event].length === 0) {
837
- delete settings.hooks[event];
838
- }
839
- }
840
- if (Object.keys(settings.hooks).length === 0) {
841
- delete settings.hooks;
842
- }
843
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
844
- console.log(` Removed ${removedCount} orbital hook registrations from settings.local.json`);
845
- }
846
- } catch {
847
- console.warn(' Warning: could not parse settings.local.json');
1175
+ const removedHooks = removeAllOrbitalHooks(settingsPath);
1176
+ console.log(` Removed ${removedHooks} orbital hook registrations`);
1177
+
1178
+ // 2. Delete template files (synced + pinned, not modified or user-owned)
1179
+ let filesRemoved = 0;
1180
+ for (const filePath of toRemove) {
1181
+ const absPath = path.join(claudeDir, filePath);
1182
+ if (fs.existsSync(absPath)) {
1183
+ fs.unlinkSync(absPath);
1184
+ filesRemoved++;
848
1185
  }
849
1186
  }
1187
+ console.log(` Removed ${filesRemoved} template files`);
1188
+ if (toPreserve.length > 0) {
1189
+ console.log(` Preserved ${toPreserve.length} user/modified files`);
1190
+ }
850
1191
 
851
- // 2. Delete hooks that came from templates
852
- const hookFiles = listTemplateFiles(path.join(TEMPLATES_DIR, 'hooks'), path.join(claudeDir, 'hooks'));
853
- let hooksRemoved = 0;
854
- for (const f of hookFiles) {
855
- if (fs.existsSync(f)) {
856
- fs.unlinkSync(f);
857
- hooksRemoved++;
1192
+ // 3. Clean up empty directories
1193
+ for (const dir of ['hooks', 'skills', 'agents', 'config/workflows', 'quick', 'anti-patterns']) {
1194
+ const dirPath = path.join(claudeDir, dir);
1195
+ if (fs.existsSync(dirPath)) cleanEmptyDirs(dirPath);
1196
+ }
1197
+
1198
+ // 4. Remove generated artifacts
1199
+ for (const artifact of manifest.generatedArtifacts) {
1200
+ const artifactPath = path.join(claudeDir, artifact);
1201
+ if (fs.existsSync(artifactPath)) {
1202
+ fs.unlinkSync(artifactPath);
1203
+ console.log(` Removed .claude/${artifact}`);
858
1204
  }
859
1205
  }
860
- console.log(` Removed ${hooksRemoved} hook scripts`);
861
1206
 
862
- // 3. Delete skills that came from templates
863
- const skillFiles = listTemplateFiles(path.join(TEMPLATES_DIR, 'skills'), path.join(claudeDir, 'skills'));
864
- let skillsRemoved = 0;
865
- for (const f of skillFiles) {
866
- if (fs.existsSync(f)) {
867
- fs.unlinkSync(f);
868
- skillsRemoved++;
1207
+ // 5. Remove template-sourced config files
1208
+ const configFiles = [
1209
+ 'config/agent-triggers.json',
1210
+ 'config/workflow.json',
1211
+ 'lessons-learned.md',
1212
+ ];
1213
+ for (const file of configFiles) {
1214
+ const filePath = path.join(claudeDir, file);
1215
+ if (fs.existsSync(filePath)) {
1216
+ fs.unlinkSync(filePath);
1217
+ console.log(` Removed .claude/${file}`);
869
1218
  }
870
1219
  }
871
- const skillsDest = path.join(claudeDir, 'skills');
872
- if (fs.existsSync(skillsDest)) cleanEmptyDirs(skillsDest);
873
- console.log(` Removed ${skillsRemoved} skill files`);
874
1220
 
875
- // 4. Delete agents that came from templates
876
- const agentFiles = listTemplateFiles(path.join(TEMPLATES_DIR, 'agents'), path.join(claudeDir, 'agents'));
877
- let agentsRemoved = 0;
878
- for (const f of agentFiles) {
879
- if (fs.existsSync(f)) {
880
- fs.unlinkSync(f);
881
- agentsRemoved++;
1221
+ // Remove config/workflows/ directory entirely
1222
+ const workflowsDir = path.join(claudeDir, 'config', 'workflows');
1223
+ if (fs.existsSync(workflowsDir)) {
1224
+ fs.rmSync(workflowsDir, { recursive: true, force: true });
1225
+ console.log(` Removed .claude/config/workflows/`);
1226
+ }
1227
+
1228
+ // 6. Remove gitignore entries
1229
+ removeGitignoreEntries(projectRoot, manifest.gitignoreEntries);
1230
+ console.log(` Cleaned .gitignore`);
1231
+
1232
+ // 7. Deregister from global registry
1233
+ if (unregisterProject(projectRoot)) {
1234
+ console.log(` Removed project from ~/.orbital/config.json`);
1235
+ }
1236
+
1237
+ // 8. Remove orbital config and manifest (unless --keep-config)
1238
+ if (!keepConfig) {
1239
+ const toClean = ['orbital.config.json', 'orbital-manifest.json', 'orbital-sync.json'];
1240
+ for (const file of toClean) {
1241
+ const filePath = path.join(claudeDir, file);
1242
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
882
1243
  }
1244
+
1245
+ // Remove backups directory
1246
+ const backupsDir = path.join(claudeDir, '.orbital-backups');
1247
+ if (fs.existsSync(backupsDir)) fs.rmSync(backupsDir, { recursive: true, force: true });
1248
+
1249
+ console.log(` Removed orbital config and manifest`);
1250
+ } else {
1251
+ // Still remove the manifest — it's invalid after uninstall
1252
+ const manifestPath = path.join(claudeDir, 'orbital-manifest.json');
1253
+ if (fs.existsSync(manifestPath)) fs.unlinkSync(manifestPath);
1254
+ console.log(` Kept orbital.config.json (--keep-config)`);
883
1255
  }
884
- const agentsDest = path.join(claudeDir, 'agents');
885
- if (fs.existsSync(agentsDest)) cleanEmptyDirs(agentsDest);
886
- console.log(` Removed ${agentsRemoved} agent files`);
887
1256
 
888
- const total = removedCount + hooksRemoved + skillsRemoved + agentsRemoved;
1257
+ // Clean up remaining empty directories
1258
+ for (const dir of ['config', 'quick', 'anti-patterns', 'review-verdicts']) {
1259
+ const dirPath = path.join(claudeDir, dir);
1260
+ if (fs.existsSync(dirPath)) cleanEmptyDirs(dirPath);
1261
+ }
1262
+
1263
+ const total = removedHooks + filesRemoved;
889
1264
  console.log(`\nUninstall complete. ${total} items removed.`);
1265
+ if (toPreserve.length > 0) {
1266
+ console.log(`Note: ${toPreserve.length} user/modified files were preserved.`);
1267
+ }
1268
+ console.log(`Note: scopes/ and .claude/orbital-events/ were preserved.\n`);
1269
+ }
1270
+
1271
+ /** Legacy uninstall for projects without a manifest (backward compat). */
1272
+ function runLegacyUninstall(projectRoot: string): void {
1273
+ const claudeDir = path.join(projectRoot, '.claude');
1274
+
1275
+ // Remove orbital hooks from settings.local.json
1276
+ const settingsPath = path.join(claudeDir, 'settings.local.json');
1277
+ const removedHooks = removeAllOrbitalHooks(settingsPath);
1278
+ console.log(` Removed ${removedHooks} orbital hook registrations`);
1279
+
1280
+ // Delete hooks/skills/agents that match template files
1281
+ for (const dir of ['hooks', 'skills', 'agents']) {
1282
+ const templateFiles = listTemplateFiles(path.join(TEMPLATES_DIR, dir), path.join(claudeDir, dir));
1283
+ let removed = 0;
1284
+ for (const f of templateFiles) {
1285
+ if (fs.existsSync(f)) { fs.unlinkSync(f); removed++; }
1286
+ }
1287
+ const dirPath = path.join(claudeDir, dir);
1288
+ if (fs.existsSync(dirPath)) cleanEmptyDirs(dirPath);
1289
+ console.log(` Removed ${removed} ${dir} files`);
1290
+ }
1291
+
1292
+ console.log(`\nLegacy uninstall complete.`);
890
1293
  console.log(`Note: scopes/ and .claude/orbital-events/ were preserved.\n`);
891
1294
  }
1295
+
1296
+ /** Remove Orbital-added entries from .gitignore. */
1297
+ function removeGitignoreEntries(projectRoot: string, entries: string[]): void {
1298
+ const gitignorePath = path.join(projectRoot, '.gitignore');
1299
+ if (!fs.existsSync(gitignorePath)) return;
1300
+
1301
+ let content = fs.readFileSync(gitignorePath, 'utf-8');
1302
+ const marker = '# Orbital Command';
1303
+
1304
+ // Try to remove the entire block
1305
+ const markerIdx = content.indexOf(marker);
1306
+ if (markerIdx !== -1) {
1307
+ // Find the block boundaries: from the marker to the next non-empty/non-orbital line
1308
+ const before = content.slice(0, markerIdx).replace(/\n+$/, '');
1309
+ const after = content.slice(markerIdx);
1310
+ const lines = after.split('\n');
1311
+ let endIdx = 0;
1312
+ for (let i = 0; i < lines.length; i++) {
1313
+ const line = lines[i].trim();
1314
+ if (i === 0) { endIdx = i + 1; continue; } // skip the marker line
1315
+ if (line === '' || entries.includes(line)) { endIdx = i + 1; continue; }
1316
+ break;
1317
+ }
1318
+ const remaining = lines.slice(endIdx).join('\n');
1319
+ content = before + (remaining ? '\n' + remaining : '') + '\n';
1320
+ fs.writeFileSync(gitignorePath, content, 'utf-8');
1321
+ }
1322
+ }
1323
+
1324
+ /** Copy a single template file to a destination, resolving the template path. */
1325
+ function copyTemplateFile(templatesDir: string, claudeRelPath: string, destPath: string): void {
1326
+ const templateRelPath = reverseRemapPath(claudeRelPath);
1327
+ const srcPath = path.join(templatesDir, templateRelPath);
1328
+ if (!fs.existsSync(srcPath)) {
1329
+ throw new Error(`Template file not found: ${templateRelPath}`);
1330
+ }
1331
+ const destDir = path.dirname(destPath);
1332
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
1333
+ safeCopyTemplate(srcPath, destPath);
1334
+ }