@vscode/component-explorer-cli 0.1.1-8 → 0.2.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 +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.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 +13 -5
- 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 +15 -4
- package/dist/commands/screenshotCommand.js.map +1 -1
- package/dist/commands/serveCommand.d.ts +2 -0
- package/dist/commands/serveCommand.d.ts.map +1 -1
- package/dist/commands/serveCommand.js +36 -11
- 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 +10 -63
- 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 +97 -58
- package/dist/componentExplorer.d.ts +13 -17
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +49 -31
- 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 +92 -23
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +473 -118
- 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 +28 -24
- 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 +97 -5
- 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 +62 -3
- 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/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/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 +17 -3
- 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 +19 -5
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +447 -97
- 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/watchConfig.d.ts +19 -12
- package/dist/watchConfig.d.ts.map +1 -1
- package/dist/watchConfig.js +43 -48
- 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,18 @@ 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';
|
|
17
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';
|
|
18
26
|
|
|
19
27
|
// ---------------------------------------------------------------------------
|
|
20
28
|
// ActivityTracker — monitors idle time and triggers shutdown
|
|
@@ -90,12 +98,14 @@ class DaemonService {
|
|
|
90
98
|
_serverFactory;
|
|
91
99
|
_sessions = new Map();
|
|
92
100
|
_sessionConfigs = new Map();
|
|
101
|
+
_dynamicSessionMeta = new Map();
|
|
93
102
|
_resolvers = new Map();
|
|
94
103
|
_eventListenerCount = observableValue(this, 0);
|
|
95
104
|
_eventListeners = new Set();
|
|
96
105
|
_shutdownRequested = false;
|
|
97
106
|
_shutdownResolvers = [];
|
|
98
107
|
_activityTracker;
|
|
108
|
+
_worktreePool;
|
|
99
109
|
approvals;
|
|
100
110
|
api;
|
|
101
111
|
constructor(_config, _pipeName, _logger, _browserFactory, _serverFactory, approvalStorePath, idleTimeoutMs) {
|
|
@@ -106,22 +116,20 @@ class DaemonService {
|
|
|
106
116
|
this._serverFactory = _serverFactory;
|
|
107
117
|
this.approvals = new FileApprovalStore(approvalStorePath);
|
|
108
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
|
+
}
|
|
109
122
|
this.api = this._buildApi();
|
|
110
123
|
}
|
|
111
124
|
static async create(config, logger, pipeName, options) {
|
|
112
|
-
const browserFactory = new PlaywrightBrowserPageFactory();
|
|
125
|
+
const browserFactory = new PlaywrightBrowserPageFactory(false, 30_000, logger);
|
|
113
126
|
const serverFactory = new DefaultComponentExplorerHttpServerFactory();
|
|
114
127
|
const approvalStorePath = `${config.screenshotDir}/approvals.json`;
|
|
115
128
|
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;
|
|
129
|
+
const svc = new DaemonService(config, pipeName ?? '', logger, browserFactory, serverFactory, approvalStorePath, idleTimeoutMs);
|
|
122
130
|
for (const sessionConfig of config.sessions) {
|
|
123
131
|
svc._sessionConfigs.set(sessionConfig.name, sessionConfig);
|
|
124
|
-
await svc._setupSession(sessionConfig
|
|
132
|
+
await svc._setupSession(sessionConfig);
|
|
125
133
|
}
|
|
126
134
|
return svc;
|
|
127
135
|
}
|
|
@@ -175,7 +183,10 @@ class DaemonService {
|
|
|
175
183
|
image: (args.includeImage ?? true)
|
|
176
184
|
? Buffer.from(result.screenshots[result.screenshots.length - 1].image).toString('base64')
|
|
177
185
|
: undefined,
|
|
178
|
-
|
|
186
|
+
hasError: result.hasError,
|
|
187
|
+
error: result.error,
|
|
188
|
+
events: result.events.length > 0 ? result.events : undefined,
|
|
189
|
+
resultData: result.resultData,
|
|
179
190
|
isStable,
|
|
180
191
|
stabilityScreenshots,
|
|
181
192
|
};
|
|
@@ -188,7 +199,10 @@ class DaemonService {
|
|
|
188
199
|
image: (args.includeImage ?? true)
|
|
189
200
|
? Buffer.from(result.image).toString('base64')
|
|
190
201
|
: undefined,
|
|
191
|
-
|
|
202
|
+
hasError: result.hasError,
|
|
203
|
+
error: result.error,
|
|
204
|
+
events: result.events?.length > 0 ? result.events : undefined,
|
|
205
|
+
resultData: result.resultData,
|
|
192
206
|
};
|
|
193
207
|
}),
|
|
194
208
|
takeBatch: createMethod({
|
|
@@ -203,6 +217,8 @@ class DaemonService {
|
|
|
203
217
|
const session = this.getSession(args.sessionName);
|
|
204
218
|
this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
|
|
205
219
|
const includeImages = args.includeImages ?? false;
|
|
220
|
+
this._logger.trace(`takeBatch: ${args.fixtureIds.length} fixtures, session=${args.sessionName}`);
|
|
221
|
+
const startTime = Date.now();
|
|
206
222
|
const screenshots = [];
|
|
207
223
|
for (const fixtureId of args.fixtureIds) {
|
|
208
224
|
const result = await session.explorer.screenshotFixture(fixtureId);
|
|
@@ -213,9 +229,13 @@ class DaemonService {
|
|
|
213
229
|
image: includeImages
|
|
214
230
|
? Buffer.from(result.image).toString('base64')
|
|
215
231
|
: undefined,
|
|
216
|
-
|
|
232
|
+
hasError: result.hasError,
|
|
233
|
+
error: result.error,
|
|
234
|
+
events: result.events.length > 0 ? result.events : undefined,
|
|
235
|
+
resultData: result.resultData,
|
|
217
236
|
});
|
|
218
237
|
}
|
|
238
|
+
this._logger.trace(`takeBatch: done in ${Date.now() - startTime}ms`);
|
|
219
239
|
return { sourceTreeId: args.sourceTreeId, screenshots };
|
|
220
240
|
}),
|
|
221
241
|
compare: createMethod({
|
|
@@ -233,13 +253,23 @@ class DaemonService {
|
|
|
233
253
|
const currentSession = this.getSession(args.currentSessionName);
|
|
234
254
|
this.assertSourceTreeId(args.baselineSessionName, args.baselineSourceTreeId);
|
|
235
255
|
this.assertSourceTreeId(args.currentSessionName, args.currentSourceTreeId);
|
|
236
|
-
const baselineResult = await
|
|
256
|
+
const [baselineResult, baselineFixtures] = await Promise.all([
|
|
257
|
+
baselineSession.explorer.screenshotFixture(args.fixtureId),
|
|
258
|
+
baselineSession.explorer.listFixtures(),
|
|
259
|
+
]);
|
|
237
260
|
this.assertSourceTreeId(args.baselineSessionName, args.baselineSourceTreeId);
|
|
238
|
-
const currentResult = await
|
|
261
|
+
const [currentResult, currentFixtures] = await Promise.all([
|
|
262
|
+
currentSession.explorer.screenshotFixture(args.fixtureId),
|
|
263
|
+
currentSession.explorer.listFixtures(),
|
|
264
|
+
]);
|
|
239
265
|
this.assertSourceTreeId(args.currentSessionName, args.currentSourceTreeId);
|
|
240
266
|
const baselineHash = contentHash(baselineResult.image);
|
|
241
267
|
const currentHash = contentHash(currentResult.image);
|
|
242
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 ?? []);
|
|
243
273
|
return {
|
|
244
274
|
match: baselineHash === currentHash,
|
|
245
275
|
baselineHash,
|
|
@@ -248,8 +278,16 @@ class DaemonService {
|
|
|
248
278
|
? Buffer.from(baselineResult.image).toString('base64') : undefined,
|
|
249
279
|
currentImage: includeImages
|
|
250
280
|
? Buffer.from(currentResult.image).toString('base64') : undefined,
|
|
251
|
-
|
|
252
|
-
|
|
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,
|
|
253
291
|
approval: (baselineHash !== currentHash)
|
|
254
292
|
? this.approvals.lookup({
|
|
255
293
|
fixtureId: args.fixtureId,
|
|
@@ -317,11 +355,55 @@ class DaemonService {
|
|
|
317
355
|
events: createMethod({ args: {} }, async () => {
|
|
318
356
|
return AsyncStream.fromIterable(this.eventStream());
|
|
319
357
|
}),
|
|
320
|
-
sessions: createMethod({ args: {} }, async () => {
|
|
358
|
+
sessions: createMethod({ args: {} }, async (_args, ctx) => {
|
|
321
359
|
this._activityTracker.reportActivity();
|
|
360
|
+
this._logger.trace(`API: sessions (client=${ctx.clientName}, eventListeners=${this._eventListeners.size})`);
|
|
322
361
|
return this.getSessionInfos();
|
|
323
362
|
}),
|
|
324
|
-
|
|
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);
|
|
376
|
+
return this.getSessionInfos();
|
|
377
|
+
}),
|
|
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})`);
|
|
325
407
|
this.requestShutdown();
|
|
326
408
|
}),
|
|
327
409
|
});
|
|
@@ -335,18 +417,56 @@ class DaemonService {
|
|
|
335
417
|
return session;
|
|
336
418
|
}
|
|
337
419
|
getSessionInfos(reader) {
|
|
338
|
-
|
|
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);
|
|
339
425
|
const session = this._sessions.get(sc.name);
|
|
426
|
+
const meta = this._dynamicSessionMeta.get(sc.name);
|
|
340
427
|
if (!session) {
|
|
341
|
-
|
|
428
|
+
infos.push({ name: sc.name, sourceKind: sc.source.kind, isLoading: true });
|
|
342
429
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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;
|
|
350
470
|
}
|
|
351
471
|
waitForSession(sessionName) {
|
|
352
472
|
const sessionObs = derived(this, reader => {
|
|
@@ -374,6 +494,9 @@ class DaemonService {
|
|
|
374
494
|
let resolve;
|
|
375
495
|
let done = false;
|
|
376
496
|
const listener = (event) => {
|
|
497
|
+
if (event.type !== 'log') {
|
|
498
|
+
self._logger.trace(`Event stream: ${event.type}`);
|
|
499
|
+
}
|
|
377
500
|
if (resolve) {
|
|
378
501
|
const r = resolve;
|
|
379
502
|
resolve = undefined;
|
|
@@ -386,8 +509,10 @@ class DaemonService {
|
|
|
386
509
|
self._eventListeners.add(listener);
|
|
387
510
|
self._eventListenerCount.set(self._eventListeners.size, undefined);
|
|
388
511
|
self._activityTracker.setActive(true);
|
|
512
|
+
self._logger.debug(`Event stream opened (listeners: ${self._eventListeners.size})`);
|
|
389
513
|
const onShutdown = () => {
|
|
390
514
|
done = true;
|
|
515
|
+
cleanup();
|
|
391
516
|
if (resolve) {
|
|
392
517
|
const r = resolve;
|
|
393
518
|
resolve = undefined;
|
|
@@ -399,6 +524,7 @@ class DaemonService {
|
|
|
399
524
|
self._eventListeners.delete(listener);
|
|
400
525
|
self._eventListenerCount.set(self._eventListeners.size, undefined);
|
|
401
526
|
self._activityTracker.setActive(false);
|
|
527
|
+
self._logger.debug(`Event stream closed (listeners: ${self._eventListeners.size})`);
|
|
402
528
|
};
|
|
403
529
|
return {
|
|
404
530
|
next() {
|
|
@@ -413,6 +539,11 @@ class DaemonService {
|
|
|
413
539
|
return() {
|
|
414
540
|
done = true;
|
|
415
541
|
cleanup();
|
|
542
|
+
if (resolve) {
|
|
543
|
+
const r = resolve;
|
|
544
|
+
resolve = undefined;
|
|
545
|
+
r({ value: undefined, done: true });
|
|
546
|
+
}
|
|
416
547
|
return Promise.resolve({ value: undefined, done: true });
|
|
417
548
|
},
|
|
418
549
|
};
|
|
@@ -434,64 +565,57 @@ class DaemonService {
|
|
|
434
565
|
_sourceChangeDisposables = [];
|
|
435
566
|
_startSourceChangeWatchers() {
|
|
436
567
|
for (const [name, session] of this._sessions) {
|
|
437
|
-
|
|
438
|
-
const disposable = autorun(reader => {
|
|
439
|
-
const current = session.sourceTreeId.read(reader);
|
|
440
|
-
if (current.value !== previousValue) {
|
|
441
|
-
previousValue = current.value;
|
|
442
|
-
this._emit({ type: 'source-change', sessionName: name, sourceTreeId: current.value });
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
this._sourceChangeDisposables.push(disposable);
|
|
446
|
-
}
|
|
447
|
-
// Watch for ref changes (worktree sessions)
|
|
448
|
-
for (const [name, resolver] of this._resolvers) {
|
|
449
|
-
let previousCommit = resolver.resolvedCommit.get();
|
|
450
|
-
const disposable = autorun(reader => {
|
|
451
|
-
const commit = resolver.resolvedCommit.read(reader);
|
|
452
|
-
if (!previousCommit.equals(commit)) {
|
|
453
|
-
previousCommit = commit;
|
|
454
|
-
this._handleRefChange(name, resolver.ref, commit);
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
this._sourceChangeDisposables.push(disposable);
|
|
568
|
+
this._addSourceChangeWatcher(name, session);
|
|
458
569
|
}
|
|
459
570
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
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) {
|
|
465
588
|
return;
|
|
466
589
|
}
|
|
467
|
-
const
|
|
468
|
-
const wtInfo = await git.worktrees.info(
|
|
590
|
+
const git = this._config.repo;
|
|
591
|
+
const wtInfo = await git.worktrees.info(meta.worktreePath);
|
|
469
592
|
if (wtInfo && wtInfo.isDirty) {
|
|
470
593
|
this._logger.log(`Worktree is dirty, skipping update to ${newCommit.toShort()}`);
|
|
471
594
|
return;
|
|
472
595
|
}
|
|
473
|
-
// Dispose old session
|
|
474
|
-
const oldSession = this._sessions.get(sessionName);
|
|
475
|
-
await oldSession?.dispose();
|
|
476
|
-
// Checkout + reinstall
|
|
477
596
|
if (wtInfo) {
|
|
478
|
-
await git.worktrees.checkout(
|
|
597
|
+
await git.worktrees.checkout(meta.worktreePath, newCommit);
|
|
479
598
|
}
|
|
480
599
|
else {
|
|
481
|
-
await git.worktrees.create(
|
|
600
|
+
await git.worktrees.create(meta.worktreePath, newCommit);
|
|
482
601
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
logger: this._logger,
|
|
489
|
-
resolveViteFrom,
|
|
490
|
-
hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
|
|
491
|
-
});
|
|
492
|
-
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);
|
|
493
607
|
this._emit({ type: 'ref-change', sessionName, newCommit: newCommit.toShort() });
|
|
494
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
|
+
}
|
|
495
619
|
// -- Shutdown ------------------------------------------------------------
|
|
496
620
|
requestShutdown() {
|
|
497
621
|
this._shutdownRequested = true;
|
|
@@ -507,6 +631,10 @@ class DaemonService {
|
|
|
507
631
|
d.dispose();
|
|
508
632
|
}
|
|
509
633
|
this._sourceChangeDisposables = [];
|
|
634
|
+
for (const d of this._dynamicRefWatchers.values()) {
|
|
635
|
+
d.dispose();
|
|
636
|
+
}
|
|
637
|
+
this._dynamicRefWatchers.clear();
|
|
510
638
|
for (const session of this._sessions.values()) {
|
|
511
639
|
await session.dispose();
|
|
512
640
|
}
|
|
@@ -517,76 +645,303 @@ class DaemonService {
|
|
|
517
645
|
await this._browserFactory.dispose();
|
|
518
646
|
}
|
|
519
647
|
// -- Private helpers -----------------------------------------------------
|
|
520
|
-
/** @internal — also called by EventStreamLogger */
|
|
521
648
|
_emit(event) {
|
|
522
649
|
for (const listener of this._eventListeners) {
|
|
523
650
|
listener(event);
|
|
524
651
|
}
|
|
525
652
|
}
|
|
526
|
-
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}`);
|
|
527
712
|
if (sessionConfig.source.kind === 'worktree') {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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);
|
|
531
779
|
const resolvedCommit = resolver.resolvedCommit.get();
|
|
532
|
-
const wtInfo = await git.worktrees.info(
|
|
780
|
+
const wtInfo = await git.worktrees.info(slot.worktreePath);
|
|
533
781
|
if (!wtInfo) {
|
|
534
|
-
this._logger.log(`Creating worktree at ${
|
|
535
|
-
await git.worktrees.create(
|
|
536
|
-
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);
|
|
537
784
|
}
|
|
538
785
|
else if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
|
|
539
786
|
if (wtInfo.isDirty) {
|
|
540
|
-
this.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
+
};
|
|
546
794
|
}
|
|
795
|
+
this._logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
|
|
796
|
+
await git.worktrees.checkout(slot.worktreePath, resolvedCommit);
|
|
547
797
|
}
|
|
548
798
|
else {
|
|
549
799
|
this._logger.log(`Worktree already at ${resolvedCommit.toShort()}`);
|
|
550
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}` };
|
|
551
817
|
}
|
|
552
|
-
this._logger.log(`Starting server: ${sessionConfig.name}`);
|
|
553
|
-
// Set daemon config env var so the Vite plugin enables the daemon proxy
|
|
554
|
-
process.env.COMPONENT_EXPLORER_DAEMON_CONFIG = JSON.stringify({
|
|
555
|
-
pipeName: this._pipeName,
|
|
556
|
-
sessionName: sessionConfig.name,
|
|
557
|
-
});
|
|
558
|
-
const session = await ExplorerSession.create(sessionConfig.name, sessionConfig.viteProject, this._serverFactory, this._browserFactory, {
|
|
559
|
-
logger: this._logger,
|
|
560
|
-
resolveViteFrom: sessionConfig.source.kind === 'worktree' ? resolveViteFrom : undefined,
|
|
561
|
-
hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
|
|
562
|
-
});
|
|
563
|
-
this._sessions.set(sessionConfig.name, session);
|
|
564
|
-
this._logger.log(`Server ready: ${sessionConfig.name} (${session.serverUrl})`);
|
|
565
818
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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() };
|
|
575
850
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
+
}
|
|
919
|
+
}
|
|
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);
|
|
580
932
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
933
|
+
}
|
|
934
|
+
function arraysEqual(a, b) {
|
|
935
|
+
if (a.length !== b.length) {
|
|
936
|
+
return false;
|
|
584
937
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
938
|
+
for (let i = 0; i < a.length; i++) {
|
|
939
|
+
if (a[i] !== b[i]) {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
588
942
|
}
|
|
943
|
+
return true;
|
|
589
944
|
}
|
|
590
945
|
|
|
591
|
-
export { ActivityTracker, DaemonService, SourceTreeChangedError };
|
|
946
|
+
export { ActivityTracker, DaemonService, SourceTreeChangedError, daemonApiVersionText, pluginProtocolVersionText };
|
|
592
947
|
//# sourceMappingURL=DaemonService.js.map
|