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.
- package/LICENSE +21 -0
- package/README.md +396 -0
- package/bin/orbital.js +362 -0
- package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +84 -0
- package/dist/assets/WorkflowVisualizer-BZV40eAE.css +1 -0
- package/dist/assets/charts-D__PA1zp.js +72 -0
- package/dist/assets/index-D1G6i0nS.css +1 -0
- package/dist/assets/index-DpItvKpf.js +419 -0
- package/dist/assets/ui-BvF022GT.js +53 -0
- package/dist/assets/vendor-Dzv9lrRc.js +59 -0
- package/dist/index.html +19 -0
- package/dist/scanner-sweep.png +0 -0
- package/dist/server/server/adapters/index.js +34 -0
- package/dist/server/server/adapters/iterm2-adapter.js +29 -0
- package/dist/server/server/adapters/subprocess-adapter.js +21 -0
- package/dist/server/server/adapters/terminal-adapter.js +1 -0
- package/dist/server/server/config.js +156 -0
- package/dist/server/server/database.js +90 -0
- package/dist/server/server/index.js +372 -0
- package/dist/server/server/init.js +811 -0
- package/dist/server/server/parsers/event-parser.js +64 -0
- package/dist/server/server/parsers/scope-parser.js +188 -0
- package/dist/server/server/routes/config-routes.js +163 -0
- package/dist/server/server/routes/data-routes.js +461 -0
- package/dist/server/server/routes/dispatch-routes.js +215 -0
- package/dist/server/server/routes/git-routes.js +92 -0
- package/dist/server/server/routes/scope-routes.js +215 -0
- package/dist/server/server/routes/sprint-routes.js +116 -0
- package/dist/server/server/routes/version-routes.js +130 -0
- package/dist/server/server/routes/workflow-routes.js +185 -0
- package/dist/server/server/schema.js +90 -0
- package/dist/server/server/services/batch-orchestrator.js +253 -0
- package/dist/server/server/services/claude-session-service.js +352 -0
- package/dist/server/server/services/config-service.js +132 -0
- package/dist/server/server/services/deploy-service.js +51 -0
- package/dist/server/server/services/event-service.js +63 -0
- package/dist/server/server/services/gate-service.js +83 -0
- package/dist/server/server/services/git-service.js +309 -0
- package/dist/server/server/services/github-service.js +145 -0
- package/dist/server/server/services/readiness-service.js +184 -0
- package/dist/server/server/services/scope-cache.js +72 -0
- package/dist/server/server/services/scope-service.js +424 -0
- package/dist/server/server/services/sprint-orchestrator.js +312 -0
- package/dist/server/server/services/sprint-service.js +293 -0
- package/dist/server/server/services/workflow-service.js +397 -0
- package/dist/server/server/utils/cc-hooks-parser.js +49 -0
- package/dist/server/server/utils/dispatch-utils.js +305 -0
- package/dist/server/server/utils/logger.js +86 -0
- package/dist/server/server/utils/terminal-launcher.js +388 -0
- package/dist/server/server/utils/worktree-manager.js +98 -0
- package/dist/server/server/watchers/event-watcher.js +81 -0
- package/dist/server/server/watchers/scope-watcher.js +33 -0
- package/dist/server/shared/api-types.js +5 -0
- package/dist/server/shared/default-workflow.json +616 -0
- package/dist/server/shared/workflow-config.js +44 -0
- package/dist/server/shared/workflow-engine.js +353 -0
- package/index.html +15 -0
- package/package.json +110 -0
- package/postcss.config.js +6 -0
- package/schemas/orbital.config.schema.json +83 -0
- package/scripts/postinstall.js +24 -0
- package/scripts/start.sh +20 -0
- package/server/adapters/index.ts +41 -0
- package/server/adapters/iterm2-adapter.ts +37 -0
- package/server/adapters/subprocess-adapter.ts +25 -0
- package/server/adapters/terminal-adapter.ts +24 -0
- package/server/config.ts +234 -0
- package/server/database.ts +107 -0
- package/server/index.ts +452 -0
- package/server/init.ts +891 -0
- package/server/parsers/event-parser.ts +74 -0
- package/server/parsers/scope-parser.ts +240 -0
- package/server/routes/config-routes.ts +182 -0
- package/server/routes/data-routes.ts +548 -0
- package/server/routes/dispatch-routes.ts +275 -0
- package/server/routes/git-routes.ts +112 -0
- package/server/routes/scope-routes.ts +262 -0
- package/server/routes/sprint-routes.ts +142 -0
- package/server/routes/version-routes.ts +156 -0
- package/server/routes/workflow-routes.ts +198 -0
- package/server/schema.ts +90 -0
- package/server/services/batch-orchestrator.ts +286 -0
- package/server/services/claude-session-service.ts +441 -0
- package/server/services/config-service.ts +151 -0
- package/server/services/deploy-service.ts +98 -0
- package/server/services/event-service.ts +98 -0
- package/server/services/gate-service.ts +126 -0
- package/server/services/git-service.ts +391 -0
- package/server/services/github-service.ts +183 -0
- package/server/services/readiness-service.ts +250 -0
- package/server/services/scope-cache.ts +81 -0
- package/server/services/scope-service.ts +476 -0
- package/server/services/sprint-orchestrator.ts +361 -0
- package/server/services/sprint-service.ts +415 -0
- package/server/services/workflow-service.ts +461 -0
- package/server/utils/cc-hooks-parser.ts +70 -0
- package/server/utils/dispatch-utils.ts +395 -0
- package/server/utils/logger.ts +109 -0
- package/server/utils/terminal-launcher.ts +462 -0
- package/server/utils/worktree-manager.ts +104 -0
- package/server/watchers/event-watcher.ts +100 -0
- package/server/watchers/scope-watcher.ts +38 -0
- package/shared/api-types.ts +20 -0
- package/shared/default-workflow.json +616 -0
- package/shared/workflow-config.ts +170 -0
- package/shared/workflow-engine.ts +427 -0
- package/src/App.tsx +33 -0
- package/src/components/AgentBadge.tsx +40 -0
- package/src/components/BatchPreflightModal.tsx +115 -0
- package/src/components/CardDisplayToggle.tsx +74 -0
- package/src/components/ColumnHeaderActions.tsx +55 -0
- package/src/components/ColumnMenu.tsx +99 -0
- package/src/components/DeployHistory.tsx +141 -0
- package/src/components/DispatchModal.tsx +164 -0
- package/src/components/DispatchPopover.tsx +139 -0
- package/src/components/DragOverlay.tsx +25 -0
- package/src/components/DriftSidebar.tsx +140 -0
- package/src/components/EnvironmentStrip.tsx +88 -0
- package/src/components/ErrorBoundary.tsx +62 -0
- package/src/components/FilterChip.tsx +105 -0
- package/src/components/GateIndicator.tsx +33 -0
- package/src/components/IdeaDetailModal.tsx +190 -0
- package/src/components/IdeaFormDialog.tsx +113 -0
- package/src/components/KanbanColumn.tsx +201 -0
- package/src/components/MarkdownRenderer.tsx +114 -0
- package/src/components/NeonGrid.tsx +128 -0
- package/src/components/PromotionQueue.tsx +89 -0
- package/src/components/ScopeCard.tsx +234 -0
- package/src/components/ScopeDetailModal.tsx +255 -0
- package/src/components/ScopeFilterBar.tsx +152 -0
- package/src/components/SearchInput.tsx +102 -0
- package/src/components/SessionPanel.tsx +335 -0
- package/src/components/SprintContainer.tsx +303 -0
- package/src/components/SprintDependencyDialog.tsx +78 -0
- package/src/components/SprintPreflightModal.tsx +138 -0
- package/src/components/StatusBar.tsx +168 -0
- package/src/components/SwimCell.tsx +67 -0
- package/src/components/SwimLaneRow.tsx +94 -0
- package/src/components/SwimlaneBoardView.tsx +108 -0
- package/src/components/VersionBadge.tsx +139 -0
- package/src/components/ViewModeSelector.tsx +114 -0
- package/src/components/config/AgentChip.tsx +53 -0
- package/src/components/config/AgentCreateDialog.tsx +321 -0
- package/src/components/config/AgentEditor.tsx +175 -0
- package/src/components/config/DirectoryTree.tsx +582 -0
- package/src/components/config/FileEditor.tsx +550 -0
- package/src/components/config/HookChip.tsx +50 -0
- package/src/components/config/StageCard.tsx +198 -0
- package/src/components/config/TransitionZone.tsx +173 -0
- package/src/components/config/UnifiedWorkflowPipeline.tsx +216 -0
- package/src/components/config/WorkflowPipeline.tsx +161 -0
- package/src/components/source-control/BranchList.tsx +93 -0
- package/src/components/source-control/BranchPanel.tsx +105 -0
- package/src/components/source-control/CommitLog.tsx +100 -0
- package/src/components/source-control/CommitRow.tsx +47 -0
- package/src/components/source-control/GitHubPanel.tsx +110 -0
- package/src/components/source-control/GitHubSetupGuide.tsx +52 -0
- package/src/components/source-control/GitOverviewBar.tsx +101 -0
- package/src/components/source-control/PullRequestList.tsx +69 -0
- package/src/components/source-control/WorktreeList.tsx +80 -0
- package/src/components/ui/badge.tsx +41 -0
- package/src/components/ui/button.tsx +55 -0
- package/src/components/ui/card.tsx +78 -0
- package/src/components/ui/dialog.tsx +94 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/ui/toggle-switch.tsx +35 -0
- package/src/components/ui/tooltip.tsx +27 -0
- package/src/components/workflow/AddEdgeDialog.tsx +217 -0
- package/src/components/workflow/AddListDialog.tsx +201 -0
- package/src/components/workflow/ChecklistEditor.tsx +239 -0
- package/src/components/workflow/CommandPrefixManager.tsx +118 -0
- package/src/components/workflow/ConfigSettingsPanel.tsx +189 -0
- package/src/components/workflow/DirectionSelector.tsx +133 -0
- package/src/components/workflow/DispatchConfigPanel.tsx +180 -0
- package/src/components/workflow/EdgeDetailPanel.tsx +236 -0
- package/src/components/workflow/EdgePropertyEditor.tsx +251 -0
- package/src/components/workflow/EditToolbar.tsx +138 -0
- package/src/components/workflow/HookDetailPanel.tsx +250 -0
- package/src/components/workflow/HookExecutionLog.tsx +24 -0
- package/src/components/workflow/HookSourceModal.tsx +129 -0
- package/src/components/workflow/HooksDashboard.tsx +363 -0
- package/src/components/workflow/ListPropertyEditor.tsx +251 -0
- package/src/components/workflow/MigrationPreviewDialog.tsx +237 -0
- package/src/components/workflow/MovementRulesPanel.tsx +188 -0
- package/src/components/workflow/NodeDetailPanel.tsx +245 -0
- package/src/components/workflow/PresetSelector.tsx +414 -0
- package/src/components/workflow/SkillCommandBuilder.tsx +174 -0
- package/src/components/workflow/WorkflowEdgeComponent.tsx +145 -0
- package/src/components/workflow/WorkflowNode.tsx +147 -0
- package/src/components/workflow/graphLayout.ts +186 -0
- package/src/components/workflow/mergeHooks.ts +85 -0
- package/src/components/workflow/useEditHistory.ts +88 -0
- package/src/components/workflow/useWorkflowEditor.ts +262 -0
- package/src/components/workflow/validateConfig.ts +70 -0
- package/src/hooks/useActiveDispatches.ts +198 -0
- package/src/hooks/useBoardSettings.ts +170 -0
- package/src/hooks/useCardDisplay.ts +57 -0
- package/src/hooks/useCcHooks.ts +24 -0
- package/src/hooks/useConfigTree.ts +51 -0
- package/src/hooks/useEnforcementRules.ts +46 -0
- package/src/hooks/useEvents.ts +59 -0
- package/src/hooks/useFileEditor.ts +165 -0
- package/src/hooks/useGates.ts +57 -0
- package/src/hooks/useIdeaActions.ts +53 -0
- package/src/hooks/useKanbanDnd.ts +410 -0
- package/src/hooks/useOrbitalConfig.ts +54 -0
- package/src/hooks/usePipeline.ts +47 -0
- package/src/hooks/usePipelineData.ts +338 -0
- package/src/hooks/useReconnect.ts +25 -0
- package/src/hooks/useScopeFilters.ts +125 -0
- package/src/hooks/useScopeSessions.ts +44 -0
- package/src/hooks/useScopes.ts +67 -0
- package/src/hooks/useSearch.ts +67 -0
- package/src/hooks/useSettings.tsx +187 -0
- package/src/hooks/useSocket.ts +25 -0
- package/src/hooks/useSourceControl.ts +105 -0
- package/src/hooks/useSprintPreflight.ts +55 -0
- package/src/hooks/useSprints.ts +154 -0
- package/src/hooks/useStatusBarHighlight.ts +18 -0
- package/src/hooks/useSwimlaneBoardSettings.ts +104 -0
- package/src/hooks/useTheme.ts +9 -0
- package/src/hooks/useTransitionReadiness.ts +53 -0
- package/src/hooks/useVersion.ts +155 -0
- package/src/hooks/useViolations.ts +65 -0
- package/src/hooks/useWorkflow.tsx +125 -0
- package/src/hooks/useZoomModifier.ts +19 -0
- package/src/index.css +797 -0
- package/src/layouts/DashboardLayout.tsx +113 -0
- package/src/lib/collisionDetection.ts +20 -0
- package/src/lib/scope-fields.ts +61 -0
- package/src/lib/swimlane.ts +146 -0
- package/src/lib/utils.ts +15 -0
- package/src/main.tsx +19 -0
- package/src/socket.ts +11 -0
- package/src/types/index.ts +497 -0
- package/src/views/AgentFeed.tsx +339 -0
- package/src/views/DeployPipeline.tsx +59 -0
- package/src/views/EnforcementView.tsx +378 -0
- package/src/views/PrimitivesConfig.tsx +500 -0
- package/src/views/QualityGates.tsx +1012 -0
- package/src/views/ScopeBoard.tsx +454 -0
- package/src/views/SessionTimeline.tsx +516 -0
- package/src/views/Settings.tsx +183 -0
- package/src/views/SourceControl.tsx +95 -0
- package/src/views/WorkflowVisualizer.tsx +382 -0
- package/tailwind.config.js +161 -0
- package/templates/agents/AUTO-INVOKE.md +180 -0
- package/templates/agents/CONFLICT-RESOLUTION.md +128 -0
- package/templates/agents/QUICK-REFERENCE.md +122 -0
- package/templates/agents/README.md +188 -0
- package/templates/agents/SKILL-TRIGGERS.md +100 -0
- package/templates/agents/blue-team/frontend-designer.md +424 -0
- package/templates/agents/green-team/architect.md +526 -0
- package/templates/agents/green-team/rules-enforcer.md +131 -0
- package/templates/agents/red-team/attacker-learned.md +24 -0
- package/templates/agents/red-team/attacker.md +486 -0
- package/templates/agents/red-team/chaos.md +548 -0
- package/templates/agents/reference/component-registry.md +82 -0
- package/templates/agents/workflows/full-mode.md +218 -0
- package/templates/agents/workflows/quick-mode.md +118 -0
- package/templates/agents/workflows/security-mode.md +283 -0
- package/templates/anti-patterns/dangerous-shortcuts.md +427 -0
- package/templates/config/agent-triggers.json +92 -0
- package/templates/hooks/agent-team-gate.sh +31 -0
- package/templates/hooks/agent-trigger.sh +97 -0
- package/templates/hooks/block-push.sh +66 -0
- package/templates/hooks/block-workarounds.sh +61 -0
- package/templates/hooks/blocker-check.sh +28 -0
- package/templates/hooks/completion-checklist.sh +28 -0
- package/templates/hooks/decision-capture.sh +15 -0
- package/templates/hooks/dependency-check.sh +27 -0
- package/templates/hooks/end-session.sh +31 -0
- package/templates/hooks/exploration-logger.sh +37 -0
- package/templates/hooks/files-changed-summary.sh +37 -0
- package/templates/hooks/get-session-id.sh +49 -0
- package/templates/hooks/git-commit-guard.sh +34 -0
- package/templates/hooks/init-session.sh +93 -0
- package/templates/hooks/orbital-emit.sh +79 -0
- package/templates/hooks/orbital-report-deploy.sh +78 -0
- package/templates/hooks/orbital-report-gates.sh +40 -0
- package/templates/hooks/orbital-report-violation.sh +36 -0
- package/templates/hooks/orbital-scope-update.sh +53 -0
- package/templates/hooks/phase-verify-reminder.sh +26 -0
- package/templates/hooks/review-gate-check.sh +82 -0
- package/templates/hooks/scope-commit-logger.sh +37 -0
- package/templates/hooks/scope-create-cleanup.sh +36 -0
- package/templates/hooks/scope-create-gate.sh +80 -0
- package/templates/hooks/scope-create-tracker.sh +17 -0
- package/templates/hooks/scope-file-sync.sh +53 -0
- package/templates/hooks/scope-gate.sh +35 -0
- package/templates/hooks/scope-helpers.sh +188 -0
- package/templates/hooks/scope-lifecycle-gate.sh +139 -0
- package/templates/hooks/scope-prepare.sh +244 -0
- package/templates/hooks/scope-transition.sh +172 -0
- package/templates/hooks/session-enforcer.sh +143 -0
- package/templates/hooks/time-tracker.sh +33 -0
- package/templates/lessons-learned.md +15 -0
- package/templates/orbital.config.json +35 -0
- package/templates/presets/development.json +42 -0
- package/templates/presets/gitflow.json +712 -0
- package/templates/presets/minimal.json +23 -0
- package/templates/quick/rules.md +218 -0
- package/templates/scopes/_template.md +255 -0
- package/templates/settings-hooks.json +98 -0
- package/templates/skills/git-commit/SKILL.md +85 -0
- package/templates/skills/git-dev/SKILL.md +99 -0
- package/templates/skills/git-hotfix/SKILL.md +223 -0
- package/templates/skills/git-main/SKILL.md +84 -0
- package/templates/skills/git-production/SKILL.md +165 -0
- package/templates/skills/git-staging/SKILL.md +112 -0
- package/templates/skills/scope-create/SKILL.md +81 -0
- package/templates/skills/scope-fix-review/SKILL.md +168 -0
- package/templates/skills/scope-implement/SKILL.md +110 -0
- package/templates/skills/scope-post-review/SKILL.md +144 -0
- package/templates/skills/scope-pre-review/SKILL.md +211 -0
- package/templates/skills/scope-verify/SKILL.md +201 -0
- package/templates/skills/session-init/SKILL.md +62 -0
- package/templates/skills/session-resume/SKILL.md +201 -0
- package/templates/skills/test-checks/SKILL.md +171 -0
- package/templates/skills/test-code-review/SKILL.md +252 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +38 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
export function createGitRoutes({ gitService, githubService, engine }) {
|
|
3
|
+
const router = Router();
|
|
4
|
+
// ─── Git Overview ──────────────────────────────────────────
|
|
5
|
+
router.get('/git/overview', async (_req, res) => {
|
|
6
|
+
try {
|
|
7
|
+
const config = engine.getConfig();
|
|
8
|
+
const branchingMode = config.branchingMode ?? 'trunk';
|
|
9
|
+
const overview = await gitService.getOverview(branchingMode);
|
|
10
|
+
res.json(overview);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
res.status(500).json({ error: 'Failed to get git overview', details: String(err) });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
// ─── Commits ──────────────────────────────────────────────
|
|
17
|
+
router.get('/git/commits', async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const branch = req.query.branch || undefined;
|
|
20
|
+
const limit = Number(req.query.limit) || 50;
|
|
21
|
+
const offset = Number(req.query.offset) || 0;
|
|
22
|
+
const commits = await gitService.getCommits({ branch, limit, offset });
|
|
23
|
+
res.json(commits);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
res.status(500).json({ error: 'Failed to get commits', details: String(err) });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
// ─── Branches ──────────────────────────────────────────────
|
|
30
|
+
router.get('/git/branches', async (_req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const branches = await gitService.getBranches();
|
|
33
|
+
res.json(branches);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
res.status(500).json({ error: 'Failed to get branches', details: String(err) });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// ─── Enhanced Worktrees ────────────────────────────────────
|
|
40
|
+
router.get('/git/worktrees', async (_req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const worktrees = await gitService.getEnhancedWorktrees();
|
|
43
|
+
res.json(worktrees);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
res.status(500).json({ error: 'Failed to get worktrees', details: String(err) });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// ─── Dynamic Drift ─────────────────────────────────────────
|
|
50
|
+
router.get('/git/drift', async (_req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
// Build drift pairs from workflow lists that have gitBranch set
|
|
53
|
+
const config = engine.getConfig();
|
|
54
|
+
const listsWithBranch = config.lists
|
|
55
|
+
.filter(l => l.gitBranch)
|
|
56
|
+
.sort((a, b) => a.order - b.order);
|
|
57
|
+
const pairs = [];
|
|
58
|
+
for (let i = 0; i < listsWithBranch.length - 1; i++) {
|
|
59
|
+
pairs.push({
|
|
60
|
+
from: listsWithBranch[i].gitBranch,
|
|
61
|
+
to: listsWithBranch[i + 1].gitBranch,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const drift = await gitService.getDrift(pairs);
|
|
65
|
+
res.json(drift);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
res.status(500).json({ error: 'Failed to compute drift', details: String(err) });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// ─── GitHub Status ─────────────────────────────────────────
|
|
72
|
+
router.get('/github/status', async (_req, res) => {
|
|
73
|
+
try {
|
|
74
|
+
const status = await githubService.getStatus();
|
|
75
|
+
res.json(status);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
res.status(500).json({ error: 'Failed to get GitHub status', details: String(err) });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// ─── GitHub PRs ────────────────────────────────────────────
|
|
82
|
+
router.get('/github/prs', async (_req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const prs = await githubService.getOpenPRs();
|
|
85
|
+
res.json(prs);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
res.status(500).json({ error: 'Failed to get PRs', details: String(err) });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return router;
|
|
92
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { launchInTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
4
|
+
import { resolveDispatchEvent, linkPidToDispatch } from '../utils/dispatch-utils.js';
|
|
5
|
+
import { getConfig } from '../config.js';
|
|
6
|
+
import { createLogger } from '../utils/logger.js';
|
|
7
|
+
const log = createLogger('dispatch');
|
|
8
|
+
export function createScopeRoutes({ db, io, scopeService, readinessService, projectRoot, engine }) {
|
|
9
|
+
const router = Router();
|
|
10
|
+
// ─── Scope CRUD ──────────────────────────────────────────
|
|
11
|
+
router.get('/scopes', (_req, res) => {
|
|
12
|
+
res.json(scopeService.getAll());
|
|
13
|
+
});
|
|
14
|
+
// ─── Transition Readiness ──────────────────────────────────
|
|
15
|
+
router.get('/scopes/:id/readiness', (req, res) => {
|
|
16
|
+
const readiness = readinessService.getReadiness(Number(req.params.id));
|
|
17
|
+
if (!readiness) {
|
|
18
|
+
res.status(404).json({ error: 'Scope not found' });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
res.json(readiness);
|
|
22
|
+
});
|
|
23
|
+
/** Bulk update — must come before :id route to avoid matching "bulk" as an id */
|
|
24
|
+
router.patch('/scopes/bulk/status', (req, res) => {
|
|
25
|
+
const { scopes } = req.body;
|
|
26
|
+
if (!Array.isArray(scopes)) {
|
|
27
|
+
res.status(400).json({ error: 'Expected { scopes: [{id, status}] }' });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let updated = 0;
|
|
31
|
+
for (const { id, status } of scopes) {
|
|
32
|
+
const result = scopeService.updateStatus(id, status, 'bulk-sync');
|
|
33
|
+
if (result.ok)
|
|
34
|
+
updated++;
|
|
35
|
+
}
|
|
36
|
+
res.json({ updated, total: scopes.length });
|
|
37
|
+
});
|
|
38
|
+
router.get('/scopes/:id', (req, res) => {
|
|
39
|
+
const scope = scopeService.getById(Number(req.params.id));
|
|
40
|
+
if (!scope) {
|
|
41
|
+
res.status(404).json({ error: 'Scope not found' });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
res.json(scope);
|
|
45
|
+
});
|
|
46
|
+
router.patch('/scopes/:id', (req, res) => {
|
|
47
|
+
const id = Number(req.params.id);
|
|
48
|
+
const result = scopeService.updateScopeFrontmatter(id, req.body);
|
|
49
|
+
if (!result.ok) {
|
|
50
|
+
const code = result.code === 'NOT_FOUND' ? 404 : 400;
|
|
51
|
+
res.status(code).json({ error: result.error, code: result.code });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const scope = scopeService.getById(id);
|
|
55
|
+
res.json(scope ?? { ok: true });
|
|
56
|
+
});
|
|
57
|
+
// ─── Idea Routes ─────────────────────────────────────────
|
|
58
|
+
router.post('/ideas', (req, res) => {
|
|
59
|
+
const { title, description } = req.body;
|
|
60
|
+
if (!title?.trim()) {
|
|
61
|
+
res.status(400).json({ error: 'title is required' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const idea = scopeService.createIdeaFile(title.trim(), (description ?? '').trim());
|
|
65
|
+
res.status(201).json(idea);
|
|
66
|
+
});
|
|
67
|
+
router.patch('/ideas/:id', (req, res) => {
|
|
68
|
+
const id = Number(req.params.id);
|
|
69
|
+
const { title, description } = req.body;
|
|
70
|
+
if (!title?.trim()) {
|
|
71
|
+
res.status(400).json({ error: 'title is required' });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const updated = scopeService.updateIdeaFile(id, title.trim(), (description ?? '').trim());
|
|
75
|
+
if (!updated) {
|
|
76
|
+
res.status(404).json({ error: 'Idea not found' });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
res.json({ ok: true });
|
|
80
|
+
});
|
|
81
|
+
router.delete('/ideas/:id', (req, res) => {
|
|
82
|
+
const id = Number(req.params.id);
|
|
83
|
+
const deleted = scopeService.deleteIdeaFile(id);
|
|
84
|
+
if (!deleted) {
|
|
85
|
+
res.status(404).json({ error: 'Idea not found' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
res.json({ ok: true });
|
|
89
|
+
});
|
|
90
|
+
router.post('/ideas/:id/promote', async (req, res) => {
|
|
91
|
+
const ideaId = Number(req.params.id);
|
|
92
|
+
const result = scopeService.promoteIdea(ideaId);
|
|
93
|
+
if (!result) {
|
|
94
|
+
res.status(404).json({ error: 'Idea not found' });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const scopeId = result.id;
|
|
98
|
+
// Read command from workflow edge config (user-overridable)
|
|
99
|
+
const entryPoint = engine.getEntryPoint();
|
|
100
|
+
const targets = engine.getValidTargets(entryPoint.id);
|
|
101
|
+
const promoteTarget = targets[0] ?? 'planning';
|
|
102
|
+
const edge = engine.findEdge(entryPoint.id, promoteTarget);
|
|
103
|
+
const edgeCommand = edge ? engine.buildCommand(edge, scopeId) : null;
|
|
104
|
+
const command = edgeCommand ?? `/scope-create ${String(scopeId).padStart(3, '0')}`;
|
|
105
|
+
// Record DISPATCH event for audit trail
|
|
106
|
+
const eventId = crypto.randomUUID();
|
|
107
|
+
const eventData = {
|
|
108
|
+
command,
|
|
109
|
+
transition: { from: entryPoint.id, to: promoteTarget },
|
|
110
|
+
resolved: null,
|
|
111
|
+
};
|
|
112
|
+
db.prepare(`INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
|
|
113
|
+
VALUES (?, 'DISPATCH', ?, NULL, 'dashboard', ?, ?)`).run(eventId, scopeId, JSON.stringify(eventData), new Date().toISOString());
|
|
114
|
+
io.emit('event:new', {
|
|
115
|
+
id: eventId, type: 'DISPATCH', scope_id: scopeId,
|
|
116
|
+
session_id: null, agent: 'dashboard', data: eventData,
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
});
|
|
119
|
+
const escaped = escapeForAnsiC(command);
|
|
120
|
+
const fullCmd = `cd '${projectRoot}' && claude --dangerously-skip-permissions $'${escaped}'`;
|
|
121
|
+
const promoteSessionName = buildSessionName({ scopeId, title: result.title, command });
|
|
122
|
+
const promoteBeforePids = snapshotSessionPids(projectRoot);
|
|
123
|
+
try {
|
|
124
|
+
await launchInTerminal(fullCmd);
|
|
125
|
+
res.json({ ok: true, id: scopeId, filePath: result.filePath });
|
|
126
|
+
discoverNewSession(projectRoot, promoteBeforePids)
|
|
127
|
+
.then((session) => {
|
|
128
|
+
if (!session)
|
|
129
|
+
return;
|
|
130
|
+
linkPidToDispatch(db, eventId, session.pid);
|
|
131
|
+
if (promoteSessionName)
|
|
132
|
+
renameSession(projectRoot, session.sessionId, promoteSessionName);
|
|
133
|
+
})
|
|
134
|
+
.catch(err => log.error('PID discovery failed', { error: err.message }));
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
resolveDispatchEvent(db, io, eventId, 'failed', String(err));
|
|
138
|
+
res.status(500).json({ error: 'Failed to launch terminal', details: String(err) });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// ─── Surprise Me (AI idea generation) ────────────────────
|
|
142
|
+
let surpriseInProgress = false;
|
|
143
|
+
router.post('/ideas/surprise', (_req, res) => {
|
|
144
|
+
if (surpriseInProgress) {
|
|
145
|
+
res.status(409).json({ error: 'Surprise generation already in progress' });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
surpriseInProgress = true;
|
|
149
|
+
const nextIdStart = scopeService.getNextIceboxId();
|
|
150
|
+
const today = new Date().toISOString().split('T')[0];
|
|
151
|
+
const idRange = Array.from({ length: 5 }, (_, i) => nextIdStart + i);
|
|
152
|
+
const prompt = `You are analyzing the ${getConfig().projectName} codebase to suggest feature ideas. Your ONLY job is to create markdown files.
|
|
153
|
+
|
|
154
|
+
Create exactly 3 idea files in the scopes/icebox/ directory. Each file must use this EXACT format:
|
|
155
|
+
|
|
156
|
+
File: scopes/icebox/{ID}-{kebab-slug}.md
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
id: {ID}
|
|
160
|
+
title: "{title}"
|
|
161
|
+
status: icebox
|
|
162
|
+
ghost: true
|
|
163
|
+
created: ${today}
|
|
164
|
+
updated: ${today}
|
|
165
|
+
blocked_by: []
|
|
166
|
+
blocks: []
|
|
167
|
+
tags: []
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
{2-3 sentence description of the feature, what problem it solves, and a rough approach.}
|
|
171
|
+
|
|
172
|
+
Use these IDs: ${idRange[0]}, ${idRange[1]}, ${idRange[2]}
|
|
173
|
+
|
|
174
|
+
Rules:
|
|
175
|
+
- Focus on practical improvements: performance, UX, security, developer experience, monitoring, or reliability
|
|
176
|
+
- Be specific and actionable — not vague architectural rewrites
|
|
177
|
+
- Keep descriptions concise (2-3 sentences max)
|
|
178
|
+
- Filenames must be {ID}-{kebab-case-slug}.md
|
|
179
|
+
- The ghost: true field is required in frontmatter
|
|
180
|
+
- Do NOT create any other files or make any other changes`;
|
|
181
|
+
const child = spawn('claude', ['-p', prompt, '--output-format', 'text'], {
|
|
182
|
+
cwd: projectRoot,
|
|
183
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
184
|
+
env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: 'orbital-surprise' },
|
|
185
|
+
});
|
|
186
|
+
child.unref();
|
|
187
|
+
child.on('close', () => {
|
|
188
|
+
surpriseInProgress = false;
|
|
189
|
+
const eventId = crypto.randomUUID();
|
|
190
|
+
io.emit('event:new', {
|
|
191
|
+
id: eventId, type: 'AGENT_COMPLETED', scope_id: null,
|
|
192
|
+
session_id: null, agent: 'surprise-me',
|
|
193
|
+
data: { action: 'surprise-ideas-generated' },
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
child.on('error', () => {
|
|
198
|
+
surpriseInProgress = false;
|
|
199
|
+
});
|
|
200
|
+
res.json({ ok: true, status: 'generating' });
|
|
201
|
+
});
|
|
202
|
+
router.post('/ideas/:id/approve', (req, res) => {
|
|
203
|
+
const id = Number(req.params.id);
|
|
204
|
+
const approved = scopeService.approveGhostIdea(id);
|
|
205
|
+
if (!approved) {
|
|
206
|
+
res.status(404).json({ error: 'Ghost idea not found' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
res.json({ ok: true });
|
|
210
|
+
});
|
|
211
|
+
router.get('/ideas/surprise/status', (_req, res) => {
|
|
212
|
+
res.json({ generating: surpriseInProgress });
|
|
213
|
+
});
|
|
214
|
+
return router;
|
|
215
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
export function createSprintRoutes({ sprintService, sprintOrchestrator, batchOrchestrator }) {
|
|
3
|
+
const router = Router();
|
|
4
|
+
router.post('/sprints', (req, res) => {
|
|
5
|
+
const { name, target_column, group_type } = req.body;
|
|
6
|
+
if (!name || !name.trim()) {
|
|
7
|
+
res.status(400).json({ error: 'name is required' });
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const sprint = sprintService.create(name.trim(), { target_column, group_type });
|
|
11
|
+
res.status(201).json(sprint);
|
|
12
|
+
});
|
|
13
|
+
router.get('/sprints', (req, res) => {
|
|
14
|
+
const status = req.query.status;
|
|
15
|
+
const targetColumn = req.query.target_column;
|
|
16
|
+
const sprints = sprintService.getAll(status, targetColumn);
|
|
17
|
+
res.json(sprints);
|
|
18
|
+
});
|
|
19
|
+
router.get('/sprints/:id', (req, res) => {
|
|
20
|
+
const sprint = sprintService.getById(Number(req.params.id));
|
|
21
|
+
if (!sprint) {
|
|
22
|
+
res.status(404).json({ error: 'Sprint not found' });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
res.json(sprint);
|
|
26
|
+
});
|
|
27
|
+
router.patch('/sprints/:id', (req, res) => {
|
|
28
|
+
const { name } = req.body;
|
|
29
|
+
if (!name || !name.trim()) {
|
|
30
|
+
res.status(400).json({ error: 'name is required' });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const renamed = sprintService.rename(Number(req.params.id), name.trim());
|
|
34
|
+
if (!renamed) {
|
|
35
|
+
res.status(400).json({ error: 'Sprint not found or not in assembling state' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
res.json({ ok: true });
|
|
39
|
+
});
|
|
40
|
+
router.delete('/sprints/:id', (req, res) => {
|
|
41
|
+
const deleted = sprintService.delete(Number(req.params.id));
|
|
42
|
+
if (!deleted) {
|
|
43
|
+
res.status(400).json({ error: 'Cannot delete sprint (must be assembling)' });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
res.json({ ok: true });
|
|
47
|
+
});
|
|
48
|
+
router.post('/sprints/:id/scopes', (req, res) => {
|
|
49
|
+
const { scope_ids } = req.body;
|
|
50
|
+
if (!Array.isArray(scope_ids) || scope_ids.length === 0) {
|
|
51
|
+
res.status(400).json({ error: 'scope_ids must be a non-empty array' });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const result = sprintService.addScopes(Number(req.params.id), scope_ids);
|
|
55
|
+
if (!result) {
|
|
56
|
+
res.status(400).json({ error: 'Sprint not found or not in assembling state' });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
res.json(result);
|
|
60
|
+
});
|
|
61
|
+
router.delete('/sprints/:id/scopes', (req, res) => {
|
|
62
|
+
const { scope_ids } = req.body;
|
|
63
|
+
if (!Array.isArray(scope_ids) || scope_ids.length === 0) {
|
|
64
|
+
res.status(400).json({ error: 'scope_ids must be a non-empty array' });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const removed = sprintService.removeScopes(Number(req.params.id), scope_ids);
|
|
68
|
+
if (!removed) {
|
|
69
|
+
res.status(400).json({ error: 'Sprint not found or not in assembling state' });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
res.json({ ok: true });
|
|
73
|
+
});
|
|
74
|
+
router.post('/sprints/:id/dispatch', async (req, res) => {
|
|
75
|
+
const id = Number(req.params.id);
|
|
76
|
+
const sprint = sprintService.getById(id);
|
|
77
|
+
if (!sprint) {
|
|
78
|
+
res.status(404).json({ error: 'Sprint not found' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (sprint.group_type === 'batch') {
|
|
82
|
+
const { mergeMode } = req.body;
|
|
83
|
+
const result = await batchOrchestrator.dispatch(id, mergeMode);
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
res.status(400).json({ error: result.error });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
res.json({ ok: true });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const result = await sprintOrchestrator.startSprint(id);
|
|
92
|
+
if (!result.ok) {
|
|
93
|
+
res.status(400).json({ error: result.error });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
res.json({ ok: true, layers: result.layers });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
router.post('/sprints/:id/cancel', (req, res) => {
|
|
100
|
+
const cancelled = sprintOrchestrator.cancelSprint(Number(req.params.id));
|
|
101
|
+
if (!cancelled) {
|
|
102
|
+
res.status(400).json({ error: 'Sprint not found or cannot be cancelled' });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
res.json({ ok: true });
|
|
106
|
+
});
|
|
107
|
+
router.get('/sprints/:id/graph', (req, res) => {
|
|
108
|
+
const graph = sprintOrchestrator.getExecutionGraph(Number(req.params.id));
|
|
109
|
+
if (!graph) {
|
|
110
|
+
res.status(404).json({ error: 'Sprint not found' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
res.json(graph);
|
|
114
|
+
});
|
|
115
|
+
return router;
|
|
116
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { execFile } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { createLogger } from '../utils/logger.js';
|
|
8
|
+
const log = createLogger('version');
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
/** Resolve the root directory of the orbital-command package itself. */
|
|
11
|
+
function getOrbitalRoot() {
|
|
12
|
+
const __selfDir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
// Walk up until we find package.json (handles both dev and compiled paths)
|
|
14
|
+
let dir = __selfDir;
|
|
15
|
+
for (let i = 0; i < 6; i++) {
|
|
16
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
dir = path.dirname(dir);
|
|
20
|
+
}
|
|
21
|
+
// Fallback: assume dev layout (server/routes/ → 2 levels up)
|
|
22
|
+
return path.resolve(__selfDir, '../..');
|
|
23
|
+
}
|
|
24
|
+
async function git(args, cwd, timeoutMs = 15_000) {
|
|
25
|
+
const { stdout } = await execFileAsync('git', args, { cwd, timeout: timeoutMs });
|
|
26
|
+
return stdout.trim();
|
|
27
|
+
}
|
|
28
|
+
export function createVersionRoutes({ io }) {
|
|
29
|
+
const router = Router();
|
|
30
|
+
const orbitalRoot = getOrbitalRoot();
|
|
31
|
+
// GET /version — current version info
|
|
32
|
+
router.get('/version', async (_req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const pkgPath = path.join(orbitalRoot, 'package.json');
|
|
35
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
36
|
+
let commitSha = 'unknown';
|
|
37
|
+
let branch = 'unknown';
|
|
38
|
+
try {
|
|
39
|
+
commitSha = await git(['rev-parse', '--short', 'HEAD'], orbitalRoot);
|
|
40
|
+
branch = await git(['rev-parse', '--abbrev-ref', 'HEAD'], orbitalRoot);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Not a git repo (installed via npm) — version only
|
|
44
|
+
}
|
|
45
|
+
res.json({
|
|
46
|
+
version: pkg.version,
|
|
47
|
+
commitSha,
|
|
48
|
+
branch,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
log.error('Version route error', { error: err.message });
|
|
53
|
+
res.status(500).json({ error: `Failed to read version: ${err.message}` });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
// GET /version/check — fetch from remote and compare SHAs
|
|
57
|
+
router.get('/version/check', async (_req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
const branch = await git(['rev-parse', '--abbrev-ref', 'HEAD'], orbitalRoot);
|
|
60
|
+
await git(['fetch', 'origin', branch, '--quiet'], orbitalRoot);
|
|
61
|
+
const localSha = await git(['rev-parse', 'HEAD'], orbitalRoot);
|
|
62
|
+
const remoteSha = await git(['rev-parse', `origin/${branch}`], orbitalRoot);
|
|
63
|
+
let behindCount = 0;
|
|
64
|
+
if (localSha !== remoteSha) {
|
|
65
|
+
const countStr = await git(['rev-list', '--count', `HEAD..origin/${branch}`], orbitalRoot);
|
|
66
|
+
behindCount = parseInt(countStr, 10) || 0;
|
|
67
|
+
}
|
|
68
|
+
res.json({
|
|
69
|
+
updateAvailable: behindCount > 0,
|
|
70
|
+
behindCount,
|
|
71
|
+
localSha: localSha.slice(0, 7),
|
|
72
|
+
remoteSha: remoteSha.slice(0, 7),
|
|
73
|
+
branch,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
log.error('Version route error', { error: err.message });
|
|
78
|
+
res.status(500).json({ error: `Failed to check for updates: ${err.message}` });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// POST /version/update — git pull + npm install
|
|
82
|
+
router.post('/version/update', async (req, res) => {
|
|
83
|
+
if (req.headers['x-orbital-action'] !== 'update') {
|
|
84
|
+
res.status(403).json({ error: 'Missing required X-Orbital-Action header' });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
let stage = 'guard';
|
|
88
|
+
try {
|
|
89
|
+
// Guard: refuse if working tree is dirty
|
|
90
|
+
const status = await git(['status', '--porcelain'], orbitalRoot);
|
|
91
|
+
if (status.length > 0) {
|
|
92
|
+
res.status(409).json({
|
|
93
|
+
error: 'Working tree has uncommitted changes. Commit or stash before updating.',
|
|
94
|
+
dirty: true,
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
stage = 'pulling';
|
|
99
|
+
io.emit('version:updating', { stage });
|
|
100
|
+
await git(['pull', '--ff-only'], orbitalRoot);
|
|
101
|
+
stage = 'installing';
|
|
102
|
+
io.emit('version:updating', { stage });
|
|
103
|
+
await execFileAsync('npm', ['install'], {
|
|
104
|
+
cwd: orbitalRoot,
|
|
105
|
+
timeout: 120_000,
|
|
106
|
+
});
|
|
107
|
+
io.emit('version:updated', { success: true });
|
|
108
|
+
// Read updated version
|
|
109
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(orbitalRoot, 'package.json'), 'utf-8'));
|
|
110
|
+
const commitSha = await git(['rev-parse', '--short', 'HEAD'], orbitalRoot);
|
|
111
|
+
res.json({
|
|
112
|
+
success: true,
|
|
113
|
+
version: pkg.version,
|
|
114
|
+
commitSha,
|
|
115
|
+
message: 'Update complete. Restart the server to apply changes.',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
log.error('Version route error', { error: err.message });
|
|
120
|
+
const recovery = stage === 'installing'
|
|
121
|
+
? ' Git pull succeeded — run `npm install` manually to finish.'
|
|
122
|
+
: stage === 'pulling'
|
|
123
|
+
? ' No files were changed — safe to retry.'
|
|
124
|
+
: '';
|
|
125
|
+
io.emit('version:updated', { success: false, error: err.message });
|
|
126
|
+
res.status(500).json({ error: `Update failed at stage "${stage}": ${err.message}.${recovery}` });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return router;
|
|
130
|
+
}
|