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,98 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { Server } from 'socket.io';
|
|
3
|
+
import type { RawEvent } from '../parsers/event-parser.js';
|
|
4
|
+
import { createLogger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
const log = createLogger('event');
|
|
7
|
+
|
|
8
|
+
export type EventIngestCallback = (type: string, scopeId: unknown, data: Record<string, unknown>) => void;
|
|
9
|
+
|
|
10
|
+
export interface EventRow {
|
|
11
|
+
id: string;
|
|
12
|
+
type: string;
|
|
13
|
+
scope_id: number | null;
|
|
14
|
+
session_id: string | null;
|
|
15
|
+
agent: string | null;
|
|
16
|
+
data: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
processed: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class EventService {
|
|
22
|
+
private onIngestCallbacks: EventIngestCallback[] = [];
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private db: Database.Database,
|
|
26
|
+
private io: Server
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
/** Register a callback to be called after each successful event ingest */
|
|
30
|
+
onIngest(callback: EventIngestCallback): void {
|
|
31
|
+
this.onIngestCallbacks.push(callback);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Ingest a parsed event into the database and broadcast it */
|
|
35
|
+
ingest(event: RawEvent): void {
|
|
36
|
+
const result = this.db.prepare(
|
|
37
|
+
`INSERT OR IGNORE INTO events (id, type, scope_id, session_id, agent, data, timestamp, processed)
|
|
38
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 1)`
|
|
39
|
+
).run(
|
|
40
|
+
event.id,
|
|
41
|
+
event.type,
|
|
42
|
+
event.scope_id ?? null,
|
|
43
|
+
event.session_id ?? null,
|
|
44
|
+
event.agent ?? null,
|
|
45
|
+
JSON.stringify(event.data ?? {}),
|
|
46
|
+
event.timestamp
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Only broadcast if this was a new insert (not a duplicate)
|
|
50
|
+
if (result.changes === 0) {
|
|
51
|
+
log.debug('Event duplicate skipped', { id: event.id });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
log.info('Event ingested', { type: event.type, id: event.id, scope_id: event.scope_id, agent: event.agent });
|
|
56
|
+
const data = event.data ?? {};
|
|
57
|
+
this.io.emit('event:new', {
|
|
58
|
+
id: event.id,
|
|
59
|
+
type: event.type,
|
|
60
|
+
scope_id: event.scope_id ?? null,
|
|
61
|
+
session_id: event.session_id ?? null,
|
|
62
|
+
agent: event.agent ?? null,
|
|
63
|
+
data,
|
|
64
|
+
timestamp: event.timestamp,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Trigger event-driven inference
|
|
68
|
+
for (const cb of this.onIngestCallbacks) {
|
|
69
|
+
cb(event.type, event.scope_id ?? data.scope_id, data);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Get recent events, optionally filtered by type */
|
|
74
|
+
getRecent(limit: number = 50, type?: string): EventRow[] {
|
|
75
|
+
if (type) {
|
|
76
|
+
return this.db
|
|
77
|
+
.prepare('SELECT * FROM events WHERE type = ? ORDER BY timestamp DESC LIMIT ?')
|
|
78
|
+
.all(type, limit) as EventRow[];
|
|
79
|
+
}
|
|
80
|
+
return this.db
|
|
81
|
+
.prepare('SELECT * FROM events ORDER BY timestamp DESC LIMIT ?')
|
|
82
|
+
.all(limit) as EventRow[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Get events for a specific agent */
|
|
86
|
+
getByAgent(agent: string, limit: number = 50): EventRow[] {
|
|
87
|
+
return this.db
|
|
88
|
+
.prepare('SELECT * FROM events WHERE agent = ? ORDER BY timestamp DESC LIMIT ?')
|
|
89
|
+
.all(agent, limit) as EventRow[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Get events for a specific scope */
|
|
93
|
+
getByScope(scopeId: number, limit: number = 50): EventRow[] {
|
|
94
|
+
return this.db
|
|
95
|
+
.prepare('SELECT * FROM events WHERE scope_id = ? ORDER BY timestamp DESC LIMIT ?')
|
|
96
|
+
.all(scopeId, limit) as EventRow[];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { Server } from 'socket.io';
|
|
3
|
+
import type { GateStatus } from '../../shared/api-types.js';
|
|
4
|
+
import { createLogger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
const log = createLogger('gate');
|
|
7
|
+
|
|
8
|
+
export interface GateResult {
|
|
9
|
+
scope_id: number | null;
|
|
10
|
+
gate_name: string;
|
|
11
|
+
status: GateStatus;
|
|
12
|
+
details: string | null;
|
|
13
|
+
duration_ms: number | null;
|
|
14
|
+
commit_sha: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GateRow {
|
|
18
|
+
id: number;
|
|
19
|
+
scope_id: number | null;
|
|
20
|
+
gate_name: string;
|
|
21
|
+
status: GateStatus;
|
|
22
|
+
details: string | null;
|
|
23
|
+
duration_ms: number | null;
|
|
24
|
+
run_at: string;
|
|
25
|
+
commit_sha: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The 13 quality gates from /test-checks
|
|
29
|
+
export const GATE_NAMES = [
|
|
30
|
+
'type-check',
|
|
31
|
+
'lint',
|
|
32
|
+
'build',
|
|
33
|
+
'template-validation',
|
|
34
|
+
'doc-links',
|
|
35
|
+
'doc-freshness',
|
|
36
|
+
'rule-enforcement',
|
|
37
|
+
'no-placeholders',
|
|
38
|
+
'no-mock-data',
|
|
39
|
+
'no-shortcuts',
|
|
40
|
+
'no-default-secrets',
|
|
41
|
+
'no-stale-scopes',
|
|
42
|
+
'tests',
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
export class GateService {
|
|
46
|
+
constructor(
|
|
47
|
+
private db: Database.Database,
|
|
48
|
+
private io: Server
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
/** Record a gate result */
|
|
52
|
+
record(gate: GateResult): void {
|
|
53
|
+
const result = this.db.prepare(
|
|
54
|
+
`INSERT INTO quality_gates (scope_id, gate_name, status, details, duration_ms, run_at, commit_sha)
|
|
55
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
56
|
+
).run(
|
|
57
|
+
gate.scope_id,
|
|
58
|
+
gate.gate_name,
|
|
59
|
+
gate.status,
|
|
60
|
+
gate.details,
|
|
61
|
+
gate.duration_ms,
|
|
62
|
+
new Date().toISOString(),
|
|
63
|
+
gate.commit_sha
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
log.info('Gate recorded', { scope_id: gate.scope_id, gate: gate.gate_name, status: gate.status, duration_ms: gate.duration_ms });
|
|
67
|
+
const inserted = this.db.prepare('SELECT * FROM quality_gates WHERE id = ?').get(result.lastInsertRowid);
|
|
68
|
+
if (inserted) {
|
|
69
|
+
this.io.emit('gate:updated', inserted);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Get latest gate results for a scope */
|
|
74
|
+
getLatestForScope(scopeId: number): GateRow[] {
|
|
75
|
+
return this.db.prepare(`
|
|
76
|
+
SELECT * FROM quality_gates
|
|
77
|
+
WHERE scope_id = ? AND id IN (
|
|
78
|
+
SELECT MAX(id) FROM quality_gates
|
|
79
|
+
WHERE scope_id = ?
|
|
80
|
+
GROUP BY gate_name
|
|
81
|
+
)
|
|
82
|
+
ORDER BY gate_name
|
|
83
|
+
`).all(scopeId, scopeId) as GateRow[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Get latest gate run (all gates from most recent execution) */
|
|
87
|
+
getLatestRun(): GateRow[] {
|
|
88
|
+
// Get the most recent run_at timestamp
|
|
89
|
+
const latest = this.db.prepare(
|
|
90
|
+
'SELECT run_at FROM quality_gates ORDER BY run_at DESC LIMIT 1'
|
|
91
|
+
).get() as { run_at: string } | undefined;
|
|
92
|
+
|
|
93
|
+
if (!latest) return [];
|
|
94
|
+
|
|
95
|
+
// Get all gates from that run (within 60 seconds of each other)
|
|
96
|
+
return this.db.prepare(`
|
|
97
|
+
SELECT * FROM quality_gates
|
|
98
|
+
WHERE run_at >= datetime(?, '-60 seconds')
|
|
99
|
+
ORDER BY gate_name
|
|
100
|
+
`).all(latest.run_at) as GateRow[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Get gate history for trend chart */
|
|
104
|
+
getTrend(limit: number = 30): GateRow[] {
|
|
105
|
+
return this.db.prepare(`
|
|
106
|
+
SELECT gate_name, status, run_at, duration_ms
|
|
107
|
+
FROM quality_gates
|
|
108
|
+
ORDER BY run_at DESC
|
|
109
|
+
LIMIT ?
|
|
110
|
+
`).all(limit * GATE_NAMES.length) as GateRow[]; // Get enough to cover N runs
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Get aggregate pass/fail stats */
|
|
114
|
+
getStats(): { gate_name: string; total: number; passed: number; failed: number }[] {
|
|
115
|
+
return this.db.prepare(`
|
|
116
|
+
SELECT
|
|
117
|
+
gate_name,
|
|
118
|
+
COUNT(*) as total,
|
|
119
|
+
SUM(CASE WHEN status = 'pass' THEN 1 ELSE 0 END) as passed,
|
|
120
|
+
SUM(CASE WHEN status = 'fail' THEN 1 ELSE 0 END) as failed
|
|
121
|
+
FROM quality_gates
|
|
122
|
+
GROUP BY gate_name
|
|
123
|
+
ORDER BY gate_name
|
|
124
|
+
`).all() as { gate_name: string; total: number; passed: number; failed: number }[];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { execFile as execFileCb } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { listWorktrees } from '../utils/worktree-manager.js';
|
|
4
|
+
import type { ScopeCache } from './scope-cache.js';
|
|
5
|
+
|
|
6
|
+
const execFile = promisify(execFileCb);
|
|
7
|
+
|
|
8
|
+
// ─── Types ──────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface GitOverview {
|
|
11
|
+
branchingMode: 'trunk' | 'worktree';
|
|
12
|
+
currentBranch: string;
|
|
13
|
+
dirty: boolean;
|
|
14
|
+
detached: boolean;
|
|
15
|
+
mainHead: { sha: string; message: string; date: string } | null;
|
|
16
|
+
aheadBehind: { ahead: number; behind: number } | null;
|
|
17
|
+
worktreeCount: number;
|
|
18
|
+
featureBranchCount: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CommitEntry {
|
|
22
|
+
sha: string;
|
|
23
|
+
shortSha: string;
|
|
24
|
+
message: string;
|
|
25
|
+
author: string;
|
|
26
|
+
date: string;
|
|
27
|
+
branch: string;
|
|
28
|
+
scopeId: number | null;
|
|
29
|
+
refs: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BranchInfo {
|
|
33
|
+
name: string;
|
|
34
|
+
isRemote: boolean;
|
|
35
|
+
isCurrent: boolean;
|
|
36
|
+
headSha: string;
|
|
37
|
+
headMessage: string;
|
|
38
|
+
headDate: string;
|
|
39
|
+
aheadBehind: { ahead: number; behind: number } | null;
|
|
40
|
+
scopeId: number | null;
|
|
41
|
+
isStale: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorktreeDetail {
|
|
45
|
+
path: string;
|
|
46
|
+
branch: string;
|
|
47
|
+
head: string;
|
|
48
|
+
scopeId: number | null;
|
|
49
|
+
scopeTitle: string | null;
|
|
50
|
+
scopeStatus: string | null;
|
|
51
|
+
dirty: boolean;
|
|
52
|
+
aheadBehind: { ahead: number; behind: number } | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface DriftPair {
|
|
56
|
+
from: string;
|
|
57
|
+
to: string;
|
|
58
|
+
count: number;
|
|
59
|
+
commits: Array<{ sha: string; message: string; author: string; date: string }>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Cache Utility ──────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
interface CacheEntry<T> { data: T; ts: number }
|
|
65
|
+
|
|
66
|
+
const CACHE_TTL = 60_000; // 60 seconds
|
|
67
|
+
|
|
68
|
+
function cached<T>(cache: Map<string, CacheEntry<T>>, key: string): T | null {
|
|
69
|
+
const entry = cache.get(key);
|
|
70
|
+
if (entry && Date.now() - entry.ts < CACHE_TTL) return entry.data;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function setCache<T>(cache: Map<string, CacheEntry<T>>, key: string, data: T): void {
|
|
75
|
+
cache.set(key, { data, ts: Date.now() });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Service ────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
const SCOPE_BRANCH_RE = /(?:feat|fix|scope)[/-](?:scope-)?(\d+)/;
|
|
81
|
+
|
|
82
|
+
export class GitService {
|
|
83
|
+
private cache = new Map<string, CacheEntry<unknown>>();
|
|
84
|
+
|
|
85
|
+
constructor(
|
|
86
|
+
private projectRoot: string,
|
|
87
|
+
private scopeCache: ScopeCache,
|
|
88
|
+
) {}
|
|
89
|
+
|
|
90
|
+
private async git(args: string[], cwd?: string): Promise<string> {
|
|
91
|
+
// Uses execFile (not exec) — safe against shell injection
|
|
92
|
+
const { stdout } = await execFile('git', args, {
|
|
93
|
+
cwd: cwd ?? this.projectRoot,
|
|
94
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
95
|
+
});
|
|
96
|
+
return stdout;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Overview ──────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
async getOverview(branchingMode: 'trunk' | 'worktree'): Promise<GitOverview> {
|
|
102
|
+
const cacheKey = `overview:${branchingMode}`;
|
|
103
|
+
const hit = cached<GitOverview>(this.cache as Map<string, CacheEntry<GitOverview>>, cacheKey);
|
|
104
|
+
if (hit) return hit;
|
|
105
|
+
|
|
106
|
+
const [branchRaw, statusRaw] = await Promise.all([
|
|
107
|
+
this.git(['branch', '--show-current']).catch(() => ''),
|
|
108
|
+
this.git(['status', '--porcelain']).catch(() => ''),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const currentBranch = branchRaw.trim() || '(detached)';
|
|
112
|
+
const dirty = statusRaw.trim().length > 0;
|
|
113
|
+
const detached = !branchRaw.trim();
|
|
114
|
+
|
|
115
|
+
// Main HEAD
|
|
116
|
+
let mainHead: GitOverview['mainHead'] = null;
|
|
117
|
+
try {
|
|
118
|
+
const raw = await this.git(['log', 'HEAD', '-1', '--format=%H|%aI|%s']);
|
|
119
|
+
const [sha, date, ...msgParts] = raw.trim().split('|');
|
|
120
|
+
if (sha) mainHead = { sha, message: msgParts.join('|'), date };
|
|
121
|
+
} catch { /* no commits yet */ }
|
|
122
|
+
|
|
123
|
+
// Ahead/behind relative to origin/main (or origin/master)
|
|
124
|
+
let aheadBehind: GitOverview['aheadBehind'] = null;
|
|
125
|
+
if (!detached) {
|
|
126
|
+
try {
|
|
127
|
+
const raw = await this.git(['rev-list', '--left-right', '--count', `origin/main...${currentBranch}`]);
|
|
128
|
+
const [behind, ahead] = raw.trim().split('\t').map(Number);
|
|
129
|
+
aheadBehind = { ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
130
|
+
} catch {
|
|
131
|
+
try {
|
|
132
|
+
const raw = await this.git(['rev-list', '--left-right', '--count', `origin/master...${currentBranch}`]);
|
|
133
|
+
const [behind, ahead] = raw.trim().split('\t').map(Number);
|
|
134
|
+
aheadBehind = { ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
135
|
+
} catch { /* no remote tracking */ }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Worktree and feature branch counts
|
|
140
|
+
let worktreeCount = 0;
|
|
141
|
+
let featureBranchCount = 0;
|
|
142
|
+
try {
|
|
143
|
+
const wts = await listWorktrees(this.projectRoot);
|
|
144
|
+
worktreeCount = wts.length;
|
|
145
|
+
} catch { /* ok */ }
|
|
146
|
+
try {
|
|
147
|
+
const raw = await this.git(['branch', '--format=%(refname:short)']);
|
|
148
|
+
const branches = raw.trim().split('\n').filter(Boolean);
|
|
149
|
+
featureBranchCount = branches.filter(b => SCOPE_BRANCH_RE.test(b) || b.startsWith('feat/')).length;
|
|
150
|
+
} catch { /* ok */ }
|
|
151
|
+
|
|
152
|
+
const result: GitOverview = {
|
|
153
|
+
branchingMode,
|
|
154
|
+
currentBranch,
|
|
155
|
+
dirty,
|
|
156
|
+
detached,
|
|
157
|
+
mainHead,
|
|
158
|
+
aheadBehind,
|
|
159
|
+
worktreeCount,
|
|
160
|
+
featureBranchCount,
|
|
161
|
+
};
|
|
162
|
+
setCache(this.cache as Map<string, CacheEntry<GitOverview>>, cacheKey, result);
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Commits ──────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
async getCommits(opts: { branch?: string; limit?: number; offset?: number } = {}): Promise<CommitEntry[]> {
|
|
169
|
+
const { branch, limit = 50, offset = 0 } = opts;
|
|
170
|
+
const cacheKey = `commits:${branch ?? 'all'}:${limit}:${offset}`;
|
|
171
|
+
const hit = cached<CommitEntry[]>(this.cache as Map<string, CacheEntry<CommitEntry[]>>, cacheKey);
|
|
172
|
+
if (hit) return hit;
|
|
173
|
+
|
|
174
|
+
const args = ['log', '--format=%H|%h|%aI|%an|%s|%D'];
|
|
175
|
+
if (branch && branch !== 'all') {
|
|
176
|
+
args.push(branch);
|
|
177
|
+
} else {
|
|
178
|
+
args.push('--all');
|
|
179
|
+
}
|
|
180
|
+
args.push(`--skip=${offset}`, `-${limit}`);
|
|
181
|
+
|
|
182
|
+
let raw: string;
|
|
183
|
+
try {
|
|
184
|
+
raw = await this.git(args);
|
|
185
|
+
} catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const commits: CommitEntry[] = [];
|
|
190
|
+
for (const line of raw.trim().split('\n')) {
|
|
191
|
+
if (!line) continue;
|
|
192
|
+
const parts = line.split('|');
|
|
193
|
+
const sha = parts[0];
|
|
194
|
+
const shortSha = parts[1];
|
|
195
|
+
const date = parts[2];
|
|
196
|
+
const author = parts[3];
|
|
197
|
+
const message = parts[4];
|
|
198
|
+
const refStr = parts.slice(5).join('|');
|
|
199
|
+
|
|
200
|
+
const refs = refStr
|
|
201
|
+
? refStr.split(',').map(r => r.trim()).filter(Boolean)
|
|
202
|
+
: [];
|
|
203
|
+
|
|
204
|
+
// Extract scope ID from refs or message
|
|
205
|
+
let scopeId: number | null = null;
|
|
206
|
+
for (const ref of refs) {
|
|
207
|
+
const m = SCOPE_BRANCH_RE.exec(ref);
|
|
208
|
+
if (m) { scopeId = parseInt(m[1]); break; }
|
|
209
|
+
}
|
|
210
|
+
if (!scopeId) {
|
|
211
|
+
const m = SCOPE_BRANCH_RE.exec(message);
|
|
212
|
+
if (m) scopeId = parseInt(m[1]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Derive branch from first ref that looks like a branch
|
|
216
|
+
let branchName = '';
|
|
217
|
+
for (const ref of refs) {
|
|
218
|
+
const cleaned = ref.replace(/^HEAD -> /, '').replace(/^origin\//, '');
|
|
219
|
+
if (cleaned && !cleaned.startsWith('tag:')) {
|
|
220
|
+
branchName = cleaned;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
commits.push({ sha, shortSha, message, author, date, branch: branchName, scopeId, refs });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
setCache(this.cache as Map<string, CacheEntry<CommitEntry[]>>, cacheKey, commits);
|
|
229
|
+
return commits;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Branches ──────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
async getBranches(): Promise<BranchInfo[]> {
|
|
235
|
+
const hit = cached<BranchInfo[]>(this.cache as Map<string, CacheEntry<BranchInfo[]>>, 'branches');
|
|
236
|
+
if (hit) return hit;
|
|
237
|
+
|
|
238
|
+
let raw: string;
|
|
239
|
+
try {
|
|
240
|
+
raw = await this.git([
|
|
241
|
+
'branch', '-a',
|
|
242
|
+
'--format=%(HEAD)|%(refname:short)|%(objectname:short)|%(committerdate:iso-strict)|%(subject)',
|
|
243
|
+
]);
|
|
244
|
+
} catch {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
const STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
250
|
+
const branches: BranchInfo[] = [];
|
|
251
|
+
|
|
252
|
+
for (const line of raw.trim().split('\n')) {
|
|
253
|
+
if (!line) continue;
|
|
254
|
+
const [headMarker, name, headSha, headDate, ...msgParts] = line.split('|');
|
|
255
|
+
if (!name || name.includes('HEAD')) continue;
|
|
256
|
+
|
|
257
|
+
const isCurrent = headMarker === '*';
|
|
258
|
+
const isRemote = name.startsWith('remotes/') || name.startsWith('origin/');
|
|
259
|
+
const cleanName = name.replace(/^remotes\//, '');
|
|
260
|
+
|
|
261
|
+
// Skip remote duplicates of local branches
|
|
262
|
+
if (isRemote) {
|
|
263
|
+
const localName = cleanName.replace(/^origin\//, '');
|
|
264
|
+
if (branches.some(b => !b.isRemote && b.name === localName)) continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const scopeMatch = SCOPE_BRANCH_RE.exec(cleanName);
|
|
268
|
+
const scopeId = scopeMatch ? parseInt(scopeMatch[1]) : null;
|
|
269
|
+
const isStale = headDate ? (now - new Date(headDate).getTime() > STALE_MS) : false;
|
|
270
|
+
|
|
271
|
+
// Ahead/behind relative to origin/main
|
|
272
|
+
let aheadBehind: BranchInfo['aheadBehind'] = null;
|
|
273
|
+
if (!isRemote) {
|
|
274
|
+
try {
|
|
275
|
+
const countRaw = await this.git(['rev-list', '--left-right', '--count', `origin/main...${name}`]);
|
|
276
|
+
const [behind, ahead] = countRaw.trim().split('\t').map(Number);
|
|
277
|
+
aheadBehind = { ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
278
|
+
} catch { /* no remote */ }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
branches.push({
|
|
282
|
+
name: cleanName,
|
|
283
|
+
isRemote,
|
|
284
|
+
isCurrent,
|
|
285
|
+
headSha: headSha ?? '',
|
|
286
|
+
headMessage: msgParts.join('|'),
|
|
287
|
+
headDate: headDate ?? '',
|
|
288
|
+
aheadBehind,
|
|
289
|
+
scopeId,
|
|
290
|
+
isStale,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
setCache(this.cache as Map<string, CacheEntry<BranchInfo[]>>, 'branches', branches);
|
|
295
|
+
return branches;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ─── Enhanced Worktrees ────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
async getEnhancedWorktrees(): Promise<WorktreeDetail[]> {
|
|
301
|
+
const hit = cached<WorktreeDetail[]>(this.cache as Map<string, CacheEntry<WorktreeDetail[]>>, 'worktrees-enhanced');
|
|
302
|
+
if (hit) return hit;
|
|
303
|
+
|
|
304
|
+
let wts: Array<{ path: string; branch: string; scopeId: number }>;
|
|
305
|
+
try {
|
|
306
|
+
wts = await listWorktrees(this.projectRoot);
|
|
307
|
+
} catch {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const results: WorktreeDetail[] = [];
|
|
312
|
+
for (const wt of wts) {
|
|
313
|
+
let head = '';
|
|
314
|
+
try {
|
|
315
|
+
head = (await this.git(['rev-parse', '--short', 'HEAD'], wt.path)).trim();
|
|
316
|
+
} catch { /* ok */ }
|
|
317
|
+
|
|
318
|
+
let dirty = false;
|
|
319
|
+
try {
|
|
320
|
+
const status = (await this.git(['status', '--porcelain'], wt.path)).trim();
|
|
321
|
+
dirty = status.length > 0;
|
|
322
|
+
} catch { /* ok */ }
|
|
323
|
+
|
|
324
|
+
let aheadBehind: WorktreeDetail['aheadBehind'] = null;
|
|
325
|
+
try {
|
|
326
|
+
const branchName = wt.branch.replace(/^refs\/heads\//, '');
|
|
327
|
+
const countRaw = await this.git(['rev-list', '--left-right', '--count', `origin/main...${branchName}`], wt.path);
|
|
328
|
+
const [behind, ahead] = countRaw.trim().split('\t').map(Number);
|
|
329
|
+
aheadBehind = { ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
330
|
+
} catch { /* ok */ }
|
|
331
|
+
|
|
332
|
+
const scope = wt.scopeId ? this.scopeCache.getById(wt.scopeId) : null;
|
|
333
|
+
|
|
334
|
+
results.push({
|
|
335
|
+
path: wt.path,
|
|
336
|
+
branch: wt.branch.replace(/^refs\/heads\//, ''),
|
|
337
|
+
head,
|
|
338
|
+
scopeId: wt.scopeId,
|
|
339
|
+
scopeTitle: scope?.title ?? null,
|
|
340
|
+
scopeStatus: scope?.status ?? null,
|
|
341
|
+
dirty,
|
|
342
|
+
aheadBehind,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
setCache(this.cache as Map<string, CacheEntry<WorktreeDetail[]>>, 'worktrees-enhanced', results);
|
|
347
|
+
return results;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─── Dynamic Drift ─────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
async getDrift(gitBranches: Array<{ from: string; to: string }>): Promise<DriftPair[]> {
|
|
353
|
+
const cacheKey = `drift:${gitBranches.map(b => `${b.from}-${b.to}`).join(',')}`;
|
|
354
|
+
const hit = cached<DriftPair[]>(this.cache as Map<string, CacheEntry<DriftPair[]>>, cacheKey);
|
|
355
|
+
if (hit) return hit;
|
|
356
|
+
|
|
357
|
+
const pairs: DriftPair[] = [];
|
|
358
|
+
for (const { from, to } of gitBranches) {
|
|
359
|
+
try {
|
|
360
|
+
const raw = await this.git([
|
|
361
|
+
'log', `origin/${from}`, '--not', `origin/${to}`,
|
|
362
|
+
'--reverse', '--format=%H|%aI|%s|%an',
|
|
363
|
+
]);
|
|
364
|
+
const commits = raw.trim().split('\n').filter(Boolean).map(line => {
|
|
365
|
+
const [sha, date, ...rest] = line.split('|');
|
|
366
|
+
return { sha, date, message: rest.slice(0, -1).join('|'), author: rest[rest.length - 1] };
|
|
367
|
+
});
|
|
368
|
+
pairs.push({ from, to, count: commits.length, commits });
|
|
369
|
+
} catch {
|
|
370
|
+
pairs.push({ from, to, count: 0, commits: [] });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
setCache(this.cache as Map<string, CacheEntry<DriftPair[]>>, cacheKey, pairs);
|
|
375
|
+
return pairs;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ─── Git Status Polling ────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
async getStatusHash(): Promise<string> {
|
|
381
|
+
const [head, dirty] = await Promise.all([
|
|
382
|
+
this.git(['rev-parse', 'HEAD']).catch(() => 'none'),
|
|
383
|
+
this.git(['status', '--porcelain']).catch(() => ''),
|
|
384
|
+
]);
|
|
385
|
+
return `${head.trim()}:${dirty.trim().length > 0 ? 'dirty' : 'clean'}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
clearCache(): void {
|
|
389
|
+
this.cache.clear();
|
|
390
|
+
}
|
|
391
|
+
}
|