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
package/server/init.ts ADDED
@@ -0,0 +1,891 @@
1
+ /**
2
+ * Shared init logic — used by both the CLI (`orbital init`) and
3
+ * programmatic callers (e.g. tests).
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ // Walk up from __dirname until we find the package root (identified by templates/).
14
+ // Handles both dev (server/ → 1 hop) and compiled (dist/server/server/ → 3 hops).
15
+ function resolvePackageRoot(startDir: string): string {
16
+ let dir = startDir;
17
+ for (let i = 0; i < 5; i++) {
18
+ if (fs.existsSync(path.join(dir, 'templates'))) return dir;
19
+ dir = path.resolve(dir, '..');
20
+ }
21
+ return path.resolve(startDir, '..');
22
+ }
23
+
24
+ const PACKAGE_ROOT = resolvePackageRoot(__dirname);
25
+ const TEMPLATES_DIR = path.join(PACKAGE_ROOT, 'templates');
26
+
27
+ // ─── Helpers ─────────────────────────────────────────────────
28
+
29
+ function ensureDir(dirPath: string): boolean {
30
+ if (!fs.existsSync(dirPath)) {
31
+ fs.mkdirSync(dirPath, { recursive: true });
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ function copyDirSync(src: string, dest: string, opts: { overwrite?: boolean } = {}): { created: string[]; skipped: string[] } {
38
+ const created: string[] = [];
39
+ const skipped: string[] = [];
40
+
41
+ if (!fs.existsSync(src)) return { created, skipped };
42
+
43
+ ensureDir(dest);
44
+
45
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
46
+ const srcPath = path.join(src, entry.name);
47
+ const destPath = path.join(dest, entry.name);
48
+
49
+ if (entry.isDirectory()) {
50
+ const sub = copyDirSync(srcPath, destPath, opts);
51
+ created.push(...sub.created);
52
+ skipped.push(...sub.skipped);
53
+ } else {
54
+ if (!opts.overwrite && fs.existsSync(destPath)) {
55
+ skipped.push(destPath);
56
+ } else {
57
+ ensureDir(path.dirname(destPath));
58
+ fs.copyFileSync(srcPath, destPath);
59
+ created.push(destPath);
60
+ }
61
+ }
62
+ }
63
+ return { created, skipped };
64
+ }
65
+
66
+ function chmodScripts(dir: string): void {
67
+ if (!fs.existsSync(dir)) return;
68
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
69
+ const fullPath = path.join(dir, entry.name);
70
+ if (entry.isDirectory()) {
71
+ chmodScripts(fullPath);
72
+ } else if (entry.name.endsWith('.sh')) {
73
+ fs.chmodSync(fullPath, 0o755);
74
+ }
75
+ }
76
+ }
77
+
78
+ function pruneStaleEntries(sourceDir: string, targetDir: string): number {
79
+ if (!fs.existsSync(targetDir) || !fs.existsSync(sourceDir)) return 0;
80
+
81
+ const sourceEntries = new Set(fs.readdirSync(sourceDir));
82
+ let removed = 0;
83
+
84
+ for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) {
85
+ if (!sourceEntries.has(entry.name)) {
86
+ const fullPath = path.join(targetDir, entry.name);
87
+ fs.rmSync(fullPath, { recursive: true, force: true });
88
+ removed++;
89
+ }
90
+ }
91
+ return removed;
92
+ }
93
+
94
+ function mergeSettingsHooks(targetPath: string, sourcePath: string): void {
95
+ let target: Record<string, unknown> = {};
96
+ if (fs.existsSync(targetPath)) {
97
+ try {
98
+ target = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
99
+ } catch {
100
+ console.warn(' Warning: existing settings.local.json is malformed — creating new one');
101
+ target = {};
102
+ }
103
+ }
104
+
105
+ if (!fs.existsSync(sourcePath)) {
106
+ console.warn(' Warning: settings-hooks template not found, skipping hook registration');
107
+ return;
108
+ }
109
+
110
+ const source = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
111
+ const sourceHooks = source.hooks || {};
112
+
113
+ if (!(target as Record<string, Record<string, unknown[]>>).hooks) {
114
+ (target as Record<string, unknown>).hooks = {};
115
+ }
116
+ const targetHooks = (target as Record<string, Record<string, unknown[]>>).hooks;
117
+
118
+ for (const [event, sourceGroups] of Object.entries(sourceHooks)) {
119
+ if (!targetHooks[event]) {
120
+ targetHooks[event] = tagOrbitalGroups(sourceGroups as HookGroup[]);
121
+ continue;
122
+ }
123
+
124
+ for (const sourceGroup of sourceGroups as HookGroup[]) {
125
+ const sourceMatcher = sourceGroup.matcher || '__none__';
126
+ const targetGroup = (targetHooks[event] as HookGroup[]).find(
127
+ (g) => (g.matcher || '__none__') === sourceMatcher
128
+ );
129
+
130
+ if (!targetGroup) {
131
+ (targetHooks[event] as HookGroup[]).push(tagOrbitalGroup(sourceGroup));
132
+ continue;
133
+ }
134
+
135
+ for (const hook of sourceGroup.hooks || []) {
136
+ const taggedHook = { ...hook, _orbital: true };
137
+ const alreadyPresent = (targetGroup.hooks || []).some(
138
+ (h) => h.command === hook.command
139
+ );
140
+ if (!alreadyPresent) {
141
+ if (!targetGroup.hooks) targetGroup.hooks = [];
142
+ targetGroup.hooks.push(taggedHook);
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ fs.writeFileSync(targetPath, JSON.stringify(target, null, 2) + '\n', 'utf8');
149
+ }
150
+
151
+ interface HookEntry {
152
+ command: string;
153
+ _orbital?: boolean;
154
+ [key: string]: unknown;
155
+ }
156
+
157
+ interface HookGroup {
158
+ matcher?: string;
159
+ hooks?: HookEntry[];
160
+ [key: string]: unknown;
161
+ }
162
+
163
+ function tagOrbitalGroups(groups: HookGroup[]): HookGroup[] {
164
+ return groups.map(tagOrbitalGroup);
165
+ }
166
+
167
+ function tagOrbitalGroup(group: HookGroup): HookGroup {
168
+ return {
169
+ ...group,
170
+ hooks: (group.hooks || []).map((h) => ({ ...h, _orbital: true })),
171
+ };
172
+ }
173
+
174
+ function updateGitignore(projectRoot: string): boolean {
175
+ const gitignorePath = path.join(projectRoot, '.gitignore');
176
+ const marker = '# Orbital Command';
177
+ const lines = [
178
+ '',
179
+ marker,
180
+ 'scopes/',
181
+ '.claude/orbital/',
182
+ '.claude/orbital-events/',
183
+ '.claude/config/workflow-manifest.sh',
184
+ '',
185
+ ];
186
+
187
+ let existing = '';
188
+ if (fs.existsSync(gitignorePath)) {
189
+ existing = fs.readFileSync(gitignorePath, 'utf8');
190
+ }
191
+
192
+ if (existing.includes(marker)) {
193
+ return false;
194
+ }
195
+
196
+ fs.appendFileSync(gitignorePath, lines.join('\n'), 'utf8');
197
+ return true;
198
+ }
199
+
200
+ function generateManifest(config: Record<string, unknown>): string {
201
+ const lines: string[] = [];
202
+ const lists = ((config.lists as Array<Record<string, unknown>>) || []).sort(
203
+ (a, b) => ((a.order as number) ?? 0) - ((b.order as number) ?? 0)
204
+ );
205
+
206
+ lines.push('#!/bin/bash');
207
+ lines.push('# Auto-generated by WorkflowEngine — DO NOT EDIT');
208
+ lines.push(`# Generated: ${new Date().toISOString()}`);
209
+ lines.push(`# Workflow: "${config.name}" (version ${config.version})`);
210
+ lines.push('');
211
+
212
+ lines.push('# ─── Branching mode (trunk or worktree) ───');
213
+ lines.push(`WORKFLOW_BRANCHING_MODE="${(config.branchingMode as string) ?? 'trunk'}"`);
214
+ lines.push('');
215
+
216
+ lines.push('# ─── Valid statuses (space-separated) ───');
217
+ lines.push(`WORKFLOW_STATUSES="${lists.map((l) => l.id).join(' ')}"`);
218
+ lines.push('');
219
+
220
+ lines.push('# ─── Statuses that have a scopes/ subdirectory ───');
221
+ const dirStatuses = lists.filter((l) => l.hasDirectory).map((l) => l.id);
222
+ lines.push(`WORKFLOW_DIR_STATUSES="${dirStatuses.join(' ')}"`);
223
+ lines.push('');
224
+
225
+ lines.push('# ─── Terminal statuses ───');
226
+ const terminalStatuses = (config.terminalStatuses as string[]) || [];
227
+ lines.push(`WORKFLOW_TERMINAL_STATUSES="${terminalStatuses.join(' ')}"`);
228
+ lines.push('');
229
+
230
+ lines.push('# ─── Entry point status ───');
231
+ lines.push(`WORKFLOW_ENTRY_STATUS="${(config.entryPoint as string) || (lists[0]?.id as string) || 'todo'}"`);
232
+ lines.push('');
233
+
234
+ const listMap = new Map(lists.map((l) => [l.id, l]));
235
+
236
+ lines.push('# ─── Transition edges (from:to:sessionKey) ───');
237
+ lines.push('WORKFLOW_EDGES=(');
238
+ for (const edge of (config.edges as Array<Record<string, unknown>>) || []) {
239
+ const targetList = listMap.get(edge.to as string);
240
+ const sessionKey = (targetList?.sessionKey as string) ?? '';
241
+ lines.push(` "${edge.from}:${edge.to}:${sessionKey}"`);
242
+ }
243
+ lines.push(')');
244
+ lines.push('');
245
+
246
+ lines.push('# ─── Branch-to-transition mapping (gitBranch:from:to:sessionKey) ───');
247
+ lines.push('WORKFLOW_BRANCH_MAP=(');
248
+ for (const edge of (config.edges as Array<Record<string, unknown>>) || []) {
249
+ const targetList = listMap.get(edge.to as string);
250
+ if (targetList?.gitBranch) {
251
+ const sessionKey = (targetList.sessionKey as string) ?? '';
252
+ lines.push(` "${targetList.gitBranch}:${edge.from}:${edge.to}:${sessionKey}"`);
253
+ }
254
+ }
255
+ lines.push(')');
256
+ lines.push('');
257
+
258
+ lines.push('# ─── Helper functions ──────────────────────────────');
259
+ lines.push('');
260
+ lines.push('status_to_dir() {');
261
+ lines.push(' local scope_status="$1"');
262
+ lines.push(' for s in $WORKFLOW_DIR_STATUSES; do');
263
+ lines.push(' [ "$s" = "$scope_status" ] && echo "$scope_status" && return 0');
264
+ lines.push(' done');
265
+ lines.push(' echo "$WORKFLOW_ENTRY_STATUS"');
266
+ lines.push('}');
267
+ lines.push('');
268
+ lines.push('status_to_branch() {');
269
+ lines.push(' local status="$1"');
270
+ lines.push(' for entry in "${WORKFLOW_BRANCH_MAP[@]}"; do');
271
+ lines.push(" IFS=':' read -r branch from to skey <<< \"$entry\"");
272
+ lines.push(' [ "$to" = "$status" ] && echo "$branch" && return 0');
273
+ lines.push(' done');
274
+ lines.push(' echo ""');
275
+ lines.push('}');
276
+ lines.push('');
277
+ lines.push('is_valid_status() {');
278
+ lines.push(' local status="$1"');
279
+ lines.push(' for s in $WORKFLOW_STATUSES; do');
280
+ lines.push(' [ "$s" = "$status" ] && return 0');
281
+ lines.push(' done');
282
+ lines.push(' return 1');
283
+ lines.push('}');
284
+
285
+ return lines.join('\n') + '\n';
286
+ }
287
+
288
+ function writeManifest(claudeDir: string): boolean {
289
+ const workflowPath = path.join(claudeDir, 'config', 'workflow.json');
290
+ if (!fs.existsSync(workflowPath)) return false;
291
+
292
+ try {
293
+ const config = JSON.parse(fs.readFileSync(workflowPath, 'utf8'));
294
+ const manifest = generateManifest(config);
295
+ const manifestPath = path.join(claudeDir, 'config', 'workflow-manifest.sh');
296
+ fs.writeFileSync(manifestPath, manifest, 'utf8');
297
+ fs.chmodSync(manifestPath, 0o755);
298
+ return true;
299
+ } catch {
300
+ console.warn(' Warning: could not generate workflow manifest');
301
+ return false;
302
+ }
303
+ }
304
+
305
+ function generateIndexMd(projectRoot: string, claudeDir: string): string {
306
+ let projectName = path.basename(projectRoot);
307
+ const configPath = path.join(claudeDir, 'orbital.config.json');
308
+ if (fs.existsSync(configPath)) {
309
+ try {
310
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
311
+ if (cfg.projectName) projectName = cfg.projectName;
312
+ } catch { /* use fallback */ }
313
+ }
314
+
315
+ const skillsDir = path.join(claudeDir, 'skills');
316
+ const skills: string[] = [];
317
+ if (fs.existsSync(skillsDir)) {
318
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
319
+ if (entry.isDirectory()) skills.push(entry.name);
320
+ }
321
+ }
322
+
323
+ const workflowPath = path.join(claudeDir, 'config', 'workflow.json');
324
+ let stages: string[] = [];
325
+ if (fs.existsSync(workflowPath)) {
326
+ try {
327
+ const wf = JSON.parse(fs.readFileSync(workflowPath, 'utf8'));
328
+ stages = (wf.lists || [])
329
+ .sort((a: Record<string, unknown>, b: Record<string, unknown>) =>
330
+ ((a.order as number) ?? 0) - ((b.order as number) ?? 0))
331
+ .map((l: Record<string, unknown>) => l.id as string);
332
+ } catch { /* skip */ }
333
+ }
334
+
335
+ const skillCategories: Record<string, string[]> = {
336
+ 'Git': skills.filter((s) => s.startsWith('git-')),
337
+ 'Scope': skills.filter((s) => s.startsWith('scope-')),
338
+ 'Session': skills.filter((s) => s.startsWith('session-')),
339
+ 'Quality': skills.filter((s) => s.startsWith('test-')),
340
+ };
341
+
342
+ let skillTable = '';
343
+ for (const [cat, list] of Object.entries(skillCategories)) {
344
+ if (list.length > 0) {
345
+ skillTable += `| ${cat} | ${list.map((s) => '`/' + s + '`').join(', ')} |\n`;
346
+ }
347
+ }
348
+
349
+ return `# ${projectName} — AI Agent Index
350
+
351
+ ---
352
+ tokens: ~1K
353
+ load-when: Always load first
354
+ last-verified: ${new Date().toISOString().split('T')[0]}
355
+ ---
356
+
357
+ ## 30-Second Orientation
358
+
359
+ **Project**: ${projectName}
360
+ **Managed by**: Orbital Command
361
+
362
+ ### Critical Commands
363
+
364
+ \`\`\`bash
365
+ # Run configured quality gates (from orbital.config.json)
366
+ # Typical: type-check, lint, build, test
367
+ \`\`\`
368
+
369
+ ---
370
+
371
+ ## Decision Tree: Where Should I Look?
372
+
373
+ \`\`\`
374
+ What are you trying to do?
375
+ |
376
+ +-- "I want to IMPLEMENT a scope"
377
+ | +-- Create new scope -> /scope-create
378
+ | +-- Implement scope -> /scope-implement
379
+ | +-- Review scope -> /scope-pre-review
380
+ |
381
+ +-- "I want to COMMIT/DEPLOY"
382
+ | +-- Commit work -> /git-commit
383
+ | +-- Push to main -> /git-main
384
+ ${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
385
+ |
386
+ +-- "I want to RUN CHECKS"
387
+ | +-- Quality gates -> /test-checks
388
+ | +-- Code review -> /test-code-review
389
+ | +-- Post-impl review -> /scope-post-review
390
+ |
391
+ +-- "I need SESSION help"
392
+ | +-- Continue work -> /session-resume
393
+ |
394
+ +-- "What should I AVOID?"
395
+ | +-- anti-patterns/dangerous-shortcuts.md
396
+ |
397
+ +-- "QUICK REFERENCES"
398
+ +-- Rules -> quick/rules.md
399
+ +-- Lessons learned -> lessons-learned.md
400
+ \`\`\`
401
+
402
+ ---
403
+
404
+ ## Skills
405
+
406
+ | Category | Skills |
407
+ |----------|--------|
408
+ ${skillTable || '| (none installed) | |\n'}
409
+ ---
410
+
411
+ ## Scope System (Three-Part Documents)
412
+
413
+ Scopes live in directories matching their pipeline stage.
414
+
415
+ \`\`\`
416
+ scopes/
417
+ +-- _template.md # Copy for new scopes
418
+ ${stages.map((s) => `+-- ${s}/`).join('\n')}
419
+ \`\`\`
420
+
421
+ **Three-Part Structure**:
422
+ - **PART 1: DASHBOARD** — Quick status, progress table, recent activity
423
+ - **PART 2: SPECIFICATION** — Feature lock (locked after review, any agent can implement)
424
+ - **PART 3: PROCESS** — Working memory (exploration, decisions, uncertainties, impl log)
425
+ - **AGENT REVIEW** — Synthesized findings from agent team review
426
+
427
+ **Lifecycle**: ${stages.join(' → ')}
428
+
429
+ ---
430
+
431
+ ## File Organization
432
+
433
+ \`\`\`
434
+ .claude/
435
+ +-- INDEX.md <- You are here
436
+ +-- lessons-learned.md # Institutional memory
437
+ +-- skills/ # Invokable skills
438
+ +-- quick/ # Quick reference docs
439
+ | +-- rules.md # Project rules with verify commands
440
+ +-- agents/ # Agent specifications
441
+ +-- anti-patterns/ # What NOT to do
442
+ +-- hooks/ # Claude Code lifecycle hooks
443
+ +-- config/ # Workflow config and presets
444
+ | +-- workflow.json # Active workflow
445
+ | +-- workflow-manifest.sh # Shell variables (auto-generated)
446
+ | +-- workflows/ # Available presets
447
+ +-- orbital.config.json # Project configuration
448
+ \`\`\`
449
+
450
+ ---
451
+
452
+ ## When In Doubt
453
+
454
+ 1. **Check rules**: \`quick/rules.md\`
455
+ 2. **Follow existing patterns**: Look at similar code in codebase
456
+ 3. **Ask**: Use clarifying questions before making assumptions
457
+ 4. **Verify**: Run quality gates before committing
458
+ `;
459
+ }
460
+
461
+ // ─── Helpers used by CLI commands ────────────────────────────
462
+
463
+ function listTemplateFiles(templateSubdir: string, targetDir: string): string[] {
464
+ const files: string[] = [];
465
+ if (!fs.existsSync(templateSubdir)) return files;
466
+
467
+ for (const entry of fs.readdirSync(templateSubdir, { withFileTypes: true })) {
468
+ const targetPath = path.join(targetDir, entry.name);
469
+ if (entry.isDirectory()) {
470
+ files.push(...listTemplateFiles(path.join(templateSubdir, entry.name), targetPath));
471
+ } else {
472
+ files.push(targetPath);
473
+ }
474
+ }
475
+ return files;
476
+ }
477
+
478
+ function cleanEmptyDirs(dir: string): void {
479
+ if (!fs.existsSync(dir)) return;
480
+
481
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
482
+ if (entry.isDirectory()) {
483
+ cleanEmptyDirs(path.join(dir, entry.name));
484
+ }
485
+ }
486
+
487
+ if (fs.readdirSync(dir).length === 0) {
488
+ fs.rmdirSync(dir);
489
+ }
490
+ }
491
+
492
+ // ─── Exports ─────────────────────────────────────────────────
493
+
494
+ export { TEMPLATES_DIR, ensureDir };
495
+
496
+ export interface InitOptions {
497
+ force?: boolean;
498
+ }
499
+
500
+ export function runInit(projectRoot: string, options: InitOptions = {}): void {
501
+ const force = options.force ?? false;
502
+ const claudeDir = path.join(projectRoot, '.claude');
503
+
504
+ console.log(`\nOrbital Command — init`);
505
+ console.log(`Project root: ${projectRoot}\n`);
506
+
507
+ // 1. Create directories
508
+ const dirs = [
509
+ path.join(claudeDir, 'orbital-events'),
510
+ path.join(claudeDir, 'orbital'),
511
+ path.join(claudeDir, 'config'),
512
+ path.join(claudeDir, 'review-verdicts'),
513
+ ];
514
+ for (const dir of dirs) {
515
+ const wasCreated = ensureDir(dir);
516
+ console.log(` ${wasCreated ? 'Created' : 'Exists '} ${path.relative(projectRoot, dir)}/`);
517
+ }
518
+
519
+ // 1b. Create scopes/ subdirectories from the default workflow preset
520
+ const defaultPresetPath = path.join(TEMPLATES_DIR, 'presets', 'default.json');
521
+ let scopeDirs = ['icebox'];
522
+ try {
523
+ const preset = JSON.parse(fs.readFileSync(defaultPresetPath, 'utf8'));
524
+ if (preset.lists && Array.isArray(preset.lists)) {
525
+ scopeDirs = preset.lists.filter((l: Record<string, unknown>) => l.hasDirectory).map((l: Record<string, unknown>) => l.id as string);
526
+ }
527
+ } catch {
528
+ console.warn(' Warning: could not load default preset, creating scopes/icebox/ only');
529
+ }
530
+ for (const dirId of scopeDirs) {
531
+ const scopeDir = path.join(projectRoot, 'scopes', dirId);
532
+ const wasCreated = ensureDir(scopeDir);
533
+ console.log(` ${wasCreated ? 'Created' : 'Exists '} scopes/${dirId}/`);
534
+ }
535
+
536
+ // 1c. Copy scope template
537
+ const scopeTemplateSrc = path.join(TEMPLATES_DIR, 'scopes', '_template.md');
538
+ const scopeTemplateDest = path.join(projectRoot, 'scopes', '_template.md');
539
+ if (fs.existsSync(scopeTemplateSrc)) {
540
+ if (force || !fs.existsSync(scopeTemplateDest)) {
541
+ fs.copyFileSync(scopeTemplateSrc, scopeTemplateDest);
542
+ console.log(` ${force ? 'Reset ' : 'Created'} scopes/_template.md`);
543
+ } else {
544
+ console.log(` Exists scopes/_template.md`);
545
+ }
546
+ }
547
+
548
+ // 2. Copy orbital.config.json template
549
+ const configDest = path.join(claudeDir, 'orbital.config.json');
550
+ const configSrc = path.join(TEMPLATES_DIR, 'orbital.config.json');
551
+ const configIsNew = !fs.existsSync(configDest);
552
+ if (configIsNew) {
553
+ if (fs.existsSync(configSrc)) {
554
+ fs.copyFileSync(configSrc, configDest);
555
+ console.log(` Created .claude/orbital.config.json`);
556
+ } else {
557
+ const defaultConfig = {
558
+ serverPort: 4444,
559
+ clientPort: 4445,
560
+ projectName: path.basename(projectRoot),
561
+ };
562
+ fs.writeFileSync(configDest, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf8');
563
+ console.log(` Created .claude/orbital.config.json (default)`);
564
+ }
565
+
566
+ // Auto-detect project commands from package.json
567
+ const pkgJsonPath = path.join(projectRoot, 'package.json');
568
+ if (fs.existsSync(pkgJsonPath)) {
569
+ try {
570
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
571
+ const scripts = pkg.scripts || {};
572
+ const config = JSON.parse(fs.readFileSync(configDest, 'utf8'));
573
+ 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
+ }
588
+ } catch { /* leave defaults */ }
589
+ }
590
+ } else {
591
+ console.log(` Exists .claude/orbital.config.json`);
592
+ }
593
+
594
+ // 3. Copy hooks
595
+ console.log('');
596
+ const hooksSrc = path.join(TEMPLATES_DIR, 'hooks');
597
+ const hooksDest = path.join(claudeDir, 'hooks');
598
+ if (force) {
599
+ const pruned = pruneStaleEntries(hooksSrc, hooksDest);
600
+ if (pruned > 0) console.log(` Pruned ${pruned} stale hook entries`);
601
+ }
602
+ const hooksResult = copyDirSync(hooksSrc, hooksDest, { overwrite: force });
603
+ console.log(` Hooks ${hooksResult.created.length} copied, ${hooksResult.skipped.length} skipped`);
604
+
605
+ // 4. Copy skills
606
+ const skillsSrc = path.join(TEMPLATES_DIR, 'skills');
607
+ const skillsDest = path.join(claudeDir, 'skills');
608
+ if (force) {
609
+ const pruned = pruneStaleEntries(skillsSrc, skillsDest);
610
+ if (pruned > 0) console.log(` Pruned ${pruned} stale skill entries`);
611
+ }
612
+ const skillsResult = copyDirSync(skillsSrc, skillsDest, { overwrite: force });
613
+ console.log(` Skills ${skillsResult.created.length} copied, ${skillsResult.skipped.length} skipped`);
614
+
615
+ // 5. Copy agents
616
+ const agentsSrc = path.join(TEMPLATES_DIR, 'agents');
617
+ const agentsDest = path.join(claudeDir, 'agents');
618
+ if (force) {
619
+ const pruned = pruneStaleEntries(agentsSrc, agentsDest);
620
+ if (pruned > 0) console.log(` Pruned ${pruned} stale agent entries`);
621
+ }
622
+ const agentsResult = copyDirSync(agentsSrc, agentsDest, { overwrite: force });
623
+ console.log(` Agents ${agentsResult.created.length} copied, ${agentsResult.skipped.length} skipped`);
624
+
625
+ // 6. Copy workflow presets
626
+ const presetsSrc = path.join(TEMPLATES_DIR, 'presets');
627
+ const presetsDest = path.join(claudeDir, 'config', 'workflows');
628
+ if (fs.existsSync(presetsSrc) && fs.readdirSync(presetsSrc).length > 0) {
629
+ if (force) {
630
+ const pruned = pruneStaleEntries(presetsSrc, presetsDest);
631
+ if (pruned > 0) console.log(` Pruned ${pruned} stale preset entries`);
632
+ }
633
+ const presetsResult = copyDirSync(presetsSrc, presetsDest, { overwrite: force });
634
+ console.log(` Presets ${presetsResult.created.length} copied, ${presetsResult.skipped.length} skipped`);
635
+ }
636
+
637
+ // 6b. Reset active workflow config when --force, or create if missing
638
+ const activeWorkflowDest = path.join(claudeDir, 'config', 'workflow.json');
639
+ if (force) {
640
+ fs.copyFileSync(defaultPresetPath, activeWorkflowDest);
641
+ console.log(` Reset .claude/config/workflow.json (default workflow)`);
642
+ } else if (!fs.existsSync(activeWorkflowDest)) {
643
+ fs.copyFileSync(defaultPresetPath, activeWorkflowDest);
644
+ console.log(` Created .claude/config/workflow.json`);
645
+ } else {
646
+ console.log(` Exists .claude/config/workflow.json`);
647
+ }
648
+
649
+ // 7. Copy agent-triggers.json
650
+ const triggersSrc = path.join(TEMPLATES_DIR, 'config', 'agent-triggers.json');
651
+ const triggersDest = path.join(claudeDir, 'config', 'agent-triggers.json');
652
+ if (fs.existsSync(triggersSrc)) {
653
+ if (force || !fs.existsSync(triggersDest)) {
654
+ fs.copyFileSync(triggersSrc, triggersDest);
655
+ console.log(` Created .claude/config/agent-triggers.json`);
656
+ } else {
657
+ console.log(` Exists .claude/config/agent-triggers.json`);
658
+ }
659
+ }
660
+
661
+ // 7b. Copy quick/ templates
662
+ const quickSrc = path.join(TEMPLATES_DIR, 'quick');
663
+ const quickDest = path.join(claudeDir, 'quick');
664
+ if (fs.existsSync(quickSrc)) {
665
+ const quickResult = copyDirSync(quickSrc, quickDest, { overwrite: force });
666
+ console.log(` Quick ${quickResult.created.length} copied, ${quickResult.skipped.length} skipped`);
667
+ }
668
+
669
+ // 7c. Copy anti-patterns/ templates
670
+ const antiSrc = path.join(TEMPLATES_DIR, 'anti-patterns');
671
+ const antiDest = path.join(claudeDir, 'anti-patterns');
672
+ if (fs.existsSync(antiSrc)) {
673
+ const antiResult = copyDirSync(antiSrc, antiDest, { overwrite: force });
674
+ console.log(` Anti-pat ${antiResult.created.length} copied, ${antiResult.skipped.length} skipped`);
675
+ }
676
+
677
+ // 7d. Copy lessons-learned.md
678
+ const lessonsSrc = path.join(TEMPLATES_DIR, 'lessons-learned.md');
679
+ const lessonsDest = path.join(claudeDir, 'lessons-learned.md');
680
+ if (fs.existsSync(lessonsSrc)) {
681
+ if (force || !fs.existsSync(lessonsDest)) {
682
+ fs.copyFileSync(lessonsSrc, lessonsDest);
683
+ console.log(` Created .claude/lessons-learned.md`);
684
+ } else {
685
+ console.log(` Exists .claude/lessons-learned.md`);
686
+ }
687
+ }
688
+
689
+ // 7e. Generate workflow manifest
690
+ const manifestOk = writeManifest(claudeDir);
691
+ console.log(` ${manifestOk ? 'Created' : 'Skipped'} .claude/config/workflow-manifest.sh`);
692
+
693
+ // 7f. Generate INDEX.md
694
+ const indexDest = path.join(claudeDir, 'INDEX.md');
695
+ if (force || !fs.existsSync(indexDest)) {
696
+ const indexContent = generateIndexMd(projectRoot, claudeDir);
697
+ fs.writeFileSync(indexDest, indexContent, 'utf8');
698
+ console.log(` ${force ? 'Reset ' : 'Created'} .claude/INDEX.md`);
699
+ } else {
700
+ console.log(` Exists .claude/INDEX.md`);
701
+ }
702
+
703
+ // 8. Merge hook registrations into settings.local.json
704
+ console.log('');
705
+ const settingsTarget = path.join(claudeDir, 'settings.local.json');
706
+ const settingsSrc = path.join(TEMPLATES_DIR, 'settings-hooks.json');
707
+ mergeSettingsHooks(settingsTarget, settingsSrc);
708
+ console.log(` Merged hook registrations into .claude/settings.local.json`);
709
+
710
+ // 9. Update .gitignore
711
+ const gitignoreUpdated = updateGitignore(projectRoot);
712
+ console.log(` ${gitignoreUpdated ? 'Updated' : 'Exists '} .gitignore (Orbital patterns)`);
713
+
714
+ // 10. Make hook scripts executable
715
+ chmodScripts(hooksDest);
716
+ console.log(` chmod hook scripts set to executable`);
717
+
718
+ // Summary
719
+ const totalCreated = hooksResult.created.length + skillsResult.created.length + agentsResult.created.length;
720
+ const totalSkipped = hooksResult.skipped.length + skillsResult.skipped.length + agentsResult.skipped.length;
721
+ console.log(`\nDone. ${totalCreated} files installed, ${totalSkipped} skipped (use --force to overwrite).`);
722
+ }
723
+
724
+ export function runUpdate(projectRoot: string): void {
725
+ const claudeDir = path.join(projectRoot, '.claude');
726
+
727
+ console.log(`\nOrbital Command — update`);
728
+ console.log(`Project root: ${projectRoot}\n`);
729
+
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`);
737
+
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`);
745
+
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`);
753
+
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`);
762
+ }
763
+
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`);
770
+ }
771
+
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`);
777
+ }
778
+
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`);
784
+ }
785
+
786
+ const scopeTemplateSrc = path.join(TEMPLATES_DIR, 'scopes', '_template.md');
787
+ const scopeTemplateDest = path.join(projectRoot, 'scopes', '_template.md');
788
+ if (fs.existsSync(scopeTemplateSrc)) {
789
+ ensureDir(path.join(projectRoot, 'scopes'));
790
+ fs.copyFileSync(scopeTemplateSrc, scopeTemplateDest);
791
+ console.log(` Updated scopes/_template.md`);
792
+ }
793
+
794
+ // 5b. Regenerate workflow manifest
795
+ const manifestOk = writeManifest(claudeDir);
796
+ console.log(` ${manifestOk ? 'Updated' : 'Skipped'} .claude/config/workflow-manifest.sh`);
797
+
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`);
803
+
804
+ // 7. Make hook scripts executable
805
+ chmodScripts(hooksDest);
806
+ console.log(` chmod hook scripts set to executable`);
807
+
808
+ console.log(`\nUpdate complete.\n`);
809
+ }
810
+
811
+ export function runUninstall(projectRoot: string): void {
812
+ const claudeDir = path.join(projectRoot, '.claude');
813
+
814
+ console.log(`\nOrbital Command — uninstall`);
815
+ console.log(`Project root: ${projectRoot}\n`);
816
+
817
+ let removedCount = 0;
818
+
819
+ // 1. Remove orbital hooks from settings.local.json
820
+ 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');
848
+ }
849
+ }
850
+
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++;
858
+ }
859
+ }
860
+ console.log(` Removed ${hooksRemoved} hook scripts`);
861
+
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++;
869
+ }
870
+ }
871
+ const skillsDest = path.join(claudeDir, 'skills');
872
+ if (fs.existsSync(skillsDest)) cleanEmptyDirs(skillsDest);
873
+ console.log(` Removed ${skillsRemoved} skill files`);
874
+
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++;
882
+ }
883
+ }
884
+ const agentsDest = path.join(claudeDir, 'agents');
885
+ if (fs.existsSync(agentsDest)) cleanEmptyDirs(agentsDest);
886
+ console.log(` Removed ${agentsRemoved} agent files`);
887
+
888
+ const total = removedCount + hooksRemoved + skillsRemoved + agentsRemoved;
889
+ console.log(`\nUninstall complete. ${total} items removed.`);
890
+ console.log(`Note: scopes/ and .claude/orbital-events/ were preserved.\n`);
891
+ }