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