@vscode/component-explorer-cli 0.1.1-2 → 0.1.1-20
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 +177 -0
- package/SECURITY.md +14 -0
- package/dist/WorktreePool.d.ts +22 -0
- package/dist/WorktreePool.d.ts.map +1 -0
- package/dist/WorktreePool.js +58 -0
- package/dist/WorktreePool.js.map +1 -0
- package/dist/WorktreePool.test.d.ts +2 -0
- package/dist/WorktreePool.test.d.ts.map +1 -0
- package/dist/_virtual/_build-info.js +4 -0
- package/dist/_virtual/_build-info.js.map +1 -0
- package/dist/browserPage.d.ts +5 -0
- package/dist/browserPage.d.ts.map +1 -1
- package/dist/browserPage.js +28 -2
- package/dist/browserPage.js.map +1 -1
- package/dist/commands/acceptCommand.d.ts +2 -1
- package/dist/commands/acceptCommand.d.ts.map +1 -1
- package/dist/commands/acceptCommand.js +15 -8
- package/dist/commands/acceptCommand.js.map +1 -1
- package/dist/commands/checkStabilityCommand.d.ts +12 -0
- package/dist/commands/checkStabilityCommand.d.ts.map +1 -0
- package/dist/commands/checkStabilityCommand.js +84 -0
- package/dist/commands/checkStabilityCommand.js.map +1 -0
- package/dist/commands/compareCommand.d.ts +5 -1
- package/dist/commands/compareCommand.d.ts.map +1 -1
- package/dist/commands/compareCommand.js +56 -71
- package/dist/commands/compareCommand.js.map +1 -1
- package/dist/commands/mcpCommand.d.ts +4 -1
- package/dist/commands/mcpCommand.d.ts.map +1 -1
- package/dist/commands/mcpCommand.js +51 -8
- package/dist/commands/mcpCommand.js.map +1 -1
- package/dist/commands/screenshotCommand.d.ts +9 -2
- package/dist/commands/screenshotCommand.d.ts.map +1 -1
- package/dist/commands/screenshotCommand.js +73 -15
- package/dist/commands/screenshotCommand.js.map +1 -1
- package/dist/commands/serveCommand.d.ts +6 -1
- package/dist/commands/serveCommand.d.ts.map +1 -1
- package/dist/commands/serveCommand.js +104 -23
- package/dist/commands/serveCommand.js.map +1 -1
- package/dist/commands/watchCommand.d.ts +3 -2
- package/dist/commands/watchCommand.d.ts.map +1 -1
- package/dist/commands/watchCommand.js +28 -80
- package/dist/commands/watchCommand.js.map +1 -1
- package/dist/comparison.d.ts +70 -0
- package/dist/comparison.d.ts.map +1 -0
- package/dist/comparison.js +264 -0
- package/dist/comparison.js.map +1 -0
- package/dist/component-explorer-config.schema.json +222 -0
- package/dist/componentExplorer.d.ts +40 -2
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +118 -24
- package/dist/componentExplorer.js.map +1 -1
- package/dist/daemon/DaemonContext.d.ts +4 -0
- package/dist/daemon/DaemonContext.d.ts.map +1 -0
- package/dist/daemon/DaemonService.d.ts +146 -21
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +620 -123
- package/dist/daemon/DaemonService.js.map +1 -1
- package/dist/daemon/dynamicSessions.test.d.ts +2 -0
- package/dist/daemon/dynamicSessions.test.d.ts.map +1 -0
- package/dist/daemon/lifecycle.d.ts +8 -3
- package/dist/daemon/lifecycle.d.ts.map +1 -1
- package/dist/daemon/lifecycle.js +27 -10
- package/dist/daemon/lifecycle.js.map +1 -1
- package/dist/daemon/pipeClient.d.ts +6 -1
- package/dist/daemon/pipeClient.d.ts.map +1 -1
- package/dist/daemon/pipeClient.js +101 -8
- package/dist/daemon/pipeClient.js.map +1 -1
- package/dist/daemon/pipeServer.d.ts +2 -1
- package/dist/daemon/pipeServer.d.ts.map +1 -1
- package/dist/daemon/pipeServer.js +56 -6
- package/dist/daemon/pipeServer.js.map +1 -1
- package/dist/daemon/types.d.ts +12 -0
- package/dist/daemon/types.d.ts.map +1 -0
- package/dist/daemon/version.d.ts +10 -0
- package/dist/daemon/version.d.ts.map +1 -0
- package/dist/daemon/version.js +17 -0
- package/dist/daemon/version.js.map +1 -0
- package/dist/dependencyInstaller.d.ts +2 -2
- package/dist/dependencyInstaller.d.ts.map +1 -1
- package/dist/dependencyInstaller.js +1 -1
- package/dist/dependencyInstaller.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/disposables.js +24 -1
- package/dist/external/vscode-observables/observables/dist/disposables.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/commonFacade/deps.js +1 -4
- package/dist/external/vscode-observables/observables/dist/observableInternal/commonFacade/deps.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/index.js +2 -5
- package/dist/external/vscode-observables/observables/dist/observableInternal/index.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/consoleObservableLogger.js +30 -6
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/consoleObservableLogger.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/baseObservable.js +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/baseObservable.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/derived.js +12 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/derived.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/utils/utilsCancellation.js +55 -0
- package/dist/external/vscode-observables/observables/dist/observableInternal/utils/utilsCancellation.js.map +1 -0
- package/dist/formatValue.d.ts +2 -0
- package/dist/formatValue.d.ts.map +1 -0
- package/dist/formatValue.js +96 -0
- package/dist/formatValue.js.map +1 -0
- package/dist/formatValue.test.d.ts +2 -0
- package/dist/formatValue.test.d.ts.map +1 -0
- package/dist/git/gitIndexResolver.d.ts +25 -0
- package/dist/git/gitIndexResolver.d.ts.map +1 -0
- package/dist/git/gitIndexResolver.js +91 -0
- package/dist/git/gitIndexResolver.js.map +1 -0
- package/dist/git/gitIndexResolver.test.d.ts +2 -0
- package/dist/git/gitIndexResolver.test.d.ts.map +1 -0
- package/dist/git/gitService.d.ts +2 -0
- package/dist/git/gitService.d.ts.map +1 -1
- package/dist/git/gitService.js +6 -0
- package/dist/git/gitService.js.map +1 -1
- package/dist/git/gitUtils.js +1 -1
- package/dist/git/gitUtils.js.map +1 -1
- package/dist/git/gitWorktreeManager.d.ts +6 -0
- package/dist/git/gitWorktreeManager.d.ts.map +1 -1
- package/dist/git/gitWorktreeManager.js +42 -13
- package/dist/git/gitWorktreeManager.js.map +1 -1
- package/dist/git/gitWorktreeManager.test.d.ts +2 -0
- package/dist/git/gitWorktreeManager.test.d.ts.map +1 -0
- package/dist/git/testUtils.d.ts +13 -0
- package/dist/git/testUtils.d.ts.map +1 -0
- package/dist/httpServer.d.ts +6 -1
- package/dist/httpServer.d.ts.map +1 -1
- package/dist/httpServer.js +30 -10
- package/dist/httpServer.js.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +7 -1
- package/dist/logger.js.map +1 -1
- package/dist/mcp/McpServer.d.ts +47 -4
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +913 -155
- package/dist/mcp/McpServer.js.map +1 -1
- package/dist/mcp/TaskManager.d.ts +28 -0
- package/dist/mcp/TaskManager.d.ts.map +1 -0
- package/dist/mcp/TaskManager.js +54 -0
- package/dist/mcp/TaskManager.js.map +1 -0
- package/dist/packages/simple-api/dist/chunk-3R7GHWBM.js +137 -0
- package/dist/packages/simple-api/dist/chunk-3R7GHWBM.js.map +1 -0
- package/dist/packages/simple-api/dist/chunk-SGBCNXYH.js +24 -0
- package/dist/packages/simple-api/dist/chunk-SGBCNXYH.js.map +1 -0
- package/dist/packages/simple-api/dist/chunk-TAEFVNPN.js +27 -0
- package/dist/packages/simple-api/dist/chunk-TAEFVNPN.js.map +1 -0
- package/dist/packages/simple-api/dist/express.js +104 -0
- package/dist/packages/simple-api/dist/express.js.map +1 -0
- package/dist/resolveProject.d.ts +21 -0
- package/dist/resolveProject.d.ts.map +1 -0
- package/dist/resolveProject.js +39 -0
- package/dist/resolveProject.js.map +1 -0
- package/dist/utils.d.ts +31 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +49 -0
- package/dist/utils.js.map +1 -0
- package/dist/watchConfig.d.ts +52 -9
- package/dist/watchConfig.d.ts.map +1 -1
- package/dist/watchConfig.js +67 -62
- package/dist/watchConfig.js.map +1 -1
- package/package.json +31 -8
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/debuggerRpc.js +0 -72
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/debuggerRpc.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/devToolsLogger.js +0 -447
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/devToolsLogger.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/rpc.js +0 -64
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/rpc.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/utils.js +0 -52
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/utils.js.map +0 -1
|
@@ -1,19 +1,86 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import * as path from 'node:path';
|
|
2
3
|
import '../external/vscode-observables/observables/dist/observableInternal/index.js';
|
|
4
|
+
import { observableValue } from '../external/vscode-observables/observables/dist/observableInternal/observables/observableValue.js';
|
|
3
5
|
import '../external/vscode-observables/observables/dist/observableInternal/debugLocation.js';
|
|
4
6
|
import { autorun } from '../external/vscode-observables/observables/dist/observableInternal/reactions/autorun.js';
|
|
5
|
-
import '../external/vscode-observables/observables/dist/observableInternal/observables/derived.js';
|
|
7
|
+
import { derived } from '../external/vscode-observables/observables/dist/observableInternal/observables/derived.js';
|
|
8
|
+
import { waitForState } from '../external/vscode-observables/observables/dist/observableInternal/utils/utilsCancellation.js';
|
|
6
9
|
import '../external/vscode-observables/observables/dist/observableInternal/utils/utils.js';
|
|
7
10
|
import '../external/vscode-observables/observables/dist/observableInternal/observables/observableFromEvent.js';
|
|
8
|
-
import { createApiFactory } from '
|
|
9
|
-
import { AsyncStream } from '
|
|
11
|
+
import { createApiFactory } from '../packages/simple-api/dist/chunk-3R7GHWBM.js';
|
|
12
|
+
import { AsyncStream } from '../packages/simple-api/dist/chunk-SGBCNXYH.js';
|
|
10
13
|
import { ExplorerSession } from '../explorerSession.js';
|
|
14
|
+
import { GitIndexResolver } from '../git/gitIndexResolver.js';
|
|
15
|
+
import { DirtyWorktreeError } from '../git/gitWorktreeManager.js';
|
|
16
|
+
import { execGit } from '../git/gitUtils.js';
|
|
11
17
|
import { PlaywrightBrowserPageFactory } from '../browserPage.js';
|
|
12
18
|
import { DefaultComponentExplorerHttpServerFactory } from '../httpServer.js';
|
|
13
19
|
import { installDependencies } from '../dependencyInstaller.js';
|
|
14
20
|
import { FileApprovalStore } from './approvalStore.js';
|
|
15
21
|
import { contentHash } from '../screenshotCache.js';
|
|
22
|
+
import { WorktreePool } from '../WorktreePool.js';
|
|
23
|
+
import { ViteProjectRef } from '../viteProjectRef.js';
|
|
24
|
+
import { pluginProtocolVersionText, daemonApiVersionText } from './version.js';
|
|
25
|
+
export { isCompatibleVersion, parseVersion } from './version.js';
|
|
16
26
|
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// ActivityTracker — monitors idle time and triggers shutdown
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
class ActivityTracker {
|
|
31
|
+
_idleTimeoutMs;
|
|
32
|
+
_onIdle;
|
|
33
|
+
_logger;
|
|
34
|
+
_lastActivityTime = Date.now();
|
|
35
|
+
_idleCheckInterval;
|
|
36
|
+
_autorunDisposable;
|
|
37
|
+
_disposed = false;
|
|
38
|
+
_started = false;
|
|
39
|
+
_activeCount = 0;
|
|
40
|
+
constructor(_idleTimeoutMs, _onIdle, _logger) {
|
|
41
|
+
this._idleTimeoutMs = _idleTimeoutMs;
|
|
42
|
+
this._onIdle = _onIdle;
|
|
43
|
+
this._logger = _logger;
|
|
44
|
+
}
|
|
45
|
+
setActive(value) {
|
|
46
|
+
if (value) {
|
|
47
|
+
this._activeCount++;
|
|
48
|
+
this._lastActivityTime = Date.now();
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
this._activeCount = Math.max(0, this._activeCount - 1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
reportActivity() {
|
|
55
|
+
this._lastActivityTime = Date.now();
|
|
56
|
+
}
|
|
57
|
+
start() {
|
|
58
|
+
if (this._started || this._disposed || this._idleTimeoutMs <= 0) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this._started = true;
|
|
62
|
+
const checkIntervalMs = Math.min(30_000, this._idleTimeoutMs / 2);
|
|
63
|
+
this._idleCheckInterval = setInterval(() => {
|
|
64
|
+
if (this._activeCount > 0) {
|
|
65
|
+
this._lastActivityTime = Date.now();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const idleMs = Date.now() - this._lastActivityTime;
|
|
69
|
+
if (idleMs >= this._idleTimeoutMs) {
|
|
70
|
+
this._logger.log(`Idle for ${Math.round(idleMs / 1000)}s, shutting down`);
|
|
71
|
+
this._onIdle();
|
|
72
|
+
}
|
|
73
|
+
}, checkIntervalMs);
|
|
74
|
+
}
|
|
75
|
+
dispose() {
|
|
76
|
+
this._disposed = true;
|
|
77
|
+
this._autorunDisposable?.dispose();
|
|
78
|
+
if (this._idleCheckInterval) {
|
|
79
|
+
clearInterval(this._idleCheckInterval);
|
|
80
|
+
this._idleCheckInterval = undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
17
84
|
class SourceTreeChangedError extends Error {
|
|
18
85
|
constructor(sessionName) {
|
|
19
86
|
super(`Source tree changed for session "${sessionName}" — retry with latest sourceTreeId`);
|
|
@@ -25,38 +92,44 @@ class SourceTreeChangedError extends Error {
|
|
|
25
92
|
// ---------------------------------------------------------------------------
|
|
26
93
|
class DaemonService {
|
|
27
94
|
_config;
|
|
95
|
+
_pipeName;
|
|
28
96
|
_logger;
|
|
29
97
|
_browserFactory;
|
|
30
98
|
_serverFactory;
|
|
31
99
|
_sessions = new Map();
|
|
32
100
|
_sessionConfigs = new Map();
|
|
101
|
+
_dynamicSessionMeta = new Map();
|
|
33
102
|
_resolvers = new Map();
|
|
103
|
+
_eventListenerCount = observableValue(this, 0);
|
|
34
104
|
_eventListeners = new Set();
|
|
35
105
|
_shutdownRequested = false;
|
|
36
106
|
_shutdownResolvers = [];
|
|
107
|
+
_activityTracker;
|
|
108
|
+
_worktreePool;
|
|
37
109
|
approvals;
|
|
38
110
|
api;
|
|
39
|
-
constructor(_config, _logger, _browserFactory, _serverFactory, approvalStorePath) {
|
|
111
|
+
constructor(_config, _pipeName, _logger, _browserFactory, _serverFactory, approvalStorePath, idleTimeoutMs) {
|
|
40
112
|
this._config = _config;
|
|
113
|
+
this._pipeName = _pipeName;
|
|
41
114
|
this._logger = _logger;
|
|
42
115
|
this._browserFactory = _browserFactory;
|
|
43
116
|
this._serverFactory = _serverFactory;
|
|
44
117
|
this.approvals = new FileApprovalStore(approvalStorePath);
|
|
118
|
+
this._activityTracker = new ActivityTracker(idleTimeoutMs, () => this.requestShutdown(), this._logger);
|
|
119
|
+
if (_config.worktreePool) {
|
|
120
|
+
this._worktreePool = new WorktreePool(_config.worktreePool.maxSlots, _config.repo.worktreeRootPath());
|
|
121
|
+
}
|
|
45
122
|
this.api = this._buildApi();
|
|
46
123
|
}
|
|
47
|
-
static async create(config, logger) {
|
|
48
|
-
const browserFactory = new PlaywrightBrowserPageFactory();
|
|
124
|
+
static async create(config, logger, pipeName, options) {
|
|
125
|
+
const browserFactory = new PlaywrightBrowserPageFactory(false, 30_000, logger);
|
|
49
126
|
const serverFactory = new DefaultComponentExplorerHttpServerFactory();
|
|
50
127
|
const approvalStorePath = `${config.screenshotDir}/approvals.json`;
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const svc = new DaemonService(config, eventStreamLogger, browserFactory, serverFactory, approvalStorePath);
|
|
54
|
-
eventStreamLogger._emitter = (event) => svc._emit(event);
|
|
55
|
-
const currentSession = config.sessions.find(s => s.source.kind === 'current');
|
|
56
|
-
const resolveViteFrom = currentSession?.viteProject.cwd;
|
|
128
|
+
const idleTimeoutMs = options?.idleTimeoutMs ?? 5 * 60 * 1000; // 5 minutes default
|
|
129
|
+
const svc = new DaemonService(config, pipeName ?? '', logger, browserFactory, serverFactory, approvalStorePath, idleTimeoutMs);
|
|
57
130
|
for (const sessionConfig of config.sessions) {
|
|
58
131
|
svc._sessionConfigs.set(sessionConfig.name, sessionConfig);
|
|
59
|
-
await svc._setupSession(sessionConfig
|
|
132
|
+
await svc._setupSession(sessionConfig);
|
|
60
133
|
}
|
|
61
134
|
return svc;
|
|
62
135
|
}
|
|
@@ -71,6 +144,7 @@ class DaemonService {
|
|
|
71
144
|
sourceTreeId: z.string(),
|
|
72
145
|
},
|
|
73
146
|
}, async (args) => {
|
|
147
|
+
this._activityTracker.reportActivity();
|
|
74
148
|
this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
|
|
75
149
|
const session = this.getSession(args.sessionName);
|
|
76
150
|
const result = await session.explorer.listFixtures();
|
|
@@ -85,18 +159,50 @@ class DaemonService {
|
|
|
85
159
|
sourceTreeId: z.string(),
|
|
86
160
|
sessionName: z.string(),
|
|
87
161
|
includeImage: z.boolean().optional(),
|
|
162
|
+
stabilityCheck: z.boolean().optional(),
|
|
88
163
|
},
|
|
89
164
|
}, async (args) => {
|
|
165
|
+
this._activityTracker.reportActivity();
|
|
90
166
|
const session = this.getSession(args.sessionName);
|
|
91
167
|
this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
|
|
92
|
-
|
|
168
|
+
if (args.stabilityCheck) {
|
|
169
|
+
const result = await session.explorer.screenshotFixtureStabilityCheck(args.fixtureId);
|
|
170
|
+
this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
|
|
171
|
+
const hashes = result.screenshots.map(s => contentHash(s.image));
|
|
172
|
+
const isStable = hashes[0] === hashes[1] && hashes[1] === hashes[2];
|
|
173
|
+
const stabilityScreenshots = result.screenshots.map((s, i) => ({
|
|
174
|
+
hash: hashes[i],
|
|
175
|
+
delayMs: s.delayMs,
|
|
176
|
+
image: (args.includeImage ?? true)
|
|
177
|
+
? Buffer.from(s.image).toString('base64')
|
|
178
|
+
: undefined,
|
|
179
|
+
}));
|
|
180
|
+
return {
|
|
181
|
+
hash: hashes[hashes.length - 1],
|
|
182
|
+
sourceTreeId: args.sourceTreeId,
|
|
183
|
+
image: (args.includeImage ?? true)
|
|
184
|
+
? Buffer.from(result.screenshots[result.screenshots.length - 1].image).toString('base64')
|
|
185
|
+
: undefined,
|
|
186
|
+
hasError: result.hasError,
|
|
187
|
+
error: result.error,
|
|
188
|
+
events: result.events.length > 0 ? result.events : undefined,
|
|
189
|
+
resultData: result.resultData,
|
|
190
|
+
isStable,
|
|
191
|
+
stabilityScreenshots,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const result = await session.explorer.screenshotFixture(args.fixtureId);
|
|
93
195
|
this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
|
|
94
196
|
return {
|
|
95
|
-
hash: contentHash(
|
|
197
|
+
hash: contentHash(result.image),
|
|
96
198
|
sourceTreeId: args.sourceTreeId,
|
|
97
199
|
image: (args.includeImage ?? true)
|
|
98
|
-
? Buffer.from(
|
|
200
|
+
? Buffer.from(result.image).toString('base64')
|
|
99
201
|
: undefined,
|
|
202
|
+
hasError: result.hasError,
|
|
203
|
+
error: result.error,
|
|
204
|
+
events: result.events?.length > 0 ? result.events : undefined,
|
|
205
|
+
resultData: result.resultData,
|
|
100
206
|
};
|
|
101
207
|
}),
|
|
102
208
|
takeBatch: createMethod({
|
|
@@ -107,21 +213,29 @@ class DaemonService {
|
|
|
107
213
|
includeImages: z.boolean().optional(),
|
|
108
214
|
},
|
|
109
215
|
}, async (args) => {
|
|
216
|
+
this._activityTracker.reportActivity();
|
|
110
217
|
const session = this.getSession(args.sessionName);
|
|
111
218
|
this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
|
|
112
219
|
const includeImages = args.includeImages ?? false;
|
|
220
|
+
this._logger.trace(`takeBatch: ${args.fixtureIds.length} fixtures, session=${args.sessionName}`);
|
|
221
|
+
const startTime = Date.now();
|
|
113
222
|
const screenshots = [];
|
|
114
223
|
for (const fixtureId of args.fixtureIds) {
|
|
115
|
-
const
|
|
224
|
+
const result = await session.explorer.screenshotFixture(fixtureId);
|
|
116
225
|
this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
|
|
117
226
|
screenshots.push({
|
|
118
227
|
fixtureId,
|
|
119
|
-
hash: contentHash(
|
|
228
|
+
hash: contentHash(result.image),
|
|
120
229
|
image: includeImages
|
|
121
|
-
? Buffer.from(
|
|
230
|
+
? Buffer.from(result.image).toString('base64')
|
|
122
231
|
: undefined,
|
|
232
|
+
hasError: result.hasError,
|
|
233
|
+
error: result.error,
|
|
234
|
+
events: result.events.length > 0 ? result.events : undefined,
|
|
235
|
+
resultData: result.resultData,
|
|
123
236
|
});
|
|
124
237
|
}
|
|
238
|
+
this._logger.trace(`takeBatch: done in ${Date.now() - startTime}ms`);
|
|
125
239
|
return { sourceTreeId: args.sourceTreeId, screenshots };
|
|
126
240
|
}),
|
|
127
241
|
compare: createMethod({
|
|
@@ -134,25 +248,46 @@ class DaemonService {
|
|
|
134
248
|
includeImages: z.boolean().optional(),
|
|
135
249
|
},
|
|
136
250
|
}, async (args) => {
|
|
251
|
+
this._activityTracker.reportActivity();
|
|
137
252
|
const baselineSession = this.getSession(args.baselineSessionName);
|
|
138
253
|
const currentSession = this.getSession(args.currentSessionName);
|
|
139
254
|
this.assertSourceTreeId(args.baselineSessionName, args.baselineSourceTreeId);
|
|
140
255
|
this.assertSourceTreeId(args.currentSessionName, args.currentSourceTreeId);
|
|
141
|
-
const
|
|
256
|
+
const [baselineResult, baselineFixtures] = await Promise.all([
|
|
257
|
+
baselineSession.explorer.screenshotFixture(args.fixtureId),
|
|
258
|
+
baselineSession.explorer.listFixtures(),
|
|
259
|
+
]);
|
|
142
260
|
this.assertSourceTreeId(args.baselineSessionName, args.baselineSourceTreeId);
|
|
143
|
-
const
|
|
261
|
+
const [currentResult, currentFixtures] = await Promise.all([
|
|
262
|
+
currentSession.explorer.screenshotFixture(args.fixtureId),
|
|
263
|
+
currentSession.explorer.listFixtures(),
|
|
264
|
+
]);
|
|
144
265
|
this.assertSourceTreeId(args.currentSessionName, args.currentSourceTreeId);
|
|
145
|
-
const baselineHash = contentHash(
|
|
146
|
-
const currentHash = contentHash(
|
|
266
|
+
const baselineHash = contentHash(baselineResult.image);
|
|
267
|
+
const currentHash = contentHash(currentResult.image);
|
|
147
268
|
const includeImages = args.includeImages ?? false;
|
|
269
|
+
const currentFixture = currentFixtures.find(f => f.fixtureId === args.fixtureId);
|
|
270
|
+
const baselineFixture = baselineFixtures.find(f => f.fixtureId === args.fixtureId);
|
|
271
|
+
const labels = currentFixture?.labels;
|
|
272
|
+
const labelsChanged = baselineFixture && !arraysEqual(baselineFixture.labels, currentFixture?.labels ?? []);
|
|
148
273
|
return {
|
|
149
274
|
match: baselineHash === currentHash,
|
|
150
275
|
baselineHash,
|
|
151
276
|
currentHash,
|
|
152
277
|
baselineImage: includeImages
|
|
153
|
-
? Buffer.from(
|
|
278
|
+
? Buffer.from(baselineResult.image).toString('base64') : undefined,
|
|
154
279
|
currentImage: includeImages
|
|
155
|
-
? Buffer.from(
|
|
280
|
+
? Buffer.from(currentResult.image).toString('base64') : undefined,
|
|
281
|
+
baselineHasError: baselineResult.hasError,
|
|
282
|
+
baselineError: baselineResult.error,
|
|
283
|
+
baselineEvents: baselineResult.events.length > 0 ? baselineResult.events : undefined,
|
|
284
|
+
baselineResultData: baselineResult.resultData,
|
|
285
|
+
currentHasError: currentResult.hasError,
|
|
286
|
+
currentError: currentResult.error,
|
|
287
|
+
currentEvents: currentResult.events.length > 0 ? currentResult.events : undefined,
|
|
288
|
+
currentResultData: currentResult.resultData,
|
|
289
|
+
labels,
|
|
290
|
+
labelsBefore: labelsChanged ? baselineFixture.labels : undefined,
|
|
156
291
|
approval: (baselineHash !== currentHash)
|
|
157
292
|
? this.approvals.lookup({
|
|
158
293
|
fixtureId: args.fixtureId,
|
|
@@ -172,6 +307,7 @@ class DaemonService {
|
|
|
172
307
|
comment: z.string(),
|
|
173
308
|
},
|
|
174
309
|
}, async (args) => {
|
|
310
|
+
this._activityTracker.reportActivity();
|
|
175
311
|
this.approvals.approve(args);
|
|
176
312
|
}),
|
|
177
313
|
revoke: createMethod({
|
|
@@ -181,11 +317,13 @@ class DaemonService {
|
|
|
181
317
|
modifiedHash: z.string(),
|
|
182
318
|
},
|
|
183
319
|
}, async (args) => {
|
|
320
|
+
this._activityTracker.reportActivity();
|
|
184
321
|
this.approvals.revoke(args);
|
|
185
322
|
}),
|
|
186
323
|
list: createMethod({
|
|
187
324
|
args: { fixtureId: z.string().optional() },
|
|
188
325
|
}, async (args) => {
|
|
326
|
+
this._activityTracker.reportActivity();
|
|
189
327
|
return this.approvals.list(args.fixtureId);
|
|
190
328
|
}),
|
|
191
329
|
lookup: createMethod({
|
|
@@ -195,16 +333,77 @@ class DaemonService {
|
|
|
195
333
|
modifiedHash: z.string(),
|
|
196
334
|
},
|
|
197
335
|
}, async (args) => {
|
|
336
|
+
this._activityTracker.reportActivity();
|
|
198
337
|
return this.approvals.lookup(args) ?? null;
|
|
199
338
|
}),
|
|
200
339
|
}),
|
|
340
|
+
evaluate: createMethod({
|
|
341
|
+
args: {
|
|
342
|
+
sessionName: z.string(),
|
|
343
|
+
sourceTreeId: z.string(),
|
|
344
|
+
expression: z.string(),
|
|
345
|
+
fixtureId: z.string().optional(),
|
|
346
|
+
},
|
|
347
|
+
}, async (args) => {
|
|
348
|
+
this._activityTracker.reportActivity();
|
|
349
|
+
const session = this.getSession(args.sessionName);
|
|
350
|
+
this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
|
|
351
|
+
const result = await session.explorer.evaluateJs(args.expression, args.fixtureId);
|
|
352
|
+
this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
|
|
353
|
+
return { result };
|
|
354
|
+
}),
|
|
201
355
|
events: createMethod({ args: {} }, async () => {
|
|
202
356
|
return AsyncStream.fromIterable(this.eventStream());
|
|
203
357
|
}),
|
|
204
|
-
sessions: createMethod({ args: {} }, async () => {
|
|
358
|
+
sessions: createMethod({ args: {} }, async (_args, ctx) => {
|
|
359
|
+
this._activityTracker.reportActivity();
|
|
360
|
+
this._logger.trace(`API: sessions (client=${ctx.clientName}, eventListeners=${this._eventListeners.size})`);
|
|
361
|
+
return this.getSessionInfos();
|
|
362
|
+
}),
|
|
363
|
+
version: createMethod({ args: {} }, async () => {
|
|
364
|
+
this._activityTracker.reportActivity();
|
|
365
|
+
return {
|
|
366
|
+
daemonApiVersion: daemonApiVersionText,
|
|
367
|
+
pluginProtocolVersion: pluginProtocolVersionText,
|
|
368
|
+
};
|
|
369
|
+
}),
|
|
370
|
+
restartSession: createMethod({
|
|
371
|
+
args: { sessionName: z.string() },
|
|
372
|
+
}, async (args, ctx) => {
|
|
373
|
+
this._activityTracker.reportActivity();
|
|
374
|
+
this._logger.debug(`Restart session "${args.sessionName}" requested (client=${ctx.clientName})`);
|
|
375
|
+
await this._restartSession(args.sessionName);
|
|
205
376
|
return this.getSessionInfos();
|
|
206
377
|
}),
|
|
207
|
-
|
|
378
|
+
openSession: createMethod({
|
|
379
|
+
args: {
|
|
380
|
+
name: z.string(),
|
|
381
|
+
ref: z.string(),
|
|
382
|
+
},
|
|
383
|
+
}, async (args, ctx) => {
|
|
384
|
+
this._activityTracker.reportActivity();
|
|
385
|
+
this._logger.log(`Open session "${args.name}" @ ${args.ref} requested (client=${ctx.clientName})`);
|
|
386
|
+
return this._openDynamicSession(args.name, args.ref);
|
|
387
|
+
}),
|
|
388
|
+
closeSession: createMethod({
|
|
389
|
+
args: { name: z.string() },
|
|
390
|
+
}, async (args, ctx) => {
|
|
391
|
+
this._activityTracker.reportActivity();
|
|
392
|
+
this._logger.log(`Close session "${args.name}" requested (client=${ctx.clientName})`);
|
|
393
|
+
return this._closeDynamicSession(args.name);
|
|
394
|
+
}),
|
|
395
|
+
updateSessionRef: createMethod({
|
|
396
|
+
args: {
|
|
397
|
+
name: z.string(),
|
|
398
|
+
ref: z.string(),
|
|
399
|
+
},
|
|
400
|
+
}, async (args, ctx) => {
|
|
401
|
+
this._activityTracker.reportActivity();
|
|
402
|
+
this._logger.log(`Update session ref "${args.name}" → ${args.ref} requested (client=${ctx.clientName})`);
|
|
403
|
+
return this._updateDynamicSessionRef(args.name, args.ref);
|
|
404
|
+
}),
|
|
405
|
+
shutdown: createMethod({ args: {} }, async (_args, ctx) => {
|
|
406
|
+
this._logger.debug(`Shutdown requested via API (client=${ctx.clientName})`);
|
|
208
407
|
this.requestShutdown();
|
|
209
408
|
}),
|
|
210
409
|
});
|
|
@@ -217,16 +416,65 @@ class DaemonService {
|
|
|
217
416
|
}
|
|
218
417
|
return session;
|
|
219
418
|
}
|
|
220
|
-
getSessionInfos() {
|
|
221
|
-
|
|
419
|
+
getSessionInfos(reader) {
|
|
420
|
+
const infos = [];
|
|
421
|
+
const reportedNames = new Set();
|
|
422
|
+
// Static sessions (from config)
|
|
423
|
+
for (const sc of this._config.sessions) {
|
|
424
|
+
reportedNames.add(sc.name);
|
|
222
425
|
const session = this._sessions.get(sc.name);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
sourceKind: sc.source.kind,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
426
|
+
const meta = this._dynamicSessionMeta.get(sc.name);
|
|
427
|
+
if (!session) {
|
|
428
|
+
infos.push({ name: sc.name, sourceKind: sc.source.kind, isLoading: true });
|
|
429
|
+
}
|
|
430
|
+
else if (meta) {
|
|
431
|
+
infos.push({
|
|
432
|
+
name: sc.name,
|
|
433
|
+
sourceKind: 'worktree',
|
|
434
|
+
serverUrl: session.serverUrl,
|
|
435
|
+
sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
|
|
436
|
+
worktreePath: meta.worktreePath,
|
|
437
|
+
ref: meta.ref,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
infos.push({
|
|
442
|
+
name: sc.name,
|
|
443
|
+
sourceKind: sc.source.kind,
|
|
444
|
+
serverUrl: session.serverUrl,
|
|
445
|
+
sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Dynamic worktree sessions (not already reported as static)
|
|
450
|
+
for (const [name, meta] of this._dynamicSessionMeta) {
|
|
451
|
+
if (reportedNames.has(name)) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const session = this._sessions.get(name);
|
|
455
|
+
if (!session) {
|
|
456
|
+
infos.push({ name, sourceKind: 'worktree', isLoading: true });
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
infos.push({
|
|
460
|
+
name,
|
|
461
|
+
sourceKind: 'worktree',
|
|
462
|
+
serverUrl: session.serverUrl,
|
|
463
|
+
sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
|
|
464
|
+
worktreePath: meta.worktreePath,
|
|
465
|
+
ref: meta.ref,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return infos;
|
|
470
|
+
}
|
|
471
|
+
waitForSession(sessionName) {
|
|
472
|
+
const sessionObs = derived(this, reader => {
|
|
473
|
+
const infos = this.getSessionInfos(reader);
|
|
474
|
+
const session = sessionName ? infos.find(s => s.name === sessionName) : infos[0];
|
|
475
|
+
return session && !session.isLoading ? session : undefined;
|
|
229
476
|
});
|
|
477
|
+
return waitForState(sessionObs);
|
|
230
478
|
}
|
|
231
479
|
getSourceTreeId(sessionName) {
|
|
232
480
|
return this.getSession(sessionName).sourceTreeId.get().value;
|
|
@@ -246,6 +494,9 @@ class DaemonService {
|
|
|
246
494
|
let resolve;
|
|
247
495
|
let done = false;
|
|
248
496
|
const listener = (event) => {
|
|
497
|
+
if (event.type !== 'log') {
|
|
498
|
+
self._logger.trace(`Event stream: ${event.type}`);
|
|
499
|
+
}
|
|
249
500
|
if (resolve) {
|
|
250
501
|
const r = resolve;
|
|
251
502
|
resolve = undefined;
|
|
@@ -256,8 +507,12 @@ class DaemonService {
|
|
|
256
507
|
}
|
|
257
508
|
};
|
|
258
509
|
self._eventListeners.add(listener);
|
|
510
|
+
self._eventListenerCount.set(self._eventListeners.size, undefined);
|
|
511
|
+
self._activityTracker.setActive(true);
|
|
512
|
+
self._logger.debug(`Event stream opened (listeners: ${self._eventListeners.size})`);
|
|
259
513
|
const onShutdown = () => {
|
|
260
514
|
done = true;
|
|
515
|
+
cleanup();
|
|
261
516
|
if (resolve) {
|
|
262
517
|
const r = resolve;
|
|
263
518
|
resolve = undefined;
|
|
@@ -265,6 +520,12 @@ class DaemonService {
|
|
|
265
520
|
}
|
|
266
521
|
};
|
|
267
522
|
self._shutdownResolvers.push(onShutdown);
|
|
523
|
+
const cleanup = () => {
|
|
524
|
+
self._eventListeners.delete(listener);
|
|
525
|
+
self._eventListenerCount.set(self._eventListeners.size, undefined);
|
|
526
|
+
self._activityTracker.setActive(false);
|
|
527
|
+
self._logger.debug(`Event stream closed (listeners: ${self._eventListeners.size})`);
|
|
528
|
+
};
|
|
268
529
|
return {
|
|
269
530
|
next() {
|
|
270
531
|
if (done) {
|
|
@@ -277,7 +538,12 @@ class DaemonService {
|
|
|
277
538
|
},
|
|
278
539
|
return() {
|
|
279
540
|
done = true;
|
|
280
|
-
|
|
541
|
+
cleanup();
|
|
542
|
+
if (resolve) {
|
|
543
|
+
const r = resolve;
|
|
544
|
+
resolve = undefined;
|
|
545
|
+
r({ value: undefined, done: true });
|
|
546
|
+
}
|
|
281
547
|
return Promise.resolve({ value: undefined, done: true });
|
|
282
548
|
},
|
|
283
549
|
};
|
|
@@ -287,6 +553,7 @@ class DaemonService {
|
|
|
287
553
|
// -- Event loop ----------------------------------------------------------
|
|
288
554
|
async runEventLoop() {
|
|
289
555
|
this._startSourceChangeWatchers();
|
|
556
|
+
this._activityTracker.start();
|
|
290
557
|
await new Promise(resolve => {
|
|
291
558
|
if (this._shutdownRequested) {
|
|
292
559
|
resolve();
|
|
@@ -298,64 +565,57 @@ class DaemonService {
|
|
|
298
565
|
_sourceChangeDisposables = [];
|
|
299
566
|
_startSourceChangeWatchers() {
|
|
300
567
|
for (const [name, session] of this._sessions) {
|
|
301
|
-
|
|
302
|
-
const disposable = autorun(reader => {
|
|
303
|
-
const current = session.sourceTreeId.read(reader);
|
|
304
|
-
if (current.value !== previousValue) {
|
|
305
|
-
previousValue = current.value;
|
|
306
|
-
this._emit({ type: 'source-change', sessionName: name, sourceTreeId: current.value });
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
this._sourceChangeDisposables.push(disposable);
|
|
310
|
-
}
|
|
311
|
-
// Watch for ref changes (worktree sessions)
|
|
312
|
-
for (const [name, resolver] of this._resolvers) {
|
|
313
|
-
let previousCommit = resolver.resolvedCommit.get();
|
|
314
|
-
const disposable = autorun(reader => {
|
|
315
|
-
const commit = resolver.resolvedCommit.read(reader);
|
|
316
|
-
if (!previousCommit.equals(commit)) {
|
|
317
|
-
previousCommit = commit;
|
|
318
|
-
this._handleRefChange(name, resolver.ref, commit);
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
this._sourceChangeDisposables.push(disposable);
|
|
568
|
+
this._addSourceChangeWatcher(name, session);
|
|
322
569
|
}
|
|
323
570
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
571
|
+
_addSourceChangeWatcher(name, session) {
|
|
572
|
+
let previousValue = session.sourceTreeId.get().value;
|
|
573
|
+
const disposable = autorun(reader => {
|
|
574
|
+
const current = session.sourceTreeId.read(reader);
|
|
575
|
+
if (current.value !== previousValue) {
|
|
576
|
+
this._logger.debug(`Source tree changed: ${name} ${previousValue} → ${current.value}`);
|
|
577
|
+
previousValue = current.value;
|
|
578
|
+
this._emit({ type: 'source-change', sessionName: name, sourceTreeId: current.value });
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
this._sourceChangeDisposables.push(disposable);
|
|
582
|
+
}
|
|
583
|
+
async _handleRefChange(sessionName, ref, previousCommit, newCommit) {
|
|
584
|
+
const changedFiles = await this._getChangedFiles(previousCommit, newCommit);
|
|
585
|
+
this._logger.log(`Ref ${ref} moved to ${newCommit.toShort()} (${changedFiles.length} file(s) changed${changedFiles.length > 0 ? ': ' + changedFiles.join(', ') : ''})`);
|
|
586
|
+
const meta = this._dynamicSessionMeta.get(sessionName);
|
|
587
|
+
if (!meta) {
|
|
329
588
|
return;
|
|
330
589
|
}
|
|
331
|
-
const
|
|
332
|
-
const wtInfo = await git.worktrees.info(
|
|
590
|
+
const git = this._config.repo;
|
|
591
|
+
const wtInfo = await git.worktrees.info(meta.worktreePath);
|
|
333
592
|
if (wtInfo && wtInfo.isDirty) {
|
|
334
593
|
this._logger.log(`Worktree is dirty, skipping update to ${newCommit.toShort()}`);
|
|
335
594
|
return;
|
|
336
595
|
}
|
|
337
|
-
// Dispose old session
|
|
338
|
-
const oldSession = this._sessions.get(sessionName);
|
|
339
|
-
await oldSession?.dispose();
|
|
340
|
-
// Checkout + reinstall
|
|
341
596
|
if (wtInfo) {
|
|
342
|
-
await git.worktrees.checkout(
|
|
597
|
+
await git.worktrees.checkout(meta.worktreePath, newCommit);
|
|
343
598
|
}
|
|
344
599
|
else {
|
|
345
|
-
await git.worktrees.create(
|
|
600
|
+
await git.worktrees.create(meta.worktreePath, newCommit);
|
|
346
601
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
logger: this._logger,
|
|
353
|
-
resolveViteFrom,
|
|
354
|
-
hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
|
|
355
|
-
});
|
|
356
|
-
this._sessions.set(sessionName, newSession);
|
|
602
|
+
const sessionConfig = this._sessionConfigs.get(sessionName);
|
|
603
|
+
const installSetup = (sessionConfig?.source.kind === 'worktree' ? sessionConfig.source.install : undefined)
|
|
604
|
+
?? this._config.worktreePool?.setup
|
|
605
|
+
?? { kind: 'auto' };
|
|
606
|
+
await installDependencies(meta.worktreePath, installSetup, this._logger);
|
|
357
607
|
this._emit({ type: 'ref-change', sessionName, newCommit: newCommit.toShort() });
|
|
358
608
|
}
|
|
609
|
+
async _getChangedFiles(oldCommit, newCommit) {
|
|
610
|
+
try {
|
|
611
|
+
const output = await execGit(this._config.repo.gitRoot, ['diff', '--name-only', oldCommit.hash, newCommit.hash]);
|
|
612
|
+
return output.trim().split('\n').filter(f => f.length > 0);
|
|
613
|
+
}
|
|
614
|
+
catch (e) {
|
|
615
|
+
this._logger.log(`Failed to get changed files (${oldCommit.toShort()}..${newCommit.toShort()}): ${e instanceof Error ? e.message : e}`);
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
}
|
|
359
619
|
// -- Shutdown ------------------------------------------------------------
|
|
360
620
|
requestShutdown() {
|
|
361
621
|
this._shutdownRequested = true;
|
|
@@ -366,10 +626,15 @@ class DaemonService {
|
|
|
366
626
|
}
|
|
367
627
|
async dispose() {
|
|
368
628
|
this.requestShutdown();
|
|
629
|
+
this._activityTracker.dispose();
|
|
369
630
|
for (const d of this._sourceChangeDisposables) {
|
|
370
631
|
d.dispose();
|
|
371
632
|
}
|
|
372
633
|
this._sourceChangeDisposables = [];
|
|
634
|
+
for (const d of this._dynamicRefWatchers.values()) {
|
|
635
|
+
d.dispose();
|
|
636
|
+
}
|
|
637
|
+
this._dynamicRefWatchers.clear();
|
|
373
638
|
for (const session of this._sessions.values()) {
|
|
374
639
|
await session.dispose();
|
|
375
640
|
}
|
|
@@ -380,71 +645,303 @@ class DaemonService {
|
|
|
380
645
|
await this._browserFactory.dispose();
|
|
381
646
|
}
|
|
382
647
|
// -- Private helpers -----------------------------------------------------
|
|
383
|
-
/** @internal — also called by EventStreamLogger */
|
|
384
648
|
_emit(event) {
|
|
385
649
|
for (const listener of this._eventListeners) {
|
|
386
650
|
listener(event);
|
|
387
651
|
}
|
|
388
652
|
}
|
|
389
|
-
async
|
|
653
|
+
async _restartSession(sessionName) {
|
|
654
|
+
const config = this._sessionConfigs.get(sessionName);
|
|
655
|
+
const meta = this._dynamicSessionMeta.get(sessionName);
|
|
656
|
+
if (!config && !meta) {
|
|
657
|
+
throw new Error(`Unknown session: "${sessionName}"`);
|
|
658
|
+
}
|
|
659
|
+
const existing = this._sessions.get(sessionName);
|
|
660
|
+
if (existing) {
|
|
661
|
+
this._logger.debug(`Disposing session: ${sessionName}`);
|
|
662
|
+
await existing.dispose();
|
|
663
|
+
this._sessions.delete(sessionName);
|
|
664
|
+
}
|
|
665
|
+
this._logger.log(`Restarting server: ${sessionName}`);
|
|
666
|
+
if (meta) {
|
|
667
|
+
await this._createWorktreeExplorerSession(sessionName, meta.worktreePath);
|
|
668
|
+
}
|
|
669
|
+
else if (config) {
|
|
670
|
+
await this._createExplorerSession(config);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
async _createExplorerSession(sessionConfig) {
|
|
674
|
+
const session = await ExplorerSession.create(sessionConfig.name, sessionConfig.viteProject, this._serverFactory, this._browserFactory, {
|
|
675
|
+
logger: this._logger,
|
|
676
|
+
hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
|
|
677
|
+
daemonConfig: {
|
|
678
|
+
pipeName: this._pipeName,
|
|
679
|
+
sessionName: sessionConfig.name,
|
|
680
|
+
daemonApiVersion: daemonApiVersionText,
|
|
681
|
+
pluginProtocolVersion: pluginProtocolVersionText,
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
this._sessions.set(sessionConfig.name, session);
|
|
685
|
+
this._logger.debug(`Session ready: ${sessionConfig.name} (${session.serverUrl})`);
|
|
686
|
+
}
|
|
687
|
+
async _createWorktreeExplorerSession(sessionName, worktreePath) {
|
|
688
|
+
const configDirRelToGitRoot = path.relative(this._config.repo.gitRoot, this._config.configDir);
|
|
689
|
+
const worktreeConfigDir = path.resolve(worktreePath, configDirRelToGitRoot);
|
|
690
|
+
const viteConfigPath = path.resolve(worktreeConfigDir, this._config.defaultViteConfig);
|
|
691
|
+
const viteProject = ViteProjectRef.fromViteConfigPath(viteConfigPath);
|
|
692
|
+
const currentSession = this._config.sessions.find(s => s.source.kind === 'current');
|
|
693
|
+
const resolveViteFrom = currentSession?.viteProject.configFile;
|
|
694
|
+
this._logger.debug(`Worktree session "${sessionName}": resolveViteFrom=${resolveViteFrom}`);
|
|
695
|
+
const session = await ExplorerSession.create(sessionName, viteProject, this._serverFactory, this._browserFactory, {
|
|
696
|
+
logger: this._logger,
|
|
697
|
+
resolveViteFrom,
|
|
698
|
+
hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
|
|
699
|
+
daemonConfig: {
|
|
700
|
+
pipeName: this._pipeName,
|
|
701
|
+
sessionName,
|
|
702
|
+
daemonApiVersion: daemonApiVersionText,
|
|
703
|
+
pluginProtocolVersion: pluginProtocolVersionText,
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
this._sessions.set(sessionName, session);
|
|
707
|
+
this._logger.debug(`Session ready: ${sessionName} (${session.serverUrl})`);
|
|
708
|
+
}
|
|
709
|
+
async _setupSession(sessionConfig) {
|
|
710
|
+
this._logger.debug(`Setting up session: ${sessionConfig.name} (${sessionConfig.source.kind})`);
|
|
711
|
+
this._logger.log(`Starting server: ${sessionConfig.name}`);
|
|
390
712
|
if (sessionConfig.source.kind === 'worktree') {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
713
|
+
await this._setupWorktreeSession(sessionConfig, sessionConfig.source);
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
await this._createExplorerSession(sessionConfig);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
async _setupWorktreeSession(sessionConfig, source) {
|
|
720
|
+
if (!this._worktreePool) {
|
|
721
|
+
throw new Error(`Session "${sessionConfig.name}" requires a worktree but no worktree pool is available`);
|
|
722
|
+
}
|
|
723
|
+
const slot = this._worktreePool.allocate(sessionConfig.name);
|
|
724
|
+
const git = this._config.repo;
|
|
725
|
+
const ref = source.ref;
|
|
726
|
+
const resolver = ref === GitIndexResolver.INDEX_REF
|
|
727
|
+
? await git.createIndexResolver()
|
|
728
|
+
: await git.createCommitResolver(ref);
|
|
729
|
+
this._resolvers.set(sessionConfig.name, resolver);
|
|
730
|
+
const resolvedCommit = resolver.resolvedCommit.get();
|
|
731
|
+
const wtInfo = await git.worktrees.info(slot.worktreePath);
|
|
732
|
+
if (!wtInfo) {
|
|
733
|
+
this._logger.log(`Creating worktree at ${slot.worktreePath} (${ref} @ ${resolvedCommit.toShort()})`);
|
|
734
|
+
await git.worktrees.create(slot.worktreePath, resolvedCommit);
|
|
735
|
+
}
|
|
736
|
+
else if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
|
|
737
|
+
if (wtInfo.isDirty) {
|
|
738
|
+
throw new Error(`Worktree slot ${slot.index} is dirty. Dirty files:\n` +
|
|
739
|
+
wtInfo.dirtyFiles.map(f => ` ${f}`).join('\n'));
|
|
740
|
+
}
|
|
741
|
+
this._logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
|
|
742
|
+
await git.worktrees.checkout(slot.worktreePath, resolvedCommit);
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
this._logger.log(`Worktree already at ${resolvedCommit.toShort()}`);
|
|
746
|
+
}
|
|
747
|
+
const installSetup = source.install ?? this._config.worktreePool?.setup ?? { kind: 'auto' };
|
|
748
|
+
await installDependencies(slot.worktreePath, installSetup, this._logger);
|
|
749
|
+
this._dynamicSessionMeta.set(sessionConfig.name, { ref, worktreePath: slot.worktreePath });
|
|
750
|
+
await this._createWorktreeExplorerSession(sessionConfig.name, slot.worktreePath);
|
|
751
|
+
this._addDynamicRefWatcher(sessionConfig.name, resolver);
|
|
752
|
+
}
|
|
753
|
+
// -- Dynamic session management ------------------------------------------
|
|
754
|
+
async _openDynamicSession(name, ref) {
|
|
755
|
+
if (this._sessions.has(name) || this._sessionConfigs.has(name) || this._dynamicSessionMeta.has(name)) {
|
|
756
|
+
return { error: `Session "${name}" already exists` };
|
|
757
|
+
}
|
|
758
|
+
if (!this._worktreePool) {
|
|
759
|
+
return { error: 'No worktree pool configured. Add a "worktree" section to your component-explorer.json config.' };
|
|
760
|
+
}
|
|
761
|
+
let slot;
|
|
762
|
+
try {
|
|
763
|
+
slot = this._worktreePool.allocate(name);
|
|
764
|
+
}
|
|
765
|
+
catch (e) {
|
|
766
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
767
|
+
}
|
|
768
|
+
const git = this._config.repo;
|
|
769
|
+
const isIndex = ref === GitIndexResolver.INDEX_REF;
|
|
770
|
+
try {
|
|
771
|
+
let resolver;
|
|
772
|
+
if (isIndex) {
|
|
773
|
+
resolver = await git.createIndexResolver();
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
resolver = await git.createCommitResolver(ref);
|
|
777
|
+
}
|
|
778
|
+
this._resolvers.set(name, resolver);
|
|
394
779
|
const resolvedCommit = resolver.resolvedCommit.get();
|
|
395
|
-
const wtInfo = await git.worktrees.info(
|
|
780
|
+
const wtInfo = await git.worktrees.info(slot.worktreePath);
|
|
396
781
|
if (!wtInfo) {
|
|
397
|
-
this._logger.log(`Creating worktree at ${
|
|
398
|
-
await git.worktrees.create(
|
|
399
|
-
await installDependencies(wt.worktreePath, wt.install, this._logger);
|
|
782
|
+
this._logger.log(`Creating worktree at ${slot.worktreePath} (${ref} @ ${resolvedCommit.toShort()})`);
|
|
783
|
+
await git.worktrees.create(slot.worktreePath, resolvedCommit);
|
|
400
784
|
}
|
|
401
785
|
else if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
|
|
402
786
|
if (wtInfo.isDirty) {
|
|
403
|
-
this.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
787
|
+
this._worktreePool.release(name);
|
|
788
|
+
this._resolvers.get(name)?.dispose();
|
|
789
|
+
this._resolvers.delete(name);
|
|
790
|
+
return {
|
|
791
|
+
error: `Worktree slot ${slot.index} is dirty. Dirty files:\n` +
|
|
792
|
+
wtInfo.dirtyFiles.map(f => ` ${f}`).join('\n'),
|
|
793
|
+
};
|
|
409
794
|
}
|
|
795
|
+
this._logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
|
|
796
|
+
await git.worktrees.checkout(slot.worktreePath, resolvedCommit);
|
|
410
797
|
}
|
|
411
798
|
else {
|
|
412
799
|
this._logger.log(`Worktree already at ${resolvedCommit.toShort()}`);
|
|
413
800
|
}
|
|
801
|
+
const poolConfig = this._config.worktreePool;
|
|
802
|
+
await installDependencies(slot.worktreePath, poolConfig.setup, this._logger);
|
|
803
|
+
this._dynamicSessionMeta.set(name, { ref, worktreePath: slot.worktreePath });
|
|
804
|
+
await this._createWorktreeExplorerSession(name, slot.worktreePath);
|
|
805
|
+
this._addDynamicRefWatcher(name, resolver);
|
|
806
|
+
this._emit({ type: 'session-change' });
|
|
807
|
+
return { sessions: this.getSessionInfos() };
|
|
808
|
+
}
|
|
809
|
+
catch (e) {
|
|
810
|
+
this._worktreePool.release(name);
|
|
811
|
+
this._resolvers.get(name)?.dispose();
|
|
812
|
+
this._resolvers.delete(name);
|
|
813
|
+
this._dynamicSessionMeta.delete(name);
|
|
814
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
815
|
+
this._logger.log(`Failed to open session "${name}": ${msg}`);
|
|
816
|
+
return { error: `Failed to open session: ${msg}` };
|
|
414
817
|
}
|
|
415
|
-
this._logger.log(`Starting server: ${sessionConfig.name}`);
|
|
416
|
-
const session = await ExplorerSession.create(sessionConfig.name, sessionConfig.viteProject, this._serverFactory, this._browserFactory, {
|
|
417
|
-
logger: this._logger,
|
|
418
|
-
resolveViteFrom: sessionConfig.source.kind === 'worktree' ? resolveViteFrom : undefined,
|
|
419
|
-
hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
|
|
420
|
-
});
|
|
421
|
-
this._sessions.set(sessionConfig.name, session);
|
|
422
|
-
this._logger.log(`Server ready: ${sessionConfig.name} (${session.serverUrl})`);
|
|
423
818
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
819
|
+
async _closeDynamicSession(name) {
|
|
820
|
+
if (!this._dynamicSessionMeta.has(name)) {
|
|
821
|
+
if (this._sessionConfigs.has(name)) {
|
|
822
|
+
return { error: `Session "${name}" is a static session and cannot be closed` };
|
|
823
|
+
}
|
|
824
|
+
return { error: `Session "${name}" does not exist` };
|
|
825
|
+
}
|
|
826
|
+
const session = this._sessions.get(name);
|
|
827
|
+
if (session) {
|
|
828
|
+
this._logger.debug(`Disposing session: ${name}`);
|
|
829
|
+
await session.dispose();
|
|
830
|
+
this._sessions.delete(name);
|
|
831
|
+
}
|
|
832
|
+
const resolver = this._resolvers.get(name);
|
|
833
|
+
if (resolver) {
|
|
834
|
+
resolver.dispose();
|
|
835
|
+
this._resolvers.delete(name);
|
|
836
|
+
}
|
|
837
|
+
// Remove any ref watcher disposable
|
|
838
|
+
const watcherDisposable = this._dynamicRefWatchers.get(name);
|
|
839
|
+
if (watcherDisposable) {
|
|
840
|
+
watcherDisposable.dispose();
|
|
841
|
+
this._dynamicRefWatchers.delete(name);
|
|
842
|
+
}
|
|
843
|
+
if (this._worktreePool) {
|
|
844
|
+
this._worktreePool.release(name);
|
|
845
|
+
}
|
|
846
|
+
this._dynamicSessionMeta.delete(name);
|
|
847
|
+
this._logger.log(`Session "${name}" closed`);
|
|
848
|
+
this._emit({ type: 'session-change' });
|
|
849
|
+
return { sessions: this.getSessionInfos() };
|
|
433
850
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
851
|
+
async _updateDynamicSessionRef(name, newRef) {
|
|
852
|
+
const meta = this._dynamicSessionMeta.get(name);
|
|
853
|
+
if (!meta) {
|
|
854
|
+
if (this._sessionConfigs.has(name)) {
|
|
855
|
+
return { error: `Session "${name}" is a static session — use restartSession instead` };
|
|
856
|
+
}
|
|
857
|
+
return { error: `Session "${name}" does not exist` };
|
|
858
|
+
}
|
|
859
|
+
const git = this._config.repo;
|
|
860
|
+
// Check dirty before doing anything
|
|
861
|
+
const wtInfo = await git.worktrees.info(meta.worktreePath);
|
|
862
|
+
if (wtInfo && wtInfo.isDirty) {
|
|
863
|
+
return {
|
|
864
|
+
error: `Worktree is dirty, cannot update ref. Dirty files:\n` +
|
|
865
|
+
wtInfo.dirtyFiles.map(f => ` ${f}`).join('\n'),
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
// Dispose old resolver
|
|
869
|
+
const oldResolver = this._resolvers.get(name);
|
|
870
|
+
if (oldResolver) {
|
|
871
|
+
oldResolver.dispose();
|
|
872
|
+
this._resolvers.delete(name);
|
|
873
|
+
}
|
|
874
|
+
const oldWatcher = this._dynamicRefWatchers.get(name);
|
|
875
|
+
if (oldWatcher) {
|
|
876
|
+
oldWatcher.dispose();
|
|
877
|
+
this._dynamicRefWatchers.delete(name);
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
const isIndex = newRef === GitIndexResolver.INDEX_REF;
|
|
881
|
+
let resolver;
|
|
882
|
+
if (isIndex) {
|
|
883
|
+
resolver = await git.createIndexResolver();
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
resolver = await git.createCommitResolver(newRef);
|
|
887
|
+
}
|
|
888
|
+
this._resolvers.set(name, resolver);
|
|
889
|
+
const resolvedCommit = resolver.resolvedCommit.get();
|
|
890
|
+
// Checkout in worktree — don't restart Vite, let HMR handle it
|
|
891
|
+
if (wtInfo) {
|
|
892
|
+
if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
|
|
893
|
+
await git.worktrees.checkout(meta.worktreePath, resolvedCommit);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
await git.worktrees.create(meta.worktreePath, resolvedCommit);
|
|
898
|
+
}
|
|
899
|
+
const sessionConfig = this._sessionConfigs.get(name);
|
|
900
|
+
const installSetup = (sessionConfig?.source.kind === 'worktree' ? sessionConfig.source.install : undefined)
|
|
901
|
+
?? this._config.worktreePool?.setup
|
|
902
|
+
?? { kind: 'auto' };
|
|
903
|
+
await installDependencies(meta.worktreePath, installSetup, this._logger);
|
|
904
|
+
meta.ref = newRef;
|
|
905
|
+
this._addDynamicRefWatcher(name, resolver);
|
|
906
|
+
return { sessions: this.getSessionInfos() };
|
|
907
|
+
}
|
|
908
|
+
catch (e) {
|
|
909
|
+
if (e instanceof DirtyWorktreeError) {
|
|
910
|
+
return {
|
|
911
|
+
error: `Worktree is dirty, cannot update ref. Dirty files:\n` +
|
|
912
|
+
e.dirtyFiles.map(f => ` ${f}`).join('\n'),
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
916
|
+
this._logger.log(`Failed to update session ref "${name}": ${msg}`);
|
|
917
|
+
return { error: `Failed to update session ref: ${msg}` };
|
|
918
|
+
}
|
|
438
919
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
920
|
+
_dynamicRefWatchers = new Map();
|
|
921
|
+
_addDynamicRefWatcher(sessionName, resolver) {
|
|
922
|
+
let previousCommit = resolver.resolvedCommit.get();
|
|
923
|
+
const disposable = autorun(reader => {
|
|
924
|
+
const commit = resolver.resolvedCommit.read(reader);
|
|
925
|
+
if (!previousCommit.equals(commit)) {
|
|
926
|
+
const prev = previousCommit;
|
|
927
|
+
previousCommit = commit;
|
|
928
|
+
this._handleRefChange(sessionName, resolver.ref, prev, commit);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
this._dynamicRefWatchers.set(sessionName, disposable);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
function arraysEqual(a, b) {
|
|
935
|
+
if (a.length !== b.length) {
|
|
936
|
+
return false;
|
|
442
937
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
938
|
+
for (let i = 0; i < a.length; i++) {
|
|
939
|
+
if (a[i] !== b[i]) {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
446
942
|
}
|
|
943
|
+
return true;
|
|
447
944
|
}
|
|
448
945
|
|
|
449
|
-
export { DaemonService, SourceTreeChangedError };
|
|
946
|
+
export { ActivityTracker, DaemonService, SourceTreeChangedError, daemonApiVersionText, pluginProtocolVersionText };
|
|
450
947
|
//# sourceMappingURL=DaemonService.js.map
|