@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.
Files changed (97) 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/mcpCommand.d.ts +1 -0
  19. package/dist/commands/mcpCommand.d.ts.map +1 -1
  20. package/dist/commands/mcpCommand.js +2 -0
  21. package/dist/commands/mcpCommand.js.map +1 -1
  22. package/dist/commands/screenshotCommand.d.ts.map +1 -1
  23. package/dist/commands/screenshotCommand.js +2 -1
  24. package/dist/commands/screenshotCommand.js.map +1 -1
  25. package/dist/commands/serveCommand.d.ts.map +1 -1
  26. package/dist/commands/serveCommand.js +2 -1
  27. package/dist/commands/serveCommand.js.map +1 -1
  28. package/dist/commands/watchCommand.d.ts.map +1 -1
  29. package/dist/commands/watchCommand.js +6 -61
  30. package/dist/commands/watchCommand.js.map +1 -1
  31. package/dist/component-explorer-config.schema.json +97 -58
  32. package/dist/componentExplorer.d.ts +12 -17
  33. package/dist/componentExplorer.d.ts.map +1 -1
  34. package/dist/componentExplorer.js +27 -15
  35. package/dist/componentExplorer.js.map +1 -1
  36. package/dist/daemon/DaemonService.d.ts +72 -5
  37. package/dist/daemon/DaemonService.d.ts.map +1 -1
  38. package/dist/daemon/DaemonService.js +430 -92
  39. package/dist/daemon/DaemonService.js.map +1 -1
  40. package/dist/daemon/dynamicSessions.test.d.ts +2 -0
  41. package/dist/daemon/dynamicSessions.test.d.ts.map +1 -0
  42. package/dist/daemon/pipeClient.d.ts.map +1 -1
  43. package/dist/daemon/pipeClient.js +71 -2
  44. package/dist/daemon/pipeClient.js.map +1 -1
  45. package/dist/daemon/version.d.ts +10 -0
  46. package/dist/daemon/version.d.ts.map +1 -0
  47. package/dist/daemon/version.js +17 -0
  48. package/dist/daemon/version.js.map +1 -0
  49. package/dist/dependencyInstaller.d.ts +2 -2
  50. package/dist/dependencyInstaller.d.ts.map +1 -1
  51. package/dist/dependencyInstaller.js.map +1 -1
  52. package/dist/git/gitIndexResolver.d.ts +25 -0
  53. package/dist/git/gitIndexResolver.d.ts.map +1 -0
  54. package/dist/git/gitIndexResolver.js +91 -0
  55. package/dist/git/gitIndexResolver.js.map +1 -0
  56. package/dist/git/gitIndexResolver.test.d.ts +2 -0
  57. package/dist/git/gitIndexResolver.test.d.ts.map +1 -0
  58. package/dist/git/gitService.d.ts +2 -0
  59. package/dist/git/gitService.d.ts.map +1 -1
  60. package/dist/git/gitService.js +6 -0
  61. package/dist/git/gitService.js.map +1 -1
  62. package/dist/git/gitWorktreeManager.d.ts +6 -0
  63. package/dist/git/gitWorktreeManager.d.ts.map +1 -1
  64. package/dist/git/gitWorktreeManager.js +42 -13
  65. package/dist/git/gitWorktreeManager.js.map +1 -1
  66. package/dist/git/gitWorktreeManager.test.d.ts +2 -0
  67. package/dist/git/gitWorktreeManager.test.d.ts.map +1 -0
  68. package/dist/git/testUtils.d.ts +13 -0
  69. package/dist/git/testUtils.d.ts.map +1 -0
  70. package/dist/httpServer.d.ts +6 -1
  71. package/dist/httpServer.d.ts.map +1 -1
  72. package/dist/httpServer.js +13 -0
  73. package/dist/httpServer.js.map +1 -1
  74. package/dist/index.js +9 -2
  75. package/dist/index.js.map +1 -1
  76. package/dist/logger.d.ts +1 -0
  77. package/dist/logger.d.ts.map +1 -1
  78. package/dist/logger.js +7 -1
  79. package/dist/logger.js.map +1 -1
  80. package/dist/mcp/McpServer.d.ts +8 -0
  81. package/dist/mcp/McpServer.d.ts.map +1 -1
  82. package/dist/mcp/McpServer.js +177 -26
  83. package/dist/mcp/McpServer.js.map +1 -1
  84. package/dist/packages/simple-api/dist/{chunk-Q24JOMNK.js → chunk-TAEFVNPN.js} +1 -1
  85. package/dist/packages/simple-api/dist/chunk-TAEFVNPN.js.map +1 -0
  86. package/dist/packages/simple-api/dist/express.js +1 -1
  87. package/dist/packages/simple-api/dist/express.js.map +1 -1
  88. package/dist/utils.d.ts +7 -0
  89. package/dist/utils.d.ts.map +1 -1
  90. package/dist/utils.js +6 -7
  91. package/dist/utils.js.map +1 -1
  92. package/dist/watchConfig.d.ts +19 -12
  93. package/dist/watchConfig.d.ts.map +1 -1
  94. package/dist/watchConfig.js +43 -48
  95. package/dist/watchConfig.js.map +1 -1
  96. package/package.json +6 -2
  97. 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, config.repo, resolveViteFrom);
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
- errors: result.errors.length > 0 ? result.errors : undefined,
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
- errors: result.errors.length > 0 ? result.errors : undefined,
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
- errors: result.errors.length > 0 ? result.errors : undefined,
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
- baselineErrors: baselineResult.errors.length > 0 ? baselineResult.errors : undefined,
252
- currentErrors: currentResult.errors.length > 0 ? currentResult.errors : undefined,
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
- return this._config.sessions.map(sc => {
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
- return { name: sc.name, sourceKind: sc.source.kind, isLoading: true };
416
+ infos.push({ name: sc.name, sourceKind: sc.source.kind, isLoading: true });
344
417
  }
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
- });
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
- 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);
556
+ this._addSourceChangeWatcher(name, session);
472
557
  }
473
558
  }
474
- async _handleRefChange(sessionName, ref, newCommit) {
475
- 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') {
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 wt = sessionConfig.source.worktree;
482
- const wtInfo = await git.worktrees.info(wt.worktreePath);
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(wt.worktreePath, newCommit);
585
+ await git.worktrees.checkout(meta.worktreePath, newCommit);
496
586
  }
497
587
  else {
498
- await git.worktrees.create(wt.worktreePath, newCommit);
588
+ await git.worktrees.create(meta.worktreePath, newCommit);
499
589
  }
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);
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 _setupSession(sessionConfig, git, resolveViteFrom) {
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
- const wt = sessionConfig.source.worktree;
546
- const resolver = await git.createCommitResolver(wt.ref);
547
- this._resolvers.set(sessionConfig.name, resolver);
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(wt.worktreePath);
768
+ const wtInfo = await git.worktrees.info(slot.worktreePath);
550
769
  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);
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._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);
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
- 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,
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._sessions.set(sessionConfig.name, session);
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