orbital-command 0.1.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 (325) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +396 -0
  3. package/bin/orbital.js +362 -0
  4. package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +84 -0
  5. package/dist/assets/WorkflowVisualizer-BZV40eAE.css +1 -0
  6. package/dist/assets/charts-D__PA1zp.js +72 -0
  7. package/dist/assets/index-D1G6i0nS.css +1 -0
  8. package/dist/assets/index-DpItvKpf.js +419 -0
  9. package/dist/assets/ui-BvF022GT.js +53 -0
  10. package/dist/assets/vendor-Dzv9lrRc.js +59 -0
  11. package/dist/index.html +19 -0
  12. package/dist/scanner-sweep.png +0 -0
  13. package/dist/server/server/adapters/index.js +34 -0
  14. package/dist/server/server/adapters/iterm2-adapter.js +29 -0
  15. package/dist/server/server/adapters/subprocess-adapter.js +21 -0
  16. package/dist/server/server/adapters/terminal-adapter.js +1 -0
  17. package/dist/server/server/config.js +156 -0
  18. package/dist/server/server/database.js +90 -0
  19. package/dist/server/server/index.js +372 -0
  20. package/dist/server/server/init.js +811 -0
  21. package/dist/server/server/parsers/event-parser.js +64 -0
  22. package/dist/server/server/parsers/scope-parser.js +188 -0
  23. package/dist/server/server/routes/config-routes.js +163 -0
  24. package/dist/server/server/routes/data-routes.js +461 -0
  25. package/dist/server/server/routes/dispatch-routes.js +215 -0
  26. package/dist/server/server/routes/git-routes.js +92 -0
  27. package/dist/server/server/routes/scope-routes.js +215 -0
  28. package/dist/server/server/routes/sprint-routes.js +116 -0
  29. package/dist/server/server/routes/version-routes.js +130 -0
  30. package/dist/server/server/routes/workflow-routes.js +185 -0
  31. package/dist/server/server/schema.js +90 -0
  32. package/dist/server/server/services/batch-orchestrator.js +253 -0
  33. package/dist/server/server/services/claude-session-service.js +352 -0
  34. package/dist/server/server/services/config-service.js +132 -0
  35. package/dist/server/server/services/deploy-service.js +51 -0
  36. package/dist/server/server/services/event-service.js +63 -0
  37. package/dist/server/server/services/gate-service.js +83 -0
  38. package/dist/server/server/services/git-service.js +309 -0
  39. package/dist/server/server/services/github-service.js +145 -0
  40. package/dist/server/server/services/readiness-service.js +184 -0
  41. package/dist/server/server/services/scope-cache.js +72 -0
  42. package/dist/server/server/services/scope-service.js +424 -0
  43. package/dist/server/server/services/sprint-orchestrator.js +312 -0
  44. package/dist/server/server/services/sprint-service.js +293 -0
  45. package/dist/server/server/services/workflow-service.js +397 -0
  46. package/dist/server/server/utils/cc-hooks-parser.js +49 -0
  47. package/dist/server/server/utils/dispatch-utils.js +305 -0
  48. package/dist/server/server/utils/logger.js +86 -0
  49. package/dist/server/server/utils/terminal-launcher.js +388 -0
  50. package/dist/server/server/utils/worktree-manager.js +98 -0
  51. package/dist/server/server/watchers/event-watcher.js +81 -0
  52. package/dist/server/server/watchers/scope-watcher.js +33 -0
  53. package/dist/server/shared/api-types.js +5 -0
  54. package/dist/server/shared/default-workflow.json +616 -0
  55. package/dist/server/shared/workflow-config.js +44 -0
  56. package/dist/server/shared/workflow-engine.js +353 -0
  57. package/index.html +15 -0
  58. package/package.json +110 -0
  59. package/postcss.config.js +6 -0
  60. package/schemas/orbital.config.schema.json +83 -0
  61. package/scripts/postinstall.js +24 -0
  62. package/scripts/start.sh +20 -0
  63. package/server/adapters/index.ts +41 -0
  64. package/server/adapters/iterm2-adapter.ts +37 -0
  65. package/server/adapters/subprocess-adapter.ts +25 -0
  66. package/server/adapters/terminal-adapter.ts +24 -0
  67. package/server/config.ts +234 -0
  68. package/server/database.ts +107 -0
  69. package/server/index.ts +452 -0
  70. package/server/init.ts +891 -0
  71. package/server/parsers/event-parser.ts +74 -0
  72. package/server/parsers/scope-parser.ts +240 -0
  73. package/server/routes/config-routes.ts +182 -0
  74. package/server/routes/data-routes.ts +548 -0
  75. package/server/routes/dispatch-routes.ts +275 -0
  76. package/server/routes/git-routes.ts +112 -0
  77. package/server/routes/scope-routes.ts +262 -0
  78. package/server/routes/sprint-routes.ts +142 -0
  79. package/server/routes/version-routes.ts +156 -0
  80. package/server/routes/workflow-routes.ts +198 -0
  81. package/server/schema.ts +90 -0
  82. package/server/services/batch-orchestrator.ts +286 -0
  83. package/server/services/claude-session-service.ts +441 -0
  84. package/server/services/config-service.ts +151 -0
  85. package/server/services/deploy-service.ts +98 -0
  86. package/server/services/event-service.ts +98 -0
  87. package/server/services/gate-service.ts +126 -0
  88. package/server/services/git-service.ts +391 -0
  89. package/server/services/github-service.ts +183 -0
  90. package/server/services/readiness-service.ts +250 -0
  91. package/server/services/scope-cache.ts +81 -0
  92. package/server/services/scope-service.ts +476 -0
  93. package/server/services/sprint-orchestrator.ts +361 -0
  94. package/server/services/sprint-service.ts +415 -0
  95. package/server/services/workflow-service.ts +461 -0
  96. package/server/utils/cc-hooks-parser.ts +70 -0
  97. package/server/utils/dispatch-utils.ts +395 -0
  98. package/server/utils/logger.ts +109 -0
  99. package/server/utils/terminal-launcher.ts +462 -0
  100. package/server/utils/worktree-manager.ts +104 -0
  101. package/server/watchers/event-watcher.ts +100 -0
  102. package/server/watchers/scope-watcher.ts +38 -0
  103. package/shared/api-types.ts +20 -0
  104. package/shared/default-workflow.json +616 -0
  105. package/shared/workflow-config.ts +170 -0
  106. package/shared/workflow-engine.ts +427 -0
  107. package/src/App.tsx +33 -0
  108. package/src/components/AgentBadge.tsx +40 -0
  109. package/src/components/BatchPreflightModal.tsx +115 -0
  110. package/src/components/CardDisplayToggle.tsx +74 -0
  111. package/src/components/ColumnHeaderActions.tsx +55 -0
  112. package/src/components/ColumnMenu.tsx +99 -0
  113. package/src/components/DeployHistory.tsx +141 -0
  114. package/src/components/DispatchModal.tsx +164 -0
  115. package/src/components/DispatchPopover.tsx +139 -0
  116. package/src/components/DragOverlay.tsx +25 -0
  117. package/src/components/DriftSidebar.tsx +140 -0
  118. package/src/components/EnvironmentStrip.tsx +88 -0
  119. package/src/components/ErrorBoundary.tsx +62 -0
  120. package/src/components/FilterChip.tsx +105 -0
  121. package/src/components/GateIndicator.tsx +33 -0
  122. package/src/components/IdeaDetailModal.tsx +190 -0
  123. package/src/components/IdeaFormDialog.tsx +113 -0
  124. package/src/components/KanbanColumn.tsx +201 -0
  125. package/src/components/MarkdownRenderer.tsx +114 -0
  126. package/src/components/NeonGrid.tsx +128 -0
  127. package/src/components/PromotionQueue.tsx +89 -0
  128. package/src/components/ScopeCard.tsx +234 -0
  129. package/src/components/ScopeDetailModal.tsx +255 -0
  130. package/src/components/ScopeFilterBar.tsx +152 -0
  131. package/src/components/SearchInput.tsx +102 -0
  132. package/src/components/SessionPanel.tsx +335 -0
  133. package/src/components/SprintContainer.tsx +303 -0
  134. package/src/components/SprintDependencyDialog.tsx +78 -0
  135. package/src/components/SprintPreflightModal.tsx +138 -0
  136. package/src/components/StatusBar.tsx +168 -0
  137. package/src/components/SwimCell.tsx +67 -0
  138. package/src/components/SwimLaneRow.tsx +94 -0
  139. package/src/components/SwimlaneBoardView.tsx +108 -0
  140. package/src/components/VersionBadge.tsx +139 -0
  141. package/src/components/ViewModeSelector.tsx +114 -0
  142. package/src/components/config/AgentChip.tsx +53 -0
  143. package/src/components/config/AgentCreateDialog.tsx +321 -0
  144. package/src/components/config/AgentEditor.tsx +175 -0
  145. package/src/components/config/DirectoryTree.tsx +582 -0
  146. package/src/components/config/FileEditor.tsx +550 -0
  147. package/src/components/config/HookChip.tsx +50 -0
  148. package/src/components/config/StageCard.tsx +198 -0
  149. package/src/components/config/TransitionZone.tsx +173 -0
  150. package/src/components/config/UnifiedWorkflowPipeline.tsx +216 -0
  151. package/src/components/config/WorkflowPipeline.tsx +161 -0
  152. package/src/components/source-control/BranchList.tsx +93 -0
  153. package/src/components/source-control/BranchPanel.tsx +105 -0
  154. package/src/components/source-control/CommitLog.tsx +100 -0
  155. package/src/components/source-control/CommitRow.tsx +47 -0
  156. package/src/components/source-control/GitHubPanel.tsx +110 -0
  157. package/src/components/source-control/GitHubSetupGuide.tsx +52 -0
  158. package/src/components/source-control/GitOverviewBar.tsx +101 -0
  159. package/src/components/source-control/PullRequestList.tsx +69 -0
  160. package/src/components/source-control/WorktreeList.tsx +80 -0
  161. package/src/components/ui/badge.tsx +41 -0
  162. package/src/components/ui/button.tsx +55 -0
  163. package/src/components/ui/card.tsx +78 -0
  164. package/src/components/ui/dialog.tsx +94 -0
  165. package/src/components/ui/popover.tsx +33 -0
  166. package/src/components/ui/scroll-area.tsx +54 -0
  167. package/src/components/ui/separator.tsx +28 -0
  168. package/src/components/ui/tabs.tsx +52 -0
  169. package/src/components/ui/toggle-switch.tsx +35 -0
  170. package/src/components/ui/tooltip.tsx +27 -0
  171. package/src/components/workflow/AddEdgeDialog.tsx +217 -0
  172. package/src/components/workflow/AddListDialog.tsx +201 -0
  173. package/src/components/workflow/ChecklistEditor.tsx +239 -0
  174. package/src/components/workflow/CommandPrefixManager.tsx +118 -0
  175. package/src/components/workflow/ConfigSettingsPanel.tsx +189 -0
  176. package/src/components/workflow/DirectionSelector.tsx +133 -0
  177. package/src/components/workflow/DispatchConfigPanel.tsx +180 -0
  178. package/src/components/workflow/EdgeDetailPanel.tsx +236 -0
  179. package/src/components/workflow/EdgePropertyEditor.tsx +251 -0
  180. package/src/components/workflow/EditToolbar.tsx +138 -0
  181. package/src/components/workflow/HookDetailPanel.tsx +250 -0
  182. package/src/components/workflow/HookExecutionLog.tsx +24 -0
  183. package/src/components/workflow/HookSourceModal.tsx +129 -0
  184. package/src/components/workflow/HooksDashboard.tsx +363 -0
  185. package/src/components/workflow/ListPropertyEditor.tsx +251 -0
  186. package/src/components/workflow/MigrationPreviewDialog.tsx +237 -0
  187. package/src/components/workflow/MovementRulesPanel.tsx +188 -0
  188. package/src/components/workflow/NodeDetailPanel.tsx +245 -0
  189. package/src/components/workflow/PresetSelector.tsx +414 -0
  190. package/src/components/workflow/SkillCommandBuilder.tsx +174 -0
  191. package/src/components/workflow/WorkflowEdgeComponent.tsx +145 -0
  192. package/src/components/workflow/WorkflowNode.tsx +147 -0
  193. package/src/components/workflow/graphLayout.ts +186 -0
  194. package/src/components/workflow/mergeHooks.ts +85 -0
  195. package/src/components/workflow/useEditHistory.ts +88 -0
  196. package/src/components/workflow/useWorkflowEditor.ts +262 -0
  197. package/src/components/workflow/validateConfig.ts +70 -0
  198. package/src/hooks/useActiveDispatches.ts +198 -0
  199. package/src/hooks/useBoardSettings.ts +170 -0
  200. package/src/hooks/useCardDisplay.ts +57 -0
  201. package/src/hooks/useCcHooks.ts +24 -0
  202. package/src/hooks/useConfigTree.ts +51 -0
  203. package/src/hooks/useEnforcementRules.ts +46 -0
  204. package/src/hooks/useEvents.ts +59 -0
  205. package/src/hooks/useFileEditor.ts +165 -0
  206. package/src/hooks/useGates.ts +57 -0
  207. package/src/hooks/useIdeaActions.ts +53 -0
  208. package/src/hooks/useKanbanDnd.ts +410 -0
  209. package/src/hooks/useOrbitalConfig.ts +54 -0
  210. package/src/hooks/usePipeline.ts +47 -0
  211. package/src/hooks/usePipelineData.ts +338 -0
  212. package/src/hooks/useReconnect.ts +25 -0
  213. package/src/hooks/useScopeFilters.ts +125 -0
  214. package/src/hooks/useScopeSessions.ts +44 -0
  215. package/src/hooks/useScopes.ts +67 -0
  216. package/src/hooks/useSearch.ts +67 -0
  217. package/src/hooks/useSettings.tsx +187 -0
  218. package/src/hooks/useSocket.ts +25 -0
  219. package/src/hooks/useSourceControl.ts +105 -0
  220. package/src/hooks/useSprintPreflight.ts +55 -0
  221. package/src/hooks/useSprints.ts +154 -0
  222. package/src/hooks/useStatusBarHighlight.ts +18 -0
  223. package/src/hooks/useSwimlaneBoardSettings.ts +104 -0
  224. package/src/hooks/useTheme.ts +9 -0
  225. package/src/hooks/useTransitionReadiness.ts +53 -0
  226. package/src/hooks/useVersion.ts +155 -0
  227. package/src/hooks/useViolations.ts +65 -0
  228. package/src/hooks/useWorkflow.tsx +125 -0
  229. package/src/hooks/useZoomModifier.ts +19 -0
  230. package/src/index.css +797 -0
  231. package/src/layouts/DashboardLayout.tsx +113 -0
  232. package/src/lib/collisionDetection.ts +20 -0
  233. package/src/lib/scope-fields.ts +61 -0
  234. package/src/lib/swimlane.ts +146 -0
  235. package/src/lib/utils.ts +15 -0
  236. package/src/main.tsx +19 -0
  237. package/src/socket.ts +11 -0
  238. package/src/types/index.ts +497 -0
  239. package/src/views/AgentFeed.tsx +339 -0
  240. package/src/views/DeployPipeline.tsx +59 -0
  241. package/src/views/EnforcementView.tsx +378 -0
  242. package/src/views/PrimitivesConfig.tsx +500 -0
  243. package/src/views/QualityGates.tsx +1012 -0
  244. package/src/views/ScopeBoard.tsx +454 -0
  245. package/src/views/SessionTimeline.tsx +516 -0
  246. package/src/views/Settings.tsx +183 -0
  247. package/src/views/SourceControl.tsx +95 -0
  248. package/src/views/WorkflowVisualizer.tsx +382 -0
  249. package/tailwind.config.js +161 -0
  250. package/templates/agents/AUTO-INVOKE.md +180 -0
  251. package/templates/agents/CONFLICT-RESOLUTION.md +128 -0
  252. package/templates/agents/QUICK-REFERENCE.md +122 -0
  253. package/templates/agents/README.md +188 -0
  254. package/templates/agents/SKILL-TRIGGERS.md +100 -0
  255. package/templates/agents/blue-team/frontend-designer.md +424 -0
  256. package/templates/agents/green-team/architect.md +526 -0
  257. package/templates/agents/green-team/rules-enforcer.md +131 -0
  258. package/templates/agents/red-team/attacker-learned.md +24 -0
  259. package/templates/agents/red-team/attacker.md +486 -0
  260. package/templates/agents/red-team/chaos.md +548 -0
  261. package/templates/agents/reference/component-registry.md +82 -0
  262. package/templates/agents/workflows/full-mode.md +218 -0
  263. package/templates/agents/workflows/quick-mode.md +118 -0
  264. package/templates/agents/workflows/security-mode.md +283 -0
  265. package/templates/anti-patterns/dangerous-shortcuts.md +427 -0
  266. package/templates/config/agent-triggers.json +92 -0
  267. package/templates/hooks/agent-team-gate.sh +31 -0
  268. package/templates/hooks/agent-trigger.sh +97 -0
  269. package/templates/hooks/block-push.sh +66 -0
  270. package/templates/hooks/block-workarounds.sh +61 -0
  271. package/templates/hooks/blocker-check.sh +28 -0
  272. package/templates/hooks/completion-checklist.sh +28 -0
  273. package/templates/hooks/decision-capture.sh +15 -0
  274. package/templates/hooks/dependency-check.sh +27 -0
  275. package/templates/hooks/end-session.sh +31 -0
  276. package/templates/hooks/exploration-logger.sh +37 -0
  277. package/templates/hooks/files-changed-summary.sh +37 -0
  278. package/templates/hooks/get-session-id.sh +49 -0
  279. package/templates/hooks/git-commit-guard.sh +34 -0
  280. package/templates/hooks/init-session.sh +93 -0
  281. package/templates/hooks/orbital-emit.sh +79 -0
  282. package/templates/hooks/orbital-report-deploy.sh +78 -0
  283. package/templates/hooks/orbital-report-gates.sh +40 -0
  284. package/templates/hooks/orbital-report-violation.sh +36 -0
  285. package/templates/hooks/orbital-scope-update.sh +53 -0
  286. package/templates/hooks/phase-verify-reminder.sh +26 -0
  287. package/templates/hooks/review-gate-check.sh +82 -0
  288. package/templates/hooks/scope-commit-logger.sh +37 -0
  289. package/templates/hooks/scope-create-cleanup.sh +36 -0
  290. package/templates/hooks/scope-create-gate.sh +80 -0
  291. package/templates/hooks/scope-create-tracker.sh +17 -0
  292. package/templates/hooks/scope-file-sync.sh +53 -0
  293. package/templates/hooks/scope-gate.sh +35 -0
  294. package/templates/hooks/scope-helpers.sh +188 -0
  295. package/templates/hooks/scope-lifecycle-gate.sh +139 -0
  296. package/templates/hooks/scope-prepare.sh +244 -0
  297. package/templates/hooks/scope-transition.sh +172 -0
  298. package/templates/hooks/session-enforcer.sh +143 -0
  299. package/templates/hooks/time-tracker.sh +33 -0
  300. package/templates/lessons-learned.md +15 -0
  301. package/templates/orbital.config.json +35 -0
  302. package/templates/presets/development.json +42 -0
  303. package/templates/presets/gitflow.json +712 -0
  304. package/templates/presets/minimal.json +23 -0
  305. package/templates/quick/rules.md +218 -0
  306. package/templates/scopes/_template.md +255 -0
  307. package/templates/settings-hooks.json +98 -0
  308. package/templates/skills/git-commit/SKILL.md +85 -0
  309. package/templates/skills/git-dev/SKILL.md +99 -0
  310. package/templates/skills/git-hotfix/SKILL.md +223 -0
  311. package/templates/skills/git-main/SKILL.md +84 -0
  312. package/templates/skills/git-production/SKILL.md +165 -0
  313. package/templates/skills/git-staging/SKILL.md +112 -0
  314. package/templates/skills/scope-create/SKILL.md +81 -0
  315. package/templates/skills/scope-fix-review/SKILL.md +168 -0
  316. package/templates/skills/scope-implement/SKILL.md +110 -0
  317. package/templates/skills/scope-post-review/SKILL.md +144 -0
  318. package/templates/skills/scope-pre-review/SKILL.md +211 -0
  319. package/templates/skills/scope-verify/SKILL.md +201 -0
  320. package/templates/skills/session-init/SKILL.md +62 -0
  321. package/templates/skills/session-resume/SKILL.md +201 -0
  322. package/templates/skills/test-checks/SKILL.md +171 -0
  323. package/templates/skills/test-code-review/SKILL.md +252 -0
  324. package/tsconfig.json +25 -0
  325. package/vite.config.ts +38 -0
@@ -0,0 +1,811 @@
1
+ /**
2
+ * Shared init logic — used by both the CLI (`orbital init`) and
3
+ * programmatic callers (e.g. tests).
4
+ */
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ // Walk up from __dirname until we find the package root (identified by templates/).
11
+ // Handles both dev (server/ → 1 hop) and compiled (dist/server/server/ → 3 hops).
12
+ function resolvePackageRoot(startDir) {
13
+ let dir = startDir;
14
+ for (let i = 0; i < 5; i++) {
15
+ if (fs.existsSync(path.join(dir, 'templates')))
16
+ return dir;
17
+ dir = path.resolve(dir, '..');
18
+ }
19
+ return path.resolve(startDir, '..');
20
+ }
21
+ const PACKAGE_ROOT = resolvePackageRoot(__dirname);
22
+ const TEMPLATES_DIR = path.join(PACKAGE_ROOT, 'templates');
23
+ // ─── Helpers ─────────────────────────────────────────────────
24
+ function ensureDir(dirPath) {
25
+ if (!fs.existsSync(dirPath)) {
26
+ fs.mkdirSync(dirPath, { recursive: true });
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+ function copyDirSync(src, dest, opts = {}) {
32
+ const created = [];
33
+ const skipped = [];
34
+ if (!fs.existsSync(src))
35
+ return { created, skipped };
36
+ ensureDir(dest);
37
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
38
+ const srcPath = path.join(src, entry.name);
39
+ const destPath = path.join(dest, entry.name);
40
+ if (entry.isDirectory()) {
41
+ const sub = copyDirSync(srcPath, destPath, opts);
42
+ created.push(...sub.created);
43
+ skipped.push(...sub.skipped);
44
+ }
45
+ else {
46
+ if (!opts.overwrite && fs.existsSync(destPath)) {
47
+ skipped.push(destPath);
48
+ }
49
+ else {
50
+ ensureDir(path.dirname(destPath));
51
+ fs.copyFileSync(srcPath, destPath);
52
+ created.push(destPath);
53
+ }
54
+ }
55
+ }
56
+ return { created, skipped };
57
+ }
58
+ function chmodScripts(dir) {
59
+ if (!fs.existsSync(dir))
60
+ return;
61
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
62
+ const fullPath = path.join(dir, entry.name);
63
+ if (entry.isDirectory()) {
64
+ chmodScripts(fullPath);
65
+ }
66
+ else if (entry.name.endsWith('.sh')) {
67
+ fs.chmodSync(fullPath, 0o755);
68
+ }
69
+ }
70
+ }
71
+ function pruneStaleEntries(sourceDir, targetDir) {
72
+ if (!fs.existsSync(targetDir) || !fs.existsSync(sourceDir))
73
+ return 0;
74
+ const sourceEntries = new Set(fs.readdirSync(sourceDir));
75
+ let removed = 0;
76
+ for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) {
77
+ if (!sourceEntries.has(entry.name)) {
78
+ const fullPath = path.join(targetDir, entry.name);
79
+ fs.rmSync(fullPath, { recursive: true, force: true });
80
+ removed++;
81
+ }
82
+ }
83
+ return removed;
84
+ }
85
+ function mergeSettingsHooks(targetPath, sourcePath) {
86
+ let target = {};
87
+ if (fs.existsSync(targetPath)) {
88
+ try {
89
+ target = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
90
+ }
91
+ catch {
92
+ console.warn(' Warning: existing settings.local.json is malformed — creating new one');
93
+ target = {};
94
+ }
95
+ }
96
+ if (!fs.existsSync(sourcePath)) {
97
+ console.warn(' Warning: settings-hooks template not found, skipping hook registration');
98
+ return;
99
+ }
100
+ const source = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
101
+ const sourceHooks = source.hooks || {};
102
+ if (!target.hooks) {
103
+ target.hooks = {};
104
+ }
105
+ const targetHooks = target.hooks;
106
+ for (const [event, sourceGroups] of Object.entries(sourceHooks)) {
107
+ if (!targetHooks[event]) {
108
+ targetHooks[event] = tagOrbitalGroups(sourceGroups);
109
+ continue;
110
+ }
111
+ for (const sourceGroup of sourceGroups) {
112
+ const sourceMatcher = sourceGroup.matcher || '__none__';
113
+ const targetGroup = targetHooks[event].find((g) => (g.matcher || '__none__') === sourceMatcher);
114
+ if (!targetGroup) {
115
+ targetHooks[event].push(tagOrbitalGroup(sourceGroup));
116
+ continue;
117
+ }
118
+ for (const hook of sourceGroup.hooks || []) {
119
+ const taggedHook = { ...hook, _orbital: true };
120
+ const alreadyPresent = (targetGroup.hooks || []).some((h) => h.command === hook.command);
121
+ if (!alreadyPresent) {
122
+ if (!targetGroup.hooks)
123
+ targetGroup.hooks = [];
124
+ targetGroup.hooks.push(taggedHook);
125
+ }
126
+ }
127
+ }
128
+ }
129
+ fs.writeFileSync(targetPath, JSON.stringify(target, null, 2) + '\n', 'utf8');
130
+ }
131
+ function tagOrbitalGroups(groups) {
132
+ return groups.map(tagOrbitalGroup);
133
+ }
134
+ function tagOrbitalGroup(group) {
135
+ return {
136
+ ...group,
137
+ hooks: (group.hooks || []).map((h) => ({ ...h, _orbital: true })),
138
+ };
139
+ }
140
+ function updateGitignore(projectRoot) {
141
+ const gitignorePath = path.join(projectRoot, '.gitignore');
142
+ const marker = '# Orbital Command';
143
+ const lines = [
144
+ '',
145
+ marker,
146
+ 'scopes/',
147
+ '.claude/orbital/',
148
+ '.claude/orbital-events/',
149
+ '.claude/config/workflow-manifest.sh',
150
+ '',
151
+ ];
152
+ let existing = '';
153
+ if (fs.existsSync(gitignorePath)) {
154
+ existing = fs.readFileSync(gitignorePath, 'utf8');
155
+ }
156
+ if (existing.includes(marker)) {
157
+ return false;
158
+ }
159
+ fs.appendFileSync(gitignorePath, lines.join('\n'), 'utf8');
160
+ return true;
161
+ }
162
+ function generateManifest(config) {
163
+ const lines = [];
164
+ const lists = (config.lists || []).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
165
+ lines.push('#!/bin/bash');
166
+ lines.push('# Auto-generated by WorkflowEngine — DO NOT EDIT');
167
+ lines.push(`# Generated: ${new Date().toISOString()}`);
168
+ lines.push(`# Workflow: "${config.name}" (version ${config.version})`);
169
+ lines.push('');
170
+ lines.push('# ─── Branching mode (trunk or worktree) ───');
171
+ lines.push(`WORKFLOW_BRANCHING_MODE="${config.branchingMode ?? 'trunk'}"`);
172
+ lines.push('');
173
+ lines.push('# ─── Valid statuses (space-separated) ───');
174
+ lines.push(`WORKFLOW_STATUSES="${lists.map((l) => l.id).join(' ')}"`);
175
+ lines.push('');
176
+ lines.push('# ─── Statuses that have a scopes/ subdirectory ───');
177
+ const dirStatuses = lists.filter((l) => l.hasDirectory).map((l) => l.id);
178
+ lines.push(`WORKFLOW_DIR_STATUSES="${dirStatuses.join(' ')}"`);
179
+ lines.push('');
180
+ lines.push('# ─── Terminal statuses ───');
181
+ const terminalStatuses = config.terminalStatuses || [];
182
+ lines.push(`WORKFLOW_TERMINAL_STATUSES="${terminalStatuses.join(' ')}"`);
183
+ lines.push('');
184
+ lines.push('# ─── Entry point status ───');
185
+ lines.push(`WORKFLOW_ENTRY_STATUS="${config.entryPoint || lists[0]?.id || 'todo'}"`);
186
+ lines.push('');
187
+ const listMap = new Map(lists.map((l) => [l.id, l]));
188
+ lines.push('# ─── Transition edges (from:to:sessionKey) ───');
189
+ lines.push('WORKFLOW_EDGES=(');
190
+ for (const edge of config.edges || []) {
191
+ const targetList = listMap.get(edge.to);
192
+ const sessionKey = targetList?.sessionKey ?? '';
193
+ lines.push(` "${edge.from}:${edge.to}:${sessionKey}"`);
194
+ }
195
+ lines.push(')');
196
+ lines.push('');
197
+ lines.push('# ─── Branch-to-transition mapping (gitBranch:from:to:sessionKey) ───');
198
+ lines.push('WORKFLOW_BRANCH_MAP=(');
199
+ for (const edge of config.edges || []) {
200
+ const targetList = listMap.get(edge.to);
201
+ if (targetList?.gitBranch) {
202
+ const sessionKey = targetList.sessionKey ?? '';
203
+ lines.push(` "${targetList.gitBranch}:${edge.from}:${edge.to}:${sessionKey}"`);
204
+ }
205
+ }
206
+ lines.push(')');
207
+ lines.push('');
208
+ lines.push('# ─── Helper functions ──────────────────────────────');
209
+ lines.push('');
210
+ lines.push('status_to_dir() {');
211
+ lines.push(' local scope_status="$1"');
212
+ lines.push(' for s in $WORKFLOW_DIR_STATUSES; do');
213
+ lines.push(' [ "$s" = "$scope_status" ] && echo "$scope_status" && return 0');
214
+ lines.push(' done');
215
+ lines.push(' echo "$WORKFLOW_ENTRY_STATUS"');
216
+ lines.push('}');
217
+ lines.push('');
218
+ lines.push('status_to_branch() {');
219
+ lines.push(' local status="$1"');
220
+ lines.push(' for entry in "${WORKFLOW_BRANCH_MAP[@]}"; do');
221
+ lines.push(" IFS=':' read -r branch from to skey <<< \"$entry\"");
222
+ lines.push(' [ "$to" = "$status" ] && echo "$branch" && return 0');
223
+ lines.push(' done');
224
+ lines.push(' echo ""');
225
+ lines.push('}');
226
+ lines.push('');
227
+ lines.push('is_valid_status() {');
228
+ lines.push(' local status="$1"');
229
+ lines.push(' for s in $WORKFLOW_STATUSES; do');
230
+ lines.push(' [ "$s" = "$status" ] && return 0');
231
+ lines.push(' done');
232
+ lines.push(' return 1');
233
+ lines.push('}');
234
+ return lines.join('\n') + '\n';
235
+ }
236
+ function writeManifest(claudeDir) {
237
+ const workflowPath = path.join(claudeDir, 'config', 'workflow.json');
238
+ if (!fs.existsSync(workflowPath))
239
+ return false;
240
+ try {
241
+ const config = JSON.parse(fs.readFileSync(workflowPath, 'utf8'));
242
+ const manifest = generateManifest(config);
243
+ const manifestPath = path.join(claudeDir, 'config', 'workflow-manifest.sh');
244
+ fs.writeFileSync(manifestPath, manifest, 'utf8');
245
+ fs.chmodSync(manifestPath, 0o755);
246
+ return true;
247
+ }
248
+ catch {
249
+ console.warn(' Warning: could not generate workflow manifest');
250
+ return false;
251
+ }
252
+ }
253
+ function generateIndexMd(projectRoot, claudeDir) {
254
+ let projectName = path.basename(projectRoot);
255
+ const configPath = path.join(claudeDir, 'orbital.config.json');
256
+ if (fs.existsSync(configPath)) {
257
+ try {
258
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
259
+ if (cfg.projectName)
260
+ projectName = cfg.projectName;
261
+ }
262
+ catch { /* use fallback */ }
263
+ }
264
+ const skillsDir = path.join(claudeDir, 'skills');
265
+ const skills = [];
266
+ if (fs.existsSync(skillsDir)) {
267
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
268
+ if (entry.isDirectory())
269
+ skills.push(entry.name);
270
+ }
271
+ }
272
+ const workflowPath = path.join(claudeDir, 'config', 'workflow.json');
273
+ let stages = [];
274
+ if (fs.existsSync(workflowPath)) {
275
+ try {
276
+ const wf = JSON.parse(fs.readFileSync(workflowPath, 'utf8'));
277
+ stages = (wf.lists || [])
278
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
279
+ .map((l) => l.id);
280
+ }
281
+ catch { /* skip */ }
282
+ }
283
+ const skillCategories = {
284
+ 'Git': skills.filter((s) => s.startsWith('git-')),
285
+ 'Scope': skills.filter((s) => s.startsWith('scope-')),
286
+ 'Session': skills.filter((s) => s.startsWith('session-')),
287
+ 'Quality': skills.filter((s) => s.startsWith('test-')),
288
+ };
289
+ let skillTable = '';
290
+ for (const [cat, list] of Object.entries(skillCategories)) {
291
+ if (list.length > 0) {
292
+ skillTable += `| ${cat} | ${list.map((s) => '`/' + s + '`').join(', ')} |\n`;
293
+ }
294
+ }
295
+ return `# ${projectName} — AI Agent Index
296
+
297
+ ---
298
+ tokens: ~1K
299
+ load-when: Always load first
300
+ last-verified: ${new Date().toISOString().split('T')[0]}
301
+ ---
302
+
303
+ ## 30-Second Orientation
304
+
305
+ **Project**: ${projectName}
306
+ **Managed by**: Orbital Command
307
+
308
+ ### Critical Commands
309
+
310
+ \`\`\`bash
311
+ # Run configured quality gates (from orbital.config.json)
312
+ # Typical: type-check, lint, build, test
313
+ \`\`\`
314
+
315
+ ---
316
+
317
+ ## Decision Tree: Where Should I Look?
318
+
319
+ \`\`\`
320
+ What are you trying to do?
321
+ |
322
+ +-- "I want to IMPLEMENT a scope"
323
+ | +-- Create new scope -> /scope-create
324
+ | +-- Implement scope -> /scope-implement
325
+ | +-- Review scope -> /scope-pre-review
326
+ |
327
+ +-- "I want to COMMIT/DEPLOY"
328
+ | +-- Commit work -> /git-commit
329
+ | +-- Push to main -> /git-main
330
+ ${stages.includes('dev') ? '| +-- Merge to dev -> /git-dev\n' : ''}${stages.includes('staging') ? '| +-- PR to staging -> /git-staging\n' : ''}${stages.includes('production') ? '| +-- PR to production -> /git-production\n' : ''}| +-- Emergency fix -> /git-hotfix
331
+ |
332
+ +-- "I want to RUN CHECKS"
333
+ | +-- Quality gates -> /test-checks
334
+ | +-- Code review -> /test-code-review
335
+ | +-- Post-impl review -> /scope-post-review
336
+ |
337
+ +-- "I need SESSION help"
338
+ | +-- Continue work -> /session-resume
339
+ |
340
+ +-- "What should I AVOID?"
341
+ | +-- anti-patterns/dangerous-shortcuts.md
342
+ |
343
+ +-- "QUICK REFERENCES"
344
+ +-- Rules -> quick/rules.md
345
+ +-- Lessons learned -> lessons-learned.md
346
+ \`\`\`
347
+
348
+ ---
349
+
350
+ ## Skills
351
+
352
+ | Category | Skills |
353
+ |----------|--------|
354
+ ${skillTable || '| (none installed) | |\n'}
355
+ ---
356
+
357
+ ## Scope System (Three-Part Documents)
358
+
359
+ Scopes live in directories matching their pipeline stage.
360
+
361
+ \`\`\`
362
+ scopes/
363
+ +-- _template.md # Copy for new scopes
364
+ ${stages.map((s) => `+-- ${s}/`).join('\n')}
365
+ \`\`\`
366
+
367
+ **Three-Part Structure**:
368
+ - **PART 1: DASHBOARD** — Quick status, progress table, recent activity
369
+ - **PART 2: SPECIFICATION** — Feature lock (locked after review, any agent can implement)
370
+ - **PART 3: PROCESS** — Working memory (exploration, decisions, uncertainties, impl log)
371
+ - **AGENT REVIEW** — Synthesized findings from agent team review
372
+
373
+ **Lifecycle**: ${stages.join(' → ')}
374
+
375
+ ---
376
+
377
+ ## File Organization
378
+
379
+ \`\`\`
380
+ .claude/
381
+ +-- INDEX.md <- You are here
382
+ +-- lessons-learned.md # Institutional memory
383
+ +-- skills/ # Invokable skills
384
+ +-- quick/ # Quick reference docs
385
+ | +-- rules.md # Project rules with verify commands
386
+ +-- agents/ # Agent specifications
387
+ +-- anti-patterns/ # What NOT to do
388
+ +-- hooks/ # Claude Code lifecycle hooks
389
+ +-- config/ # Workflow config and presets
390
+ | +-- workflow.json # Active workflow
391
+ | +-- workflow-manifest.sh # Shell variables (auto-generated)
392
+ | +-- workflows/ # Available presets
393
+ +-- orbital.config.json # Project configuration
394
+ \`\`\`
395
+
396
+ ---
397
+
398
+ ## When In Doubt
399
+
400
+ 1. **Check rules**: \`quick/rules.md\`
401
+ 2. **Follow existing patterns**: Look at similar code in codebase
402
+ 3. **Ask**: Use clarifying questions before making assumptions
403
+ 4. **Verify**: Run quality gates before committing
404
+ `;
405
+ }
406
+ // ─── Helpers used by CLI commands ────────────────────────────
407
+ function listTemplateFiles(templateSubdir, targetDir) {
408
+ const files = [];
409
+ if (!fs.existsSync(templateSubdir))
410
+ return files;
411
+ for (const entry of fs.readdirSync(templateSubdir, { withFileTypes: true })) {
412
+ const targetPath = path.join(targetDir, entry.name);
413
+ if (entry.isDirectory()) {
414
+ files.push(...listTemplateFiles(path.join(templateSubdir, entry.name), targetPath));
415
+ }
416
+ else {
417
+ files.push(targetPath);
418
+ }
419
+ }
420
+ return files;
421
+ }
422
+ function cleanEmptyDirs(dir) {
423
+ if (!fs.existsSync(dir))
424
+ return;
425
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
426
+ if (entry.isDirectory()) {
427
+ cleanEmptyDirs(path.join(dir, entry.name));
428
+ }
429
+ }
430
+ if (fs.readdirSync(dir).length === 0) {
431
+ fs.rmdirSync(dir);
432
+ }
433
+ }
434
+ // ─── Exports ─────────────────────────────────────────────────
435
+ export { TEMPLATES_DIR, ensureDir };
436
+ export function runInit(projectRoot, options = {}) {
437
+ const force = options.force ?? false;
438
+ const claudeDir = path.join(projectRoot, '.claude');
439
+ console.log(`\nOrbital Command — init`);
440
+ console.log(`Project root: ${projectRoot}\n`);
441
+ // 1. Create directories
442
+ const dirs = [
443
+ path.join(claudeDir, 'orbital-events'),
444
+ path.join(claudeDir, 'orbital'),
445
+ path.join(claudeDir, 'config'),
446
+ path.join(claudeDir, 'review-verdicts'),
447
+ ];
448
+ for (const dir of dirs) {
449
+ const wasCreated = ensureDir(dir);
450
+ console.log(` ${wasCreated ? 'Created' : 'Exists '} ${path.relative(projectRoot, dir)}/`);
451
+ }
452
+ // 1b. Create scopes/ subdirectories from the default workflow preset
453
+ const defaultPresetPath = path.join(TEMPLATES_DIR, 'presets', 'default.json');
454
+ let scopeDirs = ['icebox'];
455
+ try {
456
+ const preset = JSON.parse(fs.readFileSync(defaultPresetPath, 'utf8'));
457
+ if (preset.lists && Array.isArray(preset.lists)) {
458
+ scopeDirs = preset.lists.filter((l) => l.hasDirectory).map((l) => l.id);
459
+ }
460
+ }
461
+ catch {
462
+ console.warn(' Warning: could not load default preset, creating scopes/icebox/ only');
463
+ }
464
+ for (const dirId of scopeDirs) {
465
+ const scopeDir = path.join(projectRoot, 'scopes', dirId);
466
+ const wasCreated = ensureDir(scopeDir);
467
+ console.log(` ${wasCreated ? 'Created' : 'Exists '} scopes/${dirId}/`);
468
+ }
469
+ // 1c. Copy scope template
470
+ const scopeTemplateSrc = path.join(TEMPLATES_DIR, 'scopes', '_template.md');
471
+ const scopeTemplateDest = path.join(projectRoot, 'scopes', '_template.md');
472
+ if (fs.existsSync(scopeTemplateSrc)) {
473
+ if (force || !fs.existsSync(scopeTemplateDest)) {
474
+ fs.copyFileSync(scopeTemplateSrc, scopeTemplateDest);
475
+ console.log(` ${force ? 'Reset ' : 'Created'} scopes/_template.md`);
476
+ }
477
+ else {
478
+ console.log(` Exists scopes/_template.md`);
479
+ }
480
+ }
481
+ // 2. Copy orbital.config.json template
482
+ const configDest = path.join(claudeDir, 'orbital.config.json');
483
+ const configSrc = path.join(TEMPLATES_DIR, 'orbital.config.json');
484
+ const configIsNew = !fs.existsSync(configDest);
485
+ if (configIsNew) {
486
+ if (fs.existsSync(configSrc)) {
487
+ fs.copyFileSync(configSrc, configDest);
488
+ console.log(` Created .claude/orbital.config.json`);
489
+ }
490
+ else {
491
+ const defaultConfig = {
492
+ serverPort: 4444,
493
+ clientPort: 4445,
494
+ projectName: path.basename(projectRoot),
495
+ };
496
+ fs.writeFileSync(configDest, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf8');
497
+ console.log(` Created .claude/orbital.config.json (default)`);
498
+ }
499
+ // Auto-detect project commands from package.json
500
+ const pkgJsonPath = path.join(projectRoot, 'package.json');
501
+ if (fs.existsSync(pkgJsonPath)) {
502
+ try {
503
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
504
+ const scripts = pkg.scripts || {};
505
+ const config = JSON.parse(fs.readFileSync(configDest, 'utf8'));
506
+ if (!config.commands)
507
+ config.commands = {};
508
+ let detected = 0;
509
+ if (scripts.typecheck || scripts['type-check']) {
510
+ config.commands.typeCheck = `npm run ${scripts.typecheck ? 'typecheck' : 'type-check'}`;
511
+ detected++;
512
+ }
513
+ if (scripts.lint) {
514
+ config.commands.lint = 'npm run lint';
515
+ detected++;
516
+ }
517
+ if (scripts.build) {
518
+ config.commands.build = 'npm run build';
519
+ detected++;
520
+ }
521
+ if (scripts.test) {
522
+ config.commands.test = 'npm run test';
523
+ detected++;
524
+ }
525
+ if (detected > 0) {
526
+ fs.writeFileSync(configDest, JSON.stringify(config, null, 2) + '\n', 'utf8');
527
+ console.log(` Detected ${detected} project command(s) from package.json`);
528
+ }
529
+ }
530
+ catch { /* leave defaults */ }
531
+ }
532
+ }
533
+ else {
534
+ console.log(` Exists .claude/orbital.config.json`);
535
+ }
536
+ // 3. Copy hooks
537
+ console.log('');
538
+ const hooksSrc = path.join(TEMPLATES_DIR, 'hooks');
539
+ const hooksDest = path.join(claudeDir, 'hooks');
540
+ if (force) {
541
+ const pruned = pruneStaleEntries(hooksSrc, hooksDest);
542
+ if (pruned > 0)
543
+ console.log(` Pruned ${pruned} stale hook entries`);
544
+ }
545
+ const hooksResult = copyDirSync(hooksSrc, hooksDest, { overwrite: force });
546
+ console.log(` Hooks ${hooksResult.created.length} copied, ${hooksResult.skipped.length} skipped`);
547
+ // 4. Copy skills
548
+ const skillsSrc = path.join(TEMPLATES_DIR, 'skills');
549
+ const skillsDest = path.join(claudeDir, 'skills');
550
+ if (force) {
551
+ const pruned = pruneStaleEntries(skillsSrc, skillsDest);
552
+ if (pruned > 0)
553
+ console.log(` Pruned ${pruned} stale skill entries`);
554
+ }
555
+ const skillsResult = copyDirSync(skillsSrc, skillsDest, { overwrite: force });
556
+ console.log(` Skills ${skillsResult.created.length} copied, ${skillsResult.skipped.length} skipped`);
557
+ // 5. Copy agents
558
+ const agentsSrc = path.join(TEMPLATES_DIR, 'agents');
559
+ const agentsDest = path.join(claudeDir, 'agents');
560
+ if (force) {
561
+ const pruned = pruneStaleEntries(agentsSrc, agentsDest);
562
+ if (pruned > 0)
563
+ console.log(` Pruned ${pruned} stale agent entries`);
564
+ }
565
+ const agentsResult = copyDirSync(agentsSrc, agentsDest, { overwrite: force });
566
+ console.log(` Agents ${agentsResult.created.length} copied, ${agentsResult.skipped.length} skipped`);
567
+ // 6. Copy workflow presets
568
+ const presetsSrc = path.join(TEMPLATES_DIR, 'presets');
569
+ const presetsDest = path.join(claudeDir, 'config', 'workflows');
570
+ if (fs.existsSync(presetsSrc) && fs.readdirSync(presetsSrc).length > 0) {
571
+ if (force) {
572
+ const pruned = pruneStaleEntries(presetsSrc, presetsDest);
573
+ if (pruned > 0)
574
+ console.log(` Pruned ${pruned} stale preset entries`);
575
+ }
576
+ const presetsResult = copyDirSync(presetsSrc, presetsDest, { overwrite: force });
577
+ console.log(` Presets ${presetsResult.created.length} copied, ${presetsResult.skipped.length} skipped`);
578
+ }
579
+ // 6b. Reset active workflow config when --force, or create if missing
580
+ const activeWorkflowDest = path.join(claudeDir, 'config', 'workflow.json');
581
+ if (force) {
582
+ fs.copyFileSync(defaultPresetPath, activeWorkflowDest);
583
+ console.log(` Reset .claude/config/workflow.json (default workflow)`);
584
+ }
585
+ else if (!fs.existsSync(activeWorkflowDest)) {
586
+ fs.copyFileSync(defaultPresetPath, activeWorkflowDest);
587
+ console.log(` Created .claude/config/workflow.json`);
588
+ }
589
+ else {
590
+ console.log(` Exists .claude/config/workflow.json`);
591
+ }
592
+ // 7. Copy agent-triggers.json
593
+ const triggersSrc = path.join(TEMPLATES_DIR, 'config', 'agent-triggers.json');
594
+ const triggersDest = path.join(claudeDir, 'config', 'agent-triggers.json');
595
+ if (fs.existsSync(triggersSrc)) {
596
+ if (force || !fs.existsSync(triggersDest)) {
597
+ fs.copyFileSync(triggersSrc, triggersDest);
598
+ console.log(` Created .claude/config/agent-triggers.json`);
599
+ }
600
+ else {
601
+ console.log(` Exists .claude/config/agent-triggers.json`);
602
+ }
603
+ }
604
+ // 7b. Copy quick/ templates
605
+ const quickSrc = path.join(TEMPLATES_DIR, 'quick');
606
+ const quickDest = path.join(claudeDir, 'quick');
607
+ if (fs.existsSync(quickSrc)) {
608
+ const quickResult = copyDirSync(quickSrc, quickDest, { overwrite: force });
609
+ console.log(` Quick ${quickResult.created.length} copied, ${quickResult.skipped.length} skipped`);
610
+ }
611
+ // 7c. Copy anti-patterns/ templates
612
+ const antiSrc = path.join(TEMPLATES_DIR, 'anti-patterns');
613
+ const antiDest = path.join(claudeDir, 'anti-patterns');
614
+ if (fs.existsSync(antiSrc)) {
615
+ const antiResult = copyDirSync(antiSrc, antiDest, { overwrite: force });
616
+ console.log(` Anti-pat ${antiResult.created.length} copied, ${antiResult.skipped.length} skipped`);
617
+ }
618
+ // 7d. Copy lessons-learned.md
619
+ const lessonsSrc = path.join(TEMPLATES_DIR, 'lessons-learned.md');
620
+ const lessonsDest = path.join(claudeDir, 'lessons-learned.md');
621
+ if (fs.existsSync(lessonsSrc)) {
622
+ if (force || !fs.existsSync(lessonsDest)) {
623
+ fs.copyFileSync(lessonsSrc, lessonsDest);
624
+ console.log(` Created .claude/lessons-learned.md`);
625
+ }
626
+ else {
627
+ console.log(` Exists .claude/lessons-learned.md`);
628
+ }
629
+ }
630
+ // 7e. Generate workflow manifest
631
+ const manifestOk = writeManifest(claudeDir);
632
+ console.log(` ${manifestOk ? 'Created' : 'Skipped'} .claude/config/workflow-manifest.sh`);
633
+ // 7f. Generate INDEX.md
634
+ const indexDest = path.join(claudeDir, 'INDEX.md');
635
+ if (force || !fs.existsSync(indexDest)) {
636
+ const indexContent = generateIndexMd(projectRoot, claudeDir);
637
+ fs.writeFileSync(indexDest, indexContent, 'utf8');
638
+ console.log(` ${force ? 'Reset ' : 'Created'} .claude/INDEX.md`);
639
+ }
640
+ else {
641
+ console.log(` Exists .claude/INDEX.md`);
642
+ }
643
+ // 8. Merge hook registrations into settings.local.json
644
+ console.log('');
645
+ const settingsTarget = path.join(claudeDir, 'settings.local.json');
646
+ const settingsSrc = path.join(TEMPLATES_DIR, 'settings-hooks.json');
647
+ mergeSettingsHooks(settingsTarget, settingsSrc);
648
+ console.log(` Merged hook registrations into .claude/settings.local.json`);
649
+ // 9. Update .gitignore
650
+ const gitignoreUpdated = updateGitignore(projectRoot);
651
+ console.log(` ${gitignoreUpdated ? 'Updated' : 'Exists '} .gitignore (Orbital patterns)`);
652
+ // 10. Make hook scripts executable
653
+ chmodScripts(hooksDest);
654
+ console.log(` chmod hook scripts set to executable`);
655
+ // Summary
656
+ const totalCreated = hooksResult.created.length + skillsResult.created.length + agentsResult.created.length;
657
+ const totalSkipped = hooksResult.skipped.length + skillsResult.skipped.length + agentsResult.skipped.length;
658
+ console.log(`\nDone. ${totalCreated} files installed, ${totalSkipped} skipped (use --force to overwrite).`);
659
+ }
660
+ export function runUpdate(projectRoot) {
661
+ const claudeDir = path.join(projectRoot, '.claude');
662
+ console.log(`\nOrbital Command — update`);
663
+ console.log(`Project root: ${projectRoot}\n`);
664
+ // 1. Copy hooks (overwrite) — prune stale entries first
665
+ const hooksSrc = path.join(TEMPLATES_DIR, 'hooks');
666
+ const hooksDest = path.join(claudeDir, 'hooks');
667
+ const hooksPruned = pruneStaleEntries(hooksSrc, hooksDest);
668
+ if (hooksPruned > 0)
669
+ console.log(` Pruned ${hooksPruned} stale hook entries`);
670
+ const hooksResult = copyDirSync(hooksSrc, hooksDest, { overwrite: true });
671
+ console.log(` Hooks ${hooksResult.created.length} updated`);
672
+ // 2. Copy skills (overwrite) — prune stale entries first
673
+ const skillsSrc = path.join(TEMPLATES_DIR, 'skills');
674
+ const skillsDest = path.join(claudeDir, 'skills');
675
+ const skillsPruned = pruneStaleEntries(skillsSrc, skillsDest);
676
+ if (skillsPruned > 0)
677
+ console.log(` Pruned ${skillsPruned} stale skill entries`);
678
+ const skillsResult = copyDirSync(skillsSrc, skillsDest, { overwrite: true });
679
+ console.log(` Skills ${skillsResult.created.length} updated`);
680
+ // 3. Copy agents (overwrite) — prune stale entries first
681
+ const agentsSrc = path.join(TEMPLATES_DIR, 'agents');
682
+ const agentsDest = path.join(claudeDir, 'agents');
683
+ const agentsPruned = pruneStaleEntries(agentsSrc, agentsDest);
684
+ if (agentsPruned > 0)
685
+ console.log(` Pruned ${agentsPruned} stale agent entries`);
686
+ const agentsResult = copyDirSync(agentsSrc, agentsDest, { overwrite: true });
687
+ console.log(` Agents ${agentsResult.created.length} updated`);
688
+ // 4. Update workflow presets — prune stale entries first
689
+ const presetsSrc = path.join(TEMPLATES_DIR, 'presets');
690
+ const presetsDest = path.join(claudeDir, 'config', 'workflows');
691
+ if (fs.existsSync(presetsSrc) && fs.readdirSync(presetsSrc).length > 0) {
692
+ const presetsPruned = pruneStaleEntries(presetsSrc, presetsDest);
693
+ if (presetsPruned > 0)
694
+ console.log(` Pruned ${presetsPruned} stale preset entries`);
695
+ const presetsResult = copyDirSync(presetsSrc, presetsDest, { overwrite: true });
696
+ console.log(` Presets ${presetsResult.created.length} updated`);
697
+ }
698
+ // 5. Update quick/, anti-patterns/, lessons-learned, scope template
699
+ const quickSrc = path.join(TEMPLATES_DIR, 'quick');
700
+ const quickDest = path.join(claudeDir, 'quick');
701
+ if (fs.existsSync(quickSrc)) {
702
+ const quickResult = copyDirSync(quickSrc, quickDest, { overwrite: true });
703
+ console.log(` Quick ${quickResult.created.length} updated`);
704
+ }
705
+ const antiSrc = path.join(TEMPLATES_DIR, 'anti-patterns');
706
+ const antiDest = path.join(claudeDir, 'anti-patterns');
707
+ if (fs.existsSync(antiSrc)) {
708
+ const antiResult = copyDirSync(antiSrc, antiDest, { overwrite: true });
709
+ console.log(` Anti-pat ${antiResult.created.length} updated`);
710
+ }
711
+ const lessonsSrc = path.join(TEMPLATES_DIR, 'lessons-learned.md');
712
+ const lessonsDest = path.join(claudeDir, 'lessons-learned.md');
713
+ if (fs.existsSync(lessonsSrc) && !fs.existsSync(lessonsDest)) {
714
+ fs.copyFileSync(lessonsSrc, lessonsDest);
715
+ console.log(` Created .claude/lessons-learned.md`);
716
+ }
717
+ const scopeTemplateSrc = path.join(TEMPLATES_DIR, 'scopes', '_template.md');
718
+ const scopeTemplateDest = path.join(projectRoot, 'scopes', '_template.md');
719
+ if (fs.existsSync(scopeTemplateSrc)) {
720
+ ensureDir(path.join(projectRoot, 'scopes'));
721
+ fs.copyFileSync(scopeTemplateSrc, scopeTemplateDest);
722
+ console.log(` Updated scopes/_template.md`);
723
+ }
724
+ // 5b. Regenerate workflow manifest
725
+ const manifestOk = writeManifest(claudeDir);
726
+ console.log(` ${manifestOk ? 'Updated' : 'Skipped'} .claude/config/workflow-manifest.sh`);
727
+ // 6. Re-merge settings hooks
728
+ const settingsTarget = path.join(claudeDir, 'settings.local.json');
729
+ const settingsSrc = path.join(TEMPLATES_DIR, 'settings-hooks.json');
730
+ mergeSettingsHooks(settingsTarget, settingsSrc);
731
+ console.log(` Merged hook registrations into .claude/settings.local.json`);
732
+ // 7. Make hook scripts executable
733
+ chmodScripts(hooksDest);
734
+ console.log(` chmod hook scripts set to executable`);
735
+ console.log(`\nUpdate complete.\n`);
736
+ }
737
+ export function runUninstall(projectRoot) {
738
+ const claudeDir = path.join(projectRoot, '.claude');
739
+ console.log(`\nOrbital Command — uninstall`);
740
+ console.log(`Project root: ${projectRoot}\n`);
741
+ let removedCount = 0;
742
+ // 1. Remove orbital hooks from settings.local.json
743
+ const settingsPath = path.join(claudeDir, 'settings.local.json');
744
+ if (fs.existsSync(settingsPath)) {
745
+ try {
746
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
747
+ if (settings.hooks) {
748
+ for (const [event] of Object.entries(settings.hooks)) {
749
+ for (const group of settings.hooks[event]) {
750
+ if (group.hooks) {
751
+ const before = group.hooks.length;
752
+ group.hooks = group.hooks.filter((h) => !h._orbital);
753
+ removedCount += before - group.hooks.length;
754
+ }
755
+ }
756
+ settings.hooks[event] = settings.hooks[event].filter((g) => g.hooks && g.hooks.length > 0);
757
+ if (settings.hooks[event].length === 0) {
758
+ delete settings.hooks[event];
759
+ }
760
+ }
761
+ if (Object.keys(settings.hooks).length === 0) {
762
+ delete settings.hooks;
763
+ }
764
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
765
+ console.log(` Removed ${removedCount} orbital hook registrations from settings.local.json`);
766
+ }
767
+ }
768
+ catch {
769
+ console.warn(' Warning: could not parse settings.local.json');
770
+ }
771
+ }
772
+ // 2. Delete hooks that came from templates
773
+ const hookFiles = listTemplateFiles(path.join(TEMPLATES_DIR, 'hooks'), path.join(claudeDir, 'hooks'));
774
+ let hooksRemoved = 0;
775
+ for (const f of hookFiles) {
776
+ if (fs.existsSync(f)) {
777
+ fs.unlinkSync(f);
778
+ hooksRemoved++;
779
+ }
780
+ }
781
+ console.log(` Removed ${hooksRemoved} hook scripts`);
782
+ // 3. Delete skills that came from templates
783
+ const skillFiles = listTemplateFiles(path.join(TEMPLATES_DIR, 'skills'), path.join(claudeDir, 'skills'));
784
+ let skillsRemoved = 0;
785
+ for (const f of skillFiles) {
786
+ if (fs.existsSync(f)) {
787
+ fs.unlinkSync(f);
788
+ skillsRemoved++;
789
+ }
790
+ }
791
+ const skillsDest = path.join(claudeDir, 'skills');
792
+ if (fs.existsSync(skillsDest))
793
+ cleanEmptyDirs(skillsDest);
794
+ console.log(` Removed ${skillsRemoved} skill files`);
795
+ // 4. Delete agents that came from templates
796
+ const agentFiles = listTemplateFiles(path.join(TEMPLATES_DIR, 'agents'), path.join(claudeDir, 'agents'));
797
+ let agentsRemoved = 0;
798
+ for (const f of agentFiles) {
799
+ if (fs.existsSync(f)) {
800
+ fs.unlinkSync(f);
801
+ agentsRemoved++;
802
+ }
803
+ }
804
+ const agentsDest = path.join(claudeDir, 'agents');
805
+ if (fs.existsSync(agentsDest))
806
+ cleanEmptyDirs(agentsDest);
807
+ console.log(` Removed ${agentsRemoved} agent files`);
808
+ const total = removedCount + hooksRemoved + skillsRemoved + agentsRemoved;
809
+ console.log(`\nUninstall complete. ${total} items removed.`);
810
+ console.log(`Note: scopes/ and .claude/orbital-events/ were preserved.\n`);
811
+ }