@vscode/component-explorer-cli 0.1.1-17 → 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.
Files changed (93) hide show
  1. package/dist/WorktreePool.d.ts +22 -0
  2. package/dist/WorktreePool.d.ts.map +1 -0
  3. package/dist/WorktreePool.js +58 -0
  4. package/dist/WorktreePool.js.map +1 -0
  5. package/dist/WorktreePool.test.d.ts +2 -0
  6. package/dist/WorktreePool.test.d.ts.map +1 -0
  7. package/dist/_virtual/_build-info.js +4 -0
  8. package/dist/_virtual/_build-info.js.map +1 -0
  9. package/dist/commands/acceptCommand.d.ts.map +1 -1
  10. package/dist/commands/acceptCommand.js +3 -2
  11. package/dist/commands/acceptCommand.js.map +1 -1
  12. package/dist/commands/checkStabilityCommand.d.ts.map +1 -1
  13. package/dist/commands/checkStabilityCommand.js +2 -1
  14. package/dist/commands/checkStabilityCommand.js.map +1 -1
  15. package/dist/commands/compareCommand.d.ts.map +1 -1
  16. package/dist/commands/compareCommand.js +3 -2
  17. package/dist/commands/compareCommand.js.map +1 -1
  18. package/dist/commands/screenshotCommand.d.ts.map +1 -1
  19. package/dist/commands/screenshotCommand.js +2 -1
  20. package/dist/commands/screenshotCommand.js.map +1 -1
  21. package/dist/commands/serveCommand.d.ts.map +1 -1
  22. package/dist/commands/serveCommand.js +2 -1
  23. package/dist/commands/serveCommand.js.map +1 -1
  24. package/dist/commands/watchCommand.d.ts.map +1 -1
  25. package/dist/commands/watchCommand.js +6 -61
  26. package/dist/commands/watchCommand.js.map +1 -1
  27. package/dist/component-explorer-config.schema.json +97 -58
  28. package/dist/componentExplorer.d.ts +12 -17
  29. package/dist/componentExplorer.d.ts.map +1 -1
  30. package/dist/componentExplorer.js +27 -15
  31. package/dist/componentExplorer.js.map +1 -1
  32. package/dist/daemon/DaemonService.d.ts +71 -5
  33. package/dist/daemon/DaemonService.d.ts.map +1 -1
  34. package/dist/daemon/DaemonService.js +415 -90
  35. package/dist/daemon/DaemonService.js.map +1 -1
  36. package/dist/daemon/dynamicSessions.test.d.ts +2 -0
  37. package/dist/daemon/dynamicSessions.test.d.ts.map +1 -0
  38. package/dist/daemon/pipeClient.d.ts.map +1 -1
  39. package/dist/daemon/pipeClient.js +71 -2
  40. package/dist/daemon/pipeClient.js.map +1 -1
  41. package/dist/daemon/version.d.ts +10 -0
  42. package/dist/daemon/version.d.ts.map +1 -0
  43. package/dist/daemon/version.js +17 -0
  44. package/dist/daemon/version.js.map +1 -0
  45. package/dist/dependencyInstaller.d.ts +2 -2
  46. package/dist/dependencyInstaller.d.ts.map +1 -1
  47. package/dist/dependencyInstaller.js.map +1 -1
  48. package/dist/git/gitIndexResolver.d.ts +25 -0
  49. package/dist/git/gitIndexResolver.d.ts.map +1 -0
  50. package/dist/git/gitIndexResolver.js +91 -0
  51. package/dist/git/gitIndexResolver.js.map +1 -0
  52. package/dist/git/gitIndexResolver.test.d.ts +2 -0
  53. package/dist/git/gitIndexResolver.test.d.ts.map +1 -0
  54. package/dist/git/gitService.d.ts +2 -0
  55. package/dist/git/gitService.d.ts.map +1 -1
  56. package/dist/git/gitService.js +6 -0
  57. package/dist/git/gitService.js.map +1 -1
  58. package/dist/git/gitWorktreeManager.d.ts +6 -0
  59. package/dist/git/gitWorktreeManager.d.ts.map +1 -1
  60. package/dist/git/gitWorktreeManager.js +42 -13
  61. package/dist/git/gitWorktreeManager.js.map +1 -1
  62. package/dist/git/gitWorktreeManager.test.d.ts +2 -0
  63. package/dist/git/gitWorktreeManager.test.d.ts.map +1 -0
  64. package/dist/git/testUtils.d.ts +13 -0
  65. package/dist/git/testUtils.d.ts.map +1 -0
  66. package/dist/httpServer.d.ts +6 -1
  67. package/dist/httpServer.d.ts.map +1 -1
  68. package/dist/httpServer.js +13 -0
  69. package/dist/httpServer.js.map +1 -1
  70. package/dist/index.js +9 -2
  71. package/dist/index.js.map +1 -1
  72. package/dist/logger.d.ts +1 -0
  73. package/dist/logger.d.ts.map +1 -1
  74. package/dist/logger.js +7 -1
  75. package/dist/logger.js.map +1 -1
  76. package/dist/mcp/McpServer.d.ts +6 -0
  77. package/dist/mcp/McpServer.d.ts.map +1 -1
  78. package/dist/mcp/McpServer.js +175 -26
  79. package/dist/mcp/McpServer.js.map +1 -1
  80. package/dist/packages/simple-api/dist/{chunk-Q24JOMNK.js → chunk-TAEFVNPN.js} +1 -1
  81. package/dist/packages/simple-api/dist/chunk-TAEFVNPN.js.map +1 -0
  82. package/dist/packages/simple-api/dist/express.js +1 -1
  83. package/dist/packages/simple-api/dist/express.js.map +1 -1
  84. package/dist/utils.d.ts +7 -0
  85. package/dist/utils.d.ts.map +1 -1
  86. package/dist/utils.js +6 -7
  87. package/dist/utils.js.map +1 -1
  88. package/dist/watchConfig.d.ts +19 -12
  89. package/dist/watchConfig.d.ts.map +1 -1
  90. package/dist/watchConfig.js +43 -48
  91. package/dist/watchConfig.js.map +1 -1
  92. package/package.json +6 -2
  93. 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, config.repo, resolveViteFrom);
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
- errors: result.errors.length > 0 ? result.errors : undefined,
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
- errors: result.errors.length > 0 ? result.errors : undefined,
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
- errors: result.errors.length > 0 ? result.errors : undefined,
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
- baselineErrors: baselineResult.errors.length > 0 ? baselineResult.errors : undefined,
252
- currentErrors: currentResult.errors.length > 0 ? currentResult.errors : undefined,
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
- return this._config.sessions.map(sc => {
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
- return { name: sc.name, sourceKind: sc.source.kind, isLoading: true };
415
+ infos.push({ name: sc.name, sourceKind: sc.source.kind, isLoading: true });
344
416
  }
345
- return {
346
- name: sc.name,
347
- sourceKind: sc.source.kind,
348
- serverUrl: session.serverUrl,
349
- sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
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
- let previousValue = session.sourceTreeId.get().value;
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 git = this._config.repo;
477
- const sessionConfig = this._sessionConfigs.get(sessionName);
478
- if (!sessionConfig || sessionConfig.source.kind !== 'worktree') {
572
+ const meta = this._dynamicSessionMeta.get(sessionName);
573
+ if (!meta) {
479
574
  return;
480
575
  }
481
- const wt = sessionConfig.source.worktree;
482
- const wtInfo = await git.worktrees.info(wt.worktreePath);
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(wt.worktreePath, newCommit);
583
+ await git.worktrees.checkout(meta.worktreePath, newCommit);
496
584
  }
497
585
  else {
498
- await git.worktrees.create(wt.worktreePath, newCommit);
586
+ await git.worktrees.create(meta.worktreePath, newCommit);
499
587
  }
500
- await installDependencies(wt.worktreePath, wt.install, this._logger);
501
- // Recreate session
502
- const currentSession = this._config.sessions.find(s => s.source.kind === 'current');
503
- const resolveViteFrom = currentSession?.viteProject.cwd;
504
- const newSession = await ExplorerSession.create(sessionConfig.name, sessionConfig.viteProject, this._serverFactory, this._browserFactory, {
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 _setupSession(sessionConfig, git, resolveViteFrom) {
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
- const wt = sessionConfig.source.worktree;
546
- const resolver = await git.createCommitResolver(wt.ref);
547
- this._resolvers.set(sessionConfig.name, resolver);
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(wt.worktreePath);
756
+ const wtInfo = await git.worktrees.info(slot.worktreePath);
550
757
  if (!wtInfo) {
551
- this._logger.log(`Creating worktree at ${wt.worktreePath} (${wt.ref} @ ${resolvedCommit.toShort()})`);
552
- await git.worktrees.create(wt.worktreePath, resolvedCommit);
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._logger.log(`Worktree is dirty, using existing checkout at ${wtInfo.checkedOutCommit.toShort()}`);
558
- }
559
- else {
560
- this._logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
561
- await git.worktrees.checkout(wt.worktreePath, resolvedCommit);
562
- await installDependencies(wt.worktreePath, wt.install, this._logger);
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
- this._logger.log(`Starting server: ${sessionConfig.name}`);
570
- // Set daemon config env var so the Vite plugin enables the daemon proxy
571
- process.env.COMPONENT_EXPLORER_DAEMON_CONFIG = JSON.stringify({
572
- pipeName: this._pipeName,
573
- sessionName: sessionConfig.name,
574
- });
575
- const session = await ExplorerSession.create(sessionConfig.name, sessionConfig.viteProject, this._serverFactory, this._browserFactory, {
576
- logger: this._logger,
577
- resolveViteFrom: sessionConfig.source.kind === 'worktree' ? resolveViteFrom : undefined,
578
- hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
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._sessions.set(sessionConfig.name, session);
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