@coze-arch/cli 0.0.5-alpha.5dd55b → 0.0.5-alpha.6339fe

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 (24) hide show
  1. package/lib/__templates__/nuxt-vue/nuxt.config.ts +13 -0
  2. package/lib/__templates__/taro/eslint.config.mjs +2 -8
  3. package/lib/__templates__/taro/package.json +1 -1
  4. package/lib/__templates__/taro/pnpm-lock.yaml +5 -5
  5. package/lib/__templates__/taro/src/app.tsx +1 -6
  6. package/lib/__templates__/taro/src/components/ui/accordion.tsx +1 -1
  7. package/lib/__templates__/taro/src/components/ui/breadcrumb.tsx +1 -1
  8. package/lib/__templates__/taro/src/components/ui/calendar.tsx +4 -4
  9. package/lib/__templates__/taro/src/components/ui/carousel.tsx +2 -2
  10. package/lib/__templates__/taro/src/components/ui/command.tsx +1 -1
  11. package/lib/__templates__/taro/src/components/ui/context-menu.tsx +3 -3
  12. package/lib/__templates__/taro/src/components/ui/dialog.tsx +1 -1
  13. package/lib/__templates__/taro/src/components/ui/dropdown-menu.tsx +3 -3
  14. package/lib/__templates__/taro/src/components/ui/input-otp.tsx +1 -1
  15. package/lib/__templates__/taro/src/components/ui/menubar.tsx +3 -3
  16. package/lib/__templates__/taro/src/components/ui/navigation-menu.tsx +0 -2
  17. package/lib/__templates__/taro/src/components/ui/pagination.tsx +3 -3
  18. package/lib/__templates__/taro/src/components/ui/resizable.tsx +1 -1
  19. package/lib/__templates__/taro/src/components/ui/select.tsx +4 -4
  20. package/lib/__templates__/taro/src/components/ui/sheet.tsx +1 -1
  21. package/lib/__templates__/taro/src/components/ui/toast.tsx +1 -1
  22. package/lib/cli.js +968 -122
  23. package/package.json +2 -1
  24. package/lib/__templates__/taro/components.md +0 -1686
package/lib/cli.js CHANGED
@@ -15,6 +15,7 @@ var fs$1 = require('fs/promises');
15
15
  var os = require('os');
16
16
  var jsYaml = require('js-yaml');
17
17
  var toml = require('@iarna/toml');
18
+ var fastGlob = require('fast-glob');
18
19
  var child_process = require('child_process');
19
20
  var addFormats = require('ajv-formats');
20
21
  var Ajv = require('ajv');
@@ -1490,7 +1491,7 @@ var browserClient = createBaseClient();
1490
1491
 
1491
1492
 
1492
1493
 
1493
- const log$2 = debug('slardar:transport');
1494
+ const log$6 = debug('slardar:transport');
1494
1495
  // eslint-disable-next-line @typescript-eslint/no-empty-function
1495
1496
  const noop = () => {};
1496
1497
 
@@ -1510,22 +1511,22 @@ const noop = () => {};
1510
1511
  function makeRequest(options) {
1511
1512
  const { url, method, data, success = noop, fail = noop, getResponseText = noop } = options;
1512
1513
 
1513
- log$2('Making %s request to %s', method, url);
1514
+ log$6('Making %s request to %s', method, url);
1514
1515
  if (data) {
1515
- log$2('Request data: %s', data.slice(0, 200));
1516
+ log$6('Request data: %s', data.slice(0, 200));
1516
1517
  }
1517
1518
 
1518
1519
  // 检查 URL 是否有效
1519
1520
  if (!url || typeof url !== 'string') {
1520
1521
  const error = new Error(`Invalid URL: ${url}`);
1521
- log$2('Invalid URL provided: %o', url);
1522
+ log$6('Invalid URL provided: %o', url);
1522
1523
  fail(error);
1523
1524
  return;
1524
1525
  }
1525
1526
 
1526
1527
  try {
1527
1528
  const urlObj = new URL(url);
1528
- log$2('Parsed URL - protocol: %s, hostname: %s, port: %s, path: %s',
1529
+ log$6('Parsed URL - protocol: %s, hostname: %s, port: %s, path: %s',
1529
1530
  urlObj.protocol, urlObj.hostname, urlObj.port, urlObj.pathname);
1530
1531
 
1531
1532
  const isHttps = urlObj.protocol === 'https:';
@@ -1543,34 +1544,34 @@ function makeRequest(options) {
1543
1544
  },
1544
1545
  },
1545
1546
  res => {
1546
- log$2('Response callback triggered: status=%s', res.statusCode);
1547
+ log$6('Response callback triggered: status=%s', res.statusCode);
1547
1548
  let responseText = '';
1548
1549
 
1549
1550
  res.on('data', chunk => {
1550
- log$2('Response data chunk received: %s bytes', chunk.length);
1551
+ log$6('Response data chunk received: %s bytes', chunk.length);
1551
1552
  responseText += chunk.toString();
1552
1553
  });
1553
1554
 
1554
1555
  res.on('end', () => {
1555
- log$2('Response end event fired');
1556
- log$2('Response received: status=%s, body=%s', res.statusCode, responseText.slice(0, 200));
1556
+ log$6('Response end event fired');
1557
+ log$6('Response received: status=%s, body=%s', res.statusCode, responseText.slice(0, 200));
1557
1558
  getResponseText(responseText);
1558
1559
 
1559
1560
  try {
1560
1561
  if (res.statusCode && res.statusCode >= 400) {
1561
- log$2('Request failed with status %s: %s', res.statusCode, responseText);
1562
+ log$6('Request failed with status %s: %s', res.statusCode, responseText);
1562
1563
  fail(new Error(responseText || res.statusMessage || 'Request failed'));
1563
1564
  } else if (responseText) {
1564
1565
  // eslint-disable-next-line no-restricted-syntax -- Parsing trusted Slardar API responses
1565
1566
  const result = JSON.parse(responseText);
1566
- log$2('Request succeeded');
1567
+ log$6('Request succeeded');
1567
1568
  success(result);
1568
1569
  } else {
1569
- log$2('Request succeeded with empty response');
1570
+ log$6('Request succeeded with empty response');
1570
1571
  success({});
1571
1572
  }
1572
1573
  } catch (e) {
1573
- log$2('Failed to parse response: %s', (e ).message);
1574
+ log$6('Failed to parse response: %s', (e ).message);
1574
1575
  fail(e );
1575
1576
  }
1576
1577
  });
@@ -1578,12 +1579,12 @@ function makeRequest(options) {
1578
1579
  );
1579
1580
 
1580
1581
  req.on('error', err => {
1581
- log$2('Request error: %s', err.message);
1582
+ log$6('Request error: %s', err.message);
1582
1583
  fail(err);
1583
1584
  });
1584
1585
 
1585
1586
  req.on('timeout', () => {
1586
- log$2('Request timeout');
1587
+ log$6('Request timeout');
1587
1588
  req.destroy();
1588
1589
  fail(new Error('Request timeout'));
1589
1590
  });
@@ -1594,7 +1595,7 @@ function makeRequest(options) {
1594
1595
 
1595
1596
  req.end();
1596
1597
  } catch (e) {
1597
- log$2('Exception during request: %s', (e ).message);
1598
+ log$6('Exception during request: %s', (e ).message);
1598
1599
  fail(e );
1599
1600
  }
1600
1601
  }
@@ -1605,14 +1606,14 @@ function makeRequest(options) {
1605
1606
  function createNodeTransport() {
1606
1607
  return {
1607
1608
  get(options) {
1608
- log$2('Transport GET called: %s', options.url);
1609
+ log$6('Transport GET called: %s', options.url);
1609
1610
  makeRequest({
1610
1611
  method: 'GET',
1611
1612
  ...options,
1612
1613
  });
1613
1614
  },
1614
1615
  post({ url, data }) {
1615
- log$2('Transport POST called: %s', url);
1616
+ log$6('Transport POST called: %s', url);
1616
1617
  makeRequest({
1617
1618
  method: 'POST',
1618
1619
  url,
@@ -1633,7 +1634,7 @@ function _nullishCoalesce$3(lhs, rhsFn) { if (lhs != null) { return lhs; } else
1633
1634
 
1634
1635
  // 创建 debug 实例
1635
1636
  // 使用方式: DEBUG=slardar:* your-cli-command
1636
- const log$1 = debug('slardar:reporter');
1637
+ const log$5 = debug('slardar:reporter');
1637
1638
 
1638
1639
  /**
1639
1640
  * Slardar CLI Reporter
@@ -1658,12 +1659,12 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1658
1659
  */
1659
1660
  setup(config) {
1660
1661
  if (this.initialized) {
1661
- log$1('Already initialized, skipping setup');
1662
+ log$5('Already initialized, skipping setup');
1662
1663
  return;
1663
1664
  }
1664
1665
 
1665
1666
  try {
1666
- log$1('Initializing Slardar with config:', {
1667
+ log$5('Initializing Slardar with config:', {
1667
1668
  bid: config.bid,
1668
1669
  release: config.release,
1669
1670
  env: config.env,
@@ -1696,7 +1697,7 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1696
1697
 
1697
1698
  // 添加 send 钩子来追踪事件上报
1698
1699
  this.client.on('send', (ev) => {
1699
- log$1(
1700
+ log$5(
1700
1701
  'send hook called for event: %s',
1701
1702
  (_optionalChain$6([ev, 'optionalAccess', _ => _.ev_type]) ) || 'unknown',
1702
1703
  );
@@ -1704,9 +1705,9 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1704
1705
 
1705
1706
  this.client.start();
1706
1707
  this.initialized = true;
1707
- log$1('Slardar initialized successfully');
1708
+ log$5('Slardar initialized successfully');
1708
1709
  } catch (error) {
1709
- log$1('Failed to initialize:', error);
1710
+ log$5('Failed to initialize:', error);
1710
1711
  }
1711
1712
  }
1712
1713
 
@@ -1715,7 +1716,7 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1715
1716
  */
1716
1717
  ensureInitialized() {
1717
1718
  if (!this.initialized) {
1718
- log$1('Not initialized, call setup() first');
1719
+ log$5('Not initialized, call setup() first');
1719
1720
  return false;
1720
1721
  }
1721
1722
  return true;
@@ -1737,7 +1738,7 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1737
1738
  }
1738
1739
 
1739
1740
  try {
1740
- log$1('Sending event:', { name, metrics, categories });
1741
+ log$5('Sending event:', { name, metrics, categories });
1741
1742
 
1742
1743
  // 过滤掉 undefined 值以满足 Slardar 类型要求
1743
1744
  const cleanMetrics = metrics
@@ -1762,7 +1763,7 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1762
1763
  categories: cleanCategories ,
1763
1764
  })]);
1764
1765
  } catch (error) {
1765
- log$1('Failed to send event:', error);
1766
+ log$5('Failed to send event:', error);
1766
1767
  }
1767
1768
  }
1768
1769
 
@@ -1782,7 +1783,7 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1782
1783
  }
1783
1784
 
1784
1785
  try {
1785
- log$1('Sending log:', { level, message, extra });
1786
+ log$5('Sending log:', { level, message, extra });
1786
1787
  // 使用 sendEvent 发送日志事件
1787
1788
  this.sendEvent('cli_log', undefined, {
1788
1789
  level,
@@ -1790,7 +1791,7 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1790
1791
  ...extra,
1791
1792
  });
1792
1793
  } catch (error) {
1793
- log$1('Failed to send log:', error);
1794
+ log$5('Failed to send log:', error);
1794
1795
  }
1795
1796
  }
1796
1797
 
@@ -1824,7 +1825,7 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1824
1825
  },
1825
1826
  };
1826
1827
 
1827
- log$1('Reporting JS error:', {
1828
+ log$5('Reporting JS error:', {
1828
1829
  name: error.name,
1829
1830
  message: error.message.slice(0, 100),
1830
1831
  stack: _optionalChain$6([error, 'access', _5 => _5.stack, 'optionalAccess', _6 => _6.slice, 'call', _7 => _7(0, 200)]),
@@ -1834,9 +1835,9 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1834
1835
 
1835
1836
  // 使用 Slardar 的 js_error 事件类型,这样会显示在 JS 错误总览页面
1836
1837
  this.client.report(errorEvent);
1837
- log$1('JS error reported successfully');
1838
+ log$5('JS error reported successfully');
1838
1839
  } catch (err) {
1839
- log$1('Failed to report error:', err);
1840
+ log$5('Failed to report error:', err);
1840
1841
  }
1841
1842
  }
1842
1843
 
@@ -1857,7 +1858,7 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1857
1858
  if (!this.ensureInitialized()) {
1858
1859
  return;
1859
1860
  }
1860
- log$1('Merging context:', context);
1861
+ log$5('Merging context:', context);
1861
1862
  _optionalChain$6([this, 'access', _12 => _12.client, 'access', _13 => _13.context, 'optionalAccess', _14 => _14.merge, 'call', _15 => _15(context)]);
1862
1863
  }
1863
1864
 
@@ -1894,7 +1895,7 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1894
1895
  }
1895
1896
 
1896
1897
  try {
1897
- log$1('Updating config:', config);
1898
+ log$5('Updating config:', config);
1898
1899
  this.client.config({
1899
1900
  userId: config.userId,
1900
1901
  release: config.release,
@@ -1902,7 +1903,7 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1902
1903
  env: config.env,
1903
1904
  });
1904
1905
  } catch (error) {
1905
- log$1('Failed to update config:', error);
1906
+ log$5('Failed to update config:', error);
1906
1907
  }
1907
1908
  }
1908
1909
 
@@ -1914,11 +1915,11 @@ class SlardarCLIReporter {constructor() { SlardarCLIReporter.prototype.__init.ca
1914
1915
  if (!this.ensureInitialized()) {
1915
1916
  return;
1916
1917
  }
1917
- log$1('Flushing Slardar data...');
1918
+ log$5('Flushing Slardar data...');
1918
1919
  _optionalChain$6([this, 'access', _24 => _24.client, 'access', _25 => _25.getSender, 'optionalCall', _26 => _26(), 'optionalAccess', _27 => _27.flush, 'call', _28 => _28()]);
1919
- log$1('Waiting %dms for events to be sent...', waitMs);
1920
+ log$5('Waiting %dms for events to be sent...', waitMs);
1920
1921
  await new Promise(resolve => setTimeout(resolve, waitMs));
1921
- log$1('Slardar data flushed');
1922
+ log$5('Slardar data flushed');
1922
1923
  }
1923
1924
 
1924
1925
  /**
@@ -2105,7 +2106,7 @@ const EventBuilder = {
2105
2106
  };
2106
2107
 
2107
2108
  var name = "@coze-arch/cli";
2108
- var version = "0.0.5-alpha.5dd55b";
2109
+ var version = "0.0.5-alpha.6339fe";
2109
2110
  var description = "coze coding devtools cli";
2110
2111
  var license = "MIT";
2111
2112
  var author = "fanwenjie.fe@bytedance.com";
@@ -2145,6 +2146,7 @@ var dependencies = {
2145
2146
  commander: "~12.1.0",
2146
2147
  debug: "^4.3.7",
2147
2148
  ejs: "^3.1.10",
2149
+ "fast-glob": "^3.3.3",
2148
2150
  "js-yaml": "^4.1.0",
2149
2151
  minimist: "^1.2.5",
2150
2152
  shelljs: "^0.10.0"
@@ -2209,7 +2211,7 @@ function _optionalChain$4(ops) { let lastAccessLHS = undefined; let value = ops[
2209
2211
  * Slardar 监控初始化和上报
2210
2212
  */
2211
2213
 
2212
- const log = debug('slardar:cli');
2214
+ const log$4 = debug('slardar:cli');
2213
2215
 
2214
2216
  /**
2215
2217
  * 安全执行函数包装器
@@ -2222,18 +2224,18 @@ function safeRun(
2222
2224
  ) {
2223
2225
  return ((...args) => {
2224
2226
  try {
2225
- log('Calling Slardar function: %s', name);
2227
+ log$4('Calling Slardar function: %s', name);
2226
2228
  const result = fn(...args);
2227
2229
 
2228
2230
  // 如果是 Promise,处理异步错误
2229
2231
  if (result instanceof Promise) {
2230
2232
  return result
2231
2233
  .then(res => {
2232
- log('Slardar function %s completed', name);
2234
+ log$4('Slardar function %s completed', name);
2233
2235
  return res;
2234
2236
  })
2235
2237
  .catch(error => {
2236
- log(
2238
+ log$4(
2237
2239
  'Slardar function %s failed: %s',
2238
2240
  name,
2239
2241
  (error ).message,
@@ -2242,11 +2244,11 @@ function safeRun(
2242
2244
  });
2243
2245
  }
2244
2246
 
2245
- log('Slardar function %s completed', name);
2247
+ log$4('Slardar function %s completed', name);
2246
2248
  return result;
2247
2249
  } catch (error) {
2248
2250
  // Slardar 上报失败不应影响 CLI 正常运行,但要记录错误
2249
- log('Slardar function %s failed: %s', name, (error ).message);
2251
+ log$4('Slardar function %s failed: %s', name, (error ).message);
2250
2252
  }
2251
2253
  }) ;
2252
2254
  }
@@ -2810,6 +2812,76 @@ const registerCommand$4 = program => {
2810
2812
  });
2811
2813
  };
2812
2814
 
2815
+ // ABOUTME: Temporary paths utility for Coze CLI
2816
+ // ABOUTME: Provides centralized management of temporary directories and files
2817
+
2818
+
2819
+ /**
2820
+ * Base Coze directory in user's home
2821
+ * Used for storing CLI-related temporary files, logs, and cache
2822
+ * Default: ~/.coze
2823
+ */
2824
+ const COZE_HOME_DIR = '.coze';
2825
+
2826
+ /**
2827
+ * Logs directory name
2828
+ * Default: .coze-logs
2829
+ */
2830
+ const LOGS_DIR = '.coze-logs';
2831
+
2832
+ /**
2833
+ * Get the Coze home directory path
2834
+ * Can be customized via COZE_HOME environment variable
2835
+ *
2836
+ * @returns Absolute path to Coze home directory (default: ~/.coze)
2837
+ */
2838
+ const getCozeHome = () =>
2839
+ process.env.COZE_HOME || path.join(os.homedir(), COZE_HOME_DIR);
2840
+
2841
+ /**
2842
+ * Get the logs directory path
2843
+ * Can be customized via COZE_LOG_DIR environment variable
2844
+ *
2845
+ * @returns Absolute path to logs directory (default: ~/.coze-logs)
2846
+ */
2847
+ const getCozeLogsDir = () =>
2848
+ process.env.COZE_LOG_DIR || path.join(os.homedir(), LOGS_DIR);
2849
+
2850
+ /**
2851
+ * Get path for a specific file/directory within Coze home
2852
+ *
2853
+ * @param relativePath - Relative path within Coze home directory
2854
+ * @returns Absolute path to the file/directory
2855
+ *
2856
+ * @example
2857
+ * ```ts
2858
+ * // Get routes file path
2859
+ * const routesPath = getCozeFilePath('routes.json');
2860
+ * // Returns: ~/.coze/routes.json
2861
+ *
2862
+ * // Get cache directory
2863
+ * const cacheDir = getCozeFilePath('cache');
2864
+ * // Returns: ~/.coze/cache
2865
+ * ```
2866
+ */
2867
+ const getCozeFilePath = (relativePath) =>
2868
+ path.join(getCozeHome(), relativePath);
2869
+
2870
+ /**
2871
+ * Get path for a log file
2872
+ *
2873
+ * @param filename - Log file name
2874
+ * @returns Absolute path to the log file
2875
+ *
2876
+ * @example
2877
+ * ```ts
2878
+ * const devLog = getCozeLogPath('dev.log');
2879
+ * // Returns: ~/.coze-logs/dev.log
2880
+ * ```
2881
+ */
2882
+ const getCozeLogPath = (filename) =>
2883
+ path.join(getCozeLogsDir(), filename);
2884
+
2813
2885
  // ABOUTME: This file implements the update command for coze CLI
2814
2886
  // ABOUTME: It wraps pnpm update/install to update package dependencies with logging support
2815
2887
 
@@ -2820,29 +2892,22 @@ const registerCommand$4 = program => {
2820
2892
  */
2821
2893
  const LOG_FILE_NAME$1 = 'update.log';
2822
2894
 
2823
- /**
2824
- * 获取日志目录
2825
- * 优先使用环境变量 COZE_LOG_DIR,否则使用 ~/.coze-logs
2826
- */
2827
- const getLogDir$1 = () =>
2828
- process.env.COZE_LOG_DIR || path.join(os.homedir(), '.coze-logs');
2829
-
2830
2895
  /**
2831
2896
  * 解析日志文件路径
2832
2897
  * - 如果是绝对路径,直接使用
2833
- * - 如果是相对路径,基于 getLogDir() + 相对路径
2834
- * - 如果为空,使用 getLogDir() + LOG_FILE_NAME
2898
+ * - 如果是相对路径,基于 logs 目录 + 相对路径
2899
+ * - 如果为空,使用默认日志文件路径
2835
2900
  */
2836
2901
  const resolveLogFilePath$1 = (logFile) => {
2837
2902
  if (!logFile) {
2838
- return path.join(getLogDir$1(), LOG_FILE_NAME$1);
2903
+ return getCozeLogPath(LOG_FILE_NAME$1);
2839
2904
  }
2840
2905
 
2841
2906
  if (path.isAbsolute(logFile)) {
2842
2907
  return logFile;
2843
2908
  }
2844
2909
 
2845
- return path.join(getLogDir$1(), logFile);
2910
+ return getCozeLogPath(logFile);
2846
2911
  };
2847
2912
 
2848
2913
  /**
@@ -3016,6 +3081,7 @@ const handleUpdateError = (
3016
3081
  /**
3017
3082
  * 执行 update 命令的内部实现
3018
3083
  */
3084
+ // eslint-disable-next-line max-lines-per-function
3019
3085
  const executeUpdate = (
3020
3086
  packageName,
3021
3087
  options
@@ -3629,35 +3695,805 @@ const registerCommand$2 = program => {
3629
3695
  });
3630
3696
  };
3631
3697
 
3632
- function _nullishCoalesce$1(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
3698
+ // ABOUTME: Nuxt route scanner
3699
+ // ABOUTME: Scans Nuxt file-based routing structure to extract page and server routes
3700
+
3701
+
3702
+
3703
+
3704
+ const log$3 = debug('coze:route-scanner:nuxt');
3705
+
3633
3706
  /**
3634
- * 日志文件名常量
3707
+ * Convert Nuxt file path to route path
3708
+ * Examples:
3709
+ * - app/pages/index.vue -> /
3710
+ * - pages/index.vue -> /
3711
+ * - app/pages/blog/index.vue -> /blog
3712
+ * - pages/blog/[id].vue -> /blog/[id]
3713
+ * - server/api/users.ts -> /api/users
3714
+ * - server/routes/webhook.ts -> /webhook
3635
3715
  */
3636
- const LOG_FILE_NAME = 'dev.log';
3716
+ const nuxtFilePathToRoute = (
3717
+ filePath,
3718
+ type,
3719
+ ) => {
3720
+ let route;
3721
+
3722
+ if (type === 'page') {
3723
+ // Remove pages prefix (supports both app/pages/ and pages/) and .vue extension
3724
+ route = filePath
3725
+ .replace(/^app\/pages\//, '')
3726
+ .replace(/^pages\//, '')
3727
+ .replace(/\.vue$/, '')
3728
+ .replace(/\/index$/, ''); // Remove trailing /index
3729
+
3730
+ // Handle index.vue at root (becomes empty string)
3731
+ if (route === 'index') {
3732
+ return '/';
3733
+ }
3734
+ } else {
3735
+ // API routes
3736
+ if (filePath.startsWith('server/api/')) {
3737
+ route = `/api/${filePath
3738
+ .replace(/^server\/api\//, '')
3739
+ .replace(/\.(ts|js)$/, '')}`;
3740
+ } else {
3741
+ // server/routes
3742
+ route = `/${filePath
3743
+ .replace(/^server\/routes\//, '')
3744
+ .replace(/\.(ts|js)$/, '')}`;
3745
+ }
3746
+ }
3747
+
3748
+ // Handle root route
3749
+ if (route === '' || route === '/') {
3750
+ return '/';
3751
+ }
3752
+
3753
+ // Ensure leading slash
3754
+ if (!route.startsWith('/')) {
3755
+ route = `/${route}`;
3756
+ }
3757
+
3758
+ return route;
3759
+ };
3637
3760
 
3638
3761
  /**
3639
- * 获取日志目录
3640
- * 优先使用环境变量 COZE_LOG_DIR,否则使用 ~/.coze-logs
3762
+ * Normalize Nuxt route path to standard format
3763
+ * Converts Nuxt dynamic segments to standard format:
3764
+ * - [id] -> :id
3765
+ * - [...slug] -> :slug*
3641
3766
  */
3642
- const getLogDir = () =>
3643
- process.env.COZE_LOG_DIR || path.join(os.homedir(), '.coze-logs');
3767
+ const normalizeRoutePath$1 = (routePath) =>
3768
+ routePath
3769
+ .replace(/\[\.\.\.(\w+)\]/g, ':$1*') // [...slug] -> :slug*
3770
+ .replace(/\[(\w+)\]/g, ':$1'); // [id] -> :id
3771
+ /**
3772
+ * Extract dynamic parameter names from route path
3773
+ */
3774
+ const extractParams$1 = (routePath) => {
3775
+ const params = [];
3776
+ const patterns = [
3777
+ /\[\.\.\.(\w+)\]/g, // [...slug]
3778
+ /\[(\w+)\]/g, // [id]
3779
+ ];
3780
+
3781
+ patterns.forEach(pattern => {
3782
+ const matches = routePath.matchAll(pattern);
3783
+ for (const match of matches) {
3784
+ params.push(match[1]);
3785
+ }
3786
+ });
3787
+
3788
+ return params;
3789
+ };
3790
+
3791
+ /**
3792
+ * Scan Nuxt routes from pages and server directories
3793
+ *
3794
+ * Scans for:
3795
+ * - app/pages/**\/*.vue OR pages/**\/*.vue - Page routes (both structures supported)
3796
+ * - server/api/**\/*.{ts,js} - API routes
3797
+ * - server/routes/**\/*.{ts,js} - Server routes
3798
+ *
3799
+ * Excludes:
3800
+ * - app/layouts/, layouts/ - Layout components (NOT routes)
3801
+ * - app.vue, error.vue - Special files (NOT regular routes)
3802
+ *
3803
+ * @param cwd - Current working directory
3804
+ * @returns List of detected routes
3805
+ */
3806
+ const scanNuxtRoutes = async (cwd) => {
3807
+ log$3('Scanning Nuxt routes in:', cwd);
3808
+ const routes = [];
3809
+
3810
+ // Scan page routes - support both app/pages/ and pages/ directories
3811
+ const pageFiles = await fastGlob.glob(['{app/,}pages/**/*.vue'], {
3812
+ cwd,
3813
+ ignore: [
3814
+ '**/node_modules/**',
3815
+ '**/.nuxt/**',
3816
+ '**/dist/**',
3817
+ '**/.output/**',
3818
+ // Exclude layouts directory (not routes)
3819
+ '**/layouts/**',
3820
+ 'app/layouts/**',
3821
+ // Exclude special files
3822
+ 'app.vue',
3823
+ 'error.vue',
3824
+ ],
3825
+ onlyFiles: true,
3826
+ followSymbolicLinks: false,
3827
+ });
3828
+
3829
+ log$3('Found %d page files', pageFiles.length);
3830
+
3831
+ routes.push(
3832
+ ...pageFiles.map(file => {
3833
+ const routePath = nuxtFilePathToRoute(file, 'page');
3834
+ const isDynamic = routePath.includes('[');
3835
+ const params = extractParams$1(routePath);
3836
+
3837
+ return {
3838
+ path: normalizeRoutePath$1(routePath),
3839
+ rawPath: routePath,
3840
+ dynamic: isDynamic,
3841
+ params,
3842
+ type: 'page' ,
3843
+ file,
3844
+ framework: 'nuxt' ,
3845
+ };
3846
+ }),
3847
+ );
3848
+
3849
+ // Scan API routes
3850
+ const apiFiles = await fastGlob.glob('server/api/**/*.{ts,js}', {
3851
+ cwd,
3852
+ ignore: ['**/node_modules/**', '**/dist/**', '**/.output/**'],
3853
+ onlyFiles: true,
3854
+ followSymbolicLinks: false,
3855
+ });
3856
+
3857
+ log$3('Found %d API files', apiFiles.length);
3858
+
3859
+ routes.push(
3860
+ ...apiFiles.map(file => {
3861
+ const routePath = nuxtFilePathToRoute(file, 'api');
3862
+ const isDynamic = routePath.includes('[');
3863
+ const params = extractParams$1(routePath);
3864
+
3865
+ return {
3866
+ path: normalizeRoutePath$1(routePath),
3867
+ rawPath: routePath,
3868
+ dynamic: isDynamic,
3869
+ params,
3870
+ type: 'api' ,
3871
+ file,
3872
+ framework: 'nuxt' ,
3873
+ };
3874
+ }),
3875
+ );
3876
+
3877
+ // Scan server routes
3878
+ const serverRouteFiles = await fastGlob.glob('server/routes/**/*.{ts,js}', {
3879
+ cwd,
3880
+ ignore: ['**/node_modules/**', '**/dist/**', '**/.output/**'],
3881
+ onlyFiles: true,
3882
+ followSymbolicLinks: false,
3883
+ });
3884
+
3885
+ log$3('Found %d server route files', serverRouteFiles.length);
3886
+
3887
+ routes.push(
3888
+ ...serverRouteFiles.map(file => {
3889
+ const routePath = nuxtFilePathToRoute(file, 'api');
3890
+ const isDynamic = routePath.includes('[');
3891
+ const params = extractParams$1(routePath);
3892
+
3893
+ return {
3894
+ path: normalizeRoutePath$1(routePath),
3895
+ rawPath: routePath,
3896
+ dynamic: isDynamic,
3897
+ params,
3898
+ type: 'api' ,
3899
+ file,
3900
+ framework: 'nuxt' ,
3901
+ };
3902
+ }),
3903
+ );
3904
+
3905
+ log$3('Total routes found: %d', routes.length);
3906
+ return routes;
3907
+ };
3908
+
3909
+ // ABOUTME: Next.js route scanner
3910
+ // ABOUTME: Scans Next.js App Router file structure to extract routes
3911
+
3912
+
3913
+
3914
+
3915
+ const log$2 = debug('coze:route-scanner:nextjs');
3916
+
3917
+ /**
3918
+ * Convert Next.js file path to route path
3919
+ * Examples:
3920
+ * - src/app/page.tsx -> /
3921
+ * - src/app/blog/page.tsx -> /blog
3922
+ * - src/app/blog/[id]/page.tsx -> /blog/[id]
3923
+ * - src/app/api/users/route.ts -> /api/users
3924
+ */
3925
+ const filePathToRoute = (filePath) => {
3926
+ // Remove src/app prefix and file extension
3927
+ let route = filePath
3928
+ .replace(/^src\/app/, '')
3929
+ .replace(/\/(page|route|layout)\.(tsx?|jsx?)$/, '');
3930
+
3931
+ // Handle root route
3932
+ if (route === '' || route === '/') {
3933
+ return '/';
3934
+ }
3935
+
3936
+ // Ensure leading slash
3937
+ if (!route.startsWith('/')) {
3938
+ route = `/${route}`;
3939
+ }
3940
+
3941
+ return route;
3942
+ };
3943
+
3944
+ /**
3945
+ * Normalize route path to standard format
3946
+ * Converts Next.js dynamic segments to standard format:
3947
+ * - [id] -> :id
3948
+ * - [...slug] -> :slug*
3949
+ * - [[...slug]] -> :slug*?
3950
+ */
3951
+ const normalizeRoutePath = (routePath) =>
3952
+ routePath
3953
+ .replace(/\[\[\.\.\.(\w+)\]\]/g, ':$1*?') // [[...slug]] -> :slug*?
3954
+ .replace(/\[\.\.\.(\w+)\]/g, ':$1*') // [...slug] -> :slug*
3955
+ .replace(/\[(\w+)\]/g, ':$1'); // [id] -> :id
3956
+ /**
3957
+ * Extract dynamic parameter names from route path
3958
+ * Note: Uses Set to avoid duplicates when patterns overlap (e.g., [[...slug]])
3959
+ */
3960
+ const extractParams = (routePath) => {
3961
+ const paramsSet = new Set();
3962
+ const patterns = [
3963
+ /\[\[\.\.\.(\w+)\]\]/g, // [[...slug]]
3964
+ /\[\.\.\.(\w+)\]/g, // [...slug]
3965
+ /\[(\w+)\]/g, // [id]
3966
+ ];
3967
+
3968
+ patterns.forEach(pattern => {
3969
+ const matches = routePath.matchAll(pattern);
3970
+ for (const match of matches) {
3971
+ paramsSet.add(match[1]);
3972
+ }
3973
+ });
3974
+
3975
+ return Array.from(paramsSet);
3976
+ };
3977
+
3978
+ /**
3979
+ * Determine route type based on file name
3980
+ */
3981
+ const getRouteType = (filePath) => {
3982
+ if (filePath.includes('/route.')) {
3983
+ return 'api';
3984
+ }
3985
+ return 'page';
3986
+ };
3987
+
3988
+ /**
3989
+ * Scan Next.js routes from src/app directory
3990
+ *
3991
+ * Scans for:
3992
+ * - page.tsx/jsx/ts/js - Page routes
3993
+ * - route.ts/js - API routes
3994
+ *
3995
+ * Note: layout.tsx, loading.tsx, error.tsx are NOT routes, they are UI components
3996
+ *
3997
+ * @param cwd - Current working directory
3998
+ * @returns List of detected routes
3999
+ */
4000
+ const scanNextjsRoutes = async (cwd) => {
4001
+ log$2('Scanning Next.js routes in:', cwd);
4002
+
4003
+ // Scan for page and route files only (NOT layout, loading, error, etc.)
4004
+ const files = await fastGlob.glob('src/app/**/{page,route}.{tsx,ts,jsx,js}', {
4005
+ cwd,
4006
+ ignore: [
4007
+ '**/node_modules/**',
4008
+ '**/.next/**',
4009
+ '**/dist/**',
4010
+ '**/build/**',
4011
+ '**/.turbo/**',
4012
+ ],
4013
+ onlyFiles: true,
4014
+ followSymbolicLinks: false,
4015
+ });
4016
+
4017
+ log$2('Found %d files', files.length);
4018
+
4019
+ return files.map(file => {
4020
+ log$2('Processing file:', file);
4021
+ const routePath = filePathToRoute(file);
4022
+ const isDynamic = routePath.includes('[');
4023
+ const params = extractParams(routePath);
4024
+
4025
+ return {
4026
+ path: normalizeRoutePath(routePath),
4027
+ rawPath: routePath,
4028
+ dynamic: isDynamic,
4029
+ params,
4030
+ type: getRouteType(file),
4031
+ file,
4032
+ framework: 'nextjs',
4033
+ };
4034
+ });
4035
+ };
4036
+
4037
+ // ABOUTME: Route manifest file writer
4038
+ // ABOUTME: Writes route data to JSON file with proper formatting and directory creation
4039
+
4040
+
4041
+
4042
+
4043
+ /**
4044
+ * Calculate route statistics
4045
+ */
4046
+ const calculateStats = (routes) => {
4047
+ const dynamic = routes.filter(r => r.dynamic).length;
4048
+ const staticCount = routes.length - dynamic;
4049
+
4050
+ return {
4051
+ total: routes.length,
4052
+ dynamic,
4053
+ static: staticCount,
4054
+ };
4055
+ };
4056
+
4057
+ /**
4058
+ * Resolve output file path
4059
+ * - Relative path: resolve based on cwd
4060
+ * - Absolute path: use as-is
4061
+ */
4062
+ const resolveOutputPath = (cwd, outputPath) =>
4063
+ path.isAbsolute(outputPath) ? outputPath : path.join(cwd, outputPath);
4064
+
4065
+ /**
4066
+ * Ensure parent directory exists
4067
+ * Handles edge cases like existing files with same name
4068
+ */
4069
+ const ensureDir$1 = (filePath) => {
4070
+ const dir = path.dirname(filePath);
4071
+
4072
+ // If directory already exists, verify it's actually a directory
4073
+ if (fs.existsSync(dir)) {
4074
+ const stats = fs.statSync(dir);
4075
+ if (!stats.isDirectory()) {
4076
+ throw new Error(`Path exists but is not a directory: ${dir}`);
4077
+ }
4078
+ return; // Directory exists and is valid
4079
+ }
4080
+
4081
+ // Create directory with recursive option (equivalent to mkdir -p)
4082
+ fs.mkdirSync(dir, { recursive: true });
4083
+ };
4084
+
4085
+ // JSON.stringify indent size
4086
+ const JSON_INDENT = 2;
4087
+
4088
+ /**
4089
+ * Write route manifest to JSON file
4090
+ *
4091
+ * @param cwd - Current working directory (not used when outputPath is absolute)
4092
+ * @param routes - List of routes to write
4093
+ * @param templateType - Framework type
4094
+ * @param outputPath - Output file path (absolute or relative to cwd)
4095
+ */
4096
+ const writeRoutesFile = (
4097
+ cwd,
4098
+ routes,
4099
+ templateType,
4100
+ outputPath,
4101
+ ) => {
4102
+ try {
4103
+ const resolvedPath = resolveOutputPath(cwd, outputPath);
4104
+
4105
+ // Ensure parent directory exists
4106
+ ensureDir$1(resolvedPath);
4107
+
4108
+ // Build manifest
4109
+ const manifest = {
4110
+ framework: templateType,
4111
+ generatedAt: new Date().toISOString(),
4112
+ routes,
4113
+ stats: calculateStats(routes),
4114
+ };
4115
+
4116
+ // Write to file with pretty formatting
4117
+ fs.writeFileSync(
4118
+ resolvedPath,
4119
+ JSON.stringify(manifest, null, JSON_INDENT),
4120
+ 'utf-8',
4121
+ );
4122
+ } catch (error) {
4123
+ // Wrap error with context for better debugging
4124
+ const errorMessage = error instanceof Error ? error.message : String(error);
4125
+ throw new Error(`Failed to write routes file: ${errorMessage}`);
4126
+ }
4127
+ };
4128
+
4129
+ /**
4130
+ * Get the absolute path of the output file
4131
+ * Useful for logging and testing
4132
+ */
4133
+ const getOutputFilePath = (cwd, outputPath) =>
4134
+ resolveOutputPath(cwd, outputPath);
4135
+
4136
+ // ABOUTME: Template type detection logic
4137
+ // ABOUTME: Detects framework type by checking config files and package.json dependencies
4138
+
4139
+
4140
+
4141
+
4142
+ const log$1 = debug('coze:route-scanner:detect');
4143
+
4144
+ /**
4145
+ * Check if any of the specified files exist
4146
+ */
4147
+ const hasFile = (cwd, ...files) =>
4148
+ files.some(file => fs.existsSync(path.join(cwd, file)));
4149
+
4150
+ /**
4151
+ * Detect template type by checking config files and package.json dependencies
4152
+ *
4153
+ * Detection rules (priority order):
4154
+ * 1. Next.js (highest) - has `next` dependency + next.config.{js,mjs,ts}
4155
+ * 2. Nuxt - has `nuxt` dependency + nuxt.config.{ts,js,mjs}
4156
+ * 3. Vite (lowest) - has `vite` + `express` dependencies + vite.config.{ts,js,mjs}
4157
+ *
4158
+ * @param cwd - Current working directory
4159
+ * @returns Template type or null if detection fails
4160
+ */
4161
+ const detectTemplate = (cwd) => {
4162
+ log$1('Detecting template type in:', cwd);
4163
+
4164
+ const pkgPath = path.join(cwd, 'package.json');
4165
+
4166
+ if (!fs.existsSync(pkgPath)) {
4167
+ log$1('package.json not found');
4168
+ return null;
4169
+ }
4170
+
4171
+ try {
4172
+ const pkgContent = fs.readFileSync(pkgPath, 'utf-8');
4173
+ // eslint-disable-next-line no-restricted-syntax
4174
+ const pkg = JSON.parse(pkgContent)
4175
+
4176
+
4177
+ ;
4178
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
4179
+ log$1('Dependencies found:', Object.keys(deps).join(', '));
4180
+
4181
+ // 1. Next.js - highest priority
4182
+ const hasNextDep = !!deps.next;
4183
+ const hasNextConfig = hasFile(
4184
+ cwd,
4185
+ 'next.config.js',
4186
+ 'next.config.mjs',
4187
+ 'next.config.ts',
4188
+ );
4189
+ log$1('Next.js check - dependency:', hasNextDep, 'config:', hasNextConfig);
4190
+
4191
+ if (hasNextDep && hasNextConfig) {
4192
+ log$1('✓ Detected: nextjs');
4193
+ return 'nextjs';
4194
+ }
4195
+
4196
+ // 2. Nuxt - second priority
4197
+ const hasNuxtDep = !!deps.nuxt;
4198
+ const hasNuxtConfig = hasFile(
4199
+ cwd,
4200
+ 'nuxt.config.ts',
4201
+ 'nuxt.config.js',
4202
+ 'nuxt.config.mjs',
4203
+ );
4204
+ log$1('Nuxt check - dependency:', hasNuxtDep, 'config:', hasNuxtConfig);
4205
+
4206
+ if (hasNuxtDep && hasNuxtConfig) {
4207
+ log$1('✓ Detected: nuxt');
4208
+ return 'nuxt';
4209
+ }
4210
+
4211
+ // 3. Vite - lowest priority (requires both vite and express)
4212
+ const hasViteDep = !!deps.vite;
4213
+ const hasExpressDep = !!deps.express;
4214
+ const hasViteConfig = hasFile(
4215
+ cwd,
4216
+ 'vite.config.ts',
4217
+ 'vite.config.js',
4218
+ 'vite.config.mjs',
4219
+ );
4220
+ log$1(
4221
+ 'Vite check - vite dep:',
4222
+ hasViteDep,
4223
+ 'express dep:',
4224
+ hasExpressDep,
4225
+ 'config:',
4226
+ hasViteConfig,
4227
+ );
4228
+
4229
+ if (hasViteDep && hasExpressDep && hasViteConfig) {
4230
+ log$1('✓ Detected: vite');
4231
+ return 'vite';
4232
+ }
4233
+
4234
+ log$1('✗ No template detected');
4235
+ return null;
4236
+ } catch (error) {
4237
+ // Invalid package.json or other errors
4238
+ log$1('Error during detection:', error);
4239
+ return null;
4240
+ }
4241
+ };
4242
+
4243
+ // ABOUTME: Route scanner main entry point
4244
+ // ABOUTME: Orchestrates template detection, route scanning, and polling-based updates
4245
+
4246
+
4247
+ const log = debug('coze:route-scanner:main');
4248
+
4249
+ // Polling interval in milliseconds (500ms to balance responsiveness and performance)
4250
+ const POLLING_INTERVAL_MS = 500;
4251
+
4252
+ // Store polling interval for cleanup
4253
+ let pollingInterval = null;
4254
+ let previousRoutesHash = null;
4255
+
4256
+ /**
4257
+ * Scan routes based on template type
4258
+ */
4259
+ const scanRoutes = async (
4260
+ cwd,
4261
+ templateType,
4262
+ ) => {
4263
+ switch (templateType) {
4264
+ case 'nextjs':
4265
+ return scanNextjsRoutes(cwd);
4266
+ case 'nuxt':
4267
+ return scanNuxtRoutes(cwd);
4268
+ case 'vite':
4269
+ return [];
4270
+ default:
4271
+ return [];
4272
+ }
4273
+ };
4274
+
4275
+ /**
4276
+ * Generate a simple hash for routes to detect changes
4277
+ */
4278
+ const hashRoutes = (routes) =>
4279
+ routes
4280
+ .map(r => `${r.file}:${r.type}`)
4281
+ .sort()
4282
+ .join('|');
4283
+
4284
+ /**
4285
+ * Detect and validate template type
4286
+ * @returns Template type or null if invalid/unsupported
4287
+ */
4288
+ const detectAndValidateTemplate = (cwd) => {
4289
+ log('Detecting template type');
4290
+ const templateType = detectTemplate(cwd);
4291
+
4292
+ if (!templateType) {
4293
+ log('No template type detected');
4294
+ logger.warn('Unable to detect template type, skipping route scanning');
4295
+ return null;
4296
+ }
4297
+
4298
+ log('Template detected:', templateType);
4299
+
4300
+ if (templateType === 'vite') {
4301
+ log('Skipping Vite template - not supported');
4302
+ return null;
4303
+ }
4304
+
4305
+ return templateType;
4306
+ };
4307
+
4308
+ /**
4309
+ * Perform initial route scan and write to file
4310
+ */
4311
+ const performInitialScan = async (
4312
+ cwd,
4313
+ templateType,
4314
+ outputPath,
4315
+ ) => {
4316
+ log('Starting route scanning');
4317
+ log('Performing initial scan');
4318
+ const routes = await scanRoutes(cwd, templateType);
4319
+ log('Initial scan found %d routes', routes.length);
4320
+
4321
+ log('Writing routes to file:', outputPath);
4322
+ writeRoutesFile(cwd, routes, templateType, outputPath);
4323
+
4324
+ const outputFilePath = getOutputFilePath(cwd, outputPath);
4325
+ log('Routes written to: %s (%d routes)', outputFilePath, routes.length);
4326
+
4327
+ return routes;
4328
+ };
4329
+
4330
+ /**
4331
+ * Setup polling interval to detect route changes
4332
+ */
4333
+ const setupPolling = (
4334
+ cwd,
4335
+ templateType,
4336
+ outputPath,
4337
+ initialRoutes,
4338
+ ) => {
4339
+ previousRoutesHash = hashRoutes(initialRoutes);
4340
+ log('Starting periodic route scanning (every 500ms)');
4341
+ log('About to call setInterval...');
4342
+
4343
+ pollingInterval = setInterval(async () => {
4344
+ try {
4345
+ log('Polling cycle started...');
4346
+ const updatedRoutes = await scanRoutes(cwd, templateType);
4347
+ const newHash = hashRoutes(updatedRoutes);
4348
+ log('Scanned %d routes, hash: %s vs %s', updatedRoutes.length, newHash, previousRoutesHash);
4349
+
4350
+ if (newHash !== previousRoutesHash) {
4351
+ log('Routes changed, updating file');
4352
+ writeRoutesFile(cwd, updatedRoutes, templateType, outputPath);
4353
+ previousRoutesHash = newHash;
4354
+ log('Routes updated (%d routes)', updatedRoutes.length);
4355
+ logger.info(`🔄 Routes updated (${updatedRoutes.length} routes)`);
4356
+ }
4357
+ } catch (pollingError) {
4358
+ log('Polling error:', pollingError);
4359
+ }
4360
+ }, POLLING_INTERVAL_MS);
4361
+
4362
+ log('setInterval called, returned interval ID:', pollingInterval);
4363
+ log('pollingInterval type:', typeof pollingInterval);
4364
+ log('Route polling started (checking every %dms)', POLLING_INTERVAL_MS);
4365
+
4366
+ // Test if interval is actually working by logging after 1 second
4367
+ setTimeout(() => {
4368
+ log('setTimeout test fired after 1 second - event loop is working');
4369
+ }, 1000);
4370
+ };
4371
+
4372
+ /**
4373
+ * Register cleanup handlers for process termination signals
4374
+ * Note: Do NOT listen to 'exit' event because the CLI parent process
4375
+ * exits normally after spawning the dev server child process
4376
+ */
4377
+ const registerCleanupHandlers = () => {
4378
+ const cleanup = () => {
4379
+ if (pollingInterval) {
4380
+ log('Cleaning up polling interval on process termination');
4381
+ clearInterval(pollingInterval);
4382
+ pollingInterval = null;
4383
+ previousRoutesHash = null;
4384
+ }
4385
+ };
4386
+
4387
+ // Only handle termination signals, not normal exit
4388
+ process.on('SIGINT', cleanup);
4389
+ process.on('SIGTERM', cleanup);
4390
+ };
4391
+
4392
+ /**
4393
+ * Clean up existing polling interval
4394
+ */
4395
+ const cleanupExistingInterval = () => {
4396
+ if (pollingInterval) {
4397
+ log('Clearing existing polling interval before starting new one');
4398
+ clearInterval(pollingInterval);
4399
+ pollingInterval = null;
4400
+ previousRoutesHash = null;
4401
+ }
4402
+ };
4403
+
4404
+ /**
4405
+ * Start route scanning and polling
4406
+ *
4407
+ * Workflow:
4408
+ * 1. Detect template type (Next.js/Nuxt/Vite)
4409
+ * 2. If Vite, skip scanning (too simple, not worth resources)
4410
+ * 3. Perform initial route scan
4411
+ * 4. Write routes to JSON file
4412
+ * 5. Start periodic polling (every 500ms) to detect route changes
4413
+ *
4414
+ * Note: Uses polling instead of file watching to avoid EMFILE errors
4415
+ *
4416
+ * @param cwd - Current working directory
4417
+ * @param options - Scanning options (output path, etc.)
4418
+ */
4419
+ const startRouteScanning = async (
4420
+ cwd,
4421
+ options = {},
4422
+ ) => {
4423
+ cleanupExistingInterval();
4424
+
4425
+ try {
4426
+ log('startRouteScanning called with cwd:', cwd, 'options:', options);
4427
+ // Default output to ~/.coze/routes.json (user home), can be overridden via options or env var
4428
+ // Using user home avoids conflict with project .coze config file
4429
+ const defaultOutputPath =
4430
+ process.env.COZE_ROUTES_OUTPUT || getCozeFilePath('routes.json');
4431
+ const { outputPath = defaultOutputPath } = options;
4432
+
4433
+ // Step 1: Detect and validate template
4434
+ const templateType = detectAndValidateTemplate(cwd);
4435
+ if (!templateType) {
4436
+ return;
4437
+ }
4438
+
4439
+ // Step 2: Perform initial scan
4440
+ const routes = await performInitialScan(cwd, templateType, outputPath);
4441
+
4442
+ // Step 3: Check if polling is enabled
4443
+ const enablePolling = process.env.COZE_ROUTES_WATCH !== 'false';
4444
+ if (!enablePolling) {
4445
+ log('Route polling disabled via COZE_ROUTES_WATCH=false');
4446
+ return;
4447
+ }
4448
+
4449
+ // Step 4: Setup polling and cleanup handlers
4450
+ log('About to call setupPolling...');
4451
+ setupPolling(cwd, templateType, outputPath, routes);
4452
+ log('setupPolling completed');
4453
+
4454
+ log('About to call registerCleanupHandlers...');
4455
+ registerCleanupHandlers();
4456
+ log('registerCleanupHandlers completed');
4457
+
4458
+ log('startRouteScanning function about to return');
4459
+ } catch (error) {
4460
+ log('ERROR CAUGHT in startRouteScanning:', error);
4461
+ cleanupExistingInterval();
4462
+
4463
+ // Silently handle all errors - route scanning should never break dev server
4464
+ log('Route scanning error:', error);
4465
+ logger.warn(
4466
+ '⚠️ Route scanning skipped due to error (dev server continues normally)',
4467
+ );
4468
+
4469
+ if (process.env.DEBUG) {
4470
+ logger.warn(error instanceof Error ? error.message : String(error));
4471
+ }
4472
+ }
4473
+ };
4474
+
4475
+ function _nullishCoalesce$1(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
4476
+ /**
4477
+ * 日志文件名常量
4478
+ */
4479
+ const LOG_FILE_NAME = 'dev.log';
3644
4480
 
3645
4481
  /**
3646
4482
  * 解析日志文件路径
3647
4483
  * - 如果是绝对路径,直接使用
3648
- * - 如果是相对路径,基于 getLogDir() + 相对路径
3649
- * - 如果为空,使用 getLogDir() + LOG_FILE_NAME
4484
+ * - 如果是相对路径,基于 logs 目录 + 相对路径
4485
+ * - 如果为空,使用默认日志文件路径
3650
4486
  */
3651
4487
  const resolveLogFilePath = (logFile) => {
3652
4488
  if (!logFile) {
3653
- return path.join(getLogDir(), LOG_FILE_NAME);
4489
+ return getCozeLogPath(LOG_FILE_NAME);
3654
4490
  }
3655
4491
 
3656
4492
  if (path.isAbsolute(logFile)) {
3657
4493
  return logFile;
3658
4494
  }
3659
4495
 
3660
- return path.join(getLogDir(), logFile);
4496
+ return getCozeLogPath(logFile);
3661
4497
  };
3662
4498
 
3663
4499
  /**
@@ -3703,15 +4539,21 @@ const executeRun = async (
3703
4539
  logger.info('');
3704
4540
  }
3705
4541
 
3706
- // 2. 加载 .coze 配置
4542
+ // 2. 启动路由扫描(仅在 dev 模式)
4543
+ // Note: startRouteScanning handles all errors internally and never throws
4544
+ if (commandName === 'dev') {
4545
+ await startRouteScanning(process.cwd());
4546
+ }
4547
+
4548
+ // 3. 加载 .coze 配置
3707
4549
  const config = await loadCozeConfig();
3708
4550
  const commandArgs = getCommandConfig(config, commandName);
3709
4551
 
3710
- // 3. 准备日志
4552
+ // 4. 准备日志
3711
4553
  const logFilePath = resolveLogFilePath(options.logFile);
3712
4554
  const logStream = createLogStream(logFilePath);
3713
4555
 
3714
- // 4. 执行命令
4556
+ // 5. 执行命令
3715
4557
  const commandString = commandArgs.join(' ');
3716
4558
 
3717
4559
  logger.info(`Executing: ${commandString}`);
@@ -3738,72 +4580,76 @@ const executeRun = async (
3738
4580
  logStream.write(data);
3739
4581
  })]);
3740
4582
 
3741
- childProcess.on('close', (code, signal) => {
3742
- logStream.end();
4583
+ // Wait for child process to complete
4584
+ await new Promise((resolve, reject) => {
4585
+ childProcess.on('close', (code, signal) => {
4586
+ logStream.end();
4587
+
4588
+ if (code !== 0) {
4589
+ const errorMessage = `Command exited with code ${_nullishCoalesce$1(code, () => ( 'unknown'))}${signal ? ` and signal ${signal}` : ''}`;
4590
+ logger.error(errorMessage);
4591
+ logger.error(`Check log file for details: ${logFilePath}`);
4592
+
4593
+ // 上报命令失败
4594
+ reportError(new Error(errorMessage), {
4595
+ command: commandName,
4596
+ exitCode: String(_nullishCoalesce$1(code, () => ( 'unknown'))),
4597
+ signal: _nullishCoalesce$1(signal, () => ( 'none')),
4598
+ logFile: logFilePath,
4599
+ });
4600
+ reportCommandComplete(commandName, false, Date.now() - cmdStartTime, {
4601
+ args: JSON.stringify(options),
4602
+ errorCode: _nullishCoalesce$1(code, () => ( 1)),
4603
+ errorMessage,
4604
+ });
4605
+ flushSlardar()
4606
+ .then(() => {
4607
+ process.exit(code || 1);
4608
+ })
4609
+ .catch(() => {
4610
+ // Catch any errors in the promise chain to prevent unhandled rejections
4611
+ process.exit(code || 1);
4612
+ });
4613
+ } else {
4614
+ logger.success('Command completed successfully');
4615
+ logger.info(`Log file: ${logFilePath}`);
4616
+
4617
+ // 上报命令成功
4618
+ reportCommandComplete(commandName, true, Date.now() - cmdStartTime, {
4619
+ args: JSON.stringify(options),
4620
+ });
4621
+ // flush 由 main 函数统一处理
4622
+ resolve();
4623
+ }
4624
+ });
3743
4625
 
3744
- if (code !== 0) {
3745
- const errorMessage = `Command exited with code ${_nullishCoalesce$1(code, () => ( 'unknown'))}${signal ? ` and signal ${signal}` : ''}`;
3746
- logger.error(errorMessage);
3747
- logger.error(`Check log file for details: ${logFilePath}`);
4626
+ childProcess.on('error', (error) => {
4627
+ logger.error('Failed to execute command:');
4628
+ logger.error(`Error: ${error.message}`);
4629
+ if (error.stack) {
4630
+ logger.error(`Stack trace:\n${error.stack}`);
4631
+ }
4632
+ logStream.end();
3748
4633
 
3749
- // 上报命令失败
3750
- reportError(new Error(errorMessage), {
4634
+ // 上报错误
4635
+ reportError(error, {
3751
4636
  command: commandName,
3752
- exitCode: String(_nullishCoalesce$1(code, () => ( 'unknown'))),
3753
- signal: _nullishCoalesce$1(signal, () => ( 'none')),
3754
- logFile: logFilePath,
4637
+ type: 'child_process_error',
3755
4638
  });
3756
4639
  reportCommandComplete(commandName, false, Date.now() - cmdStartTime, {
3757
4640
  args: JSON.stringify(options),
3758
- errorCode: _nullishCoalesce$1(code, () => ( 1)),
3759
- errorMessage,
4641
+ errorCode: 1,
4642
+ errorMessage: error.message,
3760
4643
  });
3761
4644
  flushSlardar()
3762
4645
  .then(() => {
3763
- process.exit(code || 1);
4646
+ process.exit(1);
3764
4647
  })
3765
4648
  .catch(() => {
3766
4649
  // Catch any errors in the promise chain to prevent unhandled rejections
3767
- process.exit(code || 1);
4650
+ process.exit(1);
3768
4651
  });
3769
- } else {
3770
- logger.success('Command completed successfully');
3771
- logger.info(`Log file: ${logFilePath}`);
3772
-
3773
- // 上报命令成功
3774
- reportCommandComplete(commandName, true, Date.now() - cmdStartTime, {
3775
- args: JSON.stringify(options),
3776
- });
3777
- // flush 由 main 函数统一处理
3778
- }
3779
- });
3780
-
3781
- childProcess.on('error', (error) => {
3782
- logger.error('Failed to execute command:');
3783
- logger.error(`Error: ${error.message}`);
3784
- if (error.stack) {
3785
- logger.error(`Stack trace:\n${error.stack}`);
3786
- }
3787
- logStream.end();
3788
-
3789
- // 上报错误
3790
- reportError(error, {
3791
- command: commandName,
3792
- type: 'child_process_error',
3793
4652
  });
3794
- reportCommandComplete(commandName, false, Date.now() - cmdStartTime, {
3795
- args: JSON.stringify(options),
3796
- errorCode: 1,
3797
- errorMessage: error.message,
3798
- });
3799
- flushSlardar()
3800
- .then(() => {
3801
- process.exit(1);
3802
- })
3803
- .catch(() => {
3804
- // Catch any errors in the promise chain to prevent unhandled rejections
3805
- process.exit(1);
3806
- });
3807
4653
  });
3808
4654
  } catch (error) {
3809
4655
  const err = error instanceof Error ? error : new Error(String(error));