@fairfox/polly 0.2.1 → 0.3.1

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.
@@ -42,9 +42,22 @@ class ProjectDetector {
42
42
  if (packageJson.dependencies?.electron || packageJson.devDependencies?.electron) {
43
43
  return this.detectElectron(packageJson);
44
44
  }
45
- if (packageJson.dependencies?.ws || packageJson.dependencies?.["socket.io"] || packageJson.dependencies?.elysia || packageJson.dependencies?.express) {
45
+ const deps = {
46
+ ...packageJson.dependencies,
47
+ ...packageJson.devDependencies
48
+ };
49
+ if (deps?.ws || deps?.["socket.io"] || deps?.elysia || deps?.express || deps?.fastify || deps?.hono || deps?.koa) {
46
50
  return this.detectWebSocketApp(packageJson);
47
51
  }
52
+ const serverResult = this.detectWebSocketApp(packageJson);
53
+ if (Object.keys(serverResult.entryPoints).length > 0) {
54
+ return serverResult;
55
+ }
56
+ } else {
57
+ const serverResult = this.detectWebSocketApp({});
58
+ if (Object.keys(serverResult.entryPoints).length > 0) {
59
+ return serverResult;
60
+ }
48
61
  }
49
62
  return this.detectGenericProject();
50
63
  }
@@ -176,37 +189,53 @@ class ProjectDetector {
176
189
  }
177
190
  detectWebSocketApp(packageJson) {
178
191
  const entryPoints = {};
192
+ const contextMapping = {};
179
193
  const serverCandidates = [
180
194
  "src/server.ts",
195
+ "src/server.js",
181
196
  "src/index.ts",
197
+ "src/index.js",
182
198
  "src/main.ts",
199
+ "src/main.js",
183
200
  "src/app.ts",
201
+ "src/app.js",
184
202
  "server.ts",
185
- "index.ts"
203
+ "server.js",
204
+ "index.ts",
205
+ "index.js"
186
206
  ];
187
- for (const candidate of serverCandidates) {
188
- const fullPath = path3.join(this.projectRoot, candidate);
189
- if (fs3.existsSync(fullPath)) {
190
- entryPoints.server = fullPath;
191
- break;
207
+ const scoredServers = this.scoreServerCandidates(serverCandidates);
208
+ if (scoredServers.length > 0) {
209
+ const best = scoredServers[0];
210
+ entryPoints.server = best.path;
211
+ if (best.hasWebSocket) {
212
+ contextMapping.server = "WebSocket Server";
213
+ } else if (best.hasHTTP) {
214
+ contextMapping.server = "HTTP Server";
215
+ } else {
216
+ contextMapping.server = "Server";
192
217
  }
193
218
  }
194
219
  const clientCandidates = [
195
220
  "src/client/index.ts",
221
+ "src/client/index.js",
196
222
  "src/client.ts",
197
- "client/index.ts"
223
+ "src/client.js",
224
+ "client/index.ts",
225
+ "client/index.js"
198
226
  ];
199
227
  for (const candidate of clientCandidates) {
200
228
  const fullPath = path3.join(this.projectRoot, candidate);
201
229
  if (fs3.existsSync(fullPath)) {
202
230
  entryPoints.client = fullPath;
231
+ contextMapping.client = "Client";
203
232
  break;
204
233
  }
205
234
  }
206
235
  return {
207
236
  type: "websocket-app",
208
237
  entryPoints,
209
- contextMapping: {
238
+ contextMapping: Object.keys(contextMapping).length > 0 ? contextMapping : {
210
239
  server: "Server",
211
240
  client: "Client"
212
241
  },
@@ -217,6 +246,98 @@ class ProjectDetector {
217
246
  }
218
247
  };
219
248
  }
249
+ scoreServerCandidates(candidates) {
250
+ const scored = [];
251
+ for (const candidate of candidates) {
252
+ const fullPath = path3.join(this.projectRoot, candidate);
253
+ if (!fs3.existsSync(fullPath))
254
+ continue;
255
+ try {
256
+ const content = fs3.readFileSync(fullPath, "utf-8");
257
+ let score = 0;
258
+ let hasWebSocket = false;
259
+ let hasHTTP = false;
260
+ let framework = null;
261
+ const patterns = {
262
+ bunWebSocket: /Bun\.serve\s*\([^)]*websocket\s*:/i,
263
+ bunHTTP: /Bun\.serve\s*\(/i,
264
+ wsServer: /new\s+WebSocket\.Server/i,
265
+ wsImport: /from\s+['"]ws['"]/,
266
+ socketIO: /io\s*\(|require\s*\(\s*['"]socket\.io['"]\s*\)/i,
267
+ elysia: /new\s+Elysia\s*\(|\.ws\s*\(/i,
268
+ elysiaImport: /from\s+['"]elysia['"]/,
269
+ express: /express\s*\(\)|app\.listen/i,
270
+ expressWs: /expressWs\s*\(/i,
271
+ httpServer: /createServer|app\.listen|server\.listen/i,
272
+ webSocket: /WebSocket|websocket|\.ws\s*\(|wss\s*:/i
273
+ };
274
+ if (patterns.bunWebSocket.test(content)) {
275
+ score += 15;
276
+ hasWebSocket = true;
277
+ hasHTTP = true;
278
+ framework = "Bun";
279
+ } else if (patterns.bunHTTP.test(content)) {
280
+ score += 10;
281
+ hasHTTP = true;
282
+ framework = "Bun";
283
+ }
284
+ if (patterns.wsServer.test(content) || patterns.wsImport.test(content)) {
285
+ score += 12;
286
+ hasWebSocket = true;
287
+ framework = framework || "ws";
288
+ }
289
+ if (patterns.socketIO.test(content)) {
290
+ score += 12;
291
+ hasWebSocket = true;
292
+ framework = framework || "socket.io";
293
+ }
294
+ if (patterns.elysia.test(content) || patterns.elysiaImport.test(content)) {
295
+ score += 10;
296
+ hasHTTP = true;
297
+ framework = framework || "Elysia";
298
+ }
299
+ if (patterns.elysiaImport.test(content) && patterns.webSocket.test(content)) {
300
+ score += 8;
301
+ hasWebSocket = true;
302
+ }
303
+ if (patterns.express.test(content)) {
304
+ score += 8;
305
+ hasHTTP = true;
306
+ framework = framework || "Express";
307
+ }
308
+ if (patterns.expressWs.test(content)) {
309
+ score += 5;
310
+ hasWebSocket = true;
311
+ }
312
+ if (patterns.httpServer.test(content) && !hasHTTP) {
313
+ score += 5;
314
+ hasHTTP = true;
315
+ }
316
+ if (patterns.webSocket.test(content) && !hasWebSocket) {
317
+ score += 3;
318
+ hasWebSocket = true;
319
+ }
320
+ if (/\.listen\s*\(/.test(content)) {
321
+ score += 5;
322
+ }
323
+ if (/export\s+default/.test(content)) {
324
+ score += 3;
325
+ }
326
+ if (candidate.includes("server")) {
327
+ score += 3;
328
+ }
329
+ if (candidate === "src/index.ts" || candidate === "src/index.js") {
330
+ score += 2;
331
+ }
332
+ if (score > 0) {
333
+ scored.push({ path: fullPath, score, hasWebSocket, hasHTTP, framework });
334
+ }
335
+ } catch (error) {
336
+ continue;
337
+ }
338
+ }
339
+ return scored.sort((a, b) => b.score - a.score);
340
+ }
220
341
  detectGenericProject() {
221
342
  const entryPoints = {};
222
343
  const tsConfigPath = path3.join(this.projectRoot, "tsconfig.json");
@@ -343,10 +464,14 @@ class ManifestParser {
343
464
  manifestPath;
344
465
  manifestData;
345
466
  baseDir;
346
- constructor(projectRoot) {
467
+ constructor(projectRoot, optional = false) {
347
468
  this.baseDir = projectRoot;
348
469
  this.manifestPath = path.join(projectRoot, "manifest.json");
349
470
  if (!fs.existsSync(this.manifestPath)) {
471
+ if (optional) {
472
+ this.manifestData = null;
473
+ return;
474
+ }
350
475
  throw new Error(`manifest.json not found at ${this.manifestPath}`);
351
476
  }
352
477
  try {
@@ -356,7 +481,13 @@ class ManifestParser {
356
481
  throw new Error(`Failed to parse manifest.json: ${error}`);
357
482
  }
358
483
  }
484
+ hasManifest() {
485
+ return this.manifestData !== null;
486
+ }
359
487
  parse() {
488
+ if (!this.manifestData) {
489
+ throw new Error("Cannot parse manifest: manifest.json not loaded. Use hasManifest() to check availability.");
490
+ }
360
491
  const manifest = this.manifestData;
361
492
  return {
362
493
  name: manifest.name || "Unknown Extension",
@@ -373,6 +504,9 @@ class ManifestParser {
373
504
  };
374
505
  }
375
506
  getContextEntryPoints() {
507
+ if (!this.manifestData) {
508
+ return {};
509
+ }
376
510
  const entryPoints = {};
377
511
  const background = this.parseBackground();
378
512
  if (background) {
@@ -415,6 +549,8 @@ class ManifestParser {
415
549
  return entryPoints;
416
550
  }
417
551
  parseBackground() {
552
+ if (!this.manifestData)
553
+ return;
418
554
  const bg = this.manifestData.background;
419
555
  if (!bg)
420
556
  return;
@@ -439,6 +575,8 @@ class ManifestParser {
439
575
  return;
440
576
  }
441
577
  parseContentScripts() {
578
+ if (!this.manifestData)
579
+ return;
442
580
  const cs = this.manifestData.content_scripts;
443
581
  if (!cs || !Array.isArray(cs))
444
582
  return;
@@ -449,6 +587,8 @@ class ManifestParser {
449
587
  }));
450
588
  }
451
589
  parsePopup() {
590
+ if (!this.manifestData)
591
+ return;
452
592
  const action = this.manifestData.action || this.manifestData.browser_action;
453
593
  if (!action)
454
594
  return;
@@ -461,6 +601,8 @@ class ManifestParser {
461
601
  return;
462
602
  }
463
603
  parseOptions() {
604
+ if (!this.manifestData)
605
+ return;
464
606
  const options = this.manifestData.options_ui || this.manifestData.options_page;
465
607
  if (!options)
466
608
  return;
@@ -476,6 +618,8 @@ class ManifestParser {
476
618
  };
477
619
  }
478
620
  parseDevtools() {
621
+ if (!this.manifestData)
622
+ return;
479
623
  const devtools = this.manifestData.devtools_page;
480
624
  if (!devtools)
481
625
  return;
@@ -780,7 +924,34 @@ class FlowAnalyzer {
780
924
  const expression = node.getExpression();
781
925
  if (Node2.isPropertyAccessExpression(expression)) {
782
926
  const methodName = expression.getName();
783
- if (methodName === "send" || methodName === "emit") {
927
+ if (methodName === "send" || methodName === "emit" || methodName === "postMessage" || methodName === "broadcast") {
928
+ const args = node.getArguments();
929
+ if (args.length > 0) {
930
+ const firstArg = args[0];
931
+ let msgType;
932
+ if (Node2.isStringLiteral(firstArg)) {
933
+ msgType = firstArg.getLiteralValue();
934
+ } else if (Node2.isObjectLiteralExpression(firstArg)) {
935
+ const typeProperty = firstArg.getProperty("type");
936
+ if (typeProperty && Node2.isPropertyAssignment(typeProperty)) {
937
+ const initializer = typeProperty.getInitializer();
938
+ if (initializer && Node2.isStringLiteral(initializer)) {
939
+ msgType = initializer.getLiteralValue();
940
+ }
941
+ }
942
+ }
943
+ if (msgType === messageType) {
944
+ senders.push({
945
+ context,
946
+ file: filePath,
947
+ line: node.getStartLineNumber()
948
+ });
949
+ }
950
+ }
951
+ }
952
+ }
953
+ if (Node2.isIdentifier(expression)) {
954
+ if (expression.getText() === "postMessage") {
784
955
  const args = node.getArguments();
785
956
  if (args.length > 0) {
786
957
  const firstArg = args[0];
@@ -932,6 +1103,15 @@ class FlowAnalyzer {
932
1103
  if (path2.includes("/offscreen/") || path2.includes("\\offscreen\\")) {
933
1104
  return "offscreen";
934
1105
  }
1106
+ if (path2.includes("/server/") || path2.includes("\\server\\") || path2.includes("/server.")) {
1107
+ return "server";
1108
+ }
1109
+ if (path2.includes("/client/") || path2.includes("\\client\\") || path2.includes("/client.")) {
1110
+ return "client";
1111
+ }
1112
+ if (path2.includes("/worker/") || path2.includes("\\worker\\") || path2.includes("service-worker")) {
1113
+ return "worker";
1114
+ }
935
1115
  return "unknown";
936
1116
  }
937
1117
  }
@@ -1138,10 +1318,12 @@ import { Project as Project4, SyntaxKind as SyntaxKind2, Node as Node4 } from "t
1138
1318
 
1139
1319
  class HandlerExtractor {
1140
1320
  project;
1321
+ typeGuardCache;
1141
1322
  constructor(tsConfigPath) {
1142
1323
  this.project = new Project4({
1143
1324
  tsConfigFilePath: tsConfigPath
1144
1325
  });
1326
+ this.typeGuardCache = new WeakMap;
1145
1327
  }
1146
1328
  extractHandlers() {
1147
1329
  const handlers = [];
@@ -1168,7 +1350,7 @@ class HandlerExtractor {
1168
1350
  const expression = node.getExpression();
1169
1351
  if (Node4.isPropertyAccessExpression(expression)) {
1170
1352
  const methodName = expression.getName();
1171
- if (methodName === "on") {
1353
+ if (methodName === "on" || methodName === "addEventListener") {
1172
1354
  const handler = this.extractHandler(node, context, filePath);
1173
1355
  if (handler) {
1174
1356
  handlers.push(handler);
@@ -1176,6 +1358,18 @@ class HandlerExtractor {
1176
1358
  }
1177
1359
  }
1178
1360
  }
1361
+ if (Node4.isSwitchStatement(node)) {
1362
+ const switchHandlers = this.extractSwitchCaseHandlers(node, context, filePath);
1363
+ handlers.push(...switchHandlers);
1364
+ }
1365
+ if (Node4.isVariableDeclaration(node)) {
1366
+ const mapHandlers = this.extractHandlerMapPattern(node, context, filePath);
1367
+ handlers.push(...mapHandlers);
1368
+ }
1369
+ if (Node4.isIfStatement(node)) {
1370
+ const typeGuardHandlers = this.extractTypeGuardHandlers(node, context, filePath);
1371
+ handlers.push(...typeGuardHandlers);
1372
+ }
1179
1373
  });
1180
1374
  return handlers;
1181
1375
  }
@@ -1317,6 +1511,182 @@ class HandlerExtractor {
1317
1511
  }
1318
1512
  return;
1319
1513
  }
1514
+ extractSwitchCaseHandlers(switchNode, context, filePath) {
1515
+ const handlers = [];
1516
+ try {
1517
+ const expression = switchNode.getExpression();
1518
+ const expressionText = expression.getText();
1519
+ if (!/\.(type|kind|event|action)/.test(expressionText)) {
1520
+ return handlers;
1521
+ }
1522
+ const caseClauses = switchNode.getClauses();
1523
+ for (const clause of caseClauses) {
1524
+ if (Node4.isCaseClause(clause)) {
1525
+ const caseExpr = clause.getExpression();
1526
+ let messageType = null;
1527
+ if (Node4.isStringLiteral(caseExpr)) {
1528
+ messageType = caseExpr.getLiteralValue();
1529
+ }
1530
+ if (messageType) {
1531
+ const line = clause.getStartLineNumber();
1532
+ handlers.push({
1533
+ messageType,
1534
+ node: context,
1535
+ assignments: [],
1536
+ preconditions: [],
1537
+ postconditions: [],
1538
+ location: { file: filePath, line }
1539
+ });
1540
+ }
1541
+ }
1542
+ }
1543
+ } catch (error) {}
1544
+ return handlers;
1545
+ }
1546
+ extractHandlerMapPattern(varDecl, context, filePath) {
1547
+ const handlers = [];
1548
+ try {
1549
+ const initializer = varDecl.getInitializer();
1550
+ if (!initializer || !Node4.isObjectLiteralExpression(initializer)) {
1551
+ return handlers;
1552
+ }
1553
+ const varName = varDecl.getName().toLowerCase();
1554
+ if (!/(handler|listener|callback|event)s?/.test(varName)) {
1555
+ return handlers;
1556
+ }
1557
+ const properties = initializer.getProperties();
1558
+ for (const prop of properties) {
1559
+ if (Node4.isPropertyAssignment(prop)) {
1560
+ const nameNode = prop.getNameNode();
1561
+ let messageType = null;
1562
+ if (Node4.isStringLiteral(nameNode)) {
1563
+ messageType = nameNode.getLiteralValue();
1564
+ } else if (Node4.isIdentifier(nameNode)) {
1565
+ messageType = nameNode.getText();
1566
+ }
1567
+ if (messageType) {
1568
+ const line = prop.getStartLineNumber();
1569
+ handlers.push({
1570
+ messageType,
1571
+ node: context,
1572
+ assignments: [],
1573
+ preconditions: [],
1574
+ postconditions: [],
1575
+ location: { file: filePath, line }
1576
+ });
1577
+ }
1578
+ }
1579
+ }
1580
+ } catch (error) {}
1581
+ return handlers;
1582
+ }
1583
+ extractTypeGuardHandlers(ifNode, context, filePath) {
1584
+ const handlers = [];
1585
+ try {
1586
+ const sourceFile = ifNode.getSourceFile();
1587
+ let typeGuards = this.typeGuardCache.get(sourceFile);
1588
+ if (!typeGuards) {
1589
+ typeGuards = this.findTypePredicateFunctions(sourceFile);
1590
+ this.typeGuardCache.set(sourceFile, typeGuards);
1591
+ }
1592
+ if (typeGuards.size === 0) {
1593
+ return handlers;
1594
+ }
1595
+ let currentIf = ifNode;
1596
+ while (currentIf) {
1597
+ const handler = this.extractHandlerFromIfClause(currentIf, typeGuards, context, filePath);
1598
+ if (handler) {
1599
+ handlers.push(handler);
1600
+ }
1601
+ const elseStatement = currentIf.getElseStatement();
1602
+ if (elseStatement && Node4.isIfStatement(elseStatement)) {
1603
+ currentIf = elseStatement;
1604
+ } else {
1605
+ break;
1606
+ }
1607
+ }
1608
+ } catch (error) {}
1609
+ return handlers;
1610
+ }
1611
+ extractHandlerFromIfClause(ifNode, typeGuards, context, filePath) {
1612
+ try {
1613
+ const condition = ifNode.getExpression();
1614
+ if (!Node4.isCallExpression(condition)) {
1615
+ return null;
1616
+ }
1617
+ const funcExpr = condition.getExpression();
1618
+ let funcName;
1619
+ if (Node4.isIdentifier(funcExpr)) {
1620
+ funcName = funcExpr.getText();
1621
+ }
1622
+ if (!funcName || !typeGuards.has(funcName)) {
1623
+ return null;
1624
+ }
1625
+ const messageType = typeGuards.get(funcName);
1626
+ const line = ifNode.getStartLineNumber();
1627
+ return {
1628
+ messageType,
1629
+ node: context,
1630
+ assignments: [],
1631
+ preconditions: [],
1632
+ postconditions: [],
1633
+ location: { file: filePath, line }
1634
+ };
1635
+ } catch (error) {
1636
+ return null;
1637
+ }
1638
+ }
1639
+ findTypePredicateFunctions(sourceFile) {
1640
+ const typeGuards = new Map;
1641
+ sourceFile.forEachDescendant((node) => {
1642
+ if (Node4.isFunctionDeclaration(node) || Node4.isFunctionExpression(node) || Node4.isArrowFunction(node)) {
1643
+ const returnType = node.getReturnType();
1644
+ const returnTypeText = returnType.getText();
1645
+ if (/is\s+\w+/.test(returnTypeText)) {
1646
+ let functionName;
1647
+ if (Node4.isFunctionDeclaration(node)) {
1648
+ functionName = node.getName();
1649
+ } else if (Node4.isFunctionExpression(node)) {
1650
+ const parent = node.getParent();
1651
+ if (Node4.isVariableDeclaration(parent)) {
1652
+ functionName = parent.getName();
1653
+ }
1654
+ } else if (Node4.isArrowFunction(node)) {
1655
+ const parent = node.getParent();
1656
+ if (Node4.isVariableDeclaration(parent)) {
1657
+ functionName = parent.getName();
1658
+ }
1659
+ }
1660
+ if (functionName) {
1661
+ let messageType = null;
1662
+ const typeMatch = returnTypeText.match(/is\s+(\w+)/);
1663
+ if (typeMatch) {
1664
+ const typeName = typeMatch[1];
1665
+ messageType = this.extractMessageTypeFromTypeName(typeName);
1666
+ }
1667
+ if (!messageType) {
1668
+ const body = node.getBody();
1669
+ if (body) {
1670
+ const bodyText = body.getText();
1671
+ const typeValueMatch = bodyText.match(/\.type\s*===?\s*['"](\w+)['"]/);
1672
+ if (typeValueMatch) {
1673
+ messageType = typeValueMatch[1];
1674
+ }
1675
+ }
1676
+ }
1677
+ if (messageType) {
1678
+ typeGuards.set(functionName, messageType);
1679
+ }
1680
+ }
1681
+ }
1682
+ }
1683
+ });
1684
+ return typeGuards;
1685
+ }
1686
+ extractMessageTypeFromTypeName(typeName) {
1687
+ const messageType = typeName.replace(/Message$/, "").replace(/Event$/, "").replace(/Request$/, "").replace(/Command$/, "").replace(/Query$/, "").toLowerCase();
1688
+ return messageType;
1689
+ }
1320
1690
  inferContext(filePath) {
1321
1691
  const path2 = filePath.toLowerCase();
1322
1692
  if (path2.includes("/background/") || path2.includes("\\background\\")) {
@@ -1337,6 +1707,15 @@ class HandlerExtractor {
1337
1707
  if (path2.includes("/offscreen/") || path2.includes("\\offscreen\\")) {
1338
1708
  return "offscreen";
1339
1709
  }
1710
+ if (path2.includes("/server/") || path2.includes("\\server\\") || path2.includes("/server.")) {
1711
+ return "server";
1712
+ }
1713
+ if (path2.includes("/client/") || path2.includes("\\client\\") || path2.includes("/client.")) {
1714
+ return "client";
1715
+ }
1716
+ if (path2.includes("/worker/") || path2.includes("\\worker\\") || path2.includes("service-worker")) {
1717
+ return "worker";
1718
+ }
1340
1719
  return "unknown";
1341
1720
  }
1342
1721
  }
@@ -1486,10 +1865,8 @@ class ArchitectureAnalyzer {
1486
1865
  let projectConfig;
1487
1866
  let entryPoints = {};
1488
1867
  let systemInfo;
1489
- const manifestPath = path4.join(this.options.projectRoot, "manifest.json");
1490
- const hasManifest = fs4.existsSync(manifestPath);
1491
- if (hasManifest && !this.options.useProjectDetector) {
1492
- const manifestParser = new ManifestParser(this.options.projectRoot);
1868
+ const manifestParser = new ManifestParser(this.options.projectRoot, true);
1869
+ if (manifestParser.hasManifest() && !this.options.useProjectDetector) {
1493
1870
  manifest = manifestParser.parse();
1494
1871
  entryPoints = manifestParser.getContextEntryPoints();
1495
1872
  systemInfo = {
@@ -2256,11 +2633,17 @@ async function generateCommand() {
2256
2633
  console.log(color(` Using: ${tsConfigPath}`, COLORS.gray));
2257
2634
  const projectRoot = findProjectRoot();
2258
2635
  if (!projectRoot) {
2259
- console.error(color("❌ Could not find manifest.json", COLORS.red));
2260
- console.error(" Run this command from your extension project root");
2636
+ console.error(color("❌ Could not find project root", COLORS.red));
2637
+ console.error(" Run this command from a directory with manifest.json, package.json, or tsconfig.json");
2261
2638
  process.exit(1);
2262
2639
  }
2263
2640
  console.log(color(` Project: ${projectRoot}`, COLORS.gray));
2641
+ const hasManifest = fs6.existsSync(path6.join(projectRoot, "manifest.json"));
2642
+ if (hasManifest) {
2643
+ console.log(color(` Type: Chrome Extension`, COLORS.gray));
2644
+ } else {
2645
+ console.log(color(` Type: Detecting from project structure...`, COLORS.gray));
2646
+ }
2264
2647
  const analysis = await analyzeArchitecture({
2265
2648
  tsConfigPath,
2266
2649
  projectRoot
@@ -2447,7 +2830,15 @@ async function serveCommand(args) {
2447
2830
  }
2448
2831
  function showHelp() {
2449
2832
  console.log(`
2450
- ${color("bun visualize", COLORS.blue)} - Architecture visualization for web extensions
2833
+ ${color("bun visualize", COLORS.blue)} - Architecture visualization tool
2834
+
2835
+ ${color("Supports:", COLORS.blue)}
2836
+
2837
+ • Chrome Extensions (manifest.json)
2838
+ • PWAs (public/manifest.json)
2839
+ • WebSocket/Server Apps (ws, socket.io, elysia)
2840
+ • Electron Apps
2841
+ • Generic TypeScript Projects
2451
2842
 
2452
2843
  ${color("Commands:", COLORS.blue)}
2453
2844
 
@@ -2467,14 +2858,14 @@ ${color("Commands:", COLORS.blue)}
2467
2858
 
2468
2859
  ${color("Getting Started:", COLORS.blue)}
2469
2860
 
2470
- 1. Run ${color("bun visualize", COLORS.green)} from your extension project root
2861
+ 1. Run ${color("bun visualize", COLORS.green)} from your project root
2471
2862
  2. Find generated ${color("docs/architecture.dsl", COLORS.blue)}
2472
2863
  3. View with Structurizr Lite (see instructions after generation)
2473
2864
 
2474
2865
  ${color("What gets generated:", COLORS.blue)}
2475
2866
 
2476
- • System Context diagram - Extension + external systems
2477
- • Container diagram - Extension contexts (background, content, popup, etc.)
2867
+ • System Context diagram - Your app + external systems
2868
+ • Container diagram - App contexts (background, content, server, client, etc.)
2478
2869
  • Component diagrams - Internal components within contexts
2479
2870
  • Dynamic diagrams - Message flows between contexts
2480
2871
 
@@ -2500,8 +2891,7 @@ function findTsConfig() {
2500
2891
  function findProjectRoot() {
2501
2892
  const locations = [process.cwd(), path6.join(process.cwd(), "..")];
2502
2893
  for (const loc of locations) {
2503
- const manifestPath = path6.join(loc, "manifest.json");
2504
- if (fs6.existsSync(manifestPath)) {
2894
+ if (fs6.existsSync(path6.join(loc, "manifest.json")) || fs6.existsSync(path6.join(loc, "package.json")) || fs6.existsSync(path6.join(loc, "tsconfig.json"))) {
2505
2895
  return loc;
2506
2896
  }
2507
2897
  }
@@ -2519,4 +2909,4 @@ Stack trace:`, COLORS.gray));
2519
2909
  process.exit(1);
2520
2910
  });
2521
2911
 
2522
- //# debugId=42A0D5368C5C715B64756E2164756E21
2912
+ //# debugId=2DAA9097DE2E4CFB64756E2164756E21