@vscode/component-explorer-cli 0.1.1-17 → 0.1.1-19
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/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/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.map +1 -1
- package/dist/commands/checkStabilityCommand.js +2 -1
- package/dist/commands/checkStabilityCommand.js.map +1 -1
- package/dist/commands/compareCommand.d.ts.map +1 -1
- package/dist/commands/compareCommand.js +3 -2
- 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 +2 -0
- package/dist/commands/mcpCommand.js.map +1 -1
- package/dist/commands/screenshotCommand.d.ts.map +1 -1
- package/dist/commands/screenshotCommand.js +2 -1
- package/dist/commands/screenshotCommand.js.map +1 -1
- package/dist/commands/serveCommand.d.ts.map +1 -1
- package/dist/commands/serveCommand.js +2 -1
- package/dist/commands/serveCommand.js.map +1 -1
- package/dist/commands/watchCommand.d.ts.map +1 -1
- package/dist/commands/watchCommand.js +6 -61
- package/dist/commands/watchCommand.js.map +1 -1
- package/dist/component-explorer-config.schema.json +97 -58
- package/dist/componentExplorer.d.ts +12 -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 +72 -5
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +430 -92
- 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/pipeClient.d.ts.map +1 -1
- package/dist/daemon/pipeClient.js +71 -2
- package/dist/daemon/pipeClient.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 +9 -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 +8 -0
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +177 -26
- package/dist/mcp/McpServer.js.map +1 -1
- 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 +1 -1
- 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 +6 -2
- 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,6 +116,9 @@ 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) {
|
|
@@ -114,11 +127,9 @@ class DaemonService {
|
|
|
114
127
|
const approvalStorePath = `${config.screenshotDir}/approvals.json`;
|
|
115
128
|
const idleTimeoutMs = options?.idleTimeoutMs ?? 5 * 60 * 1000; // 5 minutes default
|
|
116
129
|
const svc = new DaemonService(config, pipeName ?? '', logger, browserFactory, serverFactory, approvalStorePath, idleTimeoutMs);
|
|
117
|
-
const currentSession = config.sessions.find(s => s.source.kind === 'current');
|
|
118
|
-
const resolveViteFrom = currentSession?.viteProject.cwd;
|
|
119
130
|
for (const sessionConfig of config.sessions) {
|
|
120
131
|
svc._sessionConfigs.set(sessionConfig.name, sessionConfig);
|
|
121
|
-
await svc._setupSession(sessionConfig
|
|
132
|
+
await svc._setupSession(sessionConfig);
|
|
122
133
|
}
|
|
123
134
|
return svc;
|
|
124
135
|
}
|
|
@@ -172,7 +183,10 @@ class DaemonService {
|
|
|
172
183
|
image: (args.includeImage ?? true)
|
|
173
184
|
? Buffer.from(result.screenshots[result.screenshots.length - 1].image).toString('base64')
|
|
174
185
|
: undefined,
|
|
175
|
-
|
|
186
|
+
hasError: result.hasError,
|
|
187
|
+
error: result.error,
|
|
188
|
+
events: result.events.length > 0 ? result.events : undefined,
|
|
189
|
+
resultData: result.resultData,
|
|
176
190
|
isStable,
|
|
177
191
|
stabilityScreenshots,
|
|
178
192
|
};
|
|
@@ -185,7 +199,10 @@ class DaemonService {
|
|
|
185
199
|
image: (args.includeImage ?? true)
|
|
186
200
|
? Buffer.from(result.image).toString('base64')
|
|
187
201
|
: undefined,
|
|
188
|
-
|
|
202
|
+
hasError: result.hasError,
|
|
203
|
+
error: result.error,
|
|
204
|
+
events: result.events?.length > 0 ? result.events : undefined,
|
|
205
|
+
resultData: result.resultData,
|
|
189
206
|
};
|
|
190
207
|
}),
|
|
191
208
|
takeBatch: createMethod({
|
|
@@ -212,7 +229,10 @@ class DaemonService {
|
|
|
212
229
|
image: includeImages
|
|
213
230
|
? Buffer.from(result.image).toString('base64')
|
|
214
231
|
: undefined,
|
|
215
|
-
|
|
232
|
+
hasError: result.hasError,
|
|
233
|
+
error: result.error,
|
|
234
|
+
events: result.events.length > 0 ? result.events : undefined,
|
|
235
|
+
resultData: result.resultData,
|
|
216
236
|
});
|
|
217
237
|
}
|
|
218
238
|
this._logger.trace(`takeBatch: done in ${Date.now() - startTime}ms`);
|
|
@@ -248,8 +268,14 @@ class DaemonService {
|
|
|
248
268
|
? Buffer.from(baselineResult.image).toString('base64') : undefined,
|
|
249
269
|
currentImage: includeImages
|
|
250
270
|
? Buffer.from(currentResult.image).toString('base64') : undefined,
|
|
251
|
-
|
|
252
|
-
|
|
271
|
+
baselineHasError: baselineResult.hasError,
|
|
272
|
+
baselineError: baselineResult.error,
|
|
273
|
+
baselineEvents: baselineResult.events.length > 0 ? baselineResult.events : undefined,
|
|
274
|
+
baselineResultData: baselineResult.resultData,
|
|
275
|
+
currentHasError: currentResult.hasError,
|
|
276
|
+
currentError: currentResult.error,
|
|
277
|
+
currentEvents: currentResult.events.length > 0 ? currentResult.events : undefined,
|
|
278
|
+
currentResultData: currentResult.resultData,
|
|
253
279
|
approval: (baselineHash !== currentHash)
|
|
254
280
|
? this.approvals.lookup({
|
|
255
281
|
fixtureId: args.fixtureId,
|
|
@@ -322,6 +348,48 @@ class DaemonService {
|
|
|
322
348
|
this._logger.trace(`API: sessions (client=${ctx.clientName}, eventListeners=${this._eventListeners.size})`);
|
|
323
349
|
return this.getSessionInfos();
|
|
324
350
|
}),
|
|
351
|
+
version: createMethod({ args: {} }, async () => {
|
|
352
|
+
this._activityTracker.reportActivity();
|
|
353
|
+
return {
|
|
354
|
+
daemonApiVersion: daemonApiVersionText,
|
|
355
|
+
pluginProtocolVersion: pluginProtocolVersionText,
|
|
356
|
+
};
|
|
357
|
+
}),
|
|
358
|
+
restartSession: createMethod({
|
|
359
|
+
args: { sessionName: z.string() },
|
|
360
|
+
}, async (args, ctx) => {
|
|
361
|
+
this._activityTracker.reportActivity();
|
|
362
|
+
this._logger.debug(`Restart session "${args.sessionName}" requested (client=${ctx.clientName})`);
|
|
363
|
+
await this._restartSession(args.sessionName);
|
|
364
|
+
return this.getSessionInfos();
|
|
365
|
+
}),
|
|
366
|
+
openSession: createMethod({
|
|
367
|
+
args: {
|
|
368
|
+
name: z.string(),
|
|
369
|
+
ref: z.string(),
|
|
370
|
+
},
|
|
371
|
+
}, async (args, ctx) => {
|
|
372
|
+
this._activityTracker.reportActivity();
|
|
373
|
+
this._logger.log(`Open session "${args.name}" @ ${args.ref} requested (client=${ctx.clientName})`);
|
|
374
|
+
return this._openDynamicSession(args.name, args.ref);
|
|
375
|
+
}),
|
|
376
|
+
closeSession: createMethod({
|
|
377
|
+
args: { name: z.string() },
|
|
378
|
+
}, async (args, ctx) => {
|
|
379
|
+
this._activityTracker.reportActivity();
|
|
380
|
+
this._logger.log(`Close session "${args.name}" requested (client=${ctx.clientName})`);
|
|
381
|
+
return this._closeDynamicSession(args.name);
|
|
382
|
+
}),
|
|
383
|
+
updateSessionRef: createMethod({
|
|
384
|
+
args: {
|
|
385
|
+
name: z.string(),
|
|
386
|
+
ref: z.string(),
|
|
387
|
+
},
|
|
388
|
+
}, async (args, ctx) => {
|
|
389
|
+
this._activityTracker.reportActivity();
|
|
390
|
+
this._logger.log(`Update session ref "${args.name}" → ${args.ref} requested (client=${ctx.clientName})`);
|
|
391
|
+
return this._updateDynamicSessionRef(args.name, args.ref);
|
|
392
|
+
}),
|
|
325
393
|
shutdown: createMethod({ args: {} }, async (_args, ctx) => {
|
|
326
394
|
this._logger.debug(`Shutdown requested via API (client=${ctx.clientName})`);
|
|
327
395
|
this.requestShutdown();
|
|
@@ -337,18 +405,56 @@ class DaemonService {
|
|
|
337
405
|
return session;
|
|
338
406
|
}
|
|
339
407
|
getSessionInfos(reader) {
|
|
340
|
-
|
|
408
|
+
const infos = [];
|
|
409
|
+
const reportedNames = new Set();
|
|
410
|
+
// Static sessions (from config)
|
|
411
|
+
for (const sc of this._config.sessions) {
|
|
412
|
+
reportedNames.add(sc.name);
|
|
341
413
|
const session = this._sessions.get(sc.name);
|
|
414
|
+
const meta = this._dynamicSessionMeta.get(sc.name);
|
|
342
415
|
if (!session) {
|
|
343
|
-
|
|
416
|
+
infos.push({ name: sc.name, sourceKind: sc.source.kind, isLoading: true });
|
|
344
417
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
418
|
+
else if (meta) {
|
|
419
|
+
infos.push({
|
|
420
|
+
name: sc.name,
|
|
421
|
+
sourceKind: 'worktree',
|
|
422
|
+
serverUrl: session.serverUrl,
|
|
423
|
+
sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
|
|
424
|
+
worktreePath: meta.worktreePath,
|
|
425
|
+
ref: meta.ref,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
infos.push({
|
|
430
|
+
name: sc.name,
|
|
431
|
+
sourceKind: sc.source.kind,
|
|
432
|
+
serverUrl: session.serverUrl,
|
|
433
|
+
sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Dynamic worktree sessions (not already reported as static)
|
|
438
|
+
for (const [name, meta] of this._dynamicSessionMeta) {
|
|
439
|
+
if (reportedNames.has(name)) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const session = this._sessions.get(name);
|
|
443
|
+
if (!session) {
|
|
444
|
+
infos.push({ name, sourceKind: 'worktree', isLoading: true });
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
infos.push({
|
|
448
|
+
name,
|
|
449
|
+
sourceKind: 'worktree',
|
|
450
|
+
serverUrl: session.serverUrl,
|
|
451
|
+
sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
|
|
452
|
+
worktreePath: meta.worktreePath,
|
|
453
|
+
ref: meta.ref,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return infos;
|
|
352
458
|
}
|
|
353
459
|
waitForSession(sessionName) {
|
|
354
460
|
const sessionObs = derived(this, reader => {
|
|
@@ -447,68 +553,57 @@ class DaemonService {
|
|
|
447
553
|
_sourceChangeDisposables = [];
|
|
448
554
|
_startSourceChangeWatchers() {
|
|
449
555
|
for (const [name, session] of this._sessions) {
|
|
450
|
-
|
|
451
|
-
const disposable = autorun(reader => {
|
|
452
|
-
const current = session.sourceTreeId.read(reader);
|
|
453
|
-
if (current.value !== previousValue) {
|
|
454
|
-
this._logger.debug(`Source tree changed: ${name} ${previousValue} → ${current.value}`);
|
|
455
|
-
previousValue = current.value;
|
|
456
|
-
this._emit({ type: 'source-change', sessionName: name, sourceTreeId: current.value });
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
this._sourceChangeDisposables.push(disposable);
|
|
460
|
-
}
|
|
461
|
-
// Watch for ref changes (worktree sessions)
|
|
462
|
-
for (const [name, resolver] of this._resolvers) {
|
|
463
|
-
let previousCommit = resolver.resolvedCommit.get();
|
|
464
|
-
const disposable = autorun(reader => {
|
|
465
|
-
const commit = resolver.resolvedCommit.read(reader);
|
|
466
|
-
if (!previousCommit.equals(commit)) {
|
|
467
|
-
previousCommit = commit;
|
|
468
|
-
this._handleRefChange(name, resolver.ref, commit);
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
this._sourceChangeDisposables.push(disposable);
|
|
556
|
+
this._addSourceChangeWatcher(name, session);
|
|
472
557
|
}
|
|
473
558
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
559
|
+
_addSourceChangeWatcher(name, session) {
|
|
560
|
+
let previousValue = session.sourceTreeId.get().value;
|
|
561
|
+
const disposable = autorun(reader => {
|
|
562
|
+
const current = session.sourceTreeId.read(reader);
|
|
563
|
+
if (current.value !== previousValue) {
|
|
564
|
+
this._logger.debug(`Source tree changed: ${name} ${previousValue} → ${current.value}`);
|
|
565
|
+
previousValue = current.value;
|
|
566
|
+
this._emit({ type: 'source-change', sessionName: name, sourceTreeId: current.value });
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
this._sourceChangeDisposables.push(disposable);
|
|
570
|
+
}
|
|
571
|
+
async _handleRefChange(sessionName, ref, previousCommit, newCommit) {
|
|
572
|
+
const changedFiles = await this._getChangedFiles(previousCommit, newCommit);
|
|
573
|
+
this._logger.log(`Ref ${ref} moved to ${newCommit.toShort()} (${changedFiles.length} file(s) changed${changedFiles.length > 0 ? ': ' + changedFiles.join(', ') : ''})`);
|
|
574
|
+
const meta = this._dynamicSessionMeta.get(sessionName);
|
|
575
|
+
if (!meta) {
|
|
479
576
|
return;
|
|
480
577
|
}
|
|
481
|
-
const
|
|
482
|
-
const wtInfo = await git.worktrees.info(
|
|
578
|
+
const git = this._config.repo;
|
|
579
|
+
const wtInfo = await git.worktrees.info(meta.worktreePath);
|
|
483
580
|
if (wtInfo && wtInfo.isDirty) {
|
|
484
581
|
this._logger.log(`Worktree is dirty, skipping update to ${newCommit.toShort()}`);
|
|
485
582
|
return;
|
|
486
583
|
}
|
|
487
|
-
// Dispose old session
|
|
488
|
-
const oldSession = this._sessions.get(sessionName);
|
|
489
|
-
if (oldSession) {
|
|
490
|
-
this._logger.debug(`Disposing session: ${sessionName}`);
|
|
491
|
-
await oldSession.dispose();
|
|
492
|
-
}
|
|
493
|
-
// Checkout + reinstall
|
|
494
584
|
if (wtInfo) {
|
|
495
|
-
await git.worktrees.checkout(
|
|
585
|
+
await git.worktrees.checkout(meta.worktreePath, newCommit);
|
|
496
586
|
}
|
|
497
587
|
else {
|
|
498
|
-
await git.worktrees.create(
|
|
588
|
+
await git.worktrees.create(meta.worktreePath, newCommit);
|
|
499
589
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
logger: this._logger,
|
|
506
|
-
resolveViteFrom,
|
|
507
|
-
hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
|
|
508
|
-
});
|
|
509
|
-
this._sessions.set(sessionName, newSession);
|
|
590
|
+
const sessionConfig = this._sessionConfigs.get(sessionName);
|
|
591
|
+
const installSetup = (sessionConfig?.source.kind === 'worktree' ? sessionConfig.source.install : undefined)
|
|
592
|
+
?? this._config.worktreePool?.setup
|
|
593
|
+
?? { kind: 'auto' };
|
|
594
|
+
await installDependencies(meta.worktreePath, installSetup, this._logger);
|
|
510
595
|
this._emit({ type: 'ref-change', sessionName, newCommit: newCommit.toShort() });
|
|
511
596
|
}
|
|
597
|
+
async _getChangedFiles(oldCommit, newCommit) {
|
|
598
|
+
try {
|
|
599
|
+
const output = await execGit(this._config.repo.gitRoot, ['diff', '--name-only', oldCommit.hash, newCommit.hash]);
|
|
600
|
+
return output.trim().split('\n').filter(f => f.length > 0);
|
|
601
|
+
}
|
|
602
|
+
catch (e) {
|
|
603
|
+
this._logger.log(`Failed to get changed files (${oldCommit.toShort()}..${newCommit.toShort()}): ${e instanceof Error ? e.message : e}`);
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
}
|
|
512
607
|
// -- Shutdown ------------------------------------------------------------
|
|
513
608
|
requestShutdown() {
|
|
514
609
|
this._shutdownRequested = true;
|
|
@@ -524,6 +619,10 @@ class DaemonService {
|
|
|
524
619
|
d.dispose();
|
|
525
620
|
}
|
|
526
621
|
this._sourceChangeDisposables = [];
|
|
622
|
+
for (const d of this._dynamicRefWatchers.values()) {
|
|
623
|
+
d.dispose();
|
|
624
|
+
}
|
|
625
|
+
this._dynamicRefWatchers.clear();
|
|
527
626
|
for (const session of this._sessions.values()) {
|
|
528
627
|
await session.dispose();
|
|
529
628
|
}
|
|
@@ -539,48 +638,287 @@ class DaemonService {
|
|
|
539
638
|
listener(event);
|
|
540
639
|
}
|
|
541
640
|
}
|
|
542
|
-
async
|
|
641
|
+
async _restartSession(sessionName) {
|
|
642
|
+
const config = this._sessionConfigs.get(sessionName);
|
|
643
|
+
const meta = this._dynamicSessionMeta.get(sessionName);
|
|
644
|
+
if (!config && !meta) {
|
|
645
|
+
throw new Error(`Unknown session: "${sessionName}"`);
|
|
646
|
+
}
|
|
647
|
+
const existing = this._sessions.get(sessionName);
|
|
648
|
+
if (existing) {
|
|
649
|
+
this._logger.debug(`Disposing session: ${sessionName}`);
|
|
650
|
+
await existing.dispose();
|
|
651
|
+
this._sessions.delete(sessionName);
|
|
652
|
+
}
|
|
653
|
+
this._logger.log(`Restarting server: ${sessionName}`);
|
|
654
|
+
if (meta) {
|
|
655
|
+
await this._createWorktreeExplorerSession(sessionName, meta.worktreePath);
|
|
656
|
+
}
|
|
657
|
+
else if (config) {
|
|
658
|
+
await this._createExplorerSession(config);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
async _createExplorerSession(sessionConfig) {
|
|
662
|
+
const session = await ExplorerSession.create(sessionConfig.name, sessionConfig.viteProject, this._serverFactory, this._browserFactory, {
|
|
663
|
+
logger: this._logger,
|
|
664
|
+
hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
|
|
665
|
+
daemonConfig: {
|
|
666
|
+
pipeName: this._pipeName,
|
|
667
|
+
sessionName: sessionConfig.name,
|
|
668
|
+
daemonApiVersion: daemonApiVersionText,
|
|
669
|
+
pluginProtocolVersion: pluginProtocolVersionText,
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
this._sessions.set(sessionConfig.name, session);
|
|
673
|
+
this._logger.debug(`Session ready: ${sessionConfig.name} (${session.serverUrl})`);
|
|
674
|
+
}
|
|
675
|
+
async _createWorktreeExplorerSession(sessionName, worktreePath) {
|
|
676
|
+
const configDirRelToGitRoot = path.relative(this._config.repo.gitRoot, this._config.configDir);
|
|
677
|
+
const worktreeConfigDir = path.resolve(worktreePath, configDirRelToGitRoot);
|
|
678
|
+
const viteConfigPath = path.resolve(worktreeConfigDir, this._config.defaultViteConfig);
|
|
679
|
+
const viteProject = ViteProjectRef.fromViteConfigPath(viteConfigPath);
|
|
680
|
+
const currentSession = this._config.sessions.find(s => s.source.kind === 'current');
|
|
681
|
+
const resolveViteFrom = currentSession?.viteProject.configFile;
|
|
682
|
+
this._logger.debug(`Worktree session "${sessionName}": resolveViteFrom=${resolveViteFrom}`);
|
|
683
|
+
const session = await ExplorerSession.create(sessionName, viteProject, this._serverFactory, this._browserFactory, {
|
|
684
|
+
logger: this._logger,
|
|
685
|
+
resolveViteFrom,
|
|
686
|
+
hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
|
|
687
|
+
daemonConfig: {
|
|
688
|
+
pipeName: this._pipeName,
|
|
689
|
+
sessionName,
|
|
690
|
+
daemonApiVersion: daemonApiVersionText,
|
|
691
|
+
pluginProtocolVersion: pluginProtocolVersionText,
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
this._sessions.set(sessionName, session);
|
|
695
|
+
this._logger.debug(`Session ready: ${sessionName} (${session.serverUrl})`);
|
|
696
|
+
}
|
|
697
|
+
async _setupSession(sessionConfig) {
|
|
543
698
|
this._logger.debug(`Setting up session: ${sessionConfig.name} (${sessionConfig.source.kind})`);
|
|
699
|
+
this._logger.log(`Starting server: ${sessionConfig.name}`);
|
|
544
700
|
if (sessionConfig.source.kind === 'worktree') {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
701
|
+
await this._setupWorktreeSession(sessionConfig, sessionConfig.source);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
await this._createExplorerSession(sessionConfig);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async _setupWorktreeSession(sessionConfig, source) {
|
|
708
|
+
if (!this._worktreePool) {
|
|
709
|
+
throw new Error(`Session "${sessionConfig.name}" requires a worktree but no worktree pool is available`);
|
|
710
|
+
}
|
|
711
|
+
const slot = this._worktreePool.allocate(sessionConfig.name);
|
|
712
|
+
const git = this._config.repo;
|
|
713
|
+
const ref = source.ref;
|
|
714
|
+
const resolver = ref === GitIndexResolver.INDEX_REF
|
|
715
|
+
? await git.createIndexResolver()
|
|
716
|
+
: await git.createCommitResolver(ref);
|
|
717
|
+
this._resolvers.set(sessionConfig.name, resolver);
|
|
718
|
+
const resolvedCommit = resolver.resolvedCommit.get();
|
|
719
|
+
const wtInfo = await git.worktrees.info(slot.worktreePath);
|
|
720
|
+
if (!wtInfo) {
|
|
721
|
+
this._logger.log(`Creating worktree at ${slot.worktreePath} (${ref} @ ${resolvedCommit.toShort()})`);
|
|
722
|
+
await git.worktrees.create(slot.worktreePath, resolvedCommit);
|
|
723
|
+
}
|
|
724
|
+
else if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
|
|
725
|
+
if (wtInfo.isDirty) {
|
|
726
|
+
throw new Error(`Worktree slot ${slot.index} is dirty. Dirty files:\n` +
|
|
727
|
+
wtInfo.dirtyFiles.map(f => ` ${f}`).join('\n'));
|
|
728
|
+
}
|
|
729
|
+
this._logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
|
|
730
|
+
await git.worktrees.checkout(slot.worktreePath, resolvedCommit);
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
this._logger.log(`Worktree already at ${resolvedCommit.toShort()}`);
|
|
734
|
+
}
|
|
735
|
+
const installSetup = source.install ?? this._config.worktreePool?.setup ?? { kind: 'auto' };
|
|
736
|
+
await installDependencies(slot.worktreePath, installSetup, this._logger);
|
|
737
|
+
this._dynamicSessionMeta.set(sessionConfig.name, { ref, worktreePath: slot.worktreePath });
|
|
738
|
+
await this._createWorktreeExplorerSession(sessionConfig.name, slot.worktreePath);
|
|
739
|
+
this._addDynamicRefWatcher(sessionConfig.name, resolver);
|
|
740
|
+
}
|
|
741
|
+
// -- Dynamic session management ------------------------------------------
|
|
742
|
+
async _openDynamicSession(name, ref) {
|
|
743
|
+
if (this._sessions.has(name) || this._sessionConfigs.has(name) || this._dynamicSessionMeta.has(name)) {
|
|
744
|
+
return { error: `Session "${name}" already exists` };
|
|
745
|
+
}
|
|
746
|
+
if (!this._worktreePool) {
|
|
747
|
+
return { error: 'No worktree pool configured. Add a "worktree" section to your component-explorer.json config.' };
|
|
748
|
+
}
|
|
749
|
+
let slot;
|
|
750
|
+
try {
|
|
751
|
+
slot = this._worktreePool.allocate(name);
|
|
752
|
+
}
|
|
753
|
+
catch (e) {
|
|
754
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
755
|
+
}
|
|
756
|
+
const git = this._config.repo;
|
|
757
|
+
const isIndex = ref === GitIndexResolver.INDEX_REF;
|
|
758
|
+
try {
|
|
759
|
+
let resolver;
|
|
760
|
+
if (isIndex) {
|
|
761
|
+
resolver = await git.createIndexResolver();
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
resolver = await git.createCommitResolver(ref);
|
|
765
|
+
}
|
|
766
|
+
this._resolvers.set(name, resolver);
|
|
548
767
|
const resolvedCommit = resolver.resolvedCommit.get();
|
|
549
|
-
const wtInfo = await git.worktrees.info(
|
|
768
|
+
const wtInfo = await git.worktrees.info(slot.worktreePath);
|
|
550
769
|
if (!wtInfo) {
|
|
551
|
-
this._logger.log(`Creating worktree at ${
|
|
552
|
-
await git.worktrees.create(
|
|
553
|
-
await installDependencies(wt.worktreePath, wt.install, this._logger);
|
|
770
|
+
this._logger.log(`Creating worktree at ${slot.worktreePath} (${ref} @ ${resolvedCommit.toShort()})`);
|
|
771
|
+
await git.worktrees.create(slot.worktreePath, resolvedCommit);
|
|
554
772
|
}
|
|
555
773
|
else if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
|
|
556
774
|
if (wtInfo.isDirty) {
|
|
557
|
-
this.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
775
|
+
this._worktreePool.release(name);
|
|
776
|
+
this._resolvers.get(name)?.dispose();
|
|
777
|
+
this._resolvers.delete(name);
|
|
778
|
+
return {
|
|
779
|
+
error: `Worktree slot ${slot.index} is dirty. Dirty files:\n` +
|
|
780
|
+
wtInfo.dirtyFiles.map(f => ` ${f}`).join('\n'),
|
|
781
|
+
};
|
|
563
782
|
}
|
|
783
|
+
this._logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
|
|
784
|
+
await git.worktrees.checkout(slot.worktreePath, resolvedCommit);
|
|
564
785
|
}
|
|
565
786
|
else {
|
|
566
787
|
this._logger.log(`Worktree already at ${resolvedCommit.toShort()}`);
|
|
567
788
|
}
|
|
789
|
+
const poolConfig = this._config.worktreePool;
|
|
790
|
+
await installDependencies(slot.worktreePath, poolConfig.setup, this._logger);
|
|
791
|
+
this._dynamicSessionMeta.set(name, { ref, worktreePath: slot.worktreePath });
|
|
792
|
+
await this._createWorktreeExplorerSession(name, slot.worktreePath);
|
|
793
|
+
this._addDynamicRefWatcher(name, resolver);
|
|
794
|
+
this._emit({ type: 'session-change' });
|
|
795
|
+
return { sessions: this.getSessionInfos() };
|
|
568
796
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
797
|
+
catch (e) {
|
|
798
|
+
this._worktreePool.release(name);
|
|
799
|
+
this._resolvers.get(name)?.dispose();
|
|
800
|
+
this._resolvers.delete(name);
|
|
801
|
+
this._dynamicSessionMeta.delete(name);
|
|
802
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
803
|
+
this._logger.log(`Failed to open session "${name}": ${msg}`);
|
|
804
|
+
return { error: `Failed to open session: ${msg}` };
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async _closeDynamicSession(name) {
|
|
808
|
+
if (!this._dynamicSessionMeta.has(name)) {
|
|
809
|
+
if (this._sessionConfigs.has(name)) {
|
|
810
|
+
return { error: `Session "${name}" is a static session and cannot be closed` };
|
|
811
|
+
}
|
|
812
|
+
return { error: `Session "${name}" does not exist` };
|
|
813
|
+
}
|
|
814
|
+
const session = this._sessions.get(name);
|
|
815
|
+
if (session) {
|
|
816
|
+
this._logger.debug(`Disposing session: ${name}`);
|
|
817
|
+
await session.dispose();
|
|
818
|
+
this._sessions.delete(name);
|
|
819
|
+
}
|
|
820
|
+
const resolver = this._resolvers.get(name);
|
|
821
|
+
if (resolver) {
|
|
822
|
+
resolver.dispose();
|
|
823
|
+
this._resolvers.delete(name);
|
|
824
|
+
}
|
|
825
|
+
// Remove any ref watcher disposable
|
|
826
|
+
const watcherDisposable = this._dynamicRefWatchers.get(name);
|
|
827
|
+
if (watcherDisposable) {
|
|
828
|
+
watcherDisposable.dispose();
|
|
829
|
+
this._dynamicRefWatchers.delete(name);
|
|
830
|
+
}
|
|
831
|
+
if (this._worktreePool) {
|
|
832
|
+
this._worktreePool.release(name);
|
|
833
|
+
}
|
|
834
|
+
this._dynamicSessionMeta.delete(name);
|
|
835
|
+
this._logger.log(`Session "${name}" closed`);
|
|
836
|
+
this._emit({ type: 'session-change' });
|
|
837
|
+
return { sessions: this.getSessionInfos() };
|
|
838
|
+
}
|
|
839
|
+
async _updateDynamicSessionRef(name, newRef) {
|
|
840
|
+
const meta = this._dynamicSessionMeta.get(name);
|
|
841
|
+
if (!meta) {
|
|
842
|
+
if (this._sessionConfigs.has(name)) {
|
|
843
|
+
return { error: `Session "${name}" is a static session — use restartSession instead` };
|
|
844
|
+
}
|
|
845
|
+
return { error: `Session "${name}" does not exist` };
|
|
846
|
+
}
|
|
847
|
+
const git = this._config.repo;
|
|
848
|
+
// Check dirty before doing anything
|
|
849
|
+
const wtInfo = await git.worktrees.info(meta.worktreePath);
|
|
850
|
+
if (wtInfo && wtInfo.isDirty) {
|
|
851
|
+
return {
|
|
852
|
+
error: `Worktree is dirty, cannot update ref. Dirty files:\n` +
|
|
853
|
+
wtInfo.dirtyFiles.map(f => ` ${f}`).join('\n'),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
// Dispose old resolver
|
|
857
|
+
const oldResolver = this._resolvers.get(name);
|
|
858
|
+
if (oldResolver) {
|
|
859
|
+
oldResolver.dispose();
|
|
860
|
+
this._resolvers.delete(name);
|
|
861
|
+
}
|
|
862
|
+
const oldWatcher = this._dynamicRefWatchers.get(name);
|
|
863
|
+
if (oldWatcher) {
|
|
864
|
+
oldWatcher.dispose();
|
|
865
|
+
this._dynamicRefWatchers.delete(name);
|
|
866
|
+
}
|
|
867
|
+
try {
|
|
868
|
+
const isIndex = newRef === GitIndexResolver.INDEX_REF;
|
|
869
|
+
let resolver;
|
|
870
|
+
if (isIndex) {
|
|
871
|
+
resolver = await git.createIndexResolver();
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
resolver = await git.createCommitResolver(newRef);
|
|
875
|
+
}
|
|
876
|
+
this._resolvers.set(name, resolver);
|
|
877
|
+
const resolvedCommit = resolver.resolvedCommit.get();
|
|
878
|
+
// Checkout in worktree — don't restart Vite, let HMR handle it
|
|
879
|
+
if (wtInfo) {
|
|
880
|
+
if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
|
|
881
|
+
await git.worktrees.checkout(meta.worktreePath, resolvedCommit);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
await git.worktrees.create(meta.worktreePath, resolvedCommit);
|
|
886
|
+
}
|
|
887
|
+
const sessionConfig = this._sessionConfigs.get(name);
|
|
888
|
+
const installSetup = (sessionConfig?.source.kind === 'worktree' ? sessionConfig.source.install : undefined)
|
|
889
|
+
?? this._config.worktreePool?.setup
|
|
890
|
+
?? { kind: 'auto' };
|
|
891
|
+
await installDependencies(meta.worktreePath, installSetup, this._logger);
|
|
892
|
+
meta.ref = newRef;
|
|
893
|
+
this._addDynamicRefWatcher(name, resolver);
|
|
894
|
+
return { sessions: this.getSessionInfos() };
|
|
895
|
+
}
|
|
896
|
+
catch (e) {
|
|
897
|
+
if (e instanceof DirtyWorktreeError) {
|
|
898
|
+
return {
|
|
899
|
+
error: `Worktree is dirty, cannot update ref. Dirty files:\n` +
|
|
900
|
+
e.dirtyFiles.map(f => ` ${f}`).join('\n'),
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
904
|
+
this._logger.log(`Failed to update session ref "${name}": ${msg}`);
|
|
905
|
+
return { error: `Failed to update session ref: ${msg}` };
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
_dynamicRefWatchers = new Map();
|
|
909
|
+
_addDynamicRefWatcher(sessionName, resolver) {
|
|
910
|
+
let previousCommit = resolver.resolvedCommit.get();
|
|
911
|
+
const disposable = autorun(reader => {
|
|
912
|
+
const commit = resolver.resolvedCommit.read(reader);
|
|
913
|
+
if (!previousCommit.equals(commit)) {
|
|
914
|
+
const prev = previousCommit;
|
|
915
|
+
previousCommit = commit;
|
|
916
|
+
this._handleRefChange(sessionName, resolver.ref, prev, commit);
|
|
917
|
+
}
|
|
579
918
|
});
|
|
580
|
-
this.
|
|
581
|
-
this._logger.debug(`Session ready: ${sessionConfig.name} (${session.serverUrl})`);
|
|
919
|
+
this._dynamicRefWatchers.set(sessionName, disposable);
|
|
582
920
|
}
|
|
583
921
|
}
|
|
584
922
|
|
|
585
|
-
export { ActivityTracker, DaemonService, SourceTreeChangedError };
|
|
923
|
+
export { ActivityTracker, DaemonService, SourceTreeChangedError, daemonApiVersionText, pluginProtocolVersionText };
|
|
586
924
|
//# sourceMappingURL=DaemonService.js.map
|