@vscode/component-explorer-cli 0.1.1-9 → 0.2.1-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/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 +15 -1
- package/dist/browserPage.d.ts.map +1 -1
- package/dist/browserPage.js +51 -7
- package/dist/browserPage.js.map +1 -1
- package/dist/commands/acceptCommand.d.ts.map +1 -1
- package/dist/commands/acceptCommand.js +3 -2
- 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 +1 -0
- package/dist/commands/compareCommand.d.ts.map +1 -1
- package/dist/commands/compareCommand.js +25 -4
- package/dist/commands/compareCommand.js.map +1 -1
- package/dist/commands/mcpCommand.d.ts +1 -0
- package/dist/commands/mcpCommand.d.ts.map +1 -1
- package/dist/commands/mcpCommand.js +23 -10
- package/dist/commands/mcpCommand.js.map +1 -1
- package/dist/commands/screenshotCommand.d.ts +2 -0
- package/dist/commands/screenshotCommand.d.ts.map +1 -1
- package/dist/commands/screenshotCommand.js +19 -4
- package/dist/commands/screenshotCommand.js.map +1 -1
- package/dist/commands/serveCommand.d.ts +4 -0
- package/dist/commands/serveCommand.d.ts.map +1 -1
- package/dist/commands/serveCommand.js +101 -26
- package/dist/commands/serveCommand.js.map +1 -1
- package/dist/commands/watchCommand.d.ts +2 -0
- package/dist/commands/watchCommand.d.ts.map +1 -1
- package/dist/commands/watchCommand.js +18 -66
- package/dist/commands/watchCommand.js.map +1 -1
- package/dist/comparison.d.ts +11 -1
- package/dist/comparison.d.ts.map +1 -1
- package/dist/comparison.js +25 -11
- package/dist/comparison.js.map +1 -1
- package/dist/component-explorer-config.schema.json +260 -55
- package/dist/componentExplorer.d.ts +21 -18
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +60 -19
- package/dist/componentExplorer.js.map +1 -1
- package/dist/daemon/DaemonService.d.ts +100 -11
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +512 -129
- 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 +2 -1
- package/dist/daemon/lifecycle.d.ts.map +1 -1
- package/dist/daemon/lifecycle.js +52 -30
- package/dist/daemon/lifecycle.js.map +1 -1
- package/dist/daemon/pipeClient.d.ts.map +1 -1
- package/dist/daemon/pipeClient.js +81 -2
- 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 +59 -2
- package/dist/daemon/pipeServer.js.map +1 -1
- 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.map +1 -1
- package/dist/explorerSession.d.ts +3 -3
- package/dist/explorerSession.d.ts.map +1 -1
- package/dist/explorerSession.js +26 -9
- package/dist/explorerSession.js.map +1 -1
- 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/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 +18 -7
- package/dist/httpServer.d.ts.map +1 -1
- package/dist/httpServer.js +117 -18
- package/dist/httpServer.js.map +1 -1
- package/dist/httpServer.test.d.ts +2 -0
- package/dist/httpServer.test.d.ts.map +1 -0
- 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 +18 -0
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +555 -13
- 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-Q24JOMNK.js → chunk-TAEFVNPN.js} +1 -1
- package/dist/packages/simple-api/dist/chunk-TAEFVNPN.js.map +1 -0
- package/dist/packages/simple-api/dist/express.js +11 -3
- package/dist/packages/simple-api/dist/express.js.map +1 -1
- package/dist/utils.d.ts +7 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -7
- package/dist/utils.js.map +1 -1
- package/dist/visualCache.d.ts +34 -0
- package/dist/visualCache.d.ts.map +1 -0
- package/dist/visualCache.js +90 -0
- package/dist/visualCache.js.map +1 -0
- package/dist/watchConfig.d.ts +68 -15
- package/dist/watchConfig.d.ts.map +1 -1
- package/dist/watchConfig.js +109 -65
- package/dist/watchConfig.js.map +1 -1
- package/package.json +21 -4
- package/dist/packages/simple-api/dist/chunk-Q24JOMNK.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import * as path from 'node:path';
|
|
2
3
|
import '../external/vscode-observables/observables/dist/observableInternal/index.js';
|
|
3
4
|
import { observableValue } from '../external/vscode-observables/observables/dist/observableInternal/observables/observableValue.js';
|
|
4
5
|
import '../external/vscode-observables/observables/dist/observableInternal/debugLocation.js';
|
|
@@ -10,11 +11,19 @@ import '../external/vscode-observables/observables/dist/observableInternal/obser
|
|
|
10
11
|
import { createApiFactory } from '../packages/simple-api/dist/chunk-3R7GHWBM.js';
|
|
11
12
|
import { AsyncStream } from '../packages/simple-api/dist/chunk-SGBCNXYH.js';
|
|
12
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';
|
|
13
17
|
import { PlaywrightBrowserPageFactory } from '../browserPage.js';
|
|
14
18
|
import { DefaultComponentExplorerHttpServerFactory } from '../httpServer.js';
|
|
15
19
|
import { installDependencies } from '../dependencyInstaller.js';
|
|
16
20
|
import { FileApprovalStore } from './approvalStore.js';
|
|
21
|
+
import { VisualCache } from '../visualCache.js';
|
|
17
22
|
import { contentHash } from '../screenshotCache.js';
|
|
23
|
+
import { WorktreePool } from '../WorktreePool.js';
|
|
24
|
+
import { ViteProjectRef } from '../viteProjectRef.js';
|
|
25
|
+
import { pluginProtocolVersionText, daemonApiVersionText } from './version.js';
|
|
26
|
+
export { isCompatibleVersion, parseVersion } from './version.js';
|
|
18
27
|
|
|
19
28
|
// ---------------------------------------------------------------------------
|
|
20
29
|
// ActivityTracker — monitors idle time and triggers shutdown
|
|
@@ -90,13 +99,16 @@ class DaemonService {
|
|
|
90
99
|
_serverFactory;
|
|
91
100
|
_sessions = new Map();
|
|
92
101
|
_sessionConfigs = new Map();
|
|
102
|
+
_dynamicSessionMeta = new Map();
|
|
93
103
|
_resolvers = new Map();
|
|
94
104
|
_eventListenerCount = observableValue(this, 0);
|
|
95
105
|
_eventListeners = new Set();
|
|
96
106
|
_shutdownRequested = false;
|
|
97
107
|
_shutdownResolvers = [];
|
|
98
108
|
_activityTracker;
|
|
109
|
+
_worktreePool;
|
|
99
110
|
approvals;
|
|
111
|
+
visualCache;
|
|
100
112
|
api;
|
|
101
113
|
constructor(_config, _pipeName, _logger, _browserFactory, _serverFactory, approvalStorePath, idleTimeoutMs) {
|
|
102
114
|
this._config = _config;
|
|
@@ -105,23 +117,22 @@ class DaemonService {
|
|
|
105
117
|
this._browserFactory = _browserFactory;
|
|
106
118
|
this._serverFactory = _serverFactory;
|
|
107
119
|
this.approvals = new FileApprovalStore(approvalStorePath);
|
|
120
|
+
this.visualCache = new VisualCache(`${_config.screenshotDir}/visual-cache.json`);
|
|
108
121
|
this._activityTracker = new ActivityTracker(idleTimeoutMs, () => this.requestShutdown(), this._logger);
|
|
122
|
+
if (_config.worktreePool) {
|
|
123
|
+
this._worktreePool = new WorktreePool(_config.worktreePool.maxSlots, _config.repo.worktreeRootPath());
|
|
124
|
+
}
|
|
109
125
|
this.api = this._buildApi();
|
|
110
126
|
}
|
|
111
127
|
static async create(config, logger, pipeName, options) {
|
|
112
|
-
const browserFactory = new PlaywrightBrowserPageFactory();
|
|
128
|
+
const browserFactory = new PlaywrightBrowserPageFactory(options?.headed ?? false, 30_000, logger);
|
|
113
129
|
const serverFactory = new DefaultComponentExplorerHttpServerFactory();
|
|
114
130
|
const approvalStorePath = `${config.screenshotDir}/approvals.json`;
|
|
115
131
|
const idleTimeoutMs = options?.idleTimeoutMs ?? 5 * 60 * 1000; // 5 minutes default
|
|
116
|
-
|
|
117
|
-
const eventStreamLogger = new EventStreamLogger(logger);
|
|
118
|
-
const svc = new DaemonService(config, pipeName ?? '', eventStreamLogger, browserFactory, serverFactory, approvalStorePath, idleTimeoutMs);
|
|
119
|
-
eventStreamLogger._emitter = (event) => svc._emit(event);
|
|
120
|
-
const currentSession = config.sessions.find(s => s.source.kind === 'current');
|
|
121
|
-
const resolveViteFrom = currentSession?.viteProject.cwd;
|
|
132
|
+
const svc = new DaemonService(config, pipeName ?? '', logger, browserFactory, serverFactory, approvalStorePath, idleTimeoutMs);
|
|
122
133
|
for (const sessionConfig of config.sessions) {
|
|
123
134
|
svc._sessionConfigs.set(sessionConfig.name, sessionConfig);
|
|
124
|
-
await svc._setupSession(sessionConfig
|
|
135
|
+
await svc._setupSession(sessionConfig);
|
|
125
136
|
}
|
|
126
137
|
return svc;
|
|
127
138
|
}
|
|
@@ -175,7 +186,10 @@ class DaemonService {
|
|
|
175
186
|
image: (args.includeImage ?? true)
|
|
176
187
|
? Buffer.from(result.screenshots[result.screenshots.length - 1].image).toString('base64')
|
|
177
188
|
: undefined,
|
|
178
|
-
|
|
189
|
+
hasError: result.hasError,
|
|
190
|
+
error: result.error,
|
|
191
|
+
events: result.events.length > 0 ? result.events : undefined,
|
|
192
|
+
resultData: result.resultData,
|
|
179
193
|
isStable,
|
|
180
194
|
stabilityScreenshots,
|
|
181
195
|
};
|
|
@@ -183,12 +197,15 @@ class DaemonService {
|
|
|
183
197
|
const result = await session.explorer.screenshotFixture(args.fixtureId);
|
|
184
198
|
this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
|
|
185
199
|
return {
|
|
186
|
-
hash: contentHash(result.image),
|
|
200
|
+
hash: result.image ? contentHash(result.image) : undefined,
|
|
187
201
|
sourceTreeId: args.sourceTreeId,
|
|
188
|
-
image: (args.includeImage ?? true)
|
|
202
|
+
image: (args.includeImage ?? true) && result.image
|
|
189
203
|
? Buffer.from(result.image).toString('base64')
|
|
190
204
|
: undefined,
|
|
191
|
-
|
|
205
|
+
hasError: result.hasError,
|
|
206
|
+
error: result.error,
|
|
207
|
+
events: result.events?.length > 0 ? result.events : undefined,
|
|
208
|
+
resultData: result.resultData,
|
|
192
209
|
};
|
|
193
210
|
}),
|
|
194
211
|
takeBatch: createMethod({
|
|
@@ -211,11 +228,14 @@ class DaemonService {
|
|
|
211
228
|
this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
|
|
212
229
|
screenshots.push({
|
|
213
230
|
fixtureId,
|
|
214
|
-
hash: contentHash(result.image),
|
|
215
|
-
image: includeImages
|
|
231
|
+
hash: result.image ? contentHash(result.image) : undefined,
|
|
232
|
+
image: includeImages && result.image
|
|
216
233
|
? Buffer.from(result.image).toString('base64')
|
|
217
234
|
: undefined,
|
|
218
|
-
|
|
235
|
+
hasError: result.hasError,
|
|
236
|
+
error: result.error,
|
|
237
|
+
events: result.events.length > 0 ? result.events : undefined,
|
|
238
|
+
resultData: result.resultData,
|
|
219
239
|
});
|
|
220
240
|
}
|
|
221
241
|
this._logger.trace(`takeBatch: done in ${Date.now() - startTime}ms`);
|
|
@@ -236,24 +256,42 @@ class DaemonService {
|
|
|
236
256
|
const currentSession = this.getSession(args.currentSessionName);
|
|
237
257
|
this.assertSourceTreeId(args.baselineSessionName, args.baselineSourceTreeId);
|
|
238
258
|
this.assertSourceTreeId(args.currentSessionName, args.currentSourceTreeId);
|
|
239
|
-
const baselineResult = await
|
|
259
|
+
const [baselineResult, baselineFixtures] = await Promise.all([
|
|
260
|
+
baselineSession.explorer.screenshotFixture(args.fixtureId),
|
|
261
|
+
baselineSession.explorer.listFixtures(),
|
|
262
|
+
]);
|
|
240
263
|
this.assertSourceTreeId(args.baselineSessionName, args.baselineSourceTreeId);
|
|
241
|
-
const currentResult = await
|
|
264
|
+
const [currentResult, currentFixtures] = await Promise.all([
|
|
265
|
+
currentSession.explorer.screenshotFixture(args.fixtureId),
|
|
266
|
+
currentSession.explorer.listFixtures(),
|
|
267
|
+
]);
|
|
242
268
|
this.assertSourceTreeId(args.currentSessionName, args.currentSourceTreeId);
|
|
243
|
-
const baselineHash = contentHash(baselineResult.image);
|
|
244
|
-
const currentHash = contentHash(currentResult.image);
|
|
269
|
+
const baselineHash = baselineResult.image ? contentHash(baselineResult.image) : undefined;
|
|
270
|
+
const currentHash = currentResult.image ? contentHash(currentResult.image) : undefined;
|
|
245
271
|
const includeImages = args.includeImages ?? false;
|
|
272
|
+
const currentFixture = currentFixtures.find(f => f.fixtureId === args.fixtureId);
|
|
273
|
+
const baselineFixture = baselineFixtures.find(f => f.fixtureId === args.fixtureId);
|
|
274
|
+
const labels = currentFixture?.labels;
|
|
275
|
+
const labelsChanged = baselineFixture && !arraysEqual(baselineFixture.labels, currentFixture?.labels ?? []);
|
|
246
276
|
return {
|
|
247
277
|
match: baselineHash === currentHash,
|
|
248
278
|
baselineHash,
|
|
249
279
|
currentHash,
|
|
250
|
-
baselineImage: includeImages
|
|
280
|
+
baselineImage: includeImages && baselineResult.image
|
|
251
281
|
? Buffer.from(baselineResult.image).toString('base64') : undefined,
|
|
252
|
-
currentImage: includeImages
|
|
282
|
+
currentImage: includeImages && currentResult.image
|
|
253
283
|
? Buffer.from(currentResult.image).toString('base64') : undefined,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
284
|
+
baselineHasError: baselineResult.hasError,
|
|
285
|
+
baselineError: baselineResult.error,
|
|
286
|
+
baselineEvents: baselineResult.events.length > 0 ? baselineResult.events : undefined,
|
|
287
|
+
baselineResultData: baselineResult.resultData,
|
|
288
|
+
currentHasError: currentResult.hasError,
|
|
289
|
+
currentError: currentResult.error,
|
|
290
|
+
currentEvents: currentResult.events.length > 0 ? currentResult.events : undefined,
|
|
291
|
+
currentResultData: currentResult.resultData,
|
|
292
|
+
labels,
|
|
293
|
+
labelsBefore: labelsChanged ? baselineFixture.labels : undefined,
|
|
294
|
+
approval: (baselineHash !== undefined && currentHash !== undefined && baselineHash !== currentHash)
|
|
257
295
|
? this.approvals.lookup({
|
|
258
296
|
fixtureId: args.fixtureId,
|
|
259
297
|
originalHash: baselineHash,
|
|
@@ -302,6 +340,29 @@ class DaemonService {
|
|
|
302
340
|
return this.approvals.lookup(args) ?? null;
|
|
303
341
|
}),
|
|
304
342
|
}),
|
|
343
|
+
visualReview: createApi({
|
|
344
|
+
getStatus: createMethod({
|
|
345
|
+
args: {
|
|
346
|
+
fixtureId: z.string(),
|
|
347
|
+
expectedVisualDescriptions: z.array(z.string()),
|
|
348
|
+
screenshotHash: z.string(),
|
|
349
|
+
},
|
|
350
|
+
}, async (args) => {
|
|
351
|
+
this._activityTracker.reportActivity();
|
|
352
|
+
return this.visualCache.getReviewStatus(args.fixtureId, args.expectedVisualDescriptions, args.screenshotHash);
|
|
353
|
+
}),
|
|
354
|
+
approve: createMethod({
|
|
355
|
+
args: {
|
|
356
|
+
fixtureId: z.string(),
|
|
357
|
+
expectedVisualDescriptions: z.array(z.string()),
|
|
358
|
+
screenshotHash: z.string(),
|
|
359
|
+
comment: z.string(),
|
|
360
|
+
},
|
|
361
|
+
}, async (args) => {
|
|
362
|
+
this._activityTracker.reportActivity();
|
|
363
|
+
this.visualCache.approve(args.fixtureId, args.expectedVisualDescriptions, args.screenshotHash, args.comment);
|
|
364
|
+
}),
|
|
365
|
+
}),
|
|
305
366
|
evaluate: createMethod({
|
|
306
367
|
args: {
|
|
307
368
|
sessionName: z.string(),
|
|
@@ -317,6 +378,17 @@ class DaemonService {
|
|
|
317
378
|
this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
|
|
318
379
|
return { result };
|
|
319
380
|
}),
|
|
381
|
+
setBrowserVisibility: createMethod({
|
|
382
|
+
args: { visible: z.boolean() },
|
|
383
|
+
}, async (args, ctx) => {
|
|
384
|
+
this._activityTracker.reportActivity();
|
|
385
|
+
this._logger.debug(`Set browser visibility: ${args.visible} (client=${ctx.clientName})`);
|
|
386
|
+
await this._browserFactory.setHeaded(args.visible);
|
|
387
|
+
for (const session of this._sessions.values()) {
|
|
388
|
+
session.explorer.closePage();
|
|
389
|
+
}
|
|
390
|
+
return { visible: args.visible };
|
|
391
|
+
}),
|
|
320
392
|
events: createMethod({ args: {} }, async () => {
|
|
321
393
|
return AsyncStream.fromIterable(this.eventStream());
|
|
322
394
|
}),
|
|
@@ -325,6 +397,48 @@ class DaemonService {
|
|
|
325
397
|
this._logger.trace(`API: sessions (client=${ctx.clientName}, eventListeners=${this._eventListeners.size})`);
|
|
326
398
|
return this.getSessionInfos();
|
|
327
399
|
}),
|
|
400
|
+
version: createMethod({ args: {} }, async () => {
|
|
401
|
+
this._activityTracker.reportActivity();
|
|
402
|
+
return {
|
|
403
|
+
daemonApiVersion: daemonApiVersionText,
|
|
404
|
+
pluginProtocolVersion: pluginProtocolVersionText,
|
|
405
|
+
};
|
|
406
|
+
}),
|
|
407
|
+
restartSession: createMethod({
|
|
408
|
+
args: { sessionName: z.string() },
|
|
409
|
+
}, async (args, ctx) => {
|
|
410
|
+
this._activityTracker.reportActivity();
|
|
411
|
+
this._logger.debug(`Restart session "${args.sessionName}" requested (client=${ctx.clientName})`);
|
|
412
|
+
await this._restartSession(args.sessionName);
|
|
413
|
+
return this.getSessionInfos();
|
|
414
|
+
}),
|
|
415
|
+
openSession: createMethod({
|
|
416
|
+
args: {
|
|
417
|
+
name: z.string(),
|
|
418
|
+
ref: z.string(),
|
|
419
|
+
},
|
|
420
|
+
}, async (args, ctx) => {
|
|
421
|
+
this._activityTracker.reportActivity();
|
|
422
|
+
this._logger.log(`Open session "${args.name}" @ ${args.ref} requested (client=${ctx.clientName})`);
|
|
423
|
+
return this._openDynamicSession(args.name, args.ref);
|
|
424
|
+
}),
|
|
425
|
+
closeSession: createMethod({
|
|
426
|
+
args: { name: z.string() },
|
|
427
|
+
}, async (args, ctx) => {
|
|
428
|
+
this._activityTracker.reportActivity();
|
|
429
|
+
this._logger.log(`Close session "${args.name}" requested (client=${ctx.clientName})`);
|
|
430
|
+
return this._closeDynamicSession(args.name);
|
|
431
|
+
}),
|
|
432
|
+
updateSessionRef: createMethod({
|
|
433
|
+
args: {
|
|
434
|
+
name: z.string(),
|
|
435
|
+
ref: z.string(),
|
|
436
|
+
},
|
|
437
|
+
}, async (args, ctx) => {
|
|
438
|
+
this._activityTracker.reportActivity();
|
|
439
|
+
this._logger.log(`Update session ref "${args.name}" → ${args.ref} requested (client=${ctx.clientName})`);
|
|
440
|
+
return this._updateDynamicSessionRef(args.name, args.ref);
|
|
441
|
+
}),
|
|
328
442
|
shutdown: createMethod({ args: {} }, async (_args, ctx) => {
|
|
329
443
|
this._logger.debug(`Shutdown requested via API (client=${ctx.clientName})`);
|
|
330
444
|
this.requestShutdown();
|
|
@@ -340,18 +454,56 @@ class DaemonService {
|
|
|
340
454
|
return session;
|
|
341
455
|
}
|
|
342
456
|
getSessionInfos(reader) {
|
|
343
|
-
|
|
457
|
+
const infos = [];
|
|
458
|
+
const reportedNames = new Set();
|
|
459
|
+
// Static sessions (from config)
|
|
460
|
+
for (const sc of this._config.sessions) {
|
|
461
|
+
reportedNames.add(sc.name);
|
|
344
462
|
const session = this._sessions.get(sc.name);
|
|
463
|
+
const meta = this._dynamicSessionMeta.get(sc.name);
|
|
345
464
|
if (!session) {
|
|
346
|
-
|
|
465
|
+
infos.push({ name: sc.name, sourceKind: sc.source.kind, isLoading: true });
|
|
347
466
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
467
|
+
else if (meta) {
|
|
468
|
+
infos.push({
|
|
469
|
+
name: sc.name,
|
|
470
|
+
sourceKind: 'worktree',
|
|
471
|
+
serverUrl: session.serverUrl,
|
|
472
|
+
sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
|
|
473
|
+
worktreePath: meta.worktreePath,
|
|
474
|
+
ref: meta.ref,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
infos.push({
|
|
479
|
+
name: sc.name,
|
|
480
|
+
sourceKind: sc.source.kind,
|
|
481
|
+
serverUrl: session.serverUrl,
|
|
482
|
+
sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Dynamic worktree sessions (not already reported as static)
|
|
487
|
+
for (const [name, meta] of this._dynamicSessionMeta) {
|
|
488
|
+
if (reportedNames.has(name)) {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const session = this._sessions.get(name);
|
|
492
|
+
if (!session) {
|
|
493
|
+
infos.push({ name, sourceKind: 'worktree', isLoading: true });
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
infos.push({
|
|
497
|
+
name,
|
|
498
|
+
sourceKind: 'worktree',
|
|
499
|
+
serverUrl: session.serverUrl,
|
|
500
|
+
sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
|
|
501
|
+
worktreePath: meta.worktreePath,
|
|
502
|
+
ref: meta.ref,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return infos;
|
|
355
507
|
}
|
|
356
508
|
waitForSession(sessionName) {
|
|
357
509
|
const sessionObs = derived(this, reader => {
|
|
@@ -394,8 +546,10 @@ class DaemonService {
|
|
|
394
546
|
self._eventListeners.add(listener);
|
|
395
547
|
self._eventListenerCount.set(self._eventListeners.size, undefined);
|
|
396
548
|
self._activityTracker.setActive(true);
|
|
549
|
+
self._logger.debug(`Event stream opened (listeners: ${self._eventListeners.size})`);
|
|
397
550
|
const onShutdown = () => {
|
|
398
551
|
done = true;
|
|
552
|
+
cleanup();
|
|
399
553
|
if (resolve) {
|
|
400
554
|
const r = resolve;
|
|
401
555
|
resolve = undefined;
|
|
@@ -407,6 +561,7 @@ class DaemonService {
|
|
|
407
561
|
self._eventListeners.delete(listener);
|
|
408
562
|
self._eventListenerCount.set(self._eventListeners.size, undefined);
|
|
409
563
|
self._activityTracker.setActive(false);
|
|
564
|
+
self._logger.debug(`Event stream closed (listeners: ${self._eventListeners.size})`);
|
|
410
565
|
};
|
|
411
566
|
return {
|
|
412
567
|
next() {
|
|
@@ -421,6 +576,11 @@ class DaemonService {
|
|
|
421
576
|
return() {
|
|
422
577
|
done = true;
|
|
423
578
|
cleanup();
|
|
579
|
+
if (resolve) {
|
|
580
|
+
const r = resolve;
|
|
581
|
+
resolve = undefined;
|
|
582
|
+
r({ value: undefined, done: true });
|
|
583
|
+
}
|
|
424
584
|
return Promise.resolve({ value: undefined, done: true });
|
|
425
585
|
},
|
|
426
586
|
};
|
|
@@ -442,68 +602,57 @@ class DaemonService {
|
|
|
442
602
|
_sourceChangeDisposables = [];
|
|
443
603
|
_startSourceChangeWatchers() {
|
|
444
604
|
for (const [name, session] of this._sessions) {
|
|
445
|
-
|
|
446
|
-
const disposable = autorun(reader => {
|
|
447
|
-
const current = session.sourceTreeId.read(reader);
|
|
448
|
-
if (current.value !== previousValue) {
|
|
449
|
-
this._logger.debug(`Source tree changed: ${name} ${previousValue} → ${current.value}`);
|
|
450
|
-
previousValue = current.value;
|
|
451
|
-
this._emit({ type: 'source-change', sessionName: name, sourceTreeId: current.value });
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
this._sourceChangeDisposables.push(disposable);
|
|
455
|
-
}
|
|
456
|
-
// Watch for ref changes (worktree sessions)
|
|
457
|
-
for (const [name, resolver] of this._resolvers) {
|
|
458
|
-
let previousCommit = resolver.resolvedCommit.get();
|
|
459
|
-
const disposable = autorun(reader => {
|
|
460
|
-
const commit = resolver.resolvedCommit.read(reader);
|
|
461
|
-
if (!previousCommit.equals(commit)) {
|
|
462
|
-
previousCommit = commit;
|
|
463
|
-
this._handleRefChange(name, resolver.ref, commit);
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
this._sourceChangeDisposables.push(disposable);
|
|
605
|
+
this._addSourceChangeWatcher(name, session);
|
|
467
606
|
}
|
|
468
607
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
608
|
+
_addSourceChangeWatcher(name, session) {
|
|
609
|
+
let previousValue = session.sourceTreeId.get().value;
|
|
610
|
+
const disposable = autorun(reader => {
|
|
611
|
+
const current = session.sourceTreeId.read(reader);
|
|
612
|
+
if (current.value !== previousValue) {
|
|
613
|
+
this._logger.debug(`Source tree changed: ${name} ${previousValue} → ${current.value}`);
|
|
614
|
+
previousValue = current.value;
|
|
615
|
+
this._emit({ type: 'source-change', sessionName: name, sourceTreeId: current.value });
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
this._sourceChangeDisposables.push(disposable);
|
|
619
|
+
}
|
|
620
|
+
async _handleRefChange(sessionName, ref, previousCommit, newCommit) {
|
|
621
|
+
const changedFiles = await this._getChangedFiles(previousCommit, newCommit);
|
|
622
|
+
this._logger.log(`Ref ${ref} moved to ${newCommit.toShort()} (${changedFiles.length} file(s) changed${changedFiles.length > 0 ? ': ' + changedFiles.join(', ') : ''})`);
|
|
623
|
+
const meta = this._dynamicSessionMeta.get(sessionName);
|
|
624
|
+
if (!meta) {
|
|
474
625
|
return;
|
|
475
626
|
}
|
|
476
|
-
const
|
|
477
|
-
const wtInfo = await git.worktrees.info(
|
|
627
|
+
const git = this._config.repo;
|
|
628
|
+
const wtInfo = await git.worktrees.info(meta.worktreePath);
|
|
478
629
|
if (wtInfo && wtInfo.isDirty) {
|
|
479
630
|
this._logger.log(`Worktree is dirty, skipping update to ${newCommit.toShort()}`);
|
|
480
631
|
return;
|
|
481
632
|
}
|
|
482
|
-
// Dispose old session
|
|
483
|
-
const oldSession = this._sessions.get(sessionName);
|
|
484
|
-
if (oldSession) {
|
|
485
|
-
this._logger.debug(`Disposing session: ${sessionName}`);
|
|
486
|
-
await oldSession.dispose();
|
|
487
|
-
}
|
|
488
|
-
// Checkout + reinstall
|
|
489
633
|
if (wtInfo) {
|
|
490
|
-
await git.worktrees.checkout(
|
|
634
|
+
await git.worktrees.checkout(meta.worktreePath, newCommit);
|
|
491
635
|
}
|
|
492
636
|
else {
|
|
493
|
-
await git.worktrees.create(
|
|
637
|
+
await git.worktrees.create(meta.worktreePath, newCommit);
|
|
494
638
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
logger: this._logger,
|
|
501
|
-
resolveViteFrom,
|
|
502
|
-
hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
|
|
503
|
-
});
|
|
504
|
-
this._sessions.set(sessionName, newSession);
|
|
639
|
+
const sessionConfig = this._sessionConfigs.get(sessionName);
|
|
640
|
+
const installSetup = (sessionConfig?.source.kind === 'worktree' ? sessionConfig.source.install : undefined)
|
|
641
|
+
?? this._config.worktreePool?.setup
|
|
642
|
+
?? { kind: 'auto' };
|
|
643
|
+
await installDependencies(meta.worktreePath, installSetup, this._logger);
|
|
505
644
|
this._emit({ type: 'ref-change', sessionName, newCommit: newCommit.toShort() });
|
|
506
645
|
}
|
|
646
|
+
async _getChangedFiles(oldCommit, newCommit) {
|
|
647
|
+
try {
|
|
648
|
+
const output = await execGit(this._config.repo.gitRoot, ['diff', '--name-only', oldCommit.hash, newCommit.hash]);
|
|
649
|
+
return output.trim().split('\n').filter(f => f.length > 0);
|
|
650
|
+
}
|
|
651
|
+
catch (e) {
|
|
652
|
+
this._logger.log(`Failed to get changed files (${oldCommit.toShort()}..${newCommit.toShort()}): ${e instanceof Error ? e.message : e}`);
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
}
|
|
507
656
|
// -- Shutdown ------------------------------------------------------------
|
|
508
657
|
requestShutdown() {
|
|
509
658
|
this._shutdownRequested = true;
|
|
@@ -519,6 +668,10 @@ class DaemonService {
|
|
|
519
668
|
d.dispose();
|
|
520
669
|
}
|
|
521
670
|
this._sourceChangeDisposables = [];
|
|
671
|
+
for (const d of this._dynamicRefWatchers.values()) {
|
|
672
|
+
d.dispose();
|
|
673
|
+
}
|
|
674
|
+
this._dynamicRefWatchers.clear();
|
|
522
675
|
for (const session of this._sessions.values()) {
|
|
523
676
|
await session.dispose();
|
|
524
677
|
}
|
|
@@ -529,77 +682,307 @@ class DaemonService {
|
|
|
529
682
|
await this._browserFactory.dispose();
|
|
530
683
|
}
|
|
531
684
|
// -- Private helpers -----------------------------------------------------
|
|
532
|
-
/** @internal — also called by EventStreamLogger */
|
|
533
685
|
_emit(event) {
|
|
534
686
|
for (const listener of this._eventListeners) {
|
|
535
687
|
listener(event);
|
|
536
688
|
}
|
|
537
689
|
}
|
|
538
|
-
async
|
|
690
|
+
async _restartSession(sessionName) {
|
|
691
|
+
const config = this._sessionConfigs.get(sessionName);
|
|
692
|
+
const meta = this._dynamicSessionMeta.get(sessionName);
|
|
693
|
+
if (!config && !meta) {
|
|
694
|
+
throw new Error(`Unknown session: "${sessionName}"`);
|
|
695
|
+
}
|
|
696
|
+
const existing = this._sessions.get(sessionName);
|
|
697
|
+
if (existing) {
|
|
698
|
+
this._logger.debug(`Disposing session: ${sessionName}`);
|
|
699
|
+
await existing.dispose();
|
|
700
|
+
this._sessions.delete(sessionName);
|
|
701
|
+
}
|
|
702
|
+
this._logger.log(`Restarting server: ${sessionName}`);
|
|
703
|
+
if (meta) {
|
|
704
|
+
await this._createWorktreeExplorerSession(sessionName, meta.worktreePath);
|
|
705
|
+
}
|
|
706
|
+
else if (config) {
|
|
707
|
+
await this._createExplorerSession(config);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async _createExplorerSession(sessionConfig) {
|
|
711
|
+
const session = await ExplorerSession.create(sessionConfig.name, sessionConfig.server, this._serverFactory, this._browserFactory, {
|
|
712
|
+
logger: this._logger,
|
|
713
|
+
daemonConfig: {
|
|
714
|
+
pipeName: this._pipeName,
|
|
715
|
+
sessionName: sessionConfig.name,
|
|
716
|
+
daemonApiVersion: daemonApiVersionText,
|
|
717
|
+
pluginProtocolVersion: pluginProtocolVersionText,
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
this._sessions.set(sessionConfig.name, session);
|
|
721
|
+
this._logger.debug(`Session ready: ${sessionConfig.name} (${session.serverUrl})`);
|
|
722
|
+
}
|
|
723
|
+
async _createWorktreeExplorerSession(sessionName, worktreePath) {
|
|
724
|
+
const defaultServer = this._config.defaultServerConfig;
|
|
725
|
+
if (defaultServer.kind !== 'vite') {
|
|
726
|
+
throw new Error(`Worktree sessions require a Vite server configuration`);
|
|
727
|
+
}
|
|
728
|
+
const configDirRelToGitRoot = path.relative(this._config.repo.gitRoot, this._config.configDir);
|
|
729
|
+
const worktreeConfigDir = path.resolve(worktreePath, configDirRelToGitRoot);
|
|
730
|
+
const defaultViteConfig = path.basename(defaultServer.viteProject.configFile ?? 'vite.config.ts');
|
|
731
|
+
const viteConfigPath = path.resolve(worktreeConfigDir, defaultViteConfig);
|
|
732
|
+
const viteProject = ViteProjectRef.fromViteConfigPath(viteConfigPath);
|
|
733
|
+
const currentSession = this._config.sessions.find(s => s.source.kind === 'current');
|
|
734
|
+
const resolveViteFrom = currentSession?.server.kind === 'vite' ? currentSession.server.viteProject.configFile : undefined;
|
|
735
|
+
this._logger.debug(`Worktree session "${sessionName}": resolveViteFrom=${resolveViteFrom}`);
|
|
736
|
+
const worktreeServerConfig = { kind: 'vite', viteProject, hmr: defaultServer.hmr };
|
|
737
|
+
const session = await ExplorerSession.create(sessionName, worktreeServerConfig, this._serverFactory, this._browserFactory, {
|
|
738
|
+
logger: this._logger,
|
|
739
|
+
resolveViteFrom,
|
|
740
|
+
daemonConfig: {
|
|
741
|
+
pipeName: this._pipeName,
|
|
742
|
+
sessionName,
|
|
743
|
+
daemonApiVersion: daemonApiVersionText,
|
|
744
|
+
pluginProtocolVersion: pluginProtocolVersionText,
|
|
745
|
+
},
|
|
746
|
+
});
|
|
747
|
+
this._sessions.set(sessionName, session);
|
|
748
|
+
this._logger.debug(`Session ready: ${sessionName} (${session.serverUrl})`);
|
|
749
|
+
}
|
|
750
|
+
async _setupSession(sessionConfig) {
|
|
539
751
|
this._logger.debug(`Setting up session: ${sessionConfig.name} (${sessionConfig.source.kind})`);
|
|
752
|
+
this._logger.log(`Starting server: ${sessionConfig.name}`);
|
|
540
753
|
if (sessionConfig.source.kind === 'worktree') {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
754
|
+
await this._setupWorktreeSession(sessionConfig, sessionConfig.source);
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
await this._createExplorerSession(sessionConfig);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async _setupWorktreeSession(sessionConfig, source) {
|
|
761
|
+
if (!this._worktreePool) {
|
|
762
|
+
throw new Error(`Session "${sessionConfig.name}" requires a worktree but no worktree pool is available`);
|
|
763
|
+
}
|
|
764
|
+
const slot = this._worktreePool.allocate(sessionConfig.name);
|
|
765
|
+
const git = this._config.repo;
|
|
766
|
+
const ref = source.ref;
|
|
767
|
+
const resolver = ref === GitIndexResolver.INDEX_REF
|
|
768
|
+
? await git.createIndexResolver()
|
|
769
|
+
: await git.createCommitResolver(ref);
|
|
770
|
+
this._resolvers.set(sessionConfig.name, resolver);
|
|
771
|
+
const resolvedCommit = resolver.resolvedCommit.get();
|
|
772
|
+
const wtInfo = await git.worktrees.info(slot.worktreePath);
|
|
773
|
+
if (!wtInfo) {
|
|
774
|
+
this._logger.log(`Creating worktree at ${slot.worktreePath} (${ref} @ ${resolvedCommit.toShort()})`);
|
|
775
|
+
await git.worktrees.create(slot.worktreePath, resolvedCommit);
|
|
776
|
+
}
|
|
777
|
+
else if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
|
|
778
|
+
if (wtInfo.isDirty) {
|
|
779
|
+
throw new Error(`Worktree slot ${slot.index} is dirty. Dirty files:\n` +
|
|
780
|
+
wtInfo.dirtyFiles.map(f => ` ${f}`).join('\n'));
|
|
781
|
+
}
|
|
782
|
+
this._logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
|
|
783
|
+
await git.worktrees.checkout(slot.worktreePath, resolvedCommit);
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
this._logger.log(`Worktree already at ${resolvedCommit.toShort()}`);
|
|
787
|
+
}
|
|
788
|
+
const installSetup = source.install ?? this._config.worktreePool?.setup ?? { kind: 'auto' };
|
|
789
|
+
await installDependencies(slot.worktreePath, installSetup, this._logger);
|
|
790
|
+
this._dynamicSessionMeta.set(sessionConfig.name, { ref, worktreePath: slot.worktreePath });
|
|
791
|
+
await this._createWorktreeExplorerSession(sessionConfig.name, slot.worktreePath);
|
|
792
|
+
this._addDynamicRefWatcher(sessionConfig.name, resolver);
|
|
793
|
+
}
|
|
794
|
+
// -- Dynamic session management ------------------------------------------
|
|
795
|
+
async _openDynamicSession(name, ref) {
|
|
796
|
+
if (this._sessions.has(name) || this._sessionConfigs.has(name) || this._dynamicSessionMeta.has(name)) {
|
|
797
|
+
return { error: `Session "${name}" already exists` };
|
|
798
|
+
}
|
|
799
|
+
if (!this._worktreePool) {
|
|
800
|
+
return { error: 'No worktree pool configured. Add a "worktree" section to your component-explorer.json config.' };
|
|
801
|
+
}
|
|
802
|
+
let slot;
|
|
803
|
+
try {
|
|
804
|
+
slot = this._worktreePool.allocate(name);
|
|
805
|
+
}
|
|
806
|
+
catch (e) {
|
|
807
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
808
|
+
}
|
|
809
|
+
const git = this._config.repo;
|
|
810
|
+
const isIndex = ref === GitIndexResolver.INDEX_REF;
|
|
811
|
+
try {
|
|
812
|
+
let resolver;
|
|
813
|
+
if (isIndex) {
|
|
814
|
+
resolver = await git.createIndexResolver();
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
resolver = await git.createCommitResolver(ref);
|
|
818
|
+
}
|
|
819
|
+
this._resolvers.set(name, resolver);
|
|
544
820
|
const resolvedCommit = resolver.resolvedCommit.get();
|
|
545
|
-
const wtInfo = await git.worktrees.info(
|
|
821
|
+
const wtInfo = await git.worktrees.info(slot.worktreePath);
|
|
546
822
|
if (!wtInfo) {
|
|
547
|
-
this._logger.log(`Creating worktree at ${
|
|
548
|
-
await git.worktrees.create(
|
|
549
|
-
await installDependencies(wt.worktreePath, wt.install, this._logger);
|
|
823
|
+
this._logger.log(`Creating worktree at ${slot.worktreePath} (${ref} @ ${resolvedCommit.toShort()})`);
|
|
824
|
+
await git.worktrees.create(slot.worktreePath, resolvedCommit);
|
|
550
825
|
}
|
|
551
826
|
else if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
|
|
552
827
|
if (wtInfo.isDirty) {
|
|
553
|
-
this.
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
828
|
+
this._worktreePool.release(name);
|
|
829
|
+
this._resolvers.get(name)?.dispose();
|
|
830
|
+
this._resolvers.delete(name);
|
|
831
|
+
return {
|
|
832
|
+
error: `Worktree slot ${slot.index} is dirty. Dirty files:\n` +
|
|
833
|
+
wtInfo.dirtyFiles.map(f => ` ${f}`).join('\n'),
|
|
834
|
+
};
|
|
559
835
|
}
|
|
836
|
+
this._logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
|
|
837
|
+
await git.worktrees.checkout(slot.worktreePath, resolvedCommit);
|
|
560
838
|
}
|
|
561
839
|
else {
|
|
562
840
|
this._logger.log(`Worktree already at ${resolvedCommit.toShort()}`);
|
|
563
841
|
}
|
|
842
|
+
const poolConfig = this._config.worktreePool;
|
|
843
|
+
await installDependencies(slot.worktreePath, poolConfig.setup, this._logger);
|
|
844
|
+
this._dynamicSessionMeta.set(name, { ref, worktreePath: slot.worktreePath });
|
|
845
|
+
await this._createWorktreeExplorerSession(name, slot.worktreePath);
|
|
846
|
+
this._addDynamicRefWatcher(name, resolver);
|
|
847
|
+
this._emit({ type: 'session-change' });
|
|
848
|
+
return { sessions: this.getSessionInfos() };
|
|
849
|
+
}
|
|
850
|
+
catch (e) {
|
|
851
|
+
this._worktreePool.release(name);
|
|
852
|
+
this._resolvers.get(name)?.dispose();
|
|
853
|
+
this._resolvers.delete(name);
|
|
854
|
+
this._dynamicSessionMeta.delete(name);
|
|
855
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
856
|
+
this._logger.log(`Failed to open session "${name}": ${msg}`);
|
|
857
|
+
return { error: `Failed to open session: ${msg}` };
|
|
564
858
|
}
|
|
565
|
-
this._logger.log(`Starting server: ${sessionConfig.name}`);
|
|
566
|
-
// Set daemon config env var so the Vite plugin enables the daemon proxy
|
|
567
|
-
process.env.COMPONENT_EXPLORER_DAEMON_CONFIG = JSON.stringify({
|
|
568
|
-
pipeName: this._pipeName,
|
|
569
|
-
sessionName: sessionConfig.name,
|
|
570
|
-
});
|
|
571
|
-
const session = await ExplorerSession.create(sessionConfig.name, sessionConfig.viteProject, this._serverFactory, this._browserFactory, {
|
|
572
|
-
logger: this._logger,
|
|
573
|
-
resolveViteFrom: sessionConfig.source.kind === 'worktree' ? resolveViteFrom : undefined,
|
|
574
|
-
hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
|
|
575
|
-
});
|
|
576
|
-
this._sessions.set(sessionConfig.name, session);
|
|
577
|
-
this._logger.debug(`Session ready: ${sessionConfig.name} (${session.serverUrl})`);
|
|
578
859
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
860
|
+
async _closeDynamicSession(name) {
|
|
861
|
+
if (!this._dynamicSessionMeta.has(name)) {
|
|
862
|
+
if (this._sessionConfigs.has(name)) {
|
|
863
|
+
return { error: `Session "${name}" is a static session and cannot be closed` };
|
|
864
|
+
}
|
|
865
|
+
return { error: `Session "${name}" does not exist` };
|
|
866
|
+
}
|
|
867
|
+
const session = this._sessions.get(name);
|
|
868
|
+
if (session) {
|
|
869
|
+
this._logger.debug(`Disposing session: ${name}`);
|
|
870
|
+
await session.dispose();
|
|
871
|
+
this._sessions.delete(name);
|
|
872
|
+
}
|
|
873
|
+
const resolver = this._resolvers.get(name);
|
|
874
|
+
if (resolver) {
|
|
875
|
+
resolver.dispose();
|
|
876
|
+
this._resolvers.delete(name);
|
|
877
|
+
}
|
|
878
|
+
// Remove any ref watcher disposable
|
|
879
|
+
const watcherDisposable = this._dynamicRefWatchers.get(name);
|
|
880
|
+
if (watcherDisposable) {
|
|
881
|
+
watcherDisposable.dispose();
|
|
882
|
+
this._dynamicRefWatchers.delete(name);
|
|
883
|
+
}
|
|
884
|
+
if (this._worktreePool) {
|
|
885
|
+
this._worktreePool.release(name);
|
|
886
|
+
}
|
|
887
|
+
this._dynamicSessionMeta.delete(name);
|
|
888
|
+
this._logger.log(`Session "${name}" closed`);
|
|
889
|
+
this._emit({ type: 'session-change' });
|
|
890
|
+
return { sessions: this.getSessionInfos() };
|
|
588
891
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
892
|
+
async _updateDynamicSessionRef(name, newRef) {
|
|
893
|
+
const meta = this._dynamicSessionMeta.get(name);
|
|
894
|
+
if (!meta) {
|
|
895
|
+
if (this._sessionConfigs.has(name)) {
|
|
896
|
+
return { error: `Session "${name}" is a static session — use restartSession instead` };
|
|
897
|
+
}
|
|
898
|
+
return { error: `Session "${name}" does not exist` };
|
|
899
|
+
}
|
|
900
|
+
const git = this._config.repo;
|
|
901
|
+
// Check dirty before doing anything
|
|
902
|
+
const wtInfo = await git.worktrees.info(meta.worktreePath);
|
|
903
|
+
if (wtInfo && wtInfo.isDirty) {
|
|
904
|
+
return {
|
|
905
|
+
error: `Worktree is dirty, cannot update ref. Dirty files:\n` +
|
|
906
|
+
wtInfo.dirtyFiles.map(f => ` ${f}`).join('\n'),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
// Dispose old resolver
|
|
910
|
+
const oldResolver = this._resolvers.get(name);
|
|
911
|
+
if (oldResolver) {
|
|
912
|
+
oldResolver.dispose();
|
|
913
|
+
this._resolvers.delete(name);
|
|
914
|
+
}
|
|
915
|
+
const oldWatcher = this._dynamicRefWatchers.get(name);
|
|
916
|
+
if (oldWatcher) {
|
|
917
|
+
oldWatcher.dispose();
|
|
918
|
+
this._dynamicRefWatchers.delete(name);
|
|
919
|
+
}
|
|
920
|
+
try {
|
|
921
|
+
const isIndex = newRef === GitIndexResolver.INDEX_REF;
|
|
922
|
+
let resolver;
|
|
923
|
+
if (isIndex) {
|
|
924
|
+
resolver = await git.createIndexResolver();
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
resolver = await git.createCommitResolver(newRef);
|
|
928
|
+
}
|
|
929
|
+
this._resolvers.set(name, resolver);
|
|
930
|
+
const resolvedCommit = resolver.resolvedCommit.get();
|
|
931
|
+
// Checkout in worktree — don't restart Vite, let HMR handle it
|
|
932
|
+
if (wtInfo) {
|
|
933
|
+
if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
|
|
934
|
+
await git.worktrees.checkout(meta.worktreePath, resolvedCommit);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
await git.worktrees.create(meta.worktreePath, resolvedCommit);
|
|
939
|
+
}
|
|
940
|
+
const sessionConfig = this._sessionConfigs.get(name);
|
|
941
|
+
const installSetup = (sessionConfig?.source.kind === 'worktree' ? sessionConfig.source.install : undefined)
|
|
942
|
+
?? this._config.worktreePool?.setup
|
|
943
|
+
?? { kind: 'auto' };
|
|
944
|
+
await installDependencies(meta.worktreePath, installSetup, this._logger);
|
|
945
|
+
meta.ref = newRef;
|
|
946
|
+
this._addDynamicRefWatcher(name, resolver);
|
|
947
|
+
return { sessions: this.getSessionInfos() };
|
|
948
|
+
}
|
|
949
|
+
catch (e) {
|
|
950
|
+
if (e instanceof DirtyWorktreeError) {
|
|
951
|
+
return {
|
|
952
|
+
error: `Worktree is dirty, cannot update ref. Dirty files:\n` +
|
|
953
|
+
e.dirtyFiles.map(f => ` ${f}`).join('\n'),
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
957
|
+
this._logger.log(`Failed to update session ref "${name}": ${msg}`);
|
|
958
|
+
return { error: `Failed to update session ref: ${msg}` };
|
|
959
|
+
}
|
|
593
960
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
961
|
+
_dynamicRefWatchers = new Map();
|
|
962
|
+
_addDynamicRefWatcher(sessionName, resolver) {
|
|
963
|
+
let previousCommit = resolver.resolvedCommit.get();
|
|
964
|
+
const disposable = autorun(reader => {
|
|
965
|
+
const commit = resolver.resolvedCommit.read(reader);
|
|
966
|
+
if (!previousCommit.equals(commit)) {
|
|
967
|
+
const prev = previousCommit;
|
|
968
|
+
previousCommit = commit;
|
|
969
|
+
this._handleRefChange(sessionName, resolver.ref, prev, commit);
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
this._dynamicRefWatchers.set(sessionName, disposable);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function arraysEqual(a, b) {
|
|
976
|
+
if (a.length !== b.length) {
|
|
977
|
+
return false;
|
|
597
978
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
979
|
+
for (let i = 0; i < a.length; i++) {
|
|
980
|
+
if (a[i] !== b[i]) {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
601
983
|
}
|
|
984
|
+
return true;
|
|
602
985
|
}
|
|
603
986
|
|
|
604
|
-
export { ActivityTracker, DaemonService, SourceTreeChangedError };
|
|
987
|
+
export { ActivityTracker, DaemonService, SourceTreeChangedError, daemonApiVersionText, pluginProtocolVersionText };
|
|
605
988
|
//# sourceMappingURL=DaemonService.js.map
|