orbital-command 0.1.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/orbital.js +676 -53
- package/dist/assets/PrimitivesConfig-CrmQXYh4.js +32 -0
- package/dist/assets/QualityGates-BbasOsF3.js +21 -0
- package/dist/assets/SessionTimeline-CGeJsVvy.js +1 -0
- package/dist/assets/Settings-oiM496mc.js +12 -0
- package/dist/assets/SourceControl-B1fP2nJL.js +41 -0
- package/dist/assets/WorkflowVisualizer-CWLYf-f0.js +74 -0
- package/dist/assets/arrow-down-CPy85_J6.js +6 -0
- package/dist/assets/charts-DbDg0Psc.js +68 -0
- package/dist/assets/circle-x-Cwz6ZQDV.js +6 -0
- package/dist/assets/file-text-C46Xr65c.js +6 -0
- package/dist/assets/formatDistanceToNow-BMqsSP44.js +1 -0
- package/dist/assets/globe-Cn2yNZUD.js +6 -0
- package/dist/assets/index-Aj4sV8Al.css +1 -0
- package/dist/assets/index-Bc9dK3MW.js +354 -0
- package/dist/assets/key-OPaNTWJ5.js +6 -0
- package/dist/assets/minus-GMsbpKym.js +6 -0
- package/dist/assets/shield-DwAFkDYI.js +6 -0
- package/dist/assets/ui-BmsSg9jU.js +53 -0
- package/dist/assets/useWorkflowEditor-BJkTX_NR.js +16 -0
- package/dist/assets/{vendor-Dzv9lrRc.js → vendor-Bqt8AJn2.js} +1 -1
- package/dist/assets/zap-DfbUoOty.js +11 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +6 -5
- package/dist/server/server/__tests__/data-routes.test.js +124 -0
- package/dist/server/server/__tests__/helpers/db.js +17 -0
- package/dist/server/server/__tests__/helpers/mock-emitter.js +8 -0
- package/dist/server/server/__tests__/scope-routes.test.js +137 -0
- package/dist/server/server/__tests__/sprint-routes.test.js +102 -0
- package/dist/server/server/__tests__/workflow-routes.test.js +107 -0
- package/dist/server/server/config-migrator.js +138 -0
- package/dist/server/server/config.js +17 -2
- package/dist/server/server/database.js +27 -12
- package/dist/server/server/global-config.js +143 -0
- package/dist/server/server/index.js +882 -252
- package/dist/server/server/init.js +579 -194
- package/dist/server/server/launch.js +29 -0
- package/dist/server/server/manifest-types.js +8 -0
- package/dist/server/server/manifest.js +454 -0
- package/dist/server/server/migrate-legacy.js +229 -0
- package/dist/server/server/parsers/event-parser.test.js +117 -0
- package/dist/server/server/parsers/scope-parser.js +74 -28
- package/dist/server/server/parsers/scope-parser.test.js +230 -0
- package/dist/server/server/project-context.js +255 -0
- package/dist/server/server/project-emitter.js +41 -0
- package/dist/server/server/project-manager.js +297 -0
- package/dist/server/server/routes/config-routes.js +1 -3
- package/dist/server/server/routes/data-routes.js +22 -110
- package/dist/server/server/routes/dispatch-routes.js +15 -9
- package/dist/server/server/routes/git-routes.js +74 -0
- package/dist/server/server/routes/manifest-routes.js +319 -0
- package/dist/server/server/routes/scope-routes.js +37 -23
- package/dist/server/server/routes/sync-routes.js +134 -0
- package/dist/server/server/routes/version-routes.js +1 -15
- package/dist/server/server/routes/workflow-routes.js +9 -3
- package/dist/server/server/schema.js +2 -0
- package/dist/server/server/services/batch-orchestrator.js +26 -16
- package/dist/server/server/services/claude-session-service.js +17 -14
- package/dist/server/server/services/deploy-service.test.js +119 -0
- package/dist/server/server/services/event-service.js +64 -1
- package/dist/server/server/services/event-service.test.js +191 -0
- package/dist/server/server/services/gate-service.test.js +105 -0
- package/dist/server/server/services/git-service.js +108 -4
- package/dist/server/server/services/github-service.js +110 -2
- package/dist/server/server/services/readiness-service.test.js +190 -0
- package/dist/server/server/services/scope-cache.js +5 -1
- package/dist/server/server/services/scope-cache.test.js +142 -0
- package/dist/server/server/services/scope-service.js +217 -126
- package/dist/server/server/services/scope-service.test.js +137 -0
- package/dist/server/server/services/sprint-orchestrator.js +7 -6
- package/dist/server/server/services/sprint-service.js +21 -1
- package/dist/server/server/services/sprint-service.test.js +238 -0
- package/dist/server/server/services/sync-service.js +434 -0
- package/dist/server/server/services/sync-types.js +2 -0
- package/dist/server/server/services/telemetry-service.js +143 -0
- package/dist/server/server/services/workflow-service.js +26 -5
- package/dist/server/server/services/workflow-service.test.js +159 -0
- package/dist/server/server/settings-sync.js +284 -0
- package/dist/server/server/update-planner.js +279 -0
- package/dist/server/server/utils/cc-hooks-parser.js +3 -0
- package/dist/server/server/utils/cc-hooks-parser.test.js +86 -0
- package/dist/server/server/utils/dispatch-utils.js +77 -20
- package/dist/server/server/utils/dispatch-utils.test.js +182 -0
- package/dist/server/server/utils/logger.js +37 -3
- package/dist/server/server/utils/package-info.js +30 -0
- package/dist/server/server/utils/route-helpers.js +10 -0
- package/dist/server/server/utils/terminal-launcher.js +79 -25
- package/dist/server/server/utils/worktree-manager.js +13 -4
- package/dist/server/server/validator.js +230 -0
- package/dist/server/server/watchers/global-watcher.js +63 -0
- package/dist/server/server/watchers/scope-watcher.js +27 -12
- package/dist/server/server/wizard/config-editor.js +237 -0
- package/dist/server/server/wizard/detect.js +96 -0
- package/dist/server/server/wizard/doctor.js +115 -0
- package/dist/server/server/wizard/index.js +155 -0
- package/dist/server/server/wizard/phases/confirm.js +39 -0
- package/dist/server/server/wizard/phases/project-setup.js +90 -0
- package/dist/server/server/wizard/phases/setup-wizard.js +66 -0
- package/dist/server/server/wizard/phases/welcome.js +35 -0
- package/dist/server/server/wizard/phases/workflow-setup.js +22 -0
- package/dist/server/server/wizard/types.js +29 -0
- package/dist/server/server/wizard/ui.js +74 -0
- package/dist/server/shared/__fixtures__/workflow-configs.js +75 -0
- package/dist/server/shared/default-workflow.json +65 -0
- package/dist/server/shared/onboarding-tour.test.js +81 -0
- package/dist/server/shared/project-colors.js +24 -0
- package/dist/server/shared/workflow-config.test.js +84 -0
- package/dist/server/shared/workflow-engine.test.js +302 -0
- package/dist/server/shared/workflow-normalizer.js +101 -0
- package/dist/server/shared/workflow-normalizer.test.js +100 -0
- package/dist/server/src/components/onboarding/tour-steps.js +84 -0
- package/package.json +20 -15
- package/schemas/orbital.config.schema.json +16 -1
- package/scripts/postinstall.js +55 -7
- package/server/__tests__/data-routes.test.ts +149 -0
- package/server/__tests__/helpers/db.ts +19 -0
- package/server/__tests__/helpers/mock-emitter.ts +10 -0
- package/server/__tests__/scope-routes.test.ts +157 -0
- package/server/__tests__/sprint-routes.test.ts +118 -0
- package/server/__tests__/workflow-routes.test.ts +120 -0
- package/server/config-migrator.ts +163 -0
- package/server/config.ts +26 -2
- package/server/database.ts +35 -18
- package/server/global-config.ts +200 -0
- package/server/index.ts +975 -287
- package/server/init.ts +625 -182
- package/server/launch.ts +32 -0
- package/server/manifest-types.ts +145 -0
- package/server/manifest.ts +494 -0
- package/server/migrate-legacy.ts +290 -0
- package/server/parsers/event-parser.test.ts +135 -0
- package/server/parsers/scope-parser.test.ts +270 -0
- package/server/parsers/scope-parser.ts +79 -31
- package/server/project-context.ts +309 -0
- package/server/project-emitter.ts +50 -0
- package/server/project-manager.ts +369 -0
- package/server/routes/config-routes.ts +3 -5
- package/server/routes/data-routes.ts +28 -141
- package/server/routes/dispatch-routes.ts +19 -11
- package/server/routes/git-routes.ts +77 -0
- package/server/routes/manifest-routes.ts +388 -0
- package/server/routes/scope-routes.ts +29 -25
- package/server/routes/sync-routes.ts +175 -0
- package/server/routes/version-routes.ts +1 -16
- package/server/routes/workflow-routes.ts +9 -3
- package/server/schema.ts +2 -0
- package/server/services/batch-orchestrator.ts +24 -16
- package/server/services/claude-session-service.ts +16 -14
- package/server/services/deploy-service.test.ts +145 -0
- package/server/services/deploy-service.ts +2 -2
- package/server/services/event-service.test.ts +242 -0
- package/server/services/event-service.ts +92 -3
- package/server/services/gate-service.test.ts +131 -0
- package/server/services/gate-service.ts +2 -2
- package/server/services/git-service.ts +137 -4
- package/server/services/github-service.ts +120 -2
- package/server/services/readiness-service.test.ts +217 -0
- package/server/services/scope-cache.test.ts +167 -0
- package/server/services/scope-cache.ts +4 -1
- package/server/services/scope-service.test.ts +169 -0
- package/server/services/scope-service.ts +220 -126
- package/server/services/sprint-orchestrator.ts +7 -7
- package/server/services/sprint-service.test.ts +271 -0
- package/server/services/sprint-service.ts +27 -3
- package/server/services/sync-service.ts +482 -0
- package/server/services/sync-types.ts +77 -0
- package/server/services/telemetry-service.ts +195 -0
- package/server/services/workflow-service.test.ts +190 -0
- package/server/services/workflow-service.ts +29 -9
- package/server/settings-sync.ts +359 -0
- package/server/update-planner.ts +346 -0
- package/server/utils/cc-hooks-parser.test.ts +96 -0
- package/server/utils/cc-hooks-parser.ts +4 -0
- package/server/utils/dispatch-utils.test.ts +245 -0
- package/server/utils/dispatch-utils.ts +97 -27
- package/server/utils/logger.ts +40 -3
- package/server/utils/package-info.ts +32 -0
- package/server/utils/route-helpers.ts +12 -0
- package/server/utils/terminal-launcher.ts +85 -25
- package/server/utils/worktree-manager.ts +9 -4
- package/server/validator.ts +270 -0
- package/server/watchers/global-watcher.ts +77 -0
- package/server/watchers/scope-watcher.ts +21 -9
- package/server/wizard/config-editor.ts +248 -0
- package/server/wizard/detect.ts +104 -0
- package/server/wizard/doctor.ts +114 -0
- package/server/wizard/index.ts +187 -0
- package/server/wizard/phases/confirm.ts +45 -0
- package/server/wizard/phases/project-setup.ts +106 -0
- package/server/wizard/phases/setup-wizard.ts +78 -0
- package/server/wizard/phases/welcome.ts +43 -0
- package/server/wizard/phases/workflow-setup.ts +28 -0
- package/server/wizard/types.ts +56 -0
- package/server/wizard/ui.ts +93 -0
- package/shared/__fixtures__/workflow-configs.ts +80 -0
- package/shared/default-workflow.json +65 -0
- package/shared/onboarding-tour.test.ts +94 -0
- package/shared/project-colors.ts +24 -0
- package/shared/workflow-config.test.ts +111 -0
- package/shared/workflow-config.ts +7 -0
- package/shared/workflow-engine.test.ts +388 -0
- package/shared/workflow-normalizer.test.ts +119 -0
- package/shared/workflow-normalizer.ts +118 -0
- package/templates/hooks/end-session.sh +3 -1
- package/templates/hooks/orbital-emit.sh +2 -2
- package/templates/hooks/orbital-report-deploy.sh +4 -4
- package/templates/hooks/orbital-report-gates.sh +4 -4
- package/templates/hooks/orbital-scope-update.sh +1 -1
- package/templates/hooks/scope-create-cleanup.sh +2 -2
- package/templates/hooks/scope-create-gate.sh +0 -1
- package/templates/hooks/scope-helpers.sh +18 -0
- package/templates/hooks/scope-prepare.sh +66 -11
- package/templates/migrations/renames.json +1 -0
- package/templates/orbital.config.json +7 -2
- package/templates/settings-hooks.json +1 -1
- package/templates/skills/git-commit/SKILL.md +9 -4
- package/templates/skills/git-dev/SKILL.md +8 -3
- package/templates/skills/git-main/SKILL.md +8 -2
- package/templates/skills/git-production/SKILL.md +6 -2
- package/templates/skills/git-staging/SKILL.md +8 -3
- package/templates/skills/scope-create/SKILL.md +17 -3
- package/templates/skills/scope-fix-review/SKILL.md +6 -3
- package/templates/skills/scope-implement/SKILL.md +4 -1
- package/templates/skills/scope-post-review/SKILL.md +63 -5
- package/templates/skills/scope-pre-review/SKILL.md +5 -2
- package/templates/skills/scope-verify/SKILL.md +5 -3
- package/templates/skills/test-code-review/SKILL.md +41 -33
- package/templates/skills/test-scaffold/SKILL.md +222 -0
- package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +0 -84
- package/dist/assets/charts-D__PA1zp.js +0 -72
- package/dist/assets/index-D1G6i0nS.css +0 -1
- package/dist/assets/index-DpItvKpf.js +0 -419
- package/dist/assets/ui-BvF022GT.js +0 -53
- package/index.html +0 -15
- package/postcss.config.js +0 -6
- package/src/App.tsx +0 -33
- package/src/components/AgentBadge.tsx +0 -40
- package/src/components/BatchPreflightModal.tsx +0 -115
- package/src/components/CardDisplayToggle.tsx +0 -74
- package/src/components/ColumnHeaderActions.tsx +0 -55
- package/src/components/ColumnMenu.tsx +0 -99
- package/src/components/DeployHistory.tsx +0 -141
- package/src/components/DispatchModal.tsx +0 -164
- package/src/components/DispatchPopover.tsx +0 -139
- package/src/components/DragOverlay.tsx +0 -25
- package/src/components/DriftSidebar.tsx +0 -140
- package/src/components/EnvironmentStrip.tsx +0 -88
- package/src/components/ErrorBoundary.tsx +0 -62
- package/src/components/FilterChip.tsx +0 -105
- package/src/components/GateIndicator.tsx +0 -33
- package/src/components/IdeaDetailModal.tsx +0 -190
- package/src/components/IdeaFormDialog.tsx +0 -113
- package/src/components/KanbanColumn.tsx +0 -201
- package/src/components/MarkdownRenderer.tsx +0 -114
- package/src/components/NeonGrid.tsx +0 -128
- package/src/components/PromotionQueue.tsx +0 -89
- package/src/components/ScopeCard.tsx +0 -234
- package/src/components/ScopeDetailModal.tsx +0 -255
- package/src/components/ScopeFilterBar.tsx +0 -152
- package/src/components/SearchInput.tsx +0 -102
- package/src/components/SessionPanel.tsx +0 -335
- package/src/components/SprintContainer.tsx +0 -303
- package/src/components/SprintDependencyDialog.tsx +0 -78
- package/src/components/SprintPreflightModal.tsx +0 -138
- package/src/components/StatusBar.tsx +0 -168
- package/src/components/SwimCell.tsx +0 -67
- package/src/components/SwimLaneRow.tsx +0 -94
- package/src/components/SwimlaneBoardView.tsx +0 -108
- package/src/components/VersionBadge.tsx +0 -139
- package/src/components/ViewModeSelector.tsx +0 -114
- package/src/components/config/AgentChip.tsx +0 -53
- package/src/components/config/AgentCreateDialog.tsx +0 -321
- package/src/components/config/AgentEditor.tsx +0 -175
- package/src/components/config/DirectoryTree.tsx +0 -582
- package/src/components/config/FileEditor.tsx +0 -550
- package/src/components/config/HookChip.tsx +0 -50
- package/src/components/config/StageCard.tsx +0 -198
- package/src/components/config/TransitionZone.tsx +0 -173
- package/src/components/config/UnifiedWorkflowPipeline.tsx +0 -216
- package/src/components/config/WorkflowPipeline.tsx +0 -161
- package/src/components/source-control/BranchList.tsx +0 -93
- package/src/components/source-control/BranchPanel.tsx +0 -105
- package/src/components/source-control/CommitLog.tsx +0 -100
- package/src/components/source-control/CommitRow.tsx +0 -47
- package/src/components/source-control/GitHubPanel.tsx +0 -110
- package/src/components/source-control/GitHubSetupGuide.tsx +0 -52
- package/src/components/source-control/GitOverviewBar.tsx +0 -101
- package/src/components/source-control/PullRequestList.tsx +0 -69
- package/src/components/source-control/WorktreeList.tsx +0 -80
- package/src/components/ui/badge.tsx +0 -41
- package/src/components/ui/button.tsx +0 -55
- package/src/components/ui/card.tsx +0 -78
- package/src/components/ui/dialog.tsx +0 -94
- package/src/components/ui/popover.tsx +0 -33
- package/src/components/ui/scroll-area.tsx +0 -54
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/tabs.tsx +0 -52
- package/src/components/ui/toggle-switch.tsx +0 -35
- package/src/components/ui/tooltip.tsx +0 -27
- package/src/components/workflow/AddEdgeDialog.tsx +0 -217
- package/src/components/workflow/AddListDialog.tsx +0 -201
- package/src/components/workflow/ChecklistEditor.tsx +0 -239
- package/src/components/workflow/CommandPrefixManager.tsx +0 -118
- package/src/components/workflow/ConfigSettingsPanel.tsx +0 -189
- package/src/components/workflow/DirectionSelector.tsx +0 -133
- package/src/components/workflow/DispatchConfigPanel.tsx +0 -180
- package/src/components/workflow/EdgeDetailPanel.tsx +0 -236
- package/src/components/workflow/EdgePropertyEditor.tsx +0 -251
- package/src/components/workflow/EditToolbar.tsx +0 -138
- package/src/components/workflow/HookDetailPanel.tsx +0 -250
- package/src/components/workflow/HookExecutionLog.tsx +0 -24
- package/src/components/workflow/HookSourceModal.tsx +0 -129
- package/src/components/workflow/HooksDashboard.tsx +0 -363
- package/src/components/workflow/ListPropertyEditor.tsx +0 -251
- package/src/components/workflow/MigrationPreviewDialog.tsx +0 -237
- package/src/components/workflow/MovementRulesPanel.tsx +0 -188
- package/src/components/workflow/NodeDetailPanel.tsx +0 -245
- package/src/components/workflow/PresetSelector.tsx +0 -414
- package/src/components/workflow/SkillCommandBuilder.tsx +0 -174
- package/src/components/workflow/WorkflowEdgeComponent.tsx +0 -145
- package/src/components/workflow/WorkflowNode.tsx +0 -147
- package/src/components/workflow/graphLayout.ts +0 -186
- package/src/components/workflow/mergeHooks.ts +0 -85
- package/src/components/workflow/useEditHistory.ts +0 -88
- package/src/components/workflow/useWorkflowEditor.ts +0 -262
- package/src/components/workflow/validateConfig.ts +0 -70
- package/src/hooks/useActiveDispatches.ts +0 -198
- package/src/hooks/useBoardSettings.ts +0 -170
- package/src/hooks/useCardDisplay.ts +0 -57
- package/src/hooks/useCcHooks.ts +0 -24
- package/src/hooks/useConfigTree.ts +0 -51
- package/src/hooks/useEnforcementRules.ts +0 -46
- package/src/hooks/useEvents.ts +0 -59
- package/src/hooks/useFileEditor.ts +0 -165
- package/src/hooks/useGates.ts +0 -57
- package/src/hooks/useIdeaActions.ts +0 -53
- package/src/hooks/useKanbanDnd.ts +0 -410
- package/src/hooks/useOrbitalConfig.ts +0 -54
- package/src/hooks/usePipeline.ts +0 -47
- package/src/hooks/usePipelineData.ts +0 -338
- package/src/hooks/useReconnect.ts +0 -25
- package/src/hooks/useScopeFilters.ts +0 -125
- package/src/hooks/useScopeSessions.ts +0 -44
- package/src/hooks/useScopes.ts +0 -67
- package/src/hooks/useSearch.ts +0 -67
- package/src/hooks/useSettings.tsx +0 -187
- package/src/hooks/useSocket.ts +0 -25
- package/src/hooks/useSourceControl.ts +0 -105
- package/src/hooks/useSprintPreflight.ts +0 -55
- package/src/hooks/useSprints.ts +0 -154
- package/src/hooks/useStatusBarHighlight.ts +0 -18
- package/src/hooks/useSwimlaneBoardSettings.ts +0 -104
- package/src/hooks/useTheme.ts +0 -9
- package/src/hooks/useTransitionReadiness.ts +0 -53
- package/src/hooks/useVersion.ts +0 -155
- package/src/hooks/useViolations.ts +0 -65
- package/src/hooks/useWorkflow.tsx +0 -125
- package/src/hooks/useZoomModifier.ts +0 -19
- package/src/index.css +0 -797
- package/src/layouts/DashboardLayout.tsx +0 -113
- package/src/lib/collisionDetection.ts +0 -20
- package/src/lib/scope-fields.ts +0 -61
- package/src/lib/swimlane.ts +0 -146
- package/src/lib/utils.ts +0 -15
- package/src/main.tsx +0 -19
- package/src/socket.ts +0 -11
- package/src/types/index.ts +0 -497
- package/src/views/AgentFeed.tsx +0 -339
- package/src/views/DeployPipeline.tsx +0 -59
- package/src/views/EnforcementView.tsx +0 -378
- package/src/views/PrimitivesConfig.tsx +0 -500
- package/src/views/QualityGates.tsx +0 -1012
- package/src/views/ScopeBoard.tsx +0 -454
- package/src/views/SessionTimeline.tsx +0 -516
- package/src/views/Settings.tsx +0 -183
- package/src/views/SourceControl.tsx +0 -95
- package/src/views/WorkflowVisualizer.tsx +0 -382
- package/tailwind.config.js +0 -161
- package/tsconfig.json +0 -25
- package/vite.config.ts +0 -49
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import type Database from 'better-sqlite3';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Emitter } from '../project-emitter.js';
|
|
4
4
|
import type { ScopeService } from '../services/scope-service.js';
|
|
5
|
-
import { launchInCategorizedTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
5
|
+
import { launchInCategorizedTerminal, escapeForAnsiC, shellQuote, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
6
6
|
import { resolveDispatchEvent, resolveAbandonedDispatchesForScope, getActiveScopeIds, getAbandonedScopeIds, linkPidToDispatch } from '../utils/dispatch-utils.js';
|
|
7
7
|
import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
8
8
|
import { createLogger } from '../utils/logger.js';
|
|
@@ -20,7 +20,7 @@ interface DispatchBody {
|
|
|
20
20
|
|
|
21
21
|
interface DispatchRouteDeps {
|
|
22
22
|
db: Database.Database;
|
|
23
|
-
io:
|
|
23
|
+
io: Emitter;
|
|
24
24
|
scopeService: ScopeService;
|
|
25
25
|
projectRoot: string;
|
|
26
26
|
engine: WorkflowEngine;
|
|
@@ -110,19 +110,23 @@ export function createDispatchRoutes({ db, io, scopeService, projectRoot, engine
|
|
|
110
110
|
// Launch in iTerm — interactive TUI mode (no -p) for full visibility
|
|
111
111
|
const promptText = prompt ?? command;
|
|
112
112
|
const escaped = escapeForAnsiC(promptText);
|
|
113
|
-
const fullCmd = `cd '${projectRoot}' && ORBITAL_DISPATCH_ID='${eventId}' claude --dangerously-skip-permissions $'${escaped}'`;
|
|
113
|
+
const fullCmd = `cd '${shellQuote(projectRoot)}' && ORBITAL_DISPATCH_ID='${shellQuote(eventId)}' claude --dangerously-skip-permissions $'${escaped}'`;
|
|
114
114
|
try {
|
|
115
115
|
await launchInCategorizedTerminal(command, fullCmd, sessionName);
|
|
116
116
|
res.json({ ok: true, dispatch_id: eventId, scope_id: scope_id ?? null });
|
|
117
117
|
|
|
118
|
-
// Fire-and-forget: discover session PID, link to dispatch, and rename
|
|
118
|
+
// Fire-and-forget: discover session PID, link to dispatch, and rename.
|
|
119
|
+
// If discovery fails, SESSION_START event handler will link via ORBITAL_DISPATCH_ID.
|
|
119
120
|
discoverNewSession(projectRoot, beforePids)
|
|
120
121
|
.then((session) => {
|
|
121
|
-
if (!session)
|
|
122
|
+
if (!session) {
|
|
123
|
+
log.warn('PID discovery returned null — dispatch will rely on ORBITAL_DISPATCH_ID for linkage', { dispatch_id: eventId, scope_id });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
122
126
|
linkPidToDispatch(db, eventId, session.pid);
|
|
123
127
|
if (sessionName) renameSession(projectRoot, session.sessionId, sessionName);
|
|
124
128
|
})
|
|
125
|
-
.catch(err => log.
|
|
129
|
+
.catch(err => log.warn('PID discovery failed — dispatch will rely on ORBITAL_DISPATCH_ID for linkage', { dispatch_id: eventId, error: err.message }));
|
|
126
130
|
} catch (err) {
|
|
127
131
|
if (scope_id != null && transition?.from) {
|
|
128
132
|
scopeService.updateStatus(scope_id, transition.from, 'rollback');
|
|
@@ -248,18 +252,22 @@ export function createDispatchRoutes({ db, io, scopeService, projectRoot, engine
|
|
|
248
252
|
// Launch single CLI session
|
|
249
253
|
const batchEscaped = escapeForAnsiC(command);
|
|
250
254
|
const beforePids = snapshotSessionPids(projectRoot);
|
|
251
|
-
const fullCmd = `cd '${projectRoot}' && ORBITAL_DISPATCH_ID='${eventId}' claude --dangerously-skip-permissions -p $'${batchEscaped}'`;
|
|
255
|
+
const fullCmd = `cd '${shellQuote(projectRoot)}' && ORBITAL_DISPATCH_ID='${shellQuote(eventId)}' claude --dangerously-skip-permissions -p $'${batchEscaped}'`;
|
|
252
256
|
try {
|
|
253
257
|
await launchInCategorizedTerminal(command, fullCmd);
|
|
254
258
|
res.json({ ok: true, dispatch_id: eventId, scope_ids });
|
|
255
259
|
|
|
256
|
-
// Fire-and-forget: discover session PID and link to dispatch
|
|
260
|
+
// Fire-and-forget: discover session PID and link to dispatch.
|
|
261
|
+
// If discovery fails, SESSION_START event handler will link via ORBITAL_DISPATCH_ID.
|
|
257
262
|
discoverNewSession(projectRoot, beforePids)
|
|
258
263
|
.then((session) => {
|
|
259
|
-
if (!session)
|
|
264
|
+
if (!session) {
|
|
265
|
+
log.warn('Batch PID discovery returned null — dispatch will rely on ORBITAL_DISPATCH_ID for linkage', { dispatch_id: eventId });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
260
268
|
linkPidToDispatch(db, eventId, session.pid);
|
|
261
269
|
})
|
|
262
|
-
.catch(err => log.
|
|
270
|
+
.catch(err => log.warn('Batch PID discovery failed — dispatch will rely on ORBITAL_DISPATCH_ID for linkage', { dispatch_id: eventId, error: err.message }));
|
|
263
271
|
} catch (err) {
|
|
264
272
|
if (transition?.from) {
|
|
265
273
|
for (const id of scope_ids) {
|
|
@@ -108,5 +108,82 @@ export function createGitRoutes({ gitService, githubService, engine }: GitRoutes
|
|
|
108
108
|
}
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
// ─── GitHub Auth Flow ──────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
router.post('/github/connect', async (req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const { method, token } = req.body as { method?: string; token?: string };
|
|
116
|
+
if (method === 'pat' && token) {
|
|
117
|
+
const result = await githubService.connectWithToken(token);
|
|
118
|
+
res.json(result);
|
|
119
|
+
} else {
|
|
120
|
+
const result = await githubService.connectOAuth();
|
|
121
|
+
res.json(result);
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
res.status(500).json({ error: 'Failed to connect', details: String(err) });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
router.get('/github/auth-status', async (_req, res) => {
|
|
129
|
+
try {
|
|
130
|
+
const status = await githubService.getAuthStatus();
|
|
131
|
+
res.json(status);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
res.status(500).json({ error: 'Failed to check auth', details: String(err) });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
router.post('/github/disconnect', async (_req, res) => {
|
|
138
|
+
try {
|
|
139
|
+
const result = await githubService.disconnect();
|
|
140
|
+
res.json(result);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
res.status(500).json({ error: 'Failed to disconnect', details: String(err) });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ─── GitHub CI Checks ──────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
router.get('/github/checks/:ref', async (req, res) => {
|
|
149
|
+
try {
|
|
150
|
+
const checks = await githubService.getCheckRuns(req.params.ref);
|
|
151
|
+
res.json(checks);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
res.status(500).json({ error: 'Failed to get checks', details: String(err) });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ─── Git Health Metrics ────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
router.get('/git/health', async (_req, res) => {
|
|
160
|
+
try {
|
|
161
|
+
// Get PR ages for health calculation
|
|
162
|
+
let prAges: number[] = [];
|
|
163
|
+
try {
|
|
164
|
+
const prs = await githubService.getOpenPRs();
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
prAges = prs.map(pr => Math.round((now - new Date(pr.createdAt).getTime()) / (1000 * 60 * 60 * 24)));
|
|
167
|
+
} catch { /* ok — GitHub may not be connected */ }
|
|
168
|
+
|
|
169
|
+
const health = await gitService.getHealthMetrics(prAges);
|
|
170
|
+
res.json(health);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
res.status(500).json({ error: 'Failed to get health metrics', details: String(err) });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ─── Git Activity Series ───────────────────────────────────
|
|
177
|
+
|
|
178
|
+
router.get('/git/activity', async (req, res) => {
|
|
179
|
+
try {
|
|
180
|
+
const days = Number(req.query.days) || 30;
|
|
181
|
+
const activity = await gitService.getActivitySeries(days);
|
|
182
|
+
res.json(activity);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
res.status(500).json({ error: 'Failed to get activity', details: String(err) });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
111
188
|
return router;
|
|
112
189
|
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST API routes for the manifest-based primitive management system.
|
|
3
|
+
* Exposes status, validation, update, pin/unpin, reset, and diff operations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import { execFileSync } from 'child_process';
|
|
9
|
+
import { Router } from 'express';
|
|
10
|
+
import {
|
|
11
|
+
loadManifest,
|
|
12
|
+
saveManifest,
|
|
13
|
+
hashFile,
|
|
14
|
+
computeFileStatus,
|
|
15
|
+
refreshFileStatuses,
|
|
16
|
+
summarizeManifest,
|
|
17
|
+
reverseRemapPath,
|
|
18
|
+
safeBackupFile,
|
|
19
|
+
safeCopyTemplate,
|
|
20
|
+
safeRestoreFile,
|
|
21
|
+
} from '../manifest.js';
|
|
22
|
+
import { validate } from '../validator.js';
|
|
23
|
+
import { computeUpdatePlan, loadRenameMap } from '../update-planner.js';
|
|
24
|
+
import { runInit, runUpdate } from '../init.js';
|
|
25
|
+
import { needsLegacyMigration, migrateFromLegacy } from '../migrate-legacy.js';
|
|
26
|
+
import type { Emitter } from '../project-emitter.js';
|
|
27
|
+
import { errMsg, isValidRelativePath } from '../utils/route-helpers.js';
|
|
28
|
+
|
|
29
|
+
// ─── Types ──────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
interface ManifestRouteDeps {
|
|
32
|
+
projectRoot: string;
|
|
33
|
+
templatesDir: string;
|
|
34
|
+
packageVersion: string;
|
|
35
|
+
io: Emitter;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
// ─── Route Factory ──────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export function createManifestRoutes({
|
|
42
|
+
projectRoot,
|
|
43
|
+
templatesDir,
|
|
44
|
+
packageVersion,
|
|
45
|
+
io,
|
|
46
|
+
}: ManifestRouteDeps): Router {
|
|
47
|
+
const router = Router();
|
|
48
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
49
|
+
|
|
50
|
+
// ─── GET /manifest/status — summary overview ────────────
|
|
51
|
+
|
|
52
|
+
router.get('/manifest/status', (_req, res) => {
|
|
53
|
+
try {
|
|
54
|
+
const manifest = loadManifest(projectRoot);
|
|
55
|
+
|
|
56
|
+
if (!manifest) {
|
|
57
|
+
return res.json({
|
|
58
|
+
success: true,
|
|
59
|
+
data: {
|
|
60
|
+
exists: false,
|
|
61
|
+
packageVersion,
|
|
62
|
+
installedVersion: '',
|
|
63
|
+
needsUpdate: true,
|
|
64
|
+
preset: '',
|
|
65
|
+
files: { total: 0, synced: 0, modified: 0, pinned: 0, userOwned: 0, byType: {} },
|
|
66
|
+
lastUpdated: '',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
refreshFileStatuses(manifest, claudeDir);
|
|
72
|
+
const summary = summarizeManifest(manifest);
|
|
73
|
+
|
|
74
|
+
res.json({
|
|
75
|
+
success: true,
|
|
76
|
+
data: {
|
|
77
|
+
exists: true,
|
|
78
|
+
packageVersion,
|
|
79
|
+
installedVersion: manifest.packageVersion,
|
|
80
|
+
needsUpdate: manifest.packageVersion !== packageVersion,
|
|
81
|
+
preset: manifest.preset,
|
|
82
|
+
files: summary,
|
|
83
|
+
lastUpdated: manifest.updatedAt,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ─── GET /manifest/files — file inventory ───────────────
|
|
92
|
+
|
|
93
|
+
router.get('/manifest/files', (_req, res) => {
|
|
94
|
+
try {
|
|
95
|
+
const manifest = loadManifest(projectRoot);
|
|
96
|
+
if (!manifest) {
|
|
97
|
+
return res.json({ success: true, data: [] });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
refreshFileStatuses(manifest, claudeDir);
|
|
101
|
+
|
|
102
|
+
const files = Object.entries(manifest.files).map(([filePath, record]) => ({
|
|
103
|
+
path: filePath,
|
|
104
|
+
origin: record.origin,
|
|
105
|
+
status: record.status,
|
|
106
|
+
templateHash: record.templateHash,
|
|
107
|
+
installedHash: record.installedHash,
|
|
108
|
+
pinnedAt: record.pinnedAt,
|
|
109
|
+
pinnedReason: record.pinnedReason,
|
|
110
|
+
hasPrev: fs.existsSync(path.join(claudeDir, filePath + '.prev')),
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
res.json({ success: true, data: files });
|
|
114
|
+
} catch (err) {
|
|
115
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ─── GET /manifest/validate — run validation ────────────
|
|
120
|
+
|
|
121
|
+
router.get('/manifest/validate', (_req, res) => {
|
|
122
|
+
try {
|
|
123
|
+
const report = validate(projectRoot, packageVersion);
|
|
124
|
+
res.json({ success: true, data: report });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ─── POST /manifest/init — initialize manifest ───────────
|
|
131
|
+
|
|
132
|
+
router.post('/manifest/init', (_req, res) => {
|
|
133
|
+
try {
|
|
134
|
+
// If manifest already exists, just return success
|
|
135
|
+
if (loadManifest(projectRoot)) {
|
|
136
|
+
return res.json({ success: true, message: 'Already initialized' });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// If legacy install exists, migrate it
|
|
140
|
+
if (needsLegacyMigration(projectRoot)) {
|
|
141
|
+
migrateFromLegacy(projectRoot, templatesDir, packageVersion);
|
|
142
|
+
io.emit('manifest:changed', { action: 'initialized' });
|
|
143
|
+
return res.json({ success: true });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// No existing install at all — run full init
|
|
147
|
+
runInit(projectRoot, { force: false });
|
|
148
|
+
io.emit('manifest:changed', { action: 'initialized' });
|
|
149
|
+
res.json({ success: true });
|
|
150
|
+
} catch (err) {
|
|
151
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ─── POST /manifest/update — run update or dry-run ──────
|
|
156
|
+
|
|
157
|
+
router.post('/manifest/update', (req, res) => {
|
|
158
|
+
const { dryRun = true } = req.body as { dryRun?: boolean };
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// Ensure manifest exists (migrate legacy if needed)
|
|
162
|
+
let manifest = loadManifest(projectRoot);
|
|
163
|
+
if (!manifest && needsLegacyMigration(projectRoot)) {
|
|
164
|
+
migrateFromLegacy(projectRoot, templatesDir, packageVersion);
|
|
165
|
+
manifest = loadManifest(projectRoot);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!manifest) {
|
|
169
|
+
return res.status(400).json({ success: false, error: 'No manifest. Run orbital init first.' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (dryRun) {
|
|
173
|
+
refreshFileStatuses(manifest, claudeDir);
|
|
174
|
+
const renameMap = loadRenameMap(templatesDir, manifest.packageVersion, packageVersion);
|
|
175
|
+
const plan = computeUpdatePlan({
|
|
176
|
+
templatesDir,
|
|
177
|
+
claudeDir,
|
|
178
|
+
manifest,
|
|
179
|
+
newVersion: packageVersion,
|
|
180
|
+
renameMap,
|
|
181
|
+
});
|
|
182
|
+
return res.json({ success: true, data: plan });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Execute actual update
|
|
186
|
+
runUpdate(projectRoot, { dryRun: false });
|
|
187
|
+
io.emit('manifest:changed', { action: 'updated' });
|
|
188
|
+
res.json({ success: true });
|
|
189
|
+
} catch (err) {
|
|
190
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ─── POST /manifest/pin — pin a file ───────────────────
|
|
195
|
+
|
|
196
|
+
router.post('/manifest/pin', (req, res) => {
|
|
197
|
+
const { file, reason } = req.body as { file: string; reason?: string };
|
|
198
|
+
if (!file || !isValidRelativePath(file)) {
|
|
199
|
+
return res.status(400).json({ success: false, error: 'Valid file path required' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const manifest = loadManifest(projectRoot);
|
|
204
|
+
if (!manifest) return res.status(400).json({ success: false, error: 'No manifest' });
|
|
205
|
+
|
|
206
|
+
const record = manifest.files[file];
|
|
207
|
+
if (!record) return res.status(404).json({ success: false, error: 'File not tracked' });
|
|
208
|
+
if (record.origin === 'user') return res.status(400).json({ success: false, error: 'Cannot pin user-owned file' });
|
|
209
|
+
|
|
210
|
+
record.status = 'pinned';
|
|
211
|
+
record.pinnedAt = new Date().toISOString();
|
|
212
|
+
if (reason) record.pinnedReason = reason;
|
|
213
|
+
|
|
214
|
+
saveManifest(projectRoot, manifest);
|
|
215
|
+
io.emit('manifest:changed', { action: 'pinned', file });
|
|
216
|
+
res.json({ success: true });
|
|
217
|
+
} catch (err) {
|
|
218
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ─── POST /manifest/unpin — unpin a file ────────────────
|
|
223
|
+
|
|
224
|
+
router.post('/manifest/unpin', (req, res) => {
|
|
225
|
+
const { file } = req.body as { file: string };
|
|
226
|
+
if (!file || !isValidRelativePath(file)) {
|
|
227
|
+
return res.status(400).json({ success: false, error: 'Valid file path required' });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const manifest = loadManifest(projectRoot);
|
|
232
|
+
if (!manifest) return res.status(400).json({ success: false, error: 'No manifest' });
|
|
233
|
+
|
|
234
|
+
const record = manifest.files[file];
|
|
235
|
+
if (!record || record.status !== 'pinned') {
|
|
236
|
+
return res.status(400).json({ success: false, error: 'File is not pinned' });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Clear pinned state before recomputing status
|
|
240
|
+
record.status = 'synced';
|
|
241
|
+
delete record.pinnedAt;
|
|
242
|
+
delete record.pinnedReason;
|
|
243
|
+
|
|
244
|
+
const absPath = path.join(claudeDir, file);
|
|
245
|
+
if (fs.existsSync(absPath)) {
|
|
246
|
+
const currentHash = hashFile(absPath);
|
|
247
|
+
record.status = computeFileStatus(record, currentHash);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
saveManifest(projectRoot, manifest);
|
|
251
|
+
io.emit('manifest:changed', { action: 'unpinned', file });
|
|
252
|
+
res.json({ success: true });
|
|
253
|
+
} catch (err) {
|
|
254
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ─── POST /manifest/reset — reset file to template ──────
|
|
259
|
+
|
|
260
|
+
router.post('/manifest/reset', (req, res) => {
|
|
261
|
+
const { file } = req.body as { file: string };
|
|
262
|
+
if (!file || !isValidRelativePath(file)) {
|
|
263
|
+
return res.status(400).json({ success: false, error: 'Valid file path required' });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const manifest = loadManifest(projectRoot);
|
|
268
|
+
if (!manifest) return res.status(400).json({ success: false, error: 'No manifest' });
|
|
269
|
+
|
|
270
|
+
const record = manifest.files[file];
|
|
271
|
+
if (!record || record.origin !== 'template') {
|
|
272
|
+
return res.status(400).json({ success: false, error: 'Not a template file' });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Resolve template source path
|
|
276
|
+
const templateRelPath = reverseRemapPath(file);
|
|
277
|
+
const templatePath = path.join(templatesDir, templateRelPath);
|
|
278
|
+
if (!fs.existsSync(templatePath)) {
|
|
279
|
+
return res.status(404).json({ success: false, error: 'Template file not found' });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const localPath = path.join(claudeDir, file);
|
|
283
|
+
|
|
284
|
+
// Back up current version so user can revert (symlink-safe)
|
|
285
|
+
safeBackupFile(localPath);
|
|
286
|
+
|
|
287
|
+
// Copy template to destination (skips if symlink)
|
|
288
|
+
safeCopyTemplate(templatePath, localPath);
|
|
289
|
+
|
|
290
|
+
const newHash = hashFile(localPath);
|
|
291
|
+
record.status = 'synced';
|
|
292
|
+
record.templateHash = newHash;
|
|
293
|
+
record.installedHash = newHash;
|
|
294
|
+
delete record.pinnedAt;
|
|
295
|
+
delete record.pinnedReason;
|
|
296
|
+
|
|
297
|
+
saveManifest(projectRoot, manifest);
|
|
298
|
+
io.emit('manifest:changed', { action: 'reset', file });
|
|
299
|
+
res.json({ success: true });
|
|
300
|
+
} catch (err) {
|
|
301
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// ─── POST /manifest/revert — restore file from .prev backup ──
|
|
306
|
+
|
|
307
|
+
router.post('/manifest/revert', (req, res) => {
|
|
308
|
+
const { file } = req.body as { file: string };
|
|
309
|
+
if (!file || !isValidRelativePath(file)) {
|
|
310
|
+
return res.status(400).json({ success: false, error: 'Valid file path required' });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const manifest = loadManifest(projectRoot);
|
|
315
|
+
if (!manifest) return res.status(400).json({ success: false, error: 'No manifest' });
|
|
316
|
+
|
|
317
|
+
const record = manifest.files[file];
|
|
318
|
+
if (!record) return res.status(404).json({ success: false, error: 'File not tracked' });
|
|
319
|
+
|
|
320
|
+
const localPath = path.join(claudeDir, file);
|
|
321
|
+
|
|
322
|
+
if (!safeRestoreFile(localPath)) {
|
|
323
|
+
return res.status(404).json({ success: false, error: 'No previous version available' });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Recompute status — file may now be a symlink or regular file
|
|
327
|
+
if (fs.existsSync(localPath)) {
|
|
328
|
+
const stat = fs.lstatSync(localPath);
|
|
329
|
+
if (stat.isSymbolicLink()) {
|
|
330
|
+
record.status = 'synced'; // restored symlink points at template
|
|
331
|
+
} else {
|
|
332
|
+
const currentHash = hashFile(localPath);
|
|
333
|
+
record.installedHash = currentHash;
|
|
334
|
+
record.status = computeFileStatus(record, currentHash);
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
record.status = 'missing';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
saveManifest(projectRoot, manifest);
|
|
341
|
+
io.emit('manifest:changed', { action: 'reverted', file });
|
|
342
|
+
res.json({ success: true });
|
|
343
|
+
} catch (err) {
|
|
344
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ─── GET /manifest/diff — diff template vs local ─────────
|
|
349
|
+
|
|
350
|
+
router.get('/manifest/diff', (req, res) => {
|
|
351
|
+
const file = req.query.file as string;
|
|
352
|
+
if (!file || !isValidRelativePath(file)) {
|
|
353
|
+
return res.status(400).json({ success: false, error: 'Valid file path required' });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const templateRelPath = reverseRemapPath(file);
|
|
358
|
+
const rawTemplatePath = path.join(templatesDir, templateRelPath);
|
|
359
|
+
// Resolve symlinks so git diff compares file content, not symlink metadata
|
|
360
|
+
const templatePath = fs.existsSync(rawTemplatePath) ? fs.realpathSync(rawTemplatePath) : rawTemplatePath;
|
|
361
|
+
const localPath = path.join(claudeDir, file);
|
|
362
|
+
|
|
363
|
+
if (!fs.existsSync(templatePath)) {
|
|
364
|
+
return res.status(404).json({ success: false, error: 'Template file not found' });
|
|
365
|
+
}
|
|
366
|
+
if (!fs.existsSync(localPath)) {
|
|
367
|
+
return res.status(404).json({ success: false, error: 'Local file not found' });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let diff = '';
|
|
371
|
+
try {
|
|
372
|
+
diff = execFileSync('git', ['diff', '--no-index', '--', templatePath, localPath], {
|
|
373
|
+
encoding: 'utf-8',
|
|
374
|
+
});
|
|
375
|
+
} catch (e: unknown) {
|
|
376
|
+
// git diff exits 1 when files differ
|
|
377
|
+
const err = e as { stdout?: string };
|
|
378
|
+
diff = err.stdout || 'Files differ';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
res.json({ success: true, data: { diff } });
|
|
382
|
+
} catch (err) {
|
|
383
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return router;
|
|
388
|
+
}
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import type Database from 'better-sqlite3';
|
|
4
|
-
import type {
|
|
4
|
+
import type { Emitter } from '../project-emitter.js';
|
|
5
5
|
import type { ScopeService } from '../services/scope-service.js';
|
|
6
6
|
import type { ReadinessService } from '../services/readiness-service.js';
|
|
7
7
|
import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
8
8
|
import { launchInTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
9
9
|
import { resolveDispatchEvent, linkPidToDispatch } from '../utils/dispatch-utils.js';
|
|
10
|
-
import { getConfig } from '../config.js';
|
|
11
10
|
import { createLogger } from '../utils/logger.js';
|
|
12
11
|
|
|
13
12
|
const log = createLogger('dispatch');
|
|
14
13
|
|
|
15
14
|
interface ScopeRouteDeps {
|
|
16
15
|
db: Database.Database;
|
|
17
|
-
io:
|
|
16
|
+
io: Emitter;
|
|
18
17
|
scopeService: ScopeService;
|
|
19
18
|
readinessService: ReadinessService;
|
|
20
19
|
projectRoot: string;
|
|
20
|
+
projectName: string;
|
|
21
21
|
engine: WorkflowEngine;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
function isValidSlug(slug: string): boolean {
|
|
25
|
+
return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(slug) && slug.length <= 80;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createScopeRoutes({ db, io, scopeService, readinessService, projectRoot, projectName, engine }: ScopeRouteDeps): Router {
|
|
25
29
|
const router = Router();
|
|
26
30
|
|
|
27
31
|
// ─── Scope CRUD ──────────────────────────────────────────
|
|
@@ -67,7 +71,7 @@ export function createScopeRoutes({ db, io, scopeService, readinessService, proj
|
|
|
67
71
|
|
|
68
72
|
router.patch('/scopes/:id', (req, res) => {
|
|
69
73
|
const id = Number(req.params.id);
|
|
70
|
-
const result = scopeService.
|
|
74
|
+
const result = scopeService.updateFields(id, req.body);
|
|
71
75
|
if (!result.ok) {
|
|
72
76
|
const code = result.code === 'NOT_FOUND' ? 404 : 400;
|
|
73
77
|
res.status(code).json({ error: result.error, code: result.code });
|
|
@@ -89,14 +93,15 @@ export function createScopeRoutes({ db, io, scopeService, readinessService, proj
|
|
|
89
93
|
res.status(201).json(idea);
|
|
90
94
|
});
|
|
91
95
|
|
|
92
|
-
router.patch('/ideas/:
|
|
93
|
-
const
|
|
96
|
+
router.patch('/ideas/:slug', (req, res) => {
|
|
97
|
+
const { slug } = req.params;
|
|
98
|
+
if (!isValidSlug(slug)) { res.status(400).json({ error: 'Invalid slug' }); return; }
|
|
94
99
|
const { title, description } = req.body as { title?: string; description?: string };
|
|
95
100
|
if (!title?.trim()) {
|
|
96
101
|
res.status(400).json({ error: 'title is required' });
|
|
97
102
|
return;
|
|
98
103
|
}
|
|
99
|
-
const updated = scopeService.updateIdeaFile(
|
|
104
|
+
const updated = scopeService.updateIdeaFile(slug, title.trim(), (description ?? '').trim());
|
|
100
105
|
if (!updated) {
|
|
101
106
|
res.status(404).json({ error: 'Idea not found' });
|
|
102
107
|
return;
|
|
@@ -104,9 +109,10 @@ export function createScopeRoutes({ db, io, scopeService, readinessService, proj
|
|
|
104
109
|
res.json({ ok: true });
|
|
105
110
|
});
|
|
106
111
|
|
|
107
|
-
router.delete('/ideas/:
|
|
108
|
-
const
|
|
109
|
-
|
|
112
|
+
router.delete('/ideas/:slug', (req, res) => {
|
|
113
|
+
const { slug } = req.params;
|
|
114
|
+
if (!isValidSlug(slug)) { res.status(400).json({ error: 'Invalid slug' }); return; }
|
|
115
|
+
const deleted = scopeService.deleteIdeaFile(slug);
|
|
110
116
|
if (!deleted) {
|
|
111
117
|
res.status(404).json({ error: 'Idea not found' });
|
|
112
118
|
return;
|
|
@@ -114,9 +120,10 @@ export function createScopeRoutes({ db, io, scopeService, readinessService, proj
|
|
|
114
120
|
res.json({ ok: true });
|
|
115
121
|
});
|
|
116
122
|
|
|
117
|
-
router.post('/ideas/:
|
|
118
|
-
const
|
|
119
|
-
|
|
123
|
+
router.post('/ideas/:slug/promote', async (req, res) => {
|
|
124
|
+
const { slug } = req.params;
|
|
125
|
+
if (!isValidSlug(slug)) { res.status(400).json({ error: 'Invalid slug' }); return; }
|
|
126
|
+
const result = scopeService.promoteIdea(slug);
|
|
120
127
|
if (!result) {
|
|
121
128
|
res.status(404).json({ error: 'Idea not found' });
|
|
122
129
|
return;
|
|
@@ -184,18 +191,15 @@ export function createScopeRoutes({ db, io, scopeService, readinessService, proj
|
|
|
184
191
|
}
|
|
185
192
|
surpriseInProgress = true;
|
|
186
193
|
|
|
187
|
-
const nextIdStart = scopeService.getNextIceboxId();
|
|
188
194
|
const today = new Date().toISOString().split('T')[0];
|
|
189
|
-
const idRange = Array.from({ length: 5 }, (_, i) => nextIdStart + i);
|
|
190
195
|
|
|
191
|
-
const prompt = `You are analyzing the ${
|
|
196
|
+
const prompt = `You are analyzing the ${projectName} codebase to suggest feature ideas. Your ONLY job is to create markdown files.
|
|
192
197
|
|
|
193
198
|
Create exactly 3 idea files in the scopes/icebox/ directory. Each file must use this EXACT format:
|
|
194
199
|
|
|
195
|
-
File: scopes/icebox/{
|
|
200
|
+
File: scopes/icebox/{kebab-slug}.md
|
|
196
201
|
|
|
197
202
|
---
|
|
198
|
-
id: {ID}
|
|
199
203
|
title: "{title}"
|
|
200
204
|
status: icebox
|
|
201
205
|
ghost: true
|
|
@@ -208,13 +212,12 @@ tags: []
|
|
|
208
212
|
|
|
209
213
|
{2-3 sentence description of the feature, what problem it solves, and a rough approach.}
|
|
210
214
|
|
|
211
|
-
Use these IDs: ${idRange[0]}, ${idRange[1]}, ${idRange[2]}
|
|
212
|
-
|
|
213
215
|
Rules:
|
|
214
216
|
- Focus on practical improvements: performance, UX, security, developer experience, monitoring, or reliability
|
|
215
217
|
- Be specific and actionable — not vague architectural rewrites
|
|
216
218
|
- Keep descriptions concise (2-3 sentences max)
|
|
217
|
-
- Filenames must be {
|
|
219
|
+
- Filenames must be {kebab-case-slug}.md (NO numeric prefix)
|
|
220
|
+
- Do NOT include an id field in frontmatter
|
|
218
221
|
- The ghost: true field is required in frontmatter
|
|
219
222
|
- Do NOT create any other files or make any other changes`;
|
|
220
223
|
|
|
@@ -244,9 +247,10 @@ Rules:
|
|
|
244
247
|
res.json({ ok: true, status: 'generating' });
|
|
245
248
|
});
|
|
246
249
|
|
|
247
|
-
router.post('/ideas/:
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
+
router.post('/ideas/:slug/approve', (req, res) => {
|
|
251
|
+
const { slug } = req.params;
|
|
252
|
+
if (!isValidSlug(slug)) { res.status(400).json({ error: 'Invalid slug' }); return; }
|
|
253
|
+
const approved = scopeService.approveGhostIdea(slug);
|
|
250
254
|
if (!approved) {
|
|
251
255
|
res.status(404).json({ error: 'Ghost idea not found' });
|
|
252
256
|
return;
|