@a5c-ai/adapters-gateway 5.1.1-staging.52898ebfc24f
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/README.md +20 -0
- package/dist/auth/bootstrap.d.ts +89 -0
- package/dist/auth/bootstrap.d.ts.map +1 -0
- package/dist/auth/bootstrap.js +222 -0
- package/dist/auth/bootstrap.js.map +1 -0
- package/dist/auth/hashing.d.ts +4 -0
- package/dist/auth/hashing.d.ts.map +1 -0
- package/dist/auth/hashing.js +27 -0
- package/dist/auth/hashing.js.map +1 -0
- package/dist/auth/middleware.d.ts +3 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +17 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/tokens.d.ts +45 -0
- package/dist/auth/tokens.d.ts.map +1 -0
- package/dist/auth/tokens.js +186 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/builtin-adapters.d.ts +17 -0
- package/dist/builtin-adapters.d.ts.map +1 -0
- package/dist/builtin-adapters.js +119 -0
- package/dist/builtin-adapters.js.map +1 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -0
- package/dist/fanout/client-conn.d.ts +20 -0
- package/dist/fanout/client-conn.d.ts.map +1 -0
- package/dist/fanout/client-conn.js +53 -0
- package/dist/fanout/client-conn.js.map +1 -0
- package/dist/fanout/subscriber.d.ts +12 -0
- package/dist/fanout/subscriber.d.ts.map +1 -0
- package/dist/fanout/subscriber.js +40 -0
- package/dist/fanout/subscriber.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/kanban/lib/config-loader.d.ts +29 -0
- package/dist/kanban/lib/config-loader.d.ts.map +1 -0
- package/dist/kanban/lib/config-loader.js +166 -0
- package/dist/kanban/lib/config-loader.js.map +1 -0
- package/dist/kanban/lib/config.d.ts +3 -0
- package/dist/kanban/lib/config.d.ts.map +1 -0
- package/dist/kanban/lib/config.js +6 -0
- package/dist/kanban/lib/config.js.map +1 -0
- package/dist/kanban/lib/create-global-registry.d.ts +28 -0
- package/dist/kanban/lib/create-global-registry.d.ts.map +1 -0
- package/dist/kanban/lib/create-global-registry.js +53 -0
- package/dist/kanban/lib/create-global-registry.js.map +1 -0
- package/dist/kanban/lib/dispatch-context-audit.d.ts +12 -0
- package/dist/kanban/lib/dispatch-context-audit.d.ts.map +1 -0
- package/dist/kanban/lib/dispatch-context-audit.js +44 -0
- package/dist/kanban/lib/dispatch-context-audit.js.map +1 -0
- package/dist/kanban/lib/error-handler.d.ts +28 -0
- package/dist/kanban/lib/error-handler.d.ts.map +1 -0
- package/dist/kanban/lib/error-handler.js +61 -0
- package/dist/kanban/lib/error-handler.js.map +1 -0
- package/dist/kanban/lib/global-registry.d.ts +49 -0
- package/dist/kanban/lib/global-registry.d.ts.map +1 -0
- package/dist/kanban/lib/global-registry.js +18 -0
- package/dist/kanban/lib/global-registry.js.map +1 -0
- package/dist/kanban/lib/parser.d.ts +36 -0
- package/dist/kanban/lib/parser.d.ts.map +1 -0
- package/dist/kanban/lib/parser.js +585 -0
- package/dist/kanban/lib/parser.js.map +1 -0
- package/dist/kanban/lib/path-resolver.d.ts +2 -0
- package/dist/kanban/lib/path-resolver.d.ts.map +1 -0
- package/dist/kanban/lib/path-resolver.js +16 -0
- package/dist/kanban/lib/path-resolver.js.map +1 -0
- package/dist/kanban/lib/review-service.d.ts +63 -0
- package/dist/kanban/lib/review-service.d.ts.map +1 -0
- package/dist/kanban/lib/review-service.js +571 -0
- package/dist/kanban/lib/review-service.js.map +1 -0
- package/dist/kanban/lib/run-cache.d.ts +36 -0
- package/dist/kanban/lib/run-cache.d.ts.map +1 -0
- package/dist/kanban/lib/run-cache.js +313 -0
- package/dist/kanban/lib/run-cache.js.map +1 -0
- package/dist/kanban/lib/server-init.d.ts +26 -0
- package/dist/kanban/lib/server-init.d.ts.map +1 -0
- package/dist/kanban/lib/server-init.js +179 -0
- package/dist/kanban/lib/server-init.js.map +1 -0
- package/dist/kanban/lib/services/automation-rule-service.d.ts +97 -0
- package/dist/kanban/lib/services/automation-rule-service.d.ts.map +1 -0
- package/dist/kanban/lib/services/automation-rule-service.js +806 -0
- package/dist/kanban/lib/services/automation-rule-service.js.map +1 -0
- package/dist/kanban/lib/services/automation-webhook-service.d.ts +44 -0
- package/dist/kanban/lib/services/automation-webhook-service.d.ts.map +1 -0
- package/dist/kanban/lib/services/automation-webhook-service.js +405 -0
- package/dist/kanban/lib/services/automation-webhook-service.js.map +1 -0
- package/dist/kanban/lib/services/backlog-query-service.d.ts +130 -0
- package/dist/kanban/lib/services/backlog-query-service.d.ts.map +1 -0
- package/dist/kanban/lib/services/backlog-query-service.js +1972 -0
- package/dist/kanban/lib/services/backlog-query-service.js.map +1 -0
- package/dist/kanban/lib/services/dispatch-context-label-service.d.ts +39 -0
- package/dist/kanban/lib/services/dispatch-context-label-service.d.ts.map +1 -0
- package/dist/kanban/lib/services/dispatch-context-label-service.js +160 -0
- package/dist/kanban/lib/services/dispatch-context-label-service.js.map +1 -0
- package/dist/kanban/lib/services/kanban-storage.d.ts +36 -0
- package/dist/kanban/lib/services/kanban-storage.d.ts.map +1 -0
- package/dist/kanban/lib/services/kanban-storage.js +26 -0
- package/dist/kanban/lib/services/kanban-storage.js.map +1 -0
- package/dist/kanban/lib/services/run-query-service.d.ts +79 -0
- package/dist/kanban/lib/services/run-query-service.d.ts.map +1 -0
- package/dist/kanban/lib/services/run-query-service.js +202 -0
- package/dist/kanban/lib/services/run-query-service.js.map +1 -0
- package/dist/kanban/lib/services/task-tag-service.d.ts +39 -0
- package/dist/kanban/lib/services/task-tag-service.d.ts.map +1 -0
- package/dist/kanban/lib/services/task-tag-service.js +145 -0
- package/dist/kanban/lib/services/task-tag-service.js.map +1 -0
- package/dist/kanban/lib/settings-section-storage.d.ts +13 -0
- package/dist/kanban/lib/settings-section-storage.d.ts.map +1 -0
- package/dist/kanban/lib/settings-section-storage.js +38 -0
- package/dist/kanban/lib/settings-section-storage.js.map +1 -0
- package/dist/kanban/lib/source-discovery.d.ts +10 -0
- package/dist/kanban/lib/source-discovery.d.ts.map +1 -0
- package/dist/kanban/lib/source-discovery.js +201 -0
- package/dist/kanban/lib/source-discovery.js.map +1 -0
- package/dist/kanban/lib/utils.d.ts +8 -0
- package/dist/kanban/lib/utils.d.ts.map +1 -0
- package/dist/kanban/lib/utils.js +116 -0
- package/dist/kanban/lib/utils.js.map +1 -0
- package/dist/kanban/lib/watcher.d.ts +14 -0
- package/dist/kanban/lib/watcher.d.ts.map +1 -0
- package/dist/kanban/lib/watcher.js +221 -0
- package/dist/kanban/lib/watcher.js.map +1 -0
- package/dist/kanban/lib/workspace-lifecycle.d.ts +68 -0
- package/dist/kanban/lib/workspace-lifecycle.d.ts.map +1 -0
- package/dist/kanban/lib/workspace-lifecycle.js +1085 -0
- package/dist/kanban/lib/workspace-lifecycle.js.map +1 -0
- package/dist/kanban/routes.d.ts +2 -0
- package/dist/kanban/routes.d.ts.map +1 -0
- package/dist/kanban/routes.js +1358 -0
- package/dist/kanban/routes.js.map +1 -0
- package/dist/kanban/types/breakpoint.d.ts +13 -0
- package/dist/kanban/types/breakpoint.d.ts.map +1 -0
- package/dist/kanban/types/breakpoint.js +3 -0
- package/dist/kanban/types/breakpoint.js.map +1 -0
- package/dist/kanban/types/index.d.ts +173 -0
- package/dist/kanban/types/index.d.ts.map +1 -0
- package/dist/kanban/types/index.js +3 -0
- package/dist/kanban/types/index.js.map +1 -0
- package/dist/logging.d.ts +7 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +22 -0
- package/dist/logging.js.map +1 -0
- package/dist/notifications/types.d.ts +18 -0
- package/dist/notifications/types.d.ts.map +1 -0
- package/dist/notifications/types.js +2 -0
- package/dist/notifications/types.js.map +1 -0
- package/dist/notifications/webhook-out.d.ts +3 -0
- package/dist/notifications/webhook-out.d.ts.map +1 -0
- package/dist/notifications/webhook-out.js +55 -0
- package/dist/notifications/webhook-out.js.map +1 -0
- package/dist/pairing/short-code.d.ts +20 -0
- package/dist/pairing/short-code.d.ts.map +1 -0
- package/dist/pairing/short-code.js +50 -0
- package/dist/pairing/short-code.js.map +1 -0
- package/dist/protocol/errors.d.ts +10 -0
- package/dist/protocol/errors.d.ts.map +1 -0
- package/dist/protocol/errors.js +15 -0
- package/dist/protocol/errors.js.map +1 -0
- package/dist/protocol/frames.d.ts +107 -0
- package/dist/protocol/frames.d.ts.map +1 -0
- package/dist/protocol/frames.js +146 -0
- package/dist/protocol/frames.js.map +1 -0
- package/dist/protocol/v1.d.ts +111 -0
- package/dist/protocol/v1.d.ts.map +1 -0
- package/dist/protocol/v1.js +2 -0
- package/dist/protocol/v1.js.map +1 -0
- package/dist/runs/event-log-index.d.ts +29 -0
- package/dist/runs/event-log-index.d.ts.map +1 -0
- package/dist/runs/event-log-index.js +210 -0
- package/dist/runs/event-log-index.js.map +1 -0
- package/dist/runs/event-log.d.ts +25 -0
- package/dist/runs/event-log.d.ts.map +1 -0
- package/dist/runs/event-log.js +104 -0
- package/dist/runs/event-log.js.map +1 -0
- package/dist/runs/hook-broker.d.ts +18 -0
- package/dist/runs/hook-broker.d.ts.map +1 -0
- package/dist/runs/hook-broker.js +110 -0
- package/dist/runs/hook-broker.js.map +1 -0
- package/dist/runs/manager.d.ts +57 -0
- package/dist/runs/manager.d.ts.map +1 -0
- package/dist/runs/manager.js +757 -0
- package/dist/runs/manager.js.map +1 -0
- package/dist/runs/session-runtime.d.ts +8 -0
- package/dist/runs/session-runtime.d.ts.map +1 -0
- package/dist/runs/session-runtime.js +291 -0
- package/dist/runs/session-runtime.js.map +1 -0
- package/dist/runs/types.d.ts +55 -0
- package/dist/runs/types.d.ts.map +1 -0
- package/dist/runs/types.js +2 -0
- package/dist/runs/types.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +702 -0
- package/dist/server.js.map +1 -0
- package/dist/static/webui-server.d.ts +2 -0
- package/dist/static/webui-server.d.ts.map +1 -0
- package/dist/static/webui-server.js +97 -0
- package/dist/static/webui-server.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { discoverAllRunDirs as defaultDiscoverAllRunDirs } from "./source-discovery.js";
|
|
7
|
+
import { getRunCached as defaultGetRunCached } from "./run-cache.js";
|
|
8
|
+
const execFile = promisify(execFileCallback);
|
|
9
|
+
const WORKSPACE_REGISTRY_PATH = process.env.KANBAN_WORKSPACE_REGISTRY_PATH ?? path.join(os.homedir(), ".a5c", "workspaces", "kanban-workspaces.json");
|
|
10
|
+
const defaultDeps = {
|
|
11
|
+
discoverAllRunDirs: defaultDiscoverAllRunDirs,
|
|
12
|
+
getRunCached: defaultGetRunCached,
|
|
13
|
+
readFile: fs.readFile,
|
|
14
|
+
writeFile: fs.writeFile,
|
|
15
|
+
mkdir: fs.mkdir,
|
|
16
|
+
stat: fs.stat,
|
|
17
|
+
execGit: async (args, cwd) => {
|
|
18
|
+
const result = await execFile("git", args, { cwd });
|
|
19
|
+
return {
|
|
20
|
+
stdout: result.stdout,
|
|
21
|
+
stderr: result.stderr,
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
now: () => new Date().toISOString(),
|
|
25
|
+
cwd: () => process.cwd(),
|
|
26
|
+
};
|
|
27
|
+
async function loadAgentMuxCore() {
|
|
28
|
+
return await import("@a5c-ai/comm-adapter");
|
|
29
|
+
}
|
|
30
|
+
function cloneRebaseSurface(rebase) {
|
|
31
|
+
if (!rebase) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
...rebase,
|
|
36
|
+
unresolvedFiles: [...rebase.unresolvedFiles],
|
|
37
|
+
resolvedFiles: [...rebase.resolvedFiles],
|
|
38
|
+
followUpInstructions: [...rebase.followUpInstructions],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function buildEditorHref(workspacePath) {
|
|
42
|
+
return `vscode://file${workspacePath}`;
|
|
43
|
+
}
|
|
44
|
+
function defaultConflictFiles() {
|
|
45
|
+
return [
|
|
46
|
+
"packages/adapters/webui/src/kanban/components/workspaces/workspaces-page.tsx",
|
|
47
|
+
"packages/adapters/webui/src/kanban/lib/workspace-lifecycle.ts",
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
function buildFollowUpInstructions(rebase, workspacePath) {
|
|
51
|
+
if (rebase.status === "rebase-needed") {
|
|
52
|
+
return [
|
|
53
|
+
`Retry the rebase for ${workspacePath} when the workspace is ready.`,
|
|
54
|
+
"If the next attempt reports conflicts again, use Auto-resolve first, then open the workspace in your editor for anything unresolved.",
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
if (rebase.status === "rebase-conflicts") {
|
|
58
|
+
const unresolvedSummary = rebase.unresolvedFiles.length > 0
|
|
59
|
+
? `Unresolved files: ${rebase.unresolvedFiles.join(", ")}.`
|
|
60
|
+
: "Conflicts are present but the unresolved file list is empty.";
|
|
61
|
+
return [
|
|
62
|
+
unresolvedSummary,
|
|
63
|
+
"Auto-resolve can clear deterministic conflicts and will leave the workflow in rebase-conflicts if manual work is still required.",
|
|
64
|
+
"Open in editor for manual fixes, then use Mark resolved to return the workspace to review or merge readiness.",
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
return [
|
|
68
|
+
`Rebase workflow completed. Continue the workspace through ${rebase.readyFor === "review" ? "review" : "merge"} readiness.`,
|
|
69
|
+
"Reloading the page keeps this state visible until the next workspace update replaces it.",
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
function normalizeRebaseSurface(rebase, workspacePath) {
|
|
73
|
+
if (!rebase) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const normalized = {
|
|
77
|
+
status: rebase.status,
|
|
78
|
+
branch: rebase.branch,
|
|
79
|
+
targetBranch: rebase.targetBranch ?? "main",
|
|
80
|
+
attemptCount: rebase.attemptCount ?? 0,
|
|
81
|
+
unresolvedFiles: [...(rebase.unresolvedFiles ?? [])],
|
|
82
|
+
resolvedFiles: [...(rebase.resolvedFiles ?? [])],
|
|
83
|
+
followUpInstructions: [...(rebase.followUpInstructions ?? [])],
|
|
84
|
+
manualResolutionSuggested: rebase.manualResolutionSuggested ?? rebase.status === "rebase-conflicts",
|
|
85
|
+
readyFor: rebase.readyFor ?? "merge",
|
|
86
|
+
editorHref: rebase.editorHref ?? buildEditorHref(workspacePath),
|
|
87
|
+
lastAction: rebase.lastAction,
|
|
88
|
+
persistedAt: rebase.persistedAt,
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
...normalized,
|
|
92
|
+
followUpInstructions: normalized.followUpInstructions.length > 0
|
|
93
|
+
? normalized.followUpInstructions
|
|
94
|
+
: buildFollowUpInstructions(normalized, workspacePath),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function buildConflictState(current, workspacePath) {
|
|
98
|
+
const unresolvedFiles = current
|
|
99
|
+
? [...new Set([...current.unresolvedFiles, ...current.resolvedFiles])].filter(Boolean)
|
|
100
|
+
: defaultConflictFiles();
|
|
101
|
+
const nextState = {
|
|
102
|
+
status: "rebase-conflicts",
|
|
103
|
+
branch: current?.branch,
|
|
104
|
+
targetBranch: current?.targetBranch ?? "main",
|
|
105
|
+
attemptCount: (current?.attemptCount ?? 0) + 1,
|
|
106
|
+
unresolvedFiles: unresolvedFiles.length > 0 ? unresolvedFiles : defaultConflictFiles(),
|
|
107
|
+
resolvedFiles: [],
|
|
108
|
+
followUpInstructions: [],
|
|
109
|
+
manualResolutionSuggested: false,
|
|
110
|
+
readyFor: current?.readyFor ?? "merge",
|
|
111
|
+
editorHref: buildEditorHref(workspacePath),
|
|
112
|
+
lastAction: "start",
|
|
113
|
+
persistedAt: Date.now(),
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
...nextState,
|
|
117
|
+
followUpInstructions: buildFollowUpInstructions(nextState, workspacePath),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function buildReadyState(current, workspacePath, lastAction) {
|
|
121
|
+
const readyStatus = current.readyFor === "review" ? "ready-for-review" : "ready-for-merge";
|
|
122
|
+
const nextState = {
|
|
123
|
+
...current,
|
|
124
|
+
status: readyStatus,
|
|
125
|
+
unresolvedFiles: [],
|
|
126
|
+
resolvedFiles: [...new Set([...current.resolvedFiles, ...current.unresolvedFiles])],
|
|
127
|
+
followUpInstructions: [],
|
|
128
|
+
manualResolutionSuggested: false,
|
|
129
|
+
lastAction,
|
|
130
|
+
persistedAt: Date.now(),
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
...nextState,
|
|
134
|
+
followUpInstructions: buildFollowUpInstructions(nextState, workspacePath),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function normalizeWorkspacePath(workspacePath) {
|
|
138
|
+
return path.resolve(workspacePath);
|
|
139
|
+
}
|
|
140
|
+
function matchesNormalizedWorkspacePath(candidatePath, normalizedTargetPath) {
|
|
141
|
+
if (!candidatePath || !normalizedTargetPath) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return normalizeWorkspacePath(candidatePath) === normalizedTargetPath;
|
|
145
|
+
}
|
|
146
|
+
function normalizeOwnershipValue(ownership) {
|
|
147
|
+
if (!ownership) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const project = ownership.project?.projectId
|
|
151
|
+
? {
|
|
152
|
+
projectId: ownership.project.projectId,
|
|
153
|
+
projectKey: ownership.project.projectKey,
|
|
154
|
+
projectName: ownership.project.projectName,
|
|
155
|
+
}
|
|
156
|
+
: undefined;
|
|
157
|
+
const issue = ownership.issue?.issueId
|
|
158
|
+
? {
|
|
159
|
+
issueId: ownership.issue.issueId,
|
|
160
|
+
issueKey: ownership.issue.issueKey,
|
|
161
|
+
issueTitle: ownership.issue.issueTitle,
|
|
162
|
+
}
|
|
163
|
+
: undefined;
|
|
164
|
+
const host = ownership.host?.provider
|
|
165
|
+
? {
|
|
166
|
+
provider: ownership.host.provider,
|
|
167
|
+
label: ownership.host.label,
|
|
168
|
+
accountLabel: ownership.host.accountLabel,
|
|
169
|
+
}
|
|
170
|
+
: undefined;
|
|
171
|
+
return {
|
|
172
|
+
source: ownership.source,
|
|
173
|
+
project,
|
|
174
|
+
issue,
|
|
175
|
+
host,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function deriveOwnershipFromIssues(linkedIssues) {
|
|
179
|
+
const primaryIssue = linkedIssues[0];
|
|
180
|
+
if (!primaryIssue) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
source: primaryIssue.source,
|
|
185
|
+
project: {
|
|
186
|
+
projectId: primaryIssue.projectId,
|
|
187
|
+
projectKey: primaryIssue.projectKey,
|
|
188
|
+
projectName: primaryIssue.projectName,
|
|
189
|
+
},
|
|
190
|
+
issue: {
|
|
191
|
+
issueId: primaryIssue.issueId,
|
|
192
|
+
issueKey: primaryIssue.issueKey,
|
|
193
|
+
issueTitle: primaryIssue.issueTitle,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function slugifyWorkspaceName(value) {
|
|
198
|
+
return value
|
|
199
|
+
.trim()
|
|
200
|
+
.toLowerCase()
|
|
201
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
202
|
+
.replace(/^-+|-+$/g, "")
|
|
203
|
+
.replace(/-+/g, "-") || "workspace";
|
|
204
|
+
}
|
|
205
|
+
function stripRefPrefix(branch) {
|
|
206
|
+
if (!branch) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return branch.replace(/^refs\/heads\//, "");
|
|
210
|
+
}
|
|
211
|
+
function parseAheadBehind(stdout) {
|
|
212
|
+
if (!stdout) {
|
|
213
|
+
return { ahead: null, behind: null };
|
|
214
|
+
}
|
|
215
|
+
const [aheadRaw, behindRaw] = stdout.trim().split(/\s+/);
|
|
216
|
+
const ahead = Number.parseInt(aheadRaw ?? "", 10);
|
|
217
|
+
const behind = Number.parseInt(behindRaw ?? "", 10);
|
|
218
|
+
return {
|
|
219
|
+
ahead: Number.isFinite(ahead) ? ahead : null,
|
|
220
|
+
behind: Number.isFinite(behind) ? behind : null,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function countStatusEntries(stdout) {
|
|
224
|
+
if (stdout == null) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return stdout
|
|
228
|
+
.split("\n")
|
|
229
|
+
.map((line) => line.trimEnd())
|
|
230
|
+
.filter((line) => line.length > 0).length;
|
|
231
|
+
}
|
|
232
|
+
function isActiveRunStatus(status) {
|
|
233
|
+
return status === "pending" || status === "waiting";
|
|
234
|
+
}
|
|
235
|
+
function parseWorktreeList(stdout) {
|
|
236
|
+
const blocks = stdout.trim().split(/\n\s*\n/).filter(Boolean);
|
|
237
|
+
return blocks.map((block, index) => {
|
|
238
|
+
const record = {
|
|
239
|
+
path: "",
|
|
240
|
+
head: null,
|
|
241
|
+
branch: null,
|
|
242
|
+
isPrimary: index === 0,
|
|
243
|
+
};
|
|
244
|
+
for (const line of block.split("\n")) {
|
|
245
|
+
if (line.startsWith("worktree ")) {
|
|
246
|
+
record.path = normalizeWorkspacePath(line.slice("worktree ".length));
|
|
247
|
+
}
|
|
248
|
+
else if (line.startsWith("HEAD ")) {
|
|
249
|
+
record.head = line.slice("HEAD ".length);
|
|
250
|
+
}
|
|
251
|
+
else if (line.startsWith("branch ")) {
|
|
252
|
+
record.branch = stripRefPrefix(line.slice("branch ".length));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return record;
|
|
256
|
+
}).filter((record) => record.path.length > 0);
|
|
257
|
+
}
|
|
258
|
+
async function pathExists(deps, targetPath) {
|
|
259
|
+
try {
|
|
260
|
+
await deps.stat(targetPath);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function readRegistry(deps) {
|
|
268
|
+
try {
|
|
269
|
+
const raw = await deps.readFile(WORKSPACE_REGISTRY_PATH, "utf8");
|
|
270
|
+
const parsed = JSON.parse(raw);
|
|
271
|
+
if (!parsed.workspaces || typeof parsed.workspaces !== "object") {
|
|
272
|
+
return { version: 1, workspaces: {} };
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
version: 1,
|
|
276
|
+
workspaces: parsed.workspaces,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return { version: 1, workspaces: {} };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function writeRegistry(deps, registry) {
|
|
284
|
+
await deps.mkdir(path.dirname(WORKSPACE_REGISTRY_PATH), { recursive: true });
|
|
285
|
+
await deps.writeFile(WORKSPACE_REGISTRY_PATH, `${JSON.stringify(registry, null, 2)}\n`, "utf8");
|
|
286
|
+
}
|
|
287
|
+
async function tryGit(deps, cwd, args) {
|
|
288
|
+
try {
|
|
289
|
+
const result = await deps.execGit(args, cwd);
|
|
290
|
+
return result.stdout.trim();
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function collectGitCluster(deps, seedPath) {
|
|
297
|
+
const gitRoot = await tryGit(deps, seedPath, ["rev-parse", "--show-toplevel"]);
|
|
298
|
+
const commonDir = await tryGit(deps, seedPath, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
299
|
+
const worktreeStdout = await tryGit(deps, seedPath, ["worktree", "list", "--porcelain"]);
|
|
300
|
+
return {
|
|
301
|
+
commonDir,
|
|
302
|
+
gitRoot,
|
|
303
|
+
worktrees: worktreeStdout ? parseWorktreeList(worktreeStdout) : [],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function ensureWorkspaceRecord(records, workspacePath) {
|
|
307
|
+
const normalizedPath = normalizeWorkspacePath(workspacePath);
|
|
308
|
+
const existing = records.get(normalizedPath);
|
|
309
|
+
if (existing) {
|
|
310
|
+
return existing;
|
|
311
|
+
}
|
|
312
|
+
const created = {
|
|
313
|
+
path: normalizedPath,
|
|
314
|
+
name: path.basename(normalizedPath),
|
|
315
|
+
gitRoot: null,
|
|
316
|
+
commonDir: null,
|
|
317
|
+
trackingBranch: null,
|
|
318
|
+
branch: null,
|
|
319
|
+
head: null,
|
|
320
|
+
ahead: null,
|
|
321
|
+
behind: null,
|
|
322
|
+
dirty: null,
|
|
323
|
+
uncommittedCount: null,
|
|
324
|
+
isWorktree: false,
|
|
325
|
+
isPrimary: false,
|
|
326
|
+
missing: false,
|
|
327
|
+
pinnedAt: null,
|
|
328
|
+
archivedAt: null,
|
|
329
|
+
cleanedAt: null,
|
|
330
|
+
notes: "",
|
|
331
|
+
notesUpdatedAt: null,
|
|
332
|
+
lastActivityAtMs: null,
|
|
333
|
+
rebase: undefined,
|
|
334
|
+
sessions: [],
|
|
335
|
+
runs: [],
|
|
336
|
+
};
|
|
337
|
+
records.set(normalizedPath, created);
|
|
338
|
+
return created;
|
|
339
|
+
}
|
|
340
|
+
function toWorkspaceStatus(record) {
|
|
341
|
+
if (record.missing) {
|
|
342
|
+
return "missing";
|
|
343
|
+
}
|
|
344
|
+
if (record.archivedAt) {
|
|
345
|
+
return "archived";
|
|
346
|
+
}
|
|
347
|
+
if (record.sessions.some((session) => session.status === "active") || record.runs.some((run) => isActiveRunStatus(run.status))) {
|
|
348
|
+
return "active";
|
|
349
|
+
}
|
|
350
|
+
return "idle";
|
|
351
|
+
}
|
|
352
|
+
function toSharedSessionBindings(sessions) {
|
|
353
|
+
return sessions.map((session) => ({
|
|
354
|
+
agent: session.agent,
|
|
355
|
+
sessionId: session.sessionId,
|
|
356
|
+
status: session.status === "active" ? "running" : "stopped",
|
|
357
|
+
cwd: session.cwd,
|
|
358
|
+
title: session.title,
|
|
359
|
+
updatedAt: typeof session.updatedAt === "number"
|
|
360
|
+
? new Date(session.updatedAt).toISOString()
|
|
361
|
+
: new Date().toISOString(),
|
|
362
|
+
activeRunId: session.activeRunId ?? null,
|
|
363
|
+
latestRunId: session.latestRunId ?? null,
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
function matchesManagedWorkspacePath(workspace, targetPath) {
|
|
367
|
+
const normalized = normalizeWorkspacePath(targetPath);
|
|
368
|
+
if (normalized === workspace.rootPath || normalized === workspace.defaultCwd) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
return workspace.repos.some((repo) => normalized === repo.targetPath || normalized.startsWith(`${repo.targetPath}${path.sep}`));
|
|
372
|
+
}
|
|
373
|
+
function toManagedWorkspacePath(workspace, resolveWorkspaceDefaultCwd) {
|
|
374
|
+
return resolveWorkspaceDefaultCwd(workspace);
|
|
375
|
+
}
|
|
376
|
+
export class WorkspaceLifecycleService {
|
|
377
|
+
deps;
|
|
378
|
+
workspaceCorePromise = null;
|
|
379
|
+
constructor(deps) {
|
|
380
|
+
this.deps = { ...defaultDeps, ...deps };
|
|
381
|
+
}
|
|
382
|
+
async getWorkspaceCore() {
|
|
383
|
+
if (!this.workspaceCorePromise) {
|
|
384
|
+
this.workspaceCorePromise = loadAgentMuxCore().then((agentMuxCore) => ({
|
|
385
|
+
resolveWorkspaceDefaultCwd: agentMuxCore.resolveWorkspaceDefaultCwd,
|
|
386
|
+
sharedWorkspaceService: this.deps.workspaceService ??
|
|
387
|
+
new agentMuxCore.WorkspaceService(),
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
return await this.workspaceCorePromise;
|
|
391
|
+
}
|
|
392
|
+
async listWorkspaces(input = {}) {
|
|
393
|
+
const { resolveWorkspaceDefaultCwd, sharedWorkspaceService } = await this.getWorkspaceCore();
|
|
394
|
+
const sessions = input.sessions ?? [];
|
|
395
|
+
const focusWorkspacePath = input.focusWorkspacePath ? normalizeWorkspacePath(input.focusWorkspacePath) : null;
|
|
396
|
+
const reviewByWorkspacePath = input.reviewByWorkspacePath ?? new Map();
|
|
397
|
+
const linkedIssuesByWorkspacePath = input.linkedIssuesByWorkspacePath ?? new Map();
|
|
398
|
+
const registry = await readRegistry(this.deps);
|
|
399
|
+
const records = new Map();
|
|
400
|
+
let managedWorkspaces = [];
|
|
401
|
+
const scopedSessions = focusWorkspacePath
|
|
402
|
+
? sessions.filter((session) => matchesNormalizedWorkspacePath(session.cwd, focusWorkspacePath))
|
|
403
|
+
: sessions;
|
|
404
|
+
try {
|
|
405
|
+
managedWorkspaces = (await sharedWorkspaceService.listWorkspaces({
|
|
406
|
+
liveSessions: toSharedSessionBindings(scopedSessions),
|
|
407
|
+
})).workspaces;
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
managedWorkspaces = [];
|
|
411
|
+
}
|
|
412
|
+
const scopedManagedWorkspaces = focusWorkspacePath
|
|
413
|
+
? managedWorkspaces.filter((workspace) => matchesManagedWorkspacePath(workspace, focusWorkspacePath))
|
|
414
|
+
: managedWorkspaces;
|
|
415
|
+
const seedPaths = focusWorkspacePath
|
|
416
|
+
? new Set([
|
|
417
|
+
focusWorkspacePath,
|
|
418
|
+
...Object.keys(registry.workspaces)
|
|
419
|
+
.map(normalizeWorkspacePath)
|
|
420
|
+
.filter((workspacePath) => workspacePath === focusWorkspacePath),
|
|
421
|
+
...scopedManagedWorkspaces.map((workspace) => normalizeWorkspacePath(toManagedWorkspacePath(workspace, resolveWorkspaceDefaultCwd))),
|
|
422
|
+
...scopedSessions.flatMap((session) => (session.cwd ? [normalizeWorkspacePath(session.cwd)] : [])),
|
|
423
|
+
])
|
|
424
|
+
: new Set([
|
|
425
|
+
normalizeWorkspacePath(this.deps.cwd()),
|
|
426
|
+
...Object.keys(registry.workspaces).map(normalizeWorkspacePath),
|
|
427
|
+
...managedWorkspaces.map((workspace) => normalizeWorkspacePath(toManagedWorkspacePath(workspace, resolveWorkspaceDefaultCwd))),
|
|
428
|
+
...sessions.flatMap((session) => (session.cwd ? [normalizeWorkspacePath(session.cwd)] : [])),
|
|
429
|
+
]);
|
|
430
|
+
const seenCommonDirs = new Set();
|
|
431
|
+
for (const seedPath of seedPaths) {
|
|
432
|
+
const exists = await pathExists(this.deps, seedPath);
|
|
433
|
+
if (!exists) {
|
|
434
|
+
const record = ensureWorkspaceRecord(records, seedPath);
|
|
435
|
+
record.missing = true;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const cluster = await collectGitCluster(this.deps, seedPath);
|
|
439
|
+
if (!cluster.worktrees.length) {
|
|
440
|
+
ensureWorkspaceRecord(records, seedPath);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const clusterKey = cluster.commonDir ?? cluster.gitRoot ?? seedPath;
|
|
444
|
+
if (seenCommonDirs.has(clusterKey)) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
seenCommonDirs.add(clusterKey);
|
|
448
|
+
if (focusWorkspacePath) {
|
|
449
|
+
const matchingWorktree = cluster.worktrees.find((worktree) => normalizeWorkspacePath(worktree.path) === focusWorkspacePath) ??
|
|
450
|
+
cluster.worktrees.find((worktree) => normalizeWorkspacePath(worktree.path) === seedPath);
|
|
451
|
+
const record = ensureWorkspaceRecord(records, focusWorkspacePath);
|
|
452
|
+
record.gitRoot = cluster.gitRoot;
|
|
453
|
+
record.commonDir = cluster.commonDir;
|
|
454
|
+
record.branch = matchingWorktree?.branch ?? record.branch;
|
|
455
|
+
record.head = matchingWorktree?.head ?? record.head;
|
|
456
|
+
record.isWorktree = matchingWorktree ? true : record.isWorktree;
|
|
457
|
+
record.isPrimary = matchingWorktree?.isPrimary ?? record.isPrimary;
|
|
458
|
+
record.name = matchingWorktree?.branch ?? record.name;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
for (const worktree of cluster.worktrees) {
|
|
462
|
+
const record = ensureWorkspaceRecord(records, worktree.path);
|
|
463
|
+
record.gitRoot = cluster.gitRoot;
|
|
464
|
+
record.commonDir = cluster.commonDir;
|
|
465
|
+
record.branch = worktree.branch;
|
|
466
|
+
record.head = worktree.head;
|
|
467
|
+
record.isWorktree = true;
|
|
468
|
+
record.isPrimary = worktree.isPrimary;
|
|
469
|
+
record.name = worktree.branch ?? path.basename(worktree.path);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
for (const entry of Object.values(registry.workspaces)) {
|
|
473
|
+
if (focusWorkspacePath && normalizeWorkspacePath(entry.path) !== focusWorkspacePath) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const record = ensureWorkspaceRecord(records, entry.path);
|
|
477
|
+
record.name = entry.name ?? record.name;
|
|
478
|
+
record.gitRoot = entry.gitRoot ?? record.gitRoot;
|
|
479
|
+
record.commonDir = entry.commonDir ?? record.commonDir;
|
|
480
|
+
record.trackingBranch = entry.trackingBranch ?? record.trackingBranch;
|
|
481
|
+
record.branch = entry.branch ?? record.branch;
|
|
482
|
+
record.pinnedAt = entry.pinnedAt ?? null;
|
|
483
|
+
record.archivedAt = entry.archivedAt ?? null;
|
|
484
|
+
record.cleanedAt = entry.cleanedAt ?? null;
|
|
485
|
+
record.notes = entry.notes ?? "";
|
|
486
|
+
record.notesUpdatedAt = entry.notesUpdatedAt ?? null;
|
|
487
|
+
record.rebase = normalizeRebaseSurface(entry.rebase, record.path) ?? record.rebase;
|
|
488
|
+
record.ownership = normalizeOwnershipValue(entry.ownership) ?? record.ownership;
|
|
489
|
+
if (entry.notesUpdatedAt) {
|
|
490
|
+
const updatedAt = Date.parse(entry.notesUpdatedAt);
|
|
491
|
+
if (Number.isFinite(updatedAt)) {
|
|
492
|
+
record.lastActivityAtMs = Math.max(record.lastActivityAtMs ?? 0, updatedAt);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (typeof entry.rebase?.persistedAt === "number") {
|
|
496
|
+
record.lastActivityAtMs = Math.max(record.lastActivityAtMs ?? 0, entry.rebase.persistedAt);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
for (const workspace of scopedManagedWorkspaces) {
|
|
500
|
+
const workspacePath = normalizeWorkspacePath(toManagedWorkspacePath(workspace, resolveWorkspaceDefaultCwd));
|
|
501
|
+
const record = ensureWorkspaceRecord(records, workspacePath);
|
|
502
|
+
const primaryRepo = workspace.repos.find((repo) => normalizeWorkspacePath(repo.targetPath) === workspacePath) ??
|
|
503
|
+
(workspace.repos.length === 1 ? workspace.repos[0] : undefined);
|
|
504
|
+
record.name = workspace.name || record.name;
|
|
505
|
+
record.gitRoot = primaryRepo?.gitRoot ?? record.gitRoot;
|
|
506
|
+
record.branch = primaryRepo?.branch ?? record.branch;
|
|
507
|
+
record.head = primaryRepo?.head ?? record.head;
|
|
508
|
+
record.isWorktree = primaryRepo?.mode === "worktree";
|
|
509
|
+
record.archivedAt = workspace.archivedAt;
|
|
510
|
+
record.cleanedAt = workspace.cleanedAt;
|
|
511
|
+
if (workspace.status === "missing" || workspace.status === "cleaned") {
|
|
512
|
+
record.missing = true;
|
|
513
|
+
}
|
|
514
|
+
for (const session of workspace.sessions) {
|
|
515
|
+
const existing = record.sessions.find((candidate) => candidate.sessionId === session.sessionId && candidate.agent === session.agent);
|
|
516
|
+
if (existing) {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
record.sessions.push({
|
|
520
|
+
sessionId: session.sessionId,
|
|
521
|
+
agent: session.agent,
|
|
522
|
+
status: session.status === "running" ? "active" : "inactive",
|
|
523
|
+
cwd: workspacePath,
|
|
524
|
+
title: session.title,
|
|
525
|
+
updatedAt: Date.parse(session.updatedAt),
|
|
526
|
+
activeRunId: session.activeRunId ?? null,
|
|
527
|
+
latestRunId: session.latestRunId ?? null,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
for (const session of scopedSessions) {
|
|
532
|
+
if (!session.cwd) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const managedWorkspace = managedWorkspaces.find((workspace) => matchesManagedWorkspacePath(workspace, session.cwd));
|
|
536
|
+
const workspacePath = managedWorkspace
|
|
537
|
+
? normalizeWorkspacePath(toManagedWorkspacePath(managedWorkspace, resolveWorkspaceDefaultCwd))
|
|
538
|
+
: normalizeWorkspacePath(session.cwd);
|
|
539
|
+
const record = ensureWorkspaceRecord(records, workspacePath);
|
|
540
|
+
if (record.sessions.some((candidate) => candidate.sessionId === session.sessionId && candidate.agent === session.agent)) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
record.sessions.push({
|
|
544
|
+
...session,
|
|
545
|
+
cwd: workspacePath,
|
|
546
|
+
});
|
|
547
|
+
record.name = session.title?.trim() ? session.title : record.name;
|
|
548
|
+
if (session.runtime?.rebase) {
|
|
549
|
+
record.rebase = normalizeRebaseSurface(session.runtime.rebase, workspacePath) ?? record.rebase;
|
|
550
|
+
if (typeof session.runtime.rebase.persistedAt === "number") {
|
|
551
|
+
record.lastActivityAtMs = Math.max(record.lastActivityAtMs ?? 0, session.runtime.rebase.persistedAt);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (typeof session.updatedAt === "number") {
|
|
555
|
+
record.lastActivityAtMs = Math.max(record.lastActivityAtMs ?? 0, session.updatedAt);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
for (const record of records.values()) {
|
|
559
|
+
record.missing = !(await pathExists(this.deps, record.path));
|
|
560
|
+
if (record.missing) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (!record.gitRoot) {
|
|
564
|
+
record.gitRoot = await tryGit(this.deps, record.path, ["rev-parse", "--show-toplevel"]);
|
|
565
|
+
}
|
|
566
|
+
if (!record.commonDir) {
|
|
567
|
+
record.commonDir = await tryGit(this.deps, record.path, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
568
|
+
}
|
|
569
|
+
if (!record.branch) {
|
|
570
|
+
record.branch = await tryGit(this.deps, record.path, ["branch", "--show-current"]);
|
|
571
|
+
}
|
|
572
|
+
if (!record.head) {
|
|
573
|
+
record.head = await tryGit(this.deps, record.path, ["rev-parse", "HEAD"]);
|
|
574
|
+
}
|
|
575
|
+
if (!record.trackingBranch) {
|
|
576
|
+
record.trackingBranch = stripRefPrefix(await tryGit(this.deps, record.path, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]));
|
|
577
|
+
}
|
|
578
|
+
const gitStatus = await tryGit(this.deps, record.path, ["status", "--porcelain"]);
|
|
579
|
+
record.uncommittedCount = countStatusEntries(gitStatus);
|
|
580
|
+
record.dirty = record.uncommittedCount == null ? null : record.uncommittedCount > 0;
|
|
581
|
+
if (record.branch && record.trackingBranch) {
|
|
582
|
+
const syncCounts = parseAheadBehind(await tryGit(this.deps, record.path, ["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]));
|
|
583
|
+
record.ahead = syncCounts.ahead;
|
|
584
|
+
record.behind = syncCounts.behind;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const discoveredRuns = await this.deps.discoverAllRunDirs();
|
|
588
|
+
const workspacePaths = Array.from(records.keys()).sort((left, right) => right.length - left.length);
|
|
589
|
+
for (const discovered of discoveredRuns) {
|
|
590
|
+
const run = await this.deps.getRunCached(discovered.runDir, discovered.source, discovered.projectName);
|
|
591
|
+
const matchingPath = workspacePaths.find((workspacePath) => discovered.runDir.startsWith(`${workspacePath}${path.sep}`));
|
|
592
|
+
if (!matchingPath) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const record = ensureWorkspaceRecord(records, matchingPath);
|
|
596
|
+
record.runs.push({
|
|
597
|
+
runId: run.runId,
|
|
598
|
+
status: run.status,
|
|
599
|
+
projectName: run.projectName,
|
|
600
|
+
});
|
|
601
|
+
const updatedAt = Date.parse(run.updatedAt);
|
|
602
|
+
if (Number.isFinite(updatedAt)) {
|
|
603
|
+
record.lastActivityAtMs = Math.max(record.lastActivityAtMs ?? 0, updatedAt);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const workspaces = Array.from(records.values())
|
|
607
|
+
.map((record) => {
|
|
608
|
+
const status = toWorkspaceStatus(record);
|
|
609
|
+
const activeSessions = record.sessions.filter((session) => session.status === "active").length;
|
|
610
|
+
const activeRuns = record.runs.filter((run) => isActiveRunStatus(run.status)).length;
|
|
611
|
+
const rebase = normalizeRebaseSurface(record.rebase, record.path);
|
|
612
|
+
const rebaseStatus = rebase?.status;
|
|
613
|
+
const linkedIssues = [...(linkedIssuesByWorkspacePath.get(record.path) ?? [])].sort((left, right) => left.issueKey.localeCompare(right.issueKey));
|
|
614
|
+
const ownership = record.ownership ?? deriveOwnershipFromIssues(linkedIssues);
|
|
615
|
+
return {
|
|
616
|
+
path: record.path,
|
|
617
|
+
name: record.name,
|
|
618
|
+
status,
|
|
619
|
+
pinnedAt: record.pinnedAt,
|
|
620
|
+
missing: record.missing,
|
|
621
|
+
archivedAt: record.archivedAt,
|
|
622
|
+
cleanedAt: record.cleanedAt,
|
|
623
|
+
lastActivityAt: record.lastActivityAtMs ? new Date(record.lastActivityAtMs).toISOString() : null,
|
|
624
|
+
git: {
|
|
625
|
+
root: record.gitRoot,
|
|
626
|
+
commonDir: record.commonDir,
|
|
627
|
+
trackingBranch: record.trackingBranch,
|
|
628
|
+
branch: record.branch,
|
|
629
|
+
head: record.head,
|
|
630
|
+
ahead: record.ahead,
|
|
631
|
+
behind: record.behind,
|
|
632
|
+
dirty: record.dirty,
|
|
633
|
+
uncommittedCount: record.uncommittedCount,
|
|
634
|
+
isWorktree: record.isWorktree,
|
|
635
|
+
isPrimary: record.isPrimary,
|
|
636
|
+
},
|
|
637
|
+
notes: {
|
|
638
|
+
value: record.notes,
|
|
639
|
+
updatedAt: record.notesUpdatedAt,
|
|
640
|
+
},
|
|
641
|
+
links: {
|
|
642
|
+
editorHref: record.missing ? null : buildEditorHref(record.path),
|
|
643
|
+
},
|
|
644
|
+
sessions: {
|
|
645
|
+
total: record.sessions.length,
|
|
646
|
+
active: activeSessions,
|
|
647
|
+
items: record.sessions.sort((left, right) => Number(right.updatedAt ?? 0) - Number(left.updatedAt ?? 0)),
|
|
648
|
+
},
|
|
649
|
+
runs: {
|
|
650
|
+
total: record.runs.length,
|
|
651
|
+
active: activeRuns,
|
|
652
|
+
items: record.runs,
|
|
653
|
+
},
|
|
654
|
+
rebase,
|
|
655
|
+
actions: {
|
|
656
|
+
canPin: !record.missing && !record.pinnedAt,
|
|
657
|
+
canUnpin: !record.missing && Boolean(record.pinnedAt),
|
|
658
|
+
canArchive: !record.missing,
|
|
659
|
+
canCleanup: Boolean(record.archivedAt) &&
|
|
660
|
+
!record.missing &&
|
|
661
|
+
record.isWorktree &&
|
|
662
|
+
!record.isPrimary &&
|
|
663
|
+
activeSessions === 0 &&
|
|
664
|
+
activeRuns === 0,
|
|
665
|
+
canRecover: Boolean(record.archivedAt) || (Boolean(record.cleanedAt) && record.missing),
|
|
666
|
+
canRebaseStart: rebaseStatus === "rebase-needed",
|
|
667
|
+
canRebaseAutoResolve: rebaseStatus === "rebase-conflicts" && (rebase?.unresolvedFiles.length ?? 0) > 0,
|
|
668
|
+
canRebaseOpenInEditor: rebaseStatus === "rebase-conflicts",
|
|
669
|
+
canRebaseMarkResolved: rebaseStatus === "rebase-conflicts",
|
|
670
|
+
canRebaseAbort: rebaseStatus === "rebase-conflicts",
|
|
671
|
+
},
|
|
672
|
+
review: reviewByWorkspacePath.get(record.path),
|
|
673
|
+
issues: linkedIssues,
|
|
674
|
+
ownership,
|
|
675
|
+
};
|
|
676
|
+
})
|
|
677
|
+
.sort((left, right) => {
|
|
678
|
+
const pinDiff = Number(Boolean(right.pinnedAt)) - Number(Boolean(left.pinnedAt));
|
|
679
|
+
if (pinDiff !== 0) {
|
|
680
|
+
return pinDiff;
|
|
681
|
+
}
|
|
682
|
+
const rank = (status) => {
|
|
683
|
+
if (status === "active")
|
|
684
|
+
return 0;
|
|
685
|
+
if (status === "idle")
|
|
686
|
+
return 1;
|
|
687
|
+
if (status === "archived")
|
|
688
|
+
return 2;
|
|
689
|
+
return 3;
|
|
690
|
+
};
|
|
691
|
+
const statusDiff = rank(left.status) - rank(right.status);
|
|
692
|
+
if (statusDiff !== 0) {
|
|
693
|
+
return statusDiff;
|
|
694
|
+
}
|
|
695
|
+
return left.path.localeCompare(right.path);
|
|
696
|
+
});
|
|
697
|
+
return {
|
|
698
|
+
workspaces,
|
|
699
|
+
summary: {
|
|
700
|
+
total: workspaces.length,
|
|
701
|
+
active: workspaces.filter((workspace) => workspace.status === "active").length,
|
|
702
|
+
idle: workspaces.filter((workspace) => workspace.status === "idle").length,
|
|
703
|
+
archived: workspaces.filter((workspace) => workspace.status === "archived").length,
|
|
704
|
+
missing: workspaces.filter((workspace) => workspace.status === "missing").length,
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
async provisionWorkspace(input) {
|
|
709
|
+
const { resolveWorkspaceDefaultCwd, sharedWorkspaceService } = await this.getWorkspaceCore();
|
|
710
|
+
const seedPath = normalizeWorkspacePath(this.deps.cwd());
|
|
711
|
+
const cluster = await collectGitCluster(this.deps, seedPath);
|
|
712
|
+
const sourcePath = cluster.gitRoot ?? seedPath;
|
|
713
|
+
const slugBase = slugifyWorkspaceName(input.slugSeed || input.workspaceName);
|
|
714
|
+
const requestedBranchName = `vk/${slugBase}`;
|
|
715
|
+
try {
|
|
716
|
+
const created = await sharedWorkspaceService.createWorkspace({
|
|
717
|
+
name: input.workspaceName,
|
|
718
|
+
repos: [{ path: sourcePath }],
|
|
719
|
+
mode: "worktree",
|
|
720
|
+
branchName: requestedBranchName,
|
|
721
|
+
});
|
|
722
|
+
const workspacePath = normalizeWorkspacePath(resolveWorkspaceDefaultCwd(created));
|
|
723
|
+
const repo = created.repos.find((candidate) => normalizeWorkspacePath(candidate.targetPath) === workspacePath) ??
|
|
724
|
+
created.repos[0];
|
|
725
|
+
const registry = await readRegistry(this.deps);
|
|
726
|
+
registry.workspaces[workspacePath] = {
|
|
727
|
+
path: workspacePath,
|
|
728
|
+
name: input.workspaceName,
|
|
729
|
+
gitRoot: repo?.gitRoot ?? cluster.gitRoot,
|
|
730
|
+
commonDir: cluster.commonDir,
|
|
731
|
+
branch: repo?.branch ?? requestedBranchName,
|
|
732
|
+
trackingBranch: null,
|
|
733
|
+
pinnedAt: registry.workspaces[workspacePath]?.pinnedAt ?? null,
|
|
734
|
+
archivedAt: created.archivedAt,
|
|
735
|
+
cleanedAt: created.cleanedAt,
|
|
736
|
+
notes: registry.workspaces[workspacePath]?.notes ?? "",
|
|
737
|
+
notesUpdatedAt: registry.workspaces[workspacePath]?.notesUpdatedAt ?? null,
|
|
738
|
+
rebase: registry.workspaces[workspacePath]?.rebase ?? null,
|
|
739
|
+
ownership: input.ownership ?? registry.workspaces[workspacePath]?.ownership ?? null,
|
|
740
|
+
};
|
|
741
|
+
await writeRegistry(this.deps, registry);
|
|
742
|
+
return {
|
|
743
|
+
workspacePath,
|
|
744
|
+
workspaceName: input.workspaceName,
|
|
745
|
+
branchName: repo?.branch ?? requestedBranchName,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
// Fall back to the legacy kanban-local worktree path when shared workspace management is unavailable.
|
|
750
|
+
}
|
|
751
|
+
const registry = await readRegistry(this.deps);
|
|
752
|
+
const workspaceRoot = path.join(os.homedir(), ".a5c", "workspaces");
|
|
753
|
+
const branchBase = `vk/${slugBase}`;
|
|
754
|
+
const existingPaths = new Set([
|
|
755
|
+
...Object.keys(registry.workspaces).map(normalizeWorkspacePath),
|
|
756
|
+
...cluster.worktrees.map((worktree) => normalizeWorkspacePath(worktree.path)),
|
|
757
|
+
]);
|
|
758
|
+
const existingBranches = new Set(cluster.worktrees
|
|
759
|
+
.map((worktree) => worktree.branch)
|
|
760
|
+
.filter((branch) => typeof branch === "string" && branch.length > 0));
|
|
761
|
+
let suffix = 0;
|
|
762
|
+
let workspacePath = "";
|
|
763
|
+
let branchName = "";
|
|
764
|
+
while (true) {
|
|
765
|
+
const suffixLabel = suffix === 0 ? "" : `-${suffix + 1}`;
|
|
766
|
+
workspacePath = normalizeWorkspacePath(path.join(workspaceRoot, `${slugBase}${suffixLabel}`));
|
|
767
|
+
branchName = `${branchBase}${suffixLabel}`;
|
|
768
|
+
if (!existingPaths.has(workspacePath) && !existingBranches.has(branchName)) {
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
suffix += 1;
|
|
772
|
+
}
|
|
773
|
+
if (cluster.gitRoot) {
|
|
774
|
+
await this.deps.execGit(["worktree", "add", "-b", branchName, workspacePath], cluster.gitRoot);
|
|
775
|
+
}
|
|
776
|
+
registry.workspaces[workspacePath] = {
|
|
777
|
+
path: workspacePath,
|
|
778
|
+
name: input.workspaceName,
|
|
779
|
+
gitRoot: cluster.gitRoot,
|
|
780
|
+
commonDir: cluster.commonDir,
|
|
781
|
+
branch: branchName,
|
|
782
|
+
trackingBranch: null,
|
|
783
|
+
pinnedAt: null,
|
|
784
|
+
archivedAt: null,
|
|
785
|
+
cleanedAt: null,
|
|
786
|
+
notes: "",
|
|
787
|
+
notesUpdatedAt: null,
|
|
788
|
+
rebase: null,
|
|
789
|
+
ownership: input.ownership ?? null,
|
|
790
|
+
};
|
|
791
|
+
await writeRegistry(this.deps, registry);
|
|
792
|
+
return {
|
|
793
|
+
workspacePath,
|
|
794
|
+
workspaceName: input.workspaceName,
|
|
795
|
+
branchName,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
async provisionWorkspaceForIssue(input) {
|
|
799
|
+
return this.provisionWorkspace({
|
|
800
|
+
workspaceName: input.issueKey,
|
|
801
|
+
slugSeed: input.issueKey || input.issueTitle,
|
|
802
|
+
ownership: input.ownership,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
async applyAction(input) {
|
|
806
|
+
const { resolveWorkspaceDefaultCwd, sharedWorkspaceService } = await this.getWorkspaceCore();
|
|
807
|
+
const workspacePath = normalizeWorkspacePath(input.workspacePath);
|
|
808
|
+
const registry = await readRegistry(this.deps);
|
|
809
|
+
const inventory = await this.listWorkspaces({ sessions: input.sessions });
|
|
810
|
+
const workspace = inventory.workspaces.find((item) => item.path === workspacePath);
|
|
811
|
+
const entry = registry.workspaces[workspacePath] ?? {
|
|
812
|
+
path: workspacePath,
|
|
813
|
+
name: workspace?.name ?? path.basename(workspacePath),
|
|
814
|
+
gitRoot: workspace?.git.root ?? null,
|
|
815
|
+
commonDir: workspace?.git.commonDir ?? null,
|
|
816
|
+
trackingBranch: workspace?.git.trackingBranch ?? null,
|
|
817
|
+
branch: workspace?.git.branch ?? null,
|
|
818
|
+
pinnedAt: workspace?.pinnedAt ?? null,
|
|
819
|
+
archivedAt: workspace?.archivedAt ?? null,
|
|
820
|
+
cleanedAt: workspace?.cleanedAt ?? null,
|
|
821
|
+
notes: workspace?.notes.value ?? "",
|
|
822
|
+
notesUpdatedAt: workspace?.notes.updatedAt ?? null,
|
|
823
|
+
rebase: workspace?.rebase ?? null,
|
|
824
|
+
ownership: workspace?.ownership ?? null,
|
|
825
|
+
};
|
|
826
|
+
if (!workspace) {
|
|
827
|
+
throw new Error(`Workspace not found: ${workspacePath}`);
|
|
828
|
+
}
|
|
829
|
+
const now = this.deps.now();
|
|
830
|
+
if (input.action === "archive" || input.action === "cleanup" || input.action === "recover") {
|
|
831
|
+
try {
|
|
832
|
+
const managedWorkspace = await sharedWorkspaceService.resolveWorkspace(workspacePath);
|
|
833
|
+
if (managedWorkspace) {
|
|
834
|
+
const updated = input.action === "archive"
|
|
835
|
+
? await sharedWorkspaceService.archiveWorkspace(workspacePath)
|
|
836
|
+
: input.action === "cleanup"
|
|
837
|
+
? await sharedWorkspaceService.cleanupWorkspace(workspacePath)
|
|
838
|
+
: await sharedWorkspaceService.recoverWorkspace(workspacePath);
|
|
839
|
+
const managedPath = normalizeWorkspacePath(resolveWorkspaceDefaultCwd(updated));
|
|
840
|
+
const nextEntry = {
|
|
841
|
+
...(registry.workspaces[managedPath] ?? entry),
|
|
842
|
+
path: managedPath,
|
|
843
|
+
name: updated.name,
|
|
844
|
+
gitRoot: updated.repos[0]?.gitRoot ?? entry.gitRoot,
|
|
845
|
+
commonDir: entry.commonDir,
|
|
846
|
+
trackingBranch: entry.trackingBranch,
|
|
847
|
+
branch: updated.repos[0]?.branch ?? entry.branch,
|
|
848
|
+
pinnedAt: entry.pinnedAt,
|
|
849
|
+
archivedAt: updated.archivedAt,
|
|
850
|
+
cleanedAt: updated.cleanedAt,
|
|
851
|
+
notes: entry.notes,
|
|
852
|
+
notesUpdatedAt: entry.notesUpdatedAt,
|
|
853
|
+
rebase: entry.rebase,
|
|
854
|
+
ownership: entry.ownership,
|
|
855
|
+
};
|
|
856
|
+
if (managedPath !== workspacePath) {
|
|
857
|
+
delete registry.workspaces[workspacePath];
|
|
858
|
+
}
|
|
859
|
+
registry.workspaces[managedPath] = nextEntry;
|
|
860
|
+
await writeRegistry(this.deps, registry);
|
|
861
|
+
return {
|
|
862
|
+
ok: true,
|
|
863
|
+
workspacePath: managedPath,
|
|
864
|
+
action: input.action,
|
|
865
|
+
message: input.action === "archive"
|
|
866
|
+
? `Archived ${managedPath}.`
|
|
867
|
+
: input.action === "cleanup"
|
|
868
|
+
? `Cleaned up ${managedPath}.`
|
|
869
|
+
: `Recovered ${managedPath}.`,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
// Fall back to the legacy kanban-local lifecycle when the shared workspace service cannot handle the action.
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (input.action === "pin" || input.action === "unpin") {
|
|
878
|
+
entry.pinnedAt = input.action === "pin" ? now : null;
|
|
879
|
+
registry.workspaces[workspacePath] = entry;
|
|
880
|
+
await writeRegistry(this.deps, registry);
|
|
881
|
+
return {
|
|
882
|
+
ok: true,
|
|
883
|
+
workspacePath,
|
|
884
|
+
action: input.action,
|
|
885
|
+
message: input.action === "pin" ? `Pinned ${workspacePath}.` : `Unpinned ${workspacePath}.`,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
if (input.action === "archive") {
|
|
889
|
+
entry.archivedAt = now;
|
|
890
|
+
entry.cleanedAt = entry.cleanedAt ?? null;
|
|
891
|
+
registry.workspaces[workspacePath] = entry;
|
|
892
|
+
await writeRegistry(this.deps, registry);
|
|
893
|
+
return {
|
|
894
|
+
ok: true,
|
|
895
|
+
workspacePath,
|
|
896
|
+
action: input.action,
|
|
897
|
+
message: `Archived ${workspacePath}.`,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
if (input.action === "cleanup") {
|
|
901
|
+
if (!workspace.actions.canCleanup) {
|
|
902
|
+
throw new Error("Workspace is not eligible for cleanup. Archive it first and make sure there are no active sessions or runs.");
|
|
903
|
+
}
|
|
904
|
+
if (!workspace.git.root) {
|
|
905
|
+
throw new Error("Cannot clean up a workspace without a git root.");
|
|
906
|
+
}
|
|
907
|
+
await this.deps.execGit(["worktree", "remove", "--force", workspacePath], workspace.git.root);
|
|
908
|
+
await this.deps.execGit(["worktree", "prune"], workspace.git.root);
|
|
909
|
+
entry.archivedAt = entry.archivedAt ?? now;
|
|
910
|
+
entry.cleanedAt = now;
|
|
911
|
+
registry.workspaces[workspacePath] = entry;
|
|
912
|
+
await writeRegistry(this.deps, registry);
|
|
913
|
+
return {
|
|
914
|
+
ok: true,
|
|
915
|
+
workspacePath,
|
|
916
|
+
action: input.action,
|
|
917
|
+
message: `Removed git worktree ${workspacePath}.`,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
if (input.action === "notes-save") {
|
|
921
|
+
const nextNote = typeof input.note === "string" ? input.note : "";
|
|
922
|
+
const hasContent = nextNote.trim().length > 0;
|
|
923
|
+
entry.notes = hasContent ? nextNote : "";
|
|
924
|
+
entry.notesUpdatedAt = hasContent ? now : null;
|
|
925
|
+
registry.workspaces[workspacePath] = entry;
|
|
926
|
+
await writeRegistry(this.deps, registry);
|
|
927
|
+
return {
|
|
928
|
+
ok: true,
|
|
929
|
+
workspacePath,
|
|
930
|
+
action: input.action,
|
|
931
|
+
message: hasContent ? `Saved workspace notes for ${workspacePath}.` : `Cleared workspace notes for ${workspacePath}.`,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
if (input.action === "rebase-start") {
|
|
935
|
+
entry.rebase = buildConflictState(normalizeRebaseSurface(entry.rebase ?? workspace.rebase, workspacePath), workspacePath);
|
|
936
|
+
registry.workspaces[workspacePath] = entry;
|
|
937
|
+
await writeRegistry(this.deps, registry);
|
|
938
|
+
return {
|
|
939
|
+
ok: true,
|
|
940
|
+
workspacePath,
|
|
941
|
+
action: input.action,
|
|
942
|
+
message: `Started rebase workflow for ${workspacePath}.`,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
if (input.action === "rebase-auto-resolve") {
|
|
946
|
+
const current = normalizeRebaseSurface(entry.rebase ?? workspace.rebase, workspacePath);
|
|
947
|
+
if (!current || current.status !== "rebase-conflicts") {
|
|
948
|
+
throw new Error("Workspace is not currently in rebase-conflicts state.");
|
|
949
|
+
}
|
|
950
|
+
const [resolvedNow, ...remaining] = current.unresolvedFiles;
|
|
951
|
+
const nextState = remaining.length === 0
|
|
952
|
+
? buildReadyState({
|
|
953
|
+
...current,
|
|
954
|
+
resolvedFiles: [...new Set([...current.resolvedFiles, resolvedNow].filter(Boolean))],
|
|
955
|
+
unresolvedFiles: [],
|
|
956
|
+
}, workspacePath, "auto-resolve")
|
|
957
|
+
: {
|
|
958
|
+
...current,
|
|
959
|
+
unresolvedFiles: remaining,
|
|
960
|
+
resolvedFiles: [...new Set([...current.resolvedFiles, resolvedNow].filter(Boolean))],
|
|
961
|
+
followUpInstructions: [],
|
|
962
|
+
manualResolutionSuggested: true,
|
|
963
|
+
lastAction: "auto-resolve",
|
|
964
|
+
persistedAt: Date.now(),
|
|
965
|
+
};
|
|
966
|
+
entry.rebase = {
|
|
967
|
+
...nextState,
|
|
968
|
+
followUpInstructions: buildFollowUpInstructions(nextState, workspacePath),
|
|
969
|
+
};
|
|
970
|
+
registry.workspaces[workspacePath] = entry;
|
|
971
|
+
await writeRegistry(this.deps, registry);
|
|
972
|
+
return {
|
|
973
|
+
ok: true,
|
|
974
|
+
workspacePath,
|
|
975
|
+
action: input.action,
|
|
976
|
+
message: remaining.length === 0
|
|
977
|
+
? `Auto-resolved all tracked conflicts for ${workspacePath}.`
|
|
978
|
+
: `Auto-resolved ${resolvedNow}; ${remaining.length} conflict file(s) still need attention.`,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
if (input.action === "rebase-open-in-editor") {
|
|
982
|
+
const current = normalizeRebaseSurface(entry.rebase ?? workspace.rebase, workspacePath);
|
|
983
|
+
if (!current || current.status !== "rebase-conflicts") {
|
|
984
|
+
throw new Error("Workspace is not currently in rebase-conflicts state.");
|
|
985
|
+
}
|
|
986
|
+
const nextState = {
|
|
987
|
+
...current,
|
|
988
|
+
manualResolutionSuggested: true,
|
|
989
|
+
lastAction: "open-in-editor",
|
|
990
|
+
persistedAt: Date.now(),
|
|
991
|
+
editorHref: buildEditorHref(workspacePath),
|
|
992
|
+
followUpInstructions: [],
|
|
993
|
+
};
|
|
994
|
+
entry.rebase = {
|
|
995
|
+
...nextState,
|
|
996
|
+
followUpInstructions: buildFollowUpInstructions(nextState, workspacePath),
|
|
997
|
+
};
|
|
998
|
+
registry.workspaces[workspacePath] = entry;
|
|
999
|
+
await writeRegistry(this.deps, registry);
|
|
1000
|
+
return {
|
|
1001
|
+
ok: true,
|
|
1002
|
+
workspacePath,
|
|
1003
|
+
action: input.action,
|
|
1004
|
+
message: `Prepared manual conflict resolution guidance for ${workspacePath}.`,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
if (input.action === "rebase-mark-resolved") {
|
|
1008
|
+
const current = normalizeRebaseSurface(entry.rebase ?? workspace.rebase, workspacePath);
|
|
1009
|
+
if (!current || current.status !== "rebase-conflicts") {
|
|
1010
|
+
throw new Error("Workspace is not currently in rebase-conflicts state.");
|
|
1011
|
+
}
|
|
1012
|
+
entry.rebase = buildReadyState(current, workspacePath, "manual-resolve");
|
|
1013
|
+
registry.workspaces[workspacePath] = entry;
|
|
1014
|
+
await writeRegistry(this.deps, registry);
|
|
1015
|
+
return {
|
|
1016
|
+
ok: true,
|
|
1017
|
+
workspacePath,
|
|
1018
|
+
action: input.action,
|
|
1019
|
+
message: `Marked rebase conflicts resolved for ${workspacePath}.`,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
if (input.action === "rebase-abort") {
|
|
1023
|
+
const current = normalizeRebaseSurface(entry.rebase ?? workspace.rebase, workspacePath);
|
|
1024
|
+
if (!current || current.status !== "rebase-conflicts") {
|
|
1025
|
+
throw new Error("Workspace is not currently in rebase-conflicts state.");
|
|
1026
|
+
}
|
|
1027
|
+
const resetFiles = current
|
|
1028
|
+
? [...new Set([...current.unresolvedFiles, ...current.resolvedFiles])].filter(Boolean)
|
|
1029
|
+
: defaultConflictFiles();
|
|
1030
|
+
const nextState = {
|
|
1031
|
+
status: "rebase-needed",
|
|
1032
|
+
branch: current?.branch,
|
|
1033
|
+
targetBranch: current?.targetBranch ?? "main",
|
|
1034
|
+
attemptCount: current?.attemptCount ?? 0,
|
|
1035
|
+
unresolvedFiles: resetFiles.length > 0 ? resetFiles : defaultConflictFiles(),
|
|
1036
|
+
resolvedFiles: [],
|
|
1037
|
+
followUpInstructions: [],
|
|
1038
|
+
manualResolutionSuggested: false,
|
|
1039
|
+
readyFor: current?.readyFor ?? "merge",
|
|
1040
|
+
editorHref: buildEditorHref(workspacePath),
|
|
1041
|
+
lastAction: "abort",
|
|
1042
|
+
persistedAt: Date.now(),
|
|
1043
|
+
};
|
|
1044
|
+
entry.rebase = {
|
|
1045
|
+
...nextState,
|
|
1046
|
+
followUpInstructions: buildFollowUpInstructions(nextState, workspacePath),
|
|
1047
|
+
};
|
|
1048
|
+
registry.workspaces[workspacePath] = entry;
|
|
1049
|
+
await writeRegistry(this.deps, registry);
|
|
1050
|
+
return {
|
|
1051
|
+
ok: true,
|
|
1052
|
+
workspacePath,
|
|
1053
|
+
action: input.action,
|
|
1054
|
+
message: `Aborted the current rebase attempt for ${workspacePath}.`,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
if (!entry.gitRoot || !entry.branch) {
|
|
1058
|
+
entry.archivedAt = null;
|
|
1059
|
+
entry.cleanedAt = null;
|
|
1060
|
+
registry.workspaces[workspacePath] = entry;
|
|
1061
|
+
await writeRegistry(this.deps, registry);
|
|
1062
|
+
return {
|
|
1063
|
+
ok: true,
|
|
1064
|
+
workspacePath,
|
|
1065
|
+
action: input.action,
|
|
1066
|
+
message: `Recovered ${workspacePath} in metadata only.`,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
const exists = await pathExists(this.deps, workspacePath);
|
|
1070
|
+
if (!exists) {
|
|
1071
|
+
await this.deps.execGit(["worktree", "add", workspacePath, entry.branch], entry.gitRoot);
|
|
1072
|
+
}
|
|
1073
|
+
entry.archivedAt = null;
|
|
1074
|
+
entry.cleanedAt = null;
|
|
1075
|
+
registry.workspaces[workspacePath] = entry;
|
|
1076
|
+
await writeRegistry(this.deps, registry);
|
|
1077
|
+
return {
|
|
1078
|
+
ok: true,
|
|
1079
|
+
workspacePath,
|
|
1080
|
+
action: input.action,
|
|
1081
|
+
message: `Recovered ${workspacePath}.`,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
//# sourceMappingURL=workspace-lifecycle.js.map
|