@vscode/component-explorer-cli 0.1.1-9 → 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 +11 -2
- 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 +27 -3
- 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 +27 -15
- package/dist/componentExplorer.js.map +1 -1
- package/dist/daemon/DaemonService.d.ts +75 -7
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +462 -120
- 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.map +1 -1
- package/dist/daemon/lifecycle.js +11 -24
- 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/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 +13 -0
- 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 +13 -0
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +337 -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/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({
|
|
@@ -215,7 +229,10 @@ class DaemonService {
|
|
|
215
229
|
image: includeImages
|
|
216
230
|
? Buffer.from(result.image).toString('base64')
|
|
217
231
|
: undefined,
|
|
218
|
-
|
|
232
|
+
hasError: result.hasError,
|
|
233
|
+
error: result.error,
|
|
234
|
+
events: result.events.length > 0 ? result.events : undefined,
|
|
235
|
+
resultData: result.resultData,
|
|
219
236
|
});
|
|
220
237
|
}
|
|
221
238
|
this._logger.trace(`takeBatch: done in ${Date.now() - startTime}ms`);
|
|
@@ -236,13 +253,23 @@ class DaemonService {
|
|
|
236
253
|
const currentSession = this.getSession(args.currentSessionName);
|
|
237
254
|
this.assertSourceTreeId(args.baselineSessionName, args.baselineSourceTreeId);
|
|
238
255
|
this.assertSourceTreeId(args.currentSessionName, args.currentSourceTreeId);
|
|
239
|
-
const baselineResult = await
|
|
256
|
+
const [baselineResult, baselineFixtures] = await Promise.all([
|
|
257
|
+
baselineSession.explorer.screenshotFixture(args.fixtureId),
|
|
258
|
+
baselineSession.explorer.listFixtures(),
|
|
259
|
+
]);
|
|
240
260
|
this.assertSourceTreeId(args.baselineSessionName, args.baselineSourceTreeId);
|
|
241
|
-
const currentResult = await
|
|
261
|
+
const [currentResult, currentFixtures] = await Promise.all([
|
|
262
|
+
currentSession.explorer.screenshotFixture(args.fixtureId),
|
|
263
|
+
currentSession.explorer.listFixtures(),
|
|
264
|
+
]);
|
|
242
265
|
this.assertSourceTreeId(args.currentSessionName, args.currentSourceTreeId);
|
|
243
266
|
const baselineHash = contentHash(baselineResult.image);
|
|
244
267
|
const currentHash = contentHash(currentResult.image);
|
|
245
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 ?? []);
|
|
246
273
|
return {
|
|
247
274
|
match: baselineHash === currentHash,
|
|
248
275
|
baselineHash,
|
|
@@ -251,8 +278,16 @@ class DaemonService {
|
|
|
251
278
|
? Buffer.from(baselineResult.image).toString('base64') : undefined,
|
|
252
279
|
currentImage: includeImages
|
|
253
280
|
? Buffer.from(currentResult.image).toString('base64') : undefined,
|
|
254
|
-
|
|
255
|
-
|
|
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,
|
|
256
291
|
approval: (baselineHash !== currentHash)
|
|
257
292
|
? this.approvals.lookup({
|
|
258
293
|
fixtureId: args.fixtureId,
|
|
@@ -325,6 +360,48 @@ class DaemonService {
|
|
|
325
360
|
this._logger.trace(`API: sessions (client=${ctx.clientName}, eventListeners=${this._eventListeners.size})`);
|
|
326
361
|
return this.getSessionInfos();
|
|
327
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);
|
|
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
|
+
}),
|
|
328
405
|
shutdown: createMethod({ args: {} }, async (_args, ctx) => {
|
|
329
406
|
this._logger.debug(`Shutdown requested via API (client=${ctx.clientName})`);
|
|
330
407
|
this.requestShutdown();
|
|
@@ -340,18 +417,56 @@ class DaemonService {
|
|
|
340
417
|
return session;
|
|
341
418
|
}
|
|
342
419
|
getSessionInfos(reader) {
|
|
343
|
-
|
|
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);
|
|
344
425
|
const session = this._sessions.get(sc.name);
|
|
426
|
+
const meta = this._dynamicSessionMeta.get(sc.name);
|
|
345
427
|
if (!session) {
|
|
346
|
-
|
|
428
|
+
infos.push({ name: sc.name, sourceKind: sc.source.kind, isLoading: true });
|
|
347
429
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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;
|
|
355
470
|
}
|
|
356
471
|
waitForSession(sessionName) {
|
|
357
472
|
const sessionObs = derived(this, reader => {
|
|
@@ -394,8 +509,10 @@ class DaemonService {
|
|
|
394
509
|
self._eventListeners.add(listener);
|
|
395
510
|
self._eventListenerCount.set(self._eventListeners.size, undefined);
|
|
396
511
|
self._activityTracker.setActive(true);
|
|
512
|
+
self._logger.debug(`Event stream opened (listeners: ${self._eventListeners.size})`);
|
|
397
513
|
const onShutdown = () => {
|
|
398
514
|
done = true;
|
|
515
|
+
cleanup();
|
|
399
516
|
if (resolve) {
|
|
400
517
|
const r = resolve;
|
|
401
518
|
resolve = undefined;
|
|
@@ -407,6 +524,7 @@ class DaemonService {
|
|
|
407
524
|
self._eventListeners.delete(listener);
|
|
408
525
|
self._eventListenerCount.set(self._eventListeners.size, undefined);
|
|
409
526
|
self._activityTracker.setActive(false);
|
|
527
|
+
self._logger.debug(`Event stream closed (listeners: ${self._eventListeners.size})`);
|
|
410
528
|
};
|
|
411
529
|
return {
|
|
412
530
|
next() {
|
|
@@ -421,6 +539,11 @@ class DaemonService {
|
|
|
421
539
|
return() {
|
|
422
540
|
done = true;
|
|
423
541
|
cleanup();
|
|
542
|
+
if (resolve) {
|
|
543
|
+
const r = resolve;
|
|
544
|
+
resolve = undefined;
|
|
545
|
+
r({ value: undefined, done: true });
|
|
546
|
+
}
|
|
424
547
|
return Promise.resolve({ value: undefined, done: true });
|
|
425
548
|
},
|
|
426
549
|
};
|
|
@@ -442,68 +565,57 @@ class DaemonService {
|
|
|
442
565
|
_sourceChangeDisposables = [];
|
|
443
566
|
_startSourceChangeWatchers() {
|
|
444
567
|
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);
|
|
568
|
+
this._addSourceChangeWatcher(name, session);
|
|
467
569
|
}
|
|
468
570
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
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) {
|
|
474
588
|
return;
|
|
475
589
|
}
|
|
476
|
-
const
|
|
477
|
-
const wtInfo = await git.worktrees.info(
|
|
590
|
+
const git = this._config.repo;
|
|
591
|
+
const wtInfo = await git.worktrees.info(meta.worktreePath);
|
|
478
592
|
if (wtInfo && wtInfo.isDirty) {
|
|
479
593
|
this._logger.log(`Worktree is dirty, skipping update to ${newCommit.toShort()}`);
|
|
480
594
|
return;
|
|
481
595
|
}
|
|
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
596
|
if (wtInfo) {
|
|
490
|
-
await git.worktrees.checkout(
|
|
597
|
+
await git.worktrees.checkout(meta.worktreePath, newCommit);
|
|
491
598
|
}
|
|
492
599
|
else {
|
|
493
|
-
await git.worktrees.create(
|
|
600
|
+
await git.worktrees.create(meta.worktreePath, newCommit);
|
|
494
601
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
logger: this._logger,
|
|
501
|
-
resolveViteFrom,
|
|
502
|
-
hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
|
|
503
|
-
});
|
|
504
|
-
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);
|
|
505
607
|
this._emit({ type: 'ref-change', sessionName, newCommit: newCommit.toShort() });
|
|
506
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
|
+
}
|
|
507
619
|
// -- Shutdown ------------------------------------------------------------
|
|
508
620
|
requestShutdown() {
|
|
509
621
|
this._shutdownRequested = true;
|
|
@@ -519,6 +631,10 @@ class DaemonService {
|
|
|
519
631
|
d.dispose();
|
|
520
632
|
}
|
|
521
633
|
this._sourceChangeDisposables = [];
|
|
634
|
+
for (const d of this._dynamicRefWatchers.values()) {
|
|
635
|
+
d.dispose();
|
|
636
|
+
}
|
|
637
|
+
this._dynamicRefWatchers.clear();
|
|
522
638
|
for (const session of this._sessions.values()) {
|
|
523
639
|
await session.dispose();
|
|
524
640
|
}
|
|
@@ -529,77 +645,303 @@ class DaemonService {
|
|
|
529
645
|
await this._browserFactory.dispose();
|
|
530
646
|
}
|
|
531
647
|
// -- Private helpers -----------------------------------------------------
|
|
532
|
-
/** @internal — also called by EventStreamLogger */
|
|
533
648
|
_emit(event) {
|
|
534
649
|
for (const listener of this._eventListeners) {
|
|
535
650
|
listener(event);
|
|
536
651
|
}
|
|
537
652
|
}
|
|
538
|
-
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) {
|
|
539
710
|
this._logger.debug(`Setting up session: ${sessionConfig.name} (${sessionConfig.source.kind})`);
|
|
711
|
+
this._logger.log(`Starting server: ${sessionConfig.name}`);
|
|
540
712
|
if (sessionConfig.source.kind === 'worktree') {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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);
|
|
544
779
|
const resolvedCommit = resolver.resolvedCommit.get();
|
|
545
|
-
const wtInfo = await git.worktrees.info(
|
|
780
|
+
const wtInfo = await git.worktrees.info(slot.worktreePath);
|
|
546
781
|
if (!wtInfo) {
|
|
547
|
-
this._logger.log(`Creating worktree at ${
|
|
548
|
-
await git.worktrees.create(
|
|
549
|
-
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);
|
|
550
784
|
}
|
|
551
785
|
else if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
|
|
552
786
|
if (wtInfo.isDirty) {
|
|
553
|
-
this.
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
+
};
|
|
559
794
|
}
|
|
795
|
+
this._logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
|
|
796
|
+
await git.worktrees.checkout(slot.worktreePath, resolvedCommit);
|
|
560
797
|
}
|
|
561
798
|
else {
|
|
562
799
|
this._logger.log(`Worktree already at ${resolvedCommit.toShort()}`);
|
|
563
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}` };
|
|
564
817
|
}
|
|
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
818
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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() };
|
|
850
|
+
}
|
|
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
|
+
}
|
|
588
919
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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);
|
|
593
932
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
933
|
+
}
|
|
934
|
+
function arraysEqual(a, b) {
|
|
935
|
+
if (a.length !== b.length) {
|
|
936
|
+
return false;
|
|
597
937
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
938
|
+
for (let i = 0; i < a.length; i++) {
|
|
939
|
+
if (a[i] !== b[i]) {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
601
942
|
}
|
|
943
|
+
return true;
|
|
602
944
|
}
|
|
603
945
|
|
|
604
|
-
export { ActivityTracker, DaemonService, SourceTreeChangedError };
|
|
946
|
+
export { ActivityTracker, DaemonService, SourceTreeChangedError, daemonApiVersionText, pluginProtocolVersionText };
|
|
605
947
|
//# sourceMappingURL=DaemonService.js.map
|