@vscode/component-explorer-cli 0.2.1-14 → 0.2.1-16

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.
@@ -10,118 +10,13 @@ import '../external/vscode-observables/observables/dist/observableInternal/utils
10
10
  import '../external/vscode-observables/observables/dist/observableInternal/observables/observableFromEvent.js';
11
11
  import { buildExplorerUrl } from '../utils.js';
12
12
  import { TaskManager } from './TaskManager.js';
13
+ import { isPipeConnectionError } from '../daemon/pipeClient.js';
13
14
 
14
- // ---------------------------------------------------------------------------
15
- // Client-local state
16
- // ---------------------------------------------------------------------------
17
- class ImageLruCache {
18
- _maxSize;
19
- _entries = [];
20
- constructor(_maxSize = 10) {
21
- this._maxSize = _maxSize;
22
- }
23
- put(hash, image) {
24
- const idx = this._entries.findIndex(e => e.hash === hash);
25
- if (idx !== -1) {
26
- this._entries.splice(idx, 1);
27
- }
28
- this._entries.unshift({ hash, image });
29
- if (this._entries.length > this._maxSize) {
30
- this._entries.length = this._maxSize;
31
- }
32
- }
33
- get(hash) {
34
- const idx = this._entries.findIndex(e => e.hash === hash);
35
- if (idx === -1) {
36
- return undefined;
37
- }
38
- const [entry] = this._entries.splice(idx, 1);
39
- this._entries.unshift(entry);
40
- return entry.image;
41
- }
42
- keys() {
43
- return this._entries.map(e => e.hash);
44
- }
45
- }
46
- class WatchList {
47
- _fixtureIds = new Set();
48
- _hashes = new Map();
49
- get fixtureIds() { return this._fixtureIds; }
50
- add(ids) {
51
- for (const id of ids) {
52
- this._fixtureIds.add(id);
53
- }
54
- }
55
- remove(ids) {
56
- for (const id of ids) {
57
- this._fixtureIds.delete(id);
58
- this._hashes.delete(id);
59
- }
60
- }
61
- set(ids) {
62
- this._fixtureIds.clear();
63
- this._hashes.clear();
64
- for (const id of ids) {
65
- this._fixtureIds.add(id);
66
- }
67
- }
68
- getHash(fixtureId) {
69
- return this._hashes.get(fixtureId);
70
- }
71
- setHash(fixtureId, hash) {
72
- this._hashes.set(fixtureId, hash);
73
- }
74
- toJSON() {
75
- return {
76
- fixtureIds: [...this._fixtureIds],
77
- hashes: Object.fromEntries(this._hashes),
78
- };
79
- }
80
- }
81
- function noDaemonError(hint) {
82
- let text = 'Error: No daemon is currently running.';
83
- if (hint) {
84
- text += ` ${hint}`;
85
- }
86
- else {
87
- text += ' Please start the Component Explorer daemon first by running:\n\n' +
88
- ' component-explorer serve --project <config.json>\n\n' +
89
- 'Or start it in the background:\n\n' +
90
- ' component-explorer serve --project <config.json> --background\n\n' +
91
- 'The daemon manages dev servers and enables fixture screenshots.';
92
- }
93
- return {
94
- content: [{ type: 'text', text }],
95
- isError: true,
96
- };
97
- }
98
- // ---------------------------------------------------------------------------
99
- // DaemonConnection - wrapper to avoid Proxy issues with observables
100
- // ---------------------------------------------------------------------------
101
- class DaemonConnection {
102
- client;
103
- _stale = false;
104
- constructor(client) {
105
- this.client = client;
106
- }
107
- get isStale() { return this._stale; }
108
- markStale() { this._stale = true; }
109
- }
110
- function isPipeConnectionError(e) {
111
- if (!(e instanceof Error)) {
112
- return false;
113
- }
114
- const code = e.code;
115
- if (code === 'ENOENT' || code === 'ECONNREFUSED' || code === 'ECONNRESET' || code === 'EPIPE') {
116
- return true;
117
- }
118
- return /connect ENOENT|ECONNREFUSED|ECONNRESET|EPIPE/.test(e.message);
119
- }
120
15
  // ---------------------------------------------------------------------------
121
16
  // ComponentExplorerMcpServer
122
17
  // ---------------------------------------------------------------------------
123
18
  class ComponentExplorerMcpServer extends Disposable {
124
- _daemonConnection;
19
+ _daemon;
125
20
  static async create(daemon, options) {
126
21
  const server = new ComponentExplorerMcpServer(daemon, options ?? {});
127
22
  const transport = new StdioServerTransport();
@@ -133,16 +28,12 @@ class ComponentExplorerMcpServer extends Disposable {
133
28
  _imageLru = new ImageLruCache(10);
134
29
  _taskManager = new TaskManager();
135
30
  _taskLastReportedIndex = new Map();
136
- _pollFn;
137
- _noAutostartHint;
138
31
  _multiSessionTools = [];
139
32
  _sessions = [];
140
33
  _eventStreamAbortController;
141
- constructor(_daemonConnection, options) {
34
+ constructor(_daemon, options) {
142
35
  super();
143
- this._daemonConnection = _daemonConnection;
144
- this._pollFn = options.pollFn;
145
- this._noAutostartHint = options.noAutostartHint;
36
+ this._daemon = _daemon;
146
37
  this._callTimeoutMs = options.callTimeoutMs ?? ComponentExplorerMcpServer._DEFAULT_CALL_TIMEOUT_MS;
147
38
  this._mcp = new McpServer({
148
39
  name: 'component-explorer',
@@ -150,8 +41,8 @@ class ComponentExplorerMcpServer extends Disposable {
150
41
  });
151
42
  this._registerTools();
152
43
  this._store.add(autorun(async (reader) => {
153
- const conn = this._daemonConnection.read(reader);
154
- await this._onDaemonChanged(conn?.client);
44
+ const client = this._daemon.connection.read(reader);
45
+ await this._onDaemonChanged(client);
155
46
  }));
156
47
  }
157
48
  async _onDaemonChanged(daemon) {
@@ -173,49 +64,13 @@ class ComponentExplorerMcpServer extends Disposable {
173
64
  this._sessions = [];
174
65
  }
175
66
  }
176
- _getConnection() {
177
- const conn = this._daemonConnection.get();
178
- if (conn?.isStale) {
179
- return undefined;
180
- }
181
- return conn;
182
- }
183
- async _waitForDaemon() {
184
- let conn = this._getConnection();
185
- if (conn) {
186
- return conn.client;
187
- }
188
- if (!this._pollFn) {
189
- return undefined;
190
- }
191
- this._log('debug', { type: 'waiting-for-daemon' });
192
- const startTime = Date.now();
193
- const timeout = 3000;
194
- while (Date.now() - startTime < timeout) {
195
- await this._pollFn();
196
- conn = this._getConnection();
197
- if (conn) {
198
- return conn.client;
199
- }
200
- await new Promise(resolve => setTimeout(resolve, 200));
201
- }
202
- return undefined;
203
- }
204
- _handleDisconnect() {
205
- const conn = this._daemonConnection.get();
206
- if (conn && !conn.isStale) {
207
- conn.markStale();
208
- this._sessions = [];
209
- this._log('debug', { type: 'daemon-connection-lost' });
210
- }
211
- }
212
67
  _noDaemonError() {
213
- return noDaemonError(this._noAutostartHint);
68
+ return noDaemonError(this._daemon.noDaemonHint);
214
69
  }
215
70
  static _DEFAULT_CALL_TIMEOUT_MS = 15_000;
216
71
  _callTimeoutMs;
217
72
  async _withDaemon(fn, options) {
218
- const daemon = await this._waitForDaemon();
73
+ const daemon = await this._daemon.getDaemonOrStart();
219
74
  if (!daemon) {
220
75
  return this._noDaemonError();
221
76
  }
@@ -235,7 +90,6 @@ class ComponentExplorerMcpServer extends Disposable {
235
90
  }
236
91
  if (isPipeConnectionError(e)) {
237
92
  this._log('debug', { type: 'daemon-call-failed', error: String(e) });
238
- this._handleDisconnect();
239
93
  return this._noDaemonError();
240
94
  }
241
95
  throw e;
@@ -259,7 +113,7 @@ class ComponentExplorerMcpServer extends Disposable {
259
113
  this._updateSessionSourceTreeId(event.sessionName, event.sourceTreeId);
260
114
  }
261
115
  if (event.type === 'ref-change' || event.type === 'session-change') {
262
- await this._refreshSessions();
116
+ await this._refreshSessions(daemon);
263
117
  }
264
118
  this._log(event.type === 'log' && event.level === 'debug' ? 'debug' : 'info', event);
265
119
  }
@@ -293,24 +147,11 @@ class ComponentExplorerMcpServer extends Disposable {
293
147
  s.sourceTreeId = sourceTreeId;
294
148
  }
295
149
  }
296
- async _refreshSessions() {
297
- const conn = this._getConnection();
298
- if (conn) {
299
- try {
300
- const prevCount = this._sessions.length;
301
- this._sessions = await conn.client.methods.sessions();
302
- if (this._sessions.length !== prevCount) {
303
- this._updateMultiSessionToolVisibility();
304
- }
305
- }
306
- catch (e) {
307
- if (isPipeConnectionError(e)) {
308
- this._handleDisconnect();
309
- }
310
- else {
311
- throw e;
312
- }
313
- }
150
+ async _refreshSessions(daemon) {
151
+ const prevCount = this._sessions.length;
152
+ this._sessions = await daemon.methods.sessions();
153
+ if (this._sessions.length !== prevCount) {
154
+ this._updateMultiSessionToolVisibility();
314
155
  }
315
156
  }
316
157
  _updateMultiSessionToolVisibility() {
@@ -324,14 +165,14 @@ class ComponentExplorerMcpServer extends Disposable {
324
165
  }
325
166
  }
326
167
  }
327
- async _withSourceTreeRetry(fn) {
168
+ async _withSourceTreeRetry(daemon, fn) {
328
169
  try {
329
170
  return await fn();
330
171
  }
331
172
  catch (e) {
332
173
  const msg = e instanceof Error ? e.message : String(e);
333
174
  if (msg.includes('Source tree changed')) {
334
- await this._refreshSessions();
175
+ await this._refreshSessions(daemon);
335
176
  return await fn();
336
177
  }
337
178
  throw e;
@@ -402,7 +243,7 @@ class ComponentExplorerMcpServer extends Disposable {
402
243
  }, async (args) => this._withDaemon(async (daemon) => {
403
244
  const sessionName = args.sessionName ?? this._defaultSessionName();
404
245
  this._log('debug', { type: 'tool-call', tool: 'list_fixtures', sessionName });
405
- return this._withSourceTreeRetry(async () => {
246
+ return this._withSourceTreeRetry(daemon, async () => {
406
247
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
407
248
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
408
249
  const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdPattern, args.labelPattern);
@@ -433,7 +274,7 @@ class ComponentExplorerMcpServer extends Disposable {
433
274
  const sessionName = args.sessionName ?? this._defaultSessionName();
434
275
  this._log('debug', { type: 'tool-call', tool: 'screenshot', fixtureId: args.fixtureId, sessionName });
435
276
  this._log('trace', { type: 'tool-args', tool: 'screenshot', args });
436
- return this._withSourceTreeRetry(async () => {
277
+ return this._withSourceTreeRetry(daemon, async () => {
437
278
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
438
279
  const result = await daemon.methods.screenshots.take({
439
280
  fixtureId: args.fixtureId,
@@ -522,7 +363,7 @@ class ComponentExplorerMcpServer extends Disposable {
522
363
  const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
523
364
  const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
524
365
  this._log('debug', { type: 'tool-call', tool: 'compare_screenshot', fixtureId: args.fixtureId, baselineSessionName, currentSessionName });
525
- return this._withSourceTreeRetry(async () => {
366
+ return this._withSourceTreeRetry(daemon, async () => {
526
367
  const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
527
368
  const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
528
369
  const result = await daemon.methods.screenshots.compare({
@@ -620,7 +461,7 @@ class ComponentExplorerMcpServer extends Disposable {
620
461
  }, async (args) => this._withDaemon(async (daemon) => {
621
462
  const sessionName = args.sessionName ?? this._defaultSessionName();
622
463
  this._log('debug', { type: 'tool-call', tool: 'review_visual', fixtureId: args.fixtureId, verdict: args.verdict });
623
- return this._withSourceTreeRetry(async () => {
464
+ return this._withSourceTreeRetry(daemon, async () => {
624
465
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
625
466
  // Get fixture descriptions
626
467
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
@@ -670,7 +511,7 @@ class ComponentExplorerMcpServer extends Disposable {
670
511
  }, async (args) => this._withDaemon(async (daemon) => {
671
512
  const sessionName = args.sessionName ?? this._defaultSessionName();
672
513
  this._log('debug', { type: 'tool-call', tool: 'check_visuals', sessionName });
673
- return this._withSourceTreeRetry(async () => {
514
+ return this._withSourceTreeRetry(daemon, async () => {
674
515
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
675
516
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
676
517
  if (listResult.loadError) {
@@ -744,7 +585,7 @@ class ComponentExplorerMcpServer extends Disposable {
744
585
  const sessionName = args.sessionName ?? this._defaultSessionName();
745
586
  this._log('debug', { type: 'tool-call', tool: 'evaluate_js', sessionName, hasFixtureId: !!args.fixtureId });
746
587
  this._log('trace', { type: 'tool-args', tool: 'evaluate_js', expressionLength: args.expression.length, fixtureId: args.fixtureId });
747
- return this._withSourceTreeRetry(async () => {
588
+ return this._withSourceTreeRetry(daemon, async () => {
748
589
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
749
590
  const result = await daemon.methods.evaluate({
750
591
  sessionName,
@@ -853,7 +694,7 @@ class ComponentExplorerMcpServer extends Disposable {
853
694
  }
854
695
  const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
855
696
  const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
856
- return this._withSourceTreeRetry(async () => {
697
+ return this._withSourceTreeRetry(daemon, async () => {
857
698
  const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
858
699
  const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
859
700
  const [baselineResult, currentResult] = await Promise.all([
@@ -946,7 +787,7 @@ class ComponentExplorerMcpServer extends Disposable {
946
787
  this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
947
788
  }
948
789
  if (ev.type === 'ref-change') {
949
- const refreshResult = await Promise.race([this._refreshSessions(), timeout]);
790
+ const refreshResult = await Promise.race([this._refreshSessions(daemon), timeout]);
950
791
  if (refreshResult === 'timeout') {
951
792
  return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
952
793
  }
@@ -1003,8 +844,8 @@ class ComponentExplorerMcpServer extends Disposable {
1003
844
  this._mcp.registerTool('sessions', {
1004
845
  description: 'List active sessions with their names, URLs, and current sourceTreeIds',
1005
846
  annotations: { readOnlyHint: true },
1006
- }, async () => this._withDaemon(async (_daemon) => {
1007
- await this._refreshSessions();
847
+ }, async () => this._withDaemon(async (daemon) => {
848
+ await this._refreshSessions(daemon);
1008
849
  return {
1009
850
  content: [{ type: 'text', text: JSON.stringify(this._sessions, null, 2) }],
1010
851
  };
@@ -1104,24 +945,11 @@ class ComponentExplorerMcpServer extends Disposable {
1104
945
  embedded: z.boolean().optional().describe('If true, returns an embedded single-fixture URL (minimal UI, requires fixtureId). Default: false (full explorer UI).'),
1105
946
  },
1106
947
  annotations: { readOnlyHint: true },
1107
- }, async (args) => {
948
+ }, async (args) => this._withDaemon(async (daemon) => {
1108
949
  const sessionName = args.sessionName ?? this._defaultSessionName();
1109
950
  let session = this._sessions.find(s => s.name === sessionName);
1110
951
  if (!session) {
1111
- const daemon = await this._waitForDaemon();
1112
- if (daemon) {
1113
- try {
1114
- await this._refreshSessions();
1115
- }
1116
- catch (e) {
1117
- if (isPipeConnectionError(e)) {
1118
- this._handleDisconnect();
1119
- }
1120
- else {
1121
- throw e;
1122
- }
1123
- }
1124
- }
952
+ await this._refreshSessions(daemon);
1125
953
  session = this._sessions.find(s => s.name === sessionName);
1126
954
  if (!session) {
1127
955
  return {
@@ -1170,7 +998,7 @@ class ComponentExplorerMcpServer extends Disposable {
1170
998
  return {
1171
999
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1172
1000
  };
1173
- });
1001
+ }));
1174
1002
  }
1175
1003
  _registerCheckFixtureErrors() {
1176
1004
  this._mcp.registerTool('check_fixture_errors', {
@@ -1186,7 +1014,7 @@ class ComponentExplorerMcpServer extends Disposable {
1186
1014
  }, async (args) => this._withDaemon(async (daemon) => {
1187
1015
  const sessionName = args.sessionName ?? this._defaultSessionName();
1188
1016
  this._log('debug', { type: 'tool-call', tool: 'check_fixture_errors', sessionName, fixtureIdPattern: args.fixtureIdPattern, labelPattern: args.labelPattern });
1189
- return this._withSourceTreeRetry(async () => {
1017
+ return this._withSourceTreeRetry(daemon, async () => {
1190
1018
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
1191
1019
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
1192
1020
  if (listResult.loadError) {
@@ -1278,7 +1106,7 @@ class ComponentExplorerMcpServer extends Disposable {
1278
1106
  }, async (args) => this._withDaemon(async (daemon) => {
1279
1107
  const sessionName = args.sessionName ?? this._defaultSessionName();
1280
1108
  this._log('debug', { type: 'tool-call', tool: 'check_stability', sessionName, fixtureIdPattern: args.fixtureIdPattern, labelPattern: args.labelPattern });
1281
- return this._withSourceTreeRetry(async () => {
1109
+ return this._withSourceTreeRetry(daemon, async () => {
1282
1110
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
1283
1111
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
1284
1112
  if (listResult.loadError) {
@@ -1459,6 +1287,90 @@ class ComponentExplorerMcpServer extends Disposable {
1459
1287
  }));
1460
1288
  }
1461
1289
  }
1290
+ // ---------------------------------------------------------------------------
1291
+ // Client-local state
1292
+ // ---------------------------------------------------------------------------
1293
+ class ImageLruCache {
1294
+ _maxSize;
1295
+ _entries = [];
1296
+ constructor(_maxSize = 10) {
1297
+ this._maxSize = _maxSize;
1298
+ }
1299
+ put(hash, image) {
1300
+ const idx = this._entries.findIndex(e => e.hash === hash);
1301
+ if (idx !== -1) {
1302
+ this._entries.splice(idx, 1);
1303
+ }
1304
+ this._entries.unshift({ hash, image });
1305
+ if (this._entries.length > this._maxSize) {
1306
+ this._entries.length = this._maxSize;
1307
+ }
1308
+ }
1309
+ get(hash) {
1310
+ const idx = this._entries.findIndex(e => e.hash === hash);
1311
+ if (idx === -1) {
1312
+ return undefined;
1313
+ }
1314
+ const [entry] = this._entries.splice(idx, 1);
1315
+ this._entries.unshift(entry);
1316
+ return entry.image;
1317
+ }
1318
+ keys() {
1319
+ return this._entries.map(e => e.hash);
1320
+ }
1321
+ }
1322
+ class WatchList {
1323
+ _fixtureIds = new Set();
1324
+ _hashes = new Map();
1325
+ get fixtureIds() { return this._fixtureIds; }
1326
+ add(ids) {
1327
+ for (const id of ids) {
1328
+ this._fixtureIds.add(id);
1329
+ }
1330
+ }
1331
+ remove(ids) {
1332
+ for (const id of ids) {
1333
+ this._fixtureIds.delete(id);
1334
+ this._hashes.delete(id);
1335
+ }
1336
+ }
1337
+ set(ids) {
1338
+ this._fixtureIds.clear();
1339
+ this._hashes.clear();
1340
+ for (const id of ids) {
1341
+ this._fixtureIds.add(id);
1342
+ }
1343
+ }
1344
+ getHash(fixtureId) {
1345
+ return this._hashes.get(fixtureId);
1346
+ }
1347
+ setHash(fixtureId, hash) {
1348
+ this._hashes.set(fixtureId, hash);
1349
+ }
1350
+ toJSON() {
1351
+ return {
1352
+ fixtureIds: [...this._fixtureIds],
1353
+ hashes: Object.fromEntries(this._hashes),
1354
+ };
1355
+ }
1356
+ }
1357
+ function noDaemonError(hint) {
1358
+ let text = 'Error: No daemon is currently running.';
1359
+ if (hint) {
1360
+ text += ` ${hint}`;
1361
+ }
1362
+ else {
1363
+ text += ' Please start the Component Explorer daemon first by running:\n\n' +
1364
+ ' component-explorer serve --project <config.json>\n\n' +
1365
+ 'Or start it in the background:\n\n' +
1366
+ ' component-explorer serve --project <config.json> --background\n\n' +
1367
+ 'The daemon manages dev servers and enables fixture screenshots.';
1368
+ }
1369
+ return {
1370
+ content: [{ type: 'text', text }],
1371
+ isError: true,
1372
+ };
1373
+ }
1462
1374
 
1463
- export { ComponentExplorerMcpServer, DaemonConnection };
1375
+ export { ComponentExplorerMcpServer };
1464
1376
  //# sourceMappingURL=McpServer.js.map