@lark-apaas/miaoda-cli 0.1.39-alpha.44e04b8 → 0.1.39-alpha.4ae5653

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.
package/README.md CHANGED
@@ -54,20 +54,13 @@ miaoda file ls --output json
54
54
 
55
55
  ## 全栈应用的 MCP UI 检查
56
56
 
57
- `miaoda app sync` 为 `nestjs-react-fullstack` 应用同步 `scripts/lint.js` 和独立 `scripts/check.cjs`,在
58
- `tsconfig.node.json` 中追加 `server/mcp/ui` 排除项,并保留现有配置及继承的排除路径。
59
- 根 `eslint.config.js` / `.mjs` / `.cjs` 会增量追加 UI 全局忽略,直接 ESLint 调用和编辑器检查均生效。
60
- 迁移不执行配置文件;无法静态识别的自定义导出保留并提示手动补齐。
61
- 已知模板版本的 `type:check`、`eslint` 命令会迁移到独立检查入口;自定义命令保留并提示。
62
- 类型检查及 UI 检查直接调用 `check.cjs`,不会回到 `lint.js`;旧版 sync 覆盖聚合脚本时独立入口仍保留,避免递归。
63
-
64
- - `npm run type:check`:检查服务端、客户端及 MCP UI。
65
- - `npm run type:check:mcp-ui` / `npm run eslint:mcp-ui`:单独检查 MCP UI。
66
- - `npm run lint -- --files <files...>`:按改动文件选择检查范围。
67
-
68
- 同步不会生成 MCP UI 目录。开发 MCP UI 时,由 Agent 按 MCP 指南创建
69
- `server/mcp/ui/tsconfig.json` 和根目录 `eslint.mcp-ui.config.cjs`。
70
- 没有 UI 源码时跳过独立检查;存在 UI 源码但缺少配置时明确报错。
57
+ MCP UI 使用平台预设提供的浏览器检查默认值。`npm run lint` 和 `npm run type:check` 自动覆盖 `server/mcp/ui`,模板统一提供 UI 子命令与类型配置;`lint -- --files` 保留按文件选择检查范围。没有 UI 源码时跳过 UI 检查。
58
+
59
+ 应用增加 `type:check:mcp-ui` 子命令,与 `type:check:client`、`type:check:server` 一起由 `type:check` 聚合。`scripts/typecheck-mcp-ui.js` 只在 UI 源码存在时调用应用安装的 `tsc --noEmit --project tsconfig.mcp-ui.json`;无 UI 或目录为空时成功跳过。根目录 `tsconfig.mcp-ui.json` 提供浏览器、React JSX 和严格类型检查配置,可按应用需要修改;sync 仅在该文件不存在时创建。已有 `server/mcp/ui/tsconfig.json` 的应用优先沿用原配置,保留自定义 paths/types。
60
+
61
+ UI ESLint 默认配置由平台预设提供,也可用根目录 `eslint.mcp-ui.config.js` / `.cjs` / `.mjs` 定制。`npm run lint` 包含 UI 检查;`--files` 按影响范围检查,shared 变更触发三个环境的类型检查。应用脚本不使用 `node -e` 或 TypeScript 编译器 API。
62
+
63
+ 存量 sync 不改写 ESLint 源码;UI 全局排除项由平台 ESLint 预设提供。TS 仅向已有显式 `exclude` 数组追加目录,保留注释和原项;依赖继承且未显式声明 `exclude` 的自定义配置保持不变并提示人工调整。自定义检查命令保留。lint.js 和 build.sh 仍由平台脚本模板维护,不依赖预设包中的脚本执行入口。
71
64
 
72
65
  ## 打包可本地访问的静态产物
73
66
 
@@ -70,6 +70,7 @@ exports.MIGRATE_CONFIG = {
70
70
  { type: 'file', from: 'tsconfig.json', to: 'tsconfig.json', overwrite: true },
71
71
  { type: 'file', from: 'tsconfig.app.json', to: 'tsconfig.app.json', overwrite: true },
72
72
  { type: 'file', from: 'tsconfig.node.json', to: 'tsconfig.node.json', overwrite: true },
73
+ { type: 'file', from: 'tsconfig.mcp-ui.json', to: 'tsconfig.mcp-ui.json', overwrite: false },
73
74
  // ESLint 9 flat config —— vite-react 用 eslint.config.mjs 已删,fullstack 用
74
75
  // eslint.config.js (commonjs) + extends @lark-apaas/fullstack-presets;user 通常不改
75
76
  { type: 'file', from: 'eslint.config.js', to: 'eslint.config.js', overwrite: true },
@@ -158,10 +159,16 @@ exports.MIGRATE_CONFIG = {
158
159
  command: 'tsc --noEmit --project tsconfig.app.json',
159
160
  overwrite: true,
160
161
  },
162
+ {
163
+ type: 'add-script',
164
+ name: 'type:check:mcp-ui',
165
+ command: 'node ./scripts/typecheck-mcp-ui.js',
166
+ overwrite: true,
167
+ },
161
168
  {
162
169
  type: 'add-script',
163
170
  name: 'type:check',
164
- command: 'concurrently -n "server,client" -c "blue,green" "npm run type:check:server" "npm run type:check:client"',
171
+ command: 'concurrently -n "server,client,mcp-ui" -c "blue,green,magenta" "npm run type:check:server" "npm run type:check:client" "npm run type:check:mcp-ui"',
165
172
  overwrite: true,
166
173
  },
167
174
  // ----- 平台脚本调用链 -----
@@ -159,44 +159,34 @@ exports.SYNC_CONFIG = {
159
159
  to: `npx -y ${fullstack_cli_pin_1.FULLSTACK_CLI_PIN_SPEC} sync --disable-gen-openapi`,
160
160
  ifStartsWith: 'npx -y @lark-apaas/fullstack-cli sync',
161
161
  },
162
- { type: 'eslint-ignore', to: 'eslint.config.js', pattern: 'server/mcp/ui/**' },
163
- { type: 'eslint-ignore', to: 'eslint.config.mjs', pattern: 'server/mcp/ui/**' },
164
- { type: 'eslint-ignore', to: 'eslint.config.cjs', pattern: 'server/mcp/ui/**' },
165
- // MCP UI 使用独立浏览器配置;仅迁移已知模板命令,不生成 UI 目录或配置。
162
+ // MCP UI 使用独立浏览器配置;仅迁移已知模板命令,不生成 UI 目录;根配置仅在缺失时补齐。
166
163
  {
167
164
  type: 'typescript-exclude',
168
165
  to: 'tsconfig.node.json',
169
166
  path: 'server/mcp/ui',
170
167
  },
168
+ { type: 'file', from: 'tsconfig.mcp-ui.json', to: 'tsconfig.mcp-ui.json', overwrite: false },
171
169
  {
172
170
  type: 'add-script',
173
171
  name: 'type:check:mcp-ui',
174
- command: 'node ./scripts/check.cjs --typecheck-mcp-ui',
175
- overwrite: false,
176
- },
177
- {
178
- type: 'add-script',
179
- name: 'eslint:mcp-ui',
180
- command: 'node ./scripts/check.cjs --eslint-mcp-ui',
172
+ command: 'node ./scripts/typecheck-mcp-ui.js',
181
173
  overwrite: false,
182
174
  },
183
175
  {
184
176
  type: 'patch-script',
185
177
  name: 'type:check:mcp-ui',
186
- to: 'node ./scripts/check.cjs --typecheck-mcp-ui',
187
- ifEquals: ['node ./scripts/lint.js --typecheck-mcp-ui'],
188
- },
189
- {
190
- type: 'patch-script',
191
- name: 'eslint:mcp-ui',
192
- to: 'node ./scripts/check.cjs --eslint-mcp-ui',
193
- ifEquals: ['node ./scripts/lint.js --eslint-mcp-ui'],
178
+ to: 'node ./scripts/typecheck-mcp-ui.js',
179
+ ifEquals: [
180
+ 'node ./scripts/check.cjs --typecheck-mcp-ui',
181
+ 'node ./scripts/lint.js --typecheck-mcp-ui',
182
+ ],
194
183
  },
195
184
  {
196
185
  type: 'patch-script',
197
186
  name: 'type:check',
198
- to: 'node ./scripts/check.cjs --typecheck',
187
+ to: 'concurrently -n "server,client,mcp-ui" -c "blue,green,magenta" "npm run type:check:server" "npm run type:check:client" "npm run type:check:mcp-ui"',
199
188
  ifEquals: [
189
+ 'node ./scripts/check.cjs --typecheck',
200
190
  'node ./scripts/lint.js --typecheck',
201
191
  'concurrently -n "server,client" -c "blue,green" "npm run type:check:server" "npm run type:check:client"',
202
192
  'concurrently "npm run type:check:server" "npm run type:check:client"',
@@ -206,8 +196,8 @@ exports.SYNC_CONFIG = {
206
196
  {
207
197
  type: 'patch-script',
208
198
  name: 'eslint',
209
- to: 'eslint . --quiet --ignore-pattern "server/mcp/ui/**"',
210
- ifEquals: ['eslint . --quiet', 'eslint .'],
199
+ to: 'eslint . --quiet',
200
+ ifEquals: ['eslint . --quiet --ignore-pattern "server/mcp/ui/**"'],
211
201
  },
212
202
  // ===== miaoda-cli 本地开发新增规则(fullstack-cli sync 不会执行) =====
213
203
  // M1. scripts.dev:local —— 本地用户绕过沙箱判定直接跑本地链路(npm run dev:local)。
@@ -44,7 +44,6 @@ const file_ops_1 = require("./file-ops");
44
44
  const merge_json_1 = require("./merge-json");
45
45
  const logger_1 = require("./logger");
46
46
  const typescript_exclude_1 = require("./typescript-exclude");
47
- const eslint_ignore_1 = require("./eslint-ignore");
48
47
  /**
49
48
  * 顺序执行 SyncRule[],逐条 apply。一条 rule 报错不影响后续 rule(catch + 记录),最终
50
49
  * 在 handler 层决定是否整体 fail。
@@ -108,19 +107,6 @@ function applyOne(rule, opts) {
108
107
  case 'patch-script': {
109
108
  return applyPatchScript(rule, targetDir, logPrefix);
110
109
  }
111
- case 'eslint-ignore': {
112
- const result = (0, eslint_ignore_1.addEslintGlobalIgnore)(node_path_1.default.join(targetDir, rule.to), rule.pattern);
113
- if (result.warning)
114
- (0, logger_1.log)(logPrefix, ` ⚠ ${result.warning}`);
115
- if (result.changed)
116
- (0, logger_1.log)(logPrefix, ` ✓ ${rule.to} (global ignore ${rule.pattern})`);
117
- return {
118
- rule,
119
- action: result.changed ? 'patched' : result.warning ? 'skipped' : 'noop',
120
- path: rule.to,
121
- detail: result.warning,
122
- };
123
- }
124
110
  case 'typescript-exclude': {
125
111
  const changed = (0, typescript_exclude_1.excludeTypeScriptPath)(node_path_1.default.join(targetDir, rule.to), rule.path);
126
112
  (0, logger_1.log)(logPrefix, ` ${changed ? '✓' : '○'} ${rule.to} (exclude ${rule.path})`);
@@ -5,58 +5,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.excludeTypeScriptPath = excludeTypeScriptPath;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
- const node_path_1 = __importDefault(require("node:path"));
9
- const typescript_1 = __importDefault(require("typescript"));
10
8
  const jsonc_parser_1 = require("jsonc-parser");
11
9
  const error_1 = require("./error");
12
- /** 追加排除项,保留 JSONC 注释和继承的排除路径;不创建缺失的配置。 */
10
+ /** Only append to explicit template excludes; never rewrite inherited user configuration. */
13
11
  function excludeTypeScriptPath(filename, excludedPath) {
14
12
  if (!node_fs_1.default.existsSync(filename))
15
13
  return false;
16
14
  const source = node_fs_1.default.readFileSync(filename, 'utf8');
17
15
  const errors = [];
18
16
  const raw = (0, jsonc_parser_1.parse)(source, errors, { allowTrailingComma: true });
19
- if (errors.length || !raw || typeof raw !== 'object' || Array.isArray(raw)) {
20
- throw new error_1.AppError('INVALID_CONFIG', `Invalid TypeScript configuration: ${filename}`);
21
- }
22
- const parsed = typescript_1.default.getParsedCommandLineOfConfigFile(filename, {}, {
23
- ...typescript_1.default.sys,
24
- onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
25
- throw new error_1.AppError('INVALID_CONFIG', typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
26
- },
27
- });
28
- // 初始化中的空工程允许迁移,其他错误不能被静默忽略。
29
- const diagnostics = parsed?.errors.filter((d) => d.code !== 18003 && d.code !== 18002) ?? [];
30
- if (!parsed || diagnostics.length) {
31
- throw new error_1.AppError('INVALID_CONFIG', `Cannot migrate ${filename}: ${diagnostics.map((d) => typescript_1.default.flattenDiagnosticMessageText(d.messageText, '\n')).join('; ')}`);
32
- }
33
- const effective = parsed.raw;
34
- const excludes = effective.exclude ?? [
35
- 'node_modules',
36
- 'bower_components',
37
- 'jspm_packages',
38
- ...[parsed.options.outDir, parsed.options.declarationDir]
39
- .filter((p) => Boolean(p))
40
- .map((p) => node_path_1.default.relative(node_path_1.default.dirname(filename), p).split(node_path_1.default.sep).join('/')),
41
- ];
42
- if (!Array.isArray(excludes) || excludes.some((p) => typeof p !== 'string')) {
17
+ if (errors.length || !raw || typeof raw !== 'object')
43
18
  throw new error_1.AppError('INVALID_CONFIG', `Invalid exclude array: ${filename}`);
19
+ if (raw.exclude === undefined) {
20
+ console.warn(`Cannot safely update inherited excludes in ${filename}; add ${excludedPath} to the effective TypeScript exclude configuration.`);
21
+ return false;
44
22
  }
45
- const paths = excludes;
46
- if (paths.includes(excludedPath))
23
+ if (!Array.isArray(raw.exclude) || raw.exclude.some((p) => typeof p !== 'string'))
24
+ throw new error_1.AppError('INVALID_CONFIG', `Invalid exclude array: ${filename}`);
25
+ if (raw.exclude.includes(excludedPath))
47
26
  return false;
48
- const ownExclude = raw.exclude;
49
- const formattingOptions = {
50
- insertSpaces: true,
51
- tabSize: 2,
52
- eol: source.includes('\r\n') ? '\r\n' : '\n',
53
- };
54
- const edits = Array.isArray(ownExclude)
55
- ? (0, jsonc_parser_1.modify)(source, ['exclude', ownExclude.length], excludedPath, {
56
- formattingOptions,
57
- isArrayInsertion: true,
58
- })
59
- : (0, jsonc_parser_1.modify)(source, ['exclude'], [...paths, excludedPath], { formattingOptions });
60
- node_fs_1.default.writeFileSync(filename, (0, jsonc_parser_1.applyEdits)(source, edits));
27
+ node_fs_1.default.writeFileSync(filename, (0, jsonc_parser_1.applyEdits)(source, (0, jsonc_parser_1.modify)(source, ['exclude', raw.exclude.length], excludedPath, {
28
+ isArrayInsertion: true,
29
+ formattingOptions: {
30
+ insertSpaces: true,
31
+ tabSize: 2,
32
+ eol: source.includes('\r\n') ? '\r\n' : '\n',
33
+ },
34
+ })));
61
35
  return true;
62
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.39-alpha.44e04b8",
3
+ "version": "0.1.39-alpha.4ae5653",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -33,8 +33,7 @@
33
33
  "jsonc-parser": "^3.3.1",
34
34
  "ora": "^5.4.1",
35
35
  "picocolors": "^1.1.1",
36
- "which": "^7.0.0",
37
- "typescript": "^5.8.3"
36
+ "which": "^7.0.0"
38
37
  },
39
38
  "devDependencies": {
40
39
  "@types/cross-spawn": "^6.0.6",
@@ -53,7 +52,8 @@
53
52
  "tsc-alias": "^1.8.11",
54
53
  "tsx": "^4.19.4",
55
54
  "vitest": "^4.1.4",
56
- "xml2js": "^0.6.2"
55
+ "xml2js": "^0.6.2",
56
+ "typescript": "^5.8.3"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "bash scripts/build.sh",
@@ -2,6 +2,7 @@
2
2
 
3
3
  const path = require('node:path');
4
4
  const fs = require('node:fs');
5
+ const { spawn } = require('node:child_process');
5
6
 
6
7
  const cwd = process.cwd();
7
8
 
@@ -15,7 +16,11 @@ function normalizeProjectFile(filePath) {
15
16
  }
16
17
 
17
18
  const relativePath = path.relative(cwd, absolutePath);
18
- if (relativePath.startsWith('..')) {
19
+ if (
20
+ relativePath === '..' ||
21
+ relativePath.startsWith(`..${path.sep}`) ||
22
+ path.isAbsolute(relativePath)
23
+ ) {
19
24
  console.warn(`[lint] Skip file outside project: ${filePath}`);
20
25
  return null;
21
26
  }
@@ -72,12 +77,114 @@ function canRunStylelint() {
72
77
 
73
78
  if (!pkg.scripts || !pkg.scripts.stylelint) return false;
74
79
 
75
- return STYLELINT_CONFIG_FILES.some(file => fs.existsSync(path.join(cwd, file)));
80
+ return STYLELINT_CONFIG_FILES.some(file =>
81
+ fs.existsSync(path.join(cwd, file))
82
+ );
83
+ }
84
+
85
+ function getBinName(name) {
86
+ return process.platform === 'win32' ? `${name}.cmd` : name;
87
+ }
88
+
89
+ function runCommand(command, args) {
90
+ return new Promise(resolve => {
91
+ const child = spawn(command, args, {
92
+ cwd,
93
+ stdio: 'inherit',
94
+ shell: false,
95
+ });
96
+
97
+ child.on('close', code => resolve(code ?? 1));
98
+ child.on('error', () => resolve(1));
99
+ });
100
+ }
101
+
102
+ // 串行依次执行每个任务,全部跑完后再聚合退出码:
103
+ // 降低并发资源占用、让各任务输出按顺序清晰可读,同时保留“一次暴露所有 lint 问题”的行为。
104
+ async function runTasksSerially(taskSpecs) {
105
+ let exitCode = 0;
106
+ for (const [command, args] of taskSpecs) {
107
+ const code = await runCommand(command, args);
108
+ if (code !== 0) {
109
+ exitCode = 1;
110
+ }
111
+ }
112
+ return exitCode;
113
+ }
114
+
115
+ const UI_DIR = 'server/mcp/ui';
116
+
117
+ function uiSourceFiles() {
118
+ const files = [];
119
+ function visit(relative) {
120
+ const absolute = path.join(cwd, relative);
121
+ if (!fs.existsSync(absolute)) return;
122
+ if (fs.lstatSync(absolute).isSymbolicLink()) {
123
+ throw new Error(`MCP UI 检查不支持符号链接: ${relative}`);
124
+ }
125
+ for (const entry of fs.readdirSync(absolute, { withFileTypes: true })) {
126
+ if (['node_modules', 'dist', '.git'].includes(entry.name)) continue;
127
+ const file = `${relative}/${entry.name}`;
128
+ if (entry.isSymbolicLink())
129
+ throw new Error(`MCP UI 检查不支持符号链接: ${file}`);
130
+ if (entry.isDirectory()) visit(file);
131
+ else if (isEslintTarget(file) && !/\.d\.(?:ts|mts|cts)$/.test(file))
132
+ files.push(file);
133
+ }
134
+ }
135
+ visit(UI_DIR);
136
+ return files.sort();
76
137
  }
77
138
 
78
- const { getBinName, runTasksSerially, uiTasks, packageScripts, main: runChecks } = require('./check.cjs');
79
- const UI_TYPECHECKS = ['node ./scripts/check.cjs --typecheck', 'node ./scripts/lint.js --typecheck'];
80
- function isUiFile(file) { return file.startsWith('server/mcp/ui/'); }
139
+ function uiTasks({ types = true, eslint = true } = {}) {
140
+ const sources = uiSourceFiles();
141
+ if (!sources.length) return [];
142
+ const tasks = [];
143
+ if (types) tasks.push([getBinName('npm'), ['run', 'type:check:mcp-ui']]);
144
+ if (eslint) {
145
+ const config = [
146
+ 'eslint.mcp-ui.config.js',
147
+ 'eslint.mcp-ui.config.cjs',
148
+ 'eslint.mcp-ui.config.mjs',
149
+ ].find(file => fs.existsSync(path.join(cwd, file)));
150
+ let resolvedConfig = config;
151
+ if (!resolvedConfig) {
152
+ try {
153
+ resolvedConfig =
154
+ require.resolve('@lark-apaas/fullstack-presets/lib/mcp/eslint-ui.js');
155
+ } catch {
156
+ throw new Error(
157
+ '请升级 @lark-apaas/fullstack-presets 以使用 MCP UI 默认 ESLint 配置,或提供 eslint.mcp-ui.config.cjs'
158
+ );
159
+ }
160
+ }
161
+ tasks.push([
162
+ getBinName('npx'),
163
+ [
164
+ '--no-install',
165
+ 'eslint',
166
+ '--config',
167
+ resolvedConfig,
168
+ '--quiet',
169
+ '--max-warnings',
170
+ '0',
171
+ ...sources,
172
+ ],
173
+ ]);
174
+ }
175
+ return tasks;
176
+ }
177
+
178
+ function packageScripts() {
179
+ return (
180
+ JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'))
181
+ .scripts || {}
182
+ );
183
+ }
184
+
185
+ function isUiFile(file) {
186
+ return file.startsWith('server/mcp/ui/');
187
+ }
81
188
 
82
189
  async function runDefaultLint() {
83
190
  const taskSpecs = [
@@ -85,13 +192,21 @@ async function runDefaultLint() {
85
192
  [getBinName('npm'), ['run', 'type:check']],
86
193
  ];
87
194
 
88
- // Customized type:check remains untouched; ensure full lint still checks UI.
89
- taskSpecs.push(...uiTasks({ types: !UI_TYPECHECKS.includes(packageScripts()['type:check']) }));
195
+ // 保留用户的 type:check;未聚合 UI 时由 lint 补查。
196
+ taskSpecs.push(
197
+ ...uiTasks({
198
+ types: !/\bnpm\s+run\s+type:check:mcp-ui(?=[\s"';&|)]|$)/.test(
199
+ packageScripts()['type:check'] || ''
200
+ ),
201
+ })
202
+ );
90
203
 
91
204
  if (canRunStylelint()) {
92
205
  taskSpecs.push([getBinName('npm'), ['run', 'stylelint']]);
93
206
  } else {
94
- console.warn('[lint] Skip stylelint: missing scripts.stylelint or stylelint config');
207
+ console.warn(
208
+ '[lint] Skip stylelint: missing scripts.stylelint or stylelint config'
209
+ );
95
210
  }
96
211
 
97
212
  process.exit(await runTasksSerially(taskSpecs));
@@ -99,7 +214,7 @@ async function runDefaultLint() {
99
214
 
100
215
  async function runSelectiveLint(inputFiles) {
101
216
  const normalizedFiles = Array.from(
102
- new Set(inputFiles.map(normalizeProjectFile).filter(Boolean)),
217
+ new Set(inputFiles.map(normalizeProjectFile).filter(Boolean))
103
218
  );
104
219
 
105
220
  if (normalizedFiles.length === 0) {
@@ -107,15 +222,36 @@ async function runSelectiveLint(inputFiles) {
107
222
  process.exit(0);
108
223
  }
109
224
 
110
- const eslintFiles = normalizedFiles.filter(file => fs.existsSync(path.join(cwd, file)) && isEslintTarget(file) && !isUiFile(file) && !file.startsWith('eslint.mcp-ui.config.'));
111
- const stylelintFiles = normalizedFiles.filter(file => fs.existsSync(path.join(cwd, file)) && isStylelintTarget(file));
225
+ const eslintFiles = normalizedFiles.filter(
226
+ file =>
227
+ fs.existsSync(path.join(cwd, file)) &&
228
+ isEslintTarget(file) &&
229
+ !isUiFile(file) &&
230
+ !file.startsWith('eslint.mcp-ui.config.')
231
+ );
232
+ const stylelintFiles = normalizedFiles.filter(
233
+ file => fs.existsSync(path.join(cwd, file)) && isStylelintTarget(file)
234
+ );
112
235
  const typeCheckFiles = normalizedFiles.filter(isTypeCheckTarget);
113
236
 
114
237
  const clientTypeFiles = [];
115
238
  const serverTypeFiles = [];
116
- const rootConfigChanged = normalizedFiles.some(file => /^tsconfig(?:\.[^/]*)?\.json$/.test(file));
117
- if (rootConfigChanged) { clientTypeFiles.push('config'); serverTypeFiles.push('config'); }
118
- let checkUi = rootConfigChanged || normalizedFiles.some(file => isUiFile(file) || file.startsWith('shared/') || file.startsWith('eslint.mcp-ui.config.'));
239
+ const rootConfigChanged = normalizedFiles.some(file =>
240
+ /^tsconfig(?:\.[^/]*)?\.json$/.test(file)
241
+ );
242
+ if (rootConfigChanged) {
243
+ clientTypeFiles.push('config');
244
+ serverTypeFiles.push('config');
245
+ }
246
+ let checkUi =
247
+ rootConfigChanged ||
248
+ normalizedFiles.some(
249
+ file =>
250
+ isUiFile(file) ||
251
+ file.startsWith('shared/') ||
252
+ file.startsWith('eslint.mcp-ui.config.') ||
253
+ file === 'package.json'
254
+ );
119
255
 
120
256
  for (const filePath of typeCheckFiles) {
121
257
  if (isUiFile(filePath)) {
@@ -137,9 +273,14 @@ async function runSelectiveLint(inputFiles) {
137
273
  }
138
274
 
139
275
  if (stylelintFiles.length > 0 && !canRunStylelint()) {
140
- console.warn('[lint] Skip stylelint: missing scripts.stylelint or stylelint config');
276
+ console.warn(
277
+ '[lint] Skip stylelint: missing scripts.stylelint or stylelint config'
278
+ );
141
279
  } else if (stylelintFiles.length > 0) {
142
- taskSpecs.push([getBinName('npx'), ['stylelint', '--quiet', ...stylelintFiles]]);
280
+ taskSpecs.push([
281
+ getBinName('npx'),
282
+ ['stylelint', '--quiet', ...stylelintFiles],
283
+ ]);
143
284
  }
144
285
 
145
286
  if (clientTypeFiles.length > 0) {
@@ -162,13 +303,15 @@ async function runSelectiveLint(inputFiles) {
162
303
 
163
304
  async function main() {
164
305
  const args = process.argv.slice(2);
165
- const modes = ['--typecheck', '--typecheck-mcp-ui', '--eslint-mcp-ui'];
166
- if (args.length && !(args[0] === '--files' && args.length > 1) && !(args.length === 1 && modes.includes(args[0]))) {
167
- throw new Error('用法: lint.js [--files <paths...> | --typecheck | --typecheck-mcp-ui | --eslint-mcp-ui]');
306
+ if (args.length === 1 && args[0] === '--eslint-mcp-ui') {
307
+ process.exit(await runTasksSerially(uiTasks({ types: false })));
308
+ return;
309
+ }
310
+ if (args.length && !(args[0] === '--files' && args.length > 1)) {
311
+ throw new Error('用法: lint.js [--files <paths...>]');
168
312
  }
169
- if (args[0] === '--files' && args.slice(1).some(arg => arg.startsWith('--'))) throw new Error('--files 不可与其他检查模式混用');
170
- // 兼容旧入口,只向独立检查器转发;新的 npm scripts 不再指向 lint.js。
171
- if (modes.includes(args[0])) return runChecks(args);
313
+ if (args[0] === '--files' && args.slice(1).some(arg => arg.startsWith('--')))
314
+ throw new Error('--files 不可与其他检查模式混用');
172
315
  const files = parseFilesArg(process.argv.slice(2));
173
316
  if (files === null) {
174
317
  await runDefaultLint();
@@ -183,7 +326,7 @@ async function main() {
183
326
  await runSelectiveLint(files);
184
327
  }
185
328
 
186
- main().catch((error) => {
329
+ main().catch(error => {
187
330
  const message = error instanceof Error ? error.message : String(error);
188
331
  console.error(`[lint] Failed to run lint: ${message}`);
189
332
  process.exit(1);
@@ -0,0 +1,32 @@
1
+ // MCP UI 是可选浏览器工程;没有源码时跳过,否则直接交给 tsc。
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { spawnSync } = require('node:child_process');
5
+
6
+ function hasSource(dir) {
7
+ if (!fs.existsSync(dir)) return false;
8
+ return fs.readdirSync(dir, { withFileTypes: true }).some(entry => {
9
+ if (['node_modules', 'dist'].includes(entry.name)) return false;
10
+ if (entry.isDirectory()) return hasSource(path.join(dir, entry.name));
11
+ return entry.isFile() && /\.(?:[cm]?[jt]sx?)$/.test(entry.name);
12
+ });
13
+ }
14
+
15
+ if (hasSource('server/mcp/ui')) {
16
+ // 已有应用的独立 UI 配置继续生效,避免升级覆盖自定义 paths/types。
17
+ const config = fs.existsSync('server/mcp/ui/tsconfig.json')
18
+ ? 'server/mcp/ui/tsconfig.json'
19
+ : 'tsconfig.mcp-ui.json';
20
+ const result = spawnSync(
21
+ process.execPath,
22
+ [
23
+ require.resolve('typescript/bin/tsc'),
24
+ '--noEmit',
25
+ '--project',
26
+ config,
27
+ ],
28
+ { stdio: 'inherit' }
29
+ );
30
+ if (result.error) console.error(result.error.message);
31
+ process.exitCode = result.status ?? 1;
32
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "react-jsx",
7
+ "types": [],
8
+ "strict": true,
9
+ "checkJs": true,
10
+ "lib": [
11
+ "ES2020",
12
+ "DOM",
13
+ "DOM.Iterable"
14
+ ],
15
+ "allowJs": true,
16
+ "skipLibCheck": true,
17
+ "esModuleInterop": true,
18
+ "noEmit": true,
19
+ "baseUrl": ".",
20
+ "paths": {
21
+ "@shared/*": [
22
+ "shared/*"
23
+ ]
24
+ }
25
+ },
26
+ "include": [
27
+ "server/mcp/ui/**/*"
28
+ ],
29
+ "exclude": [
30
+ "node_modules",
31
+ "dist"
32
+ ]
33
+ }
@@ -1,169 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.addEslintGlobalIgnore = addEslintGlobalIgnore;
7
- const node_fs_1 = __importDefault(require("node:fs"));
8
- const typescript_1 = __importDefault(require("typescript"));
9
- const error_1 = require("./error");
10
- /** 只解析源码,不加载用户配置;用局部插入保留自定义规则和注释。 */
11
- function addEslintGlobalIgnore(filename, pattern) {
12
- if (!node_fs_1.default.existsSync(filename))
13
- return { changed: false };
14
- const source = node_fs_1.default.readFileSync(filename, 'utf8');
15
- const file = typescript_1.default.createSourceFile(filename, source, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.JS);
16
- const diagnostics = file
17
- .parseDiagnostics;
18
- if (diagnostics.length)
19
- throw new error_1.AppError('INVALID_CONFIG', `Invalid ESLint configuration: ${filename}`);
20
- const variables = new Map();
21
- const declarations = new Map();
22
- const aliasReferences = new Set();
23
- const resolvedAliases = new Set();
24
- const exportTargets = new Set();
25
- const helpers = new Set();
26
- const namespaces = new Set();
27
- const exports = [];
28
- const moduleName = (node) => {
29
- if (!typescript_1.default.isCallExpression(node) ||
30
- !typescript_1.default.isIdentifier(node.expression) ||
31
- node.expression.text !== 'require')
32
- return;
33
- const arg = node.arguments.at(0);
34
- return arg && typescript_1.default.isStringLiteral(arg) ? arg.text : undefined;
35
- };
36
- for (const statement of file.statements) {
37
- if (typescript_1.default.isImportDeclaration(statement) && typescript_1.default.isStringLiteral(statement.moduleSpecifier)) {
38
- const module = statement.moduleSpecifier.text;
39
- const clause = statement.importClause;
40
- if (module === 'typescript-eslint' && clause?.name)
41
- namespaces.add(clause.name.text);
42
- const bindings = clause?.namedBindings;
43
- if (bindings && typescript_1.default.isNamespaceImport(bindings) && module === 'typescript-eslint')
44
- namespaces.add(bindings.name.text);
45
- if (bindings && typescript_1.default.isNamedImports(bindings)) {
46
- for (const element of bindings.elements) {
47
- const name = element.propertyName?.text ?? element.name.text;
48
- if ((module === 'eslint/config' && name === 'defineConfig') ||
49
- (module === 'typescript-eslint' && name === 'config'))
50
- helpers.add(element.name.text);
51
- }
52
- }
53
- }
54
- if (typescript_1.default.isVariableStatement(statement) && statement.declarationList.flags & typescript_1.default.NodeFlags.Const) {
55
- for (const declaration of statement.declarationList.declarations) {
56
- if (!declaration.initializer)
57
- continue;
58
- const module = moduleName(declaration.initializer);
59
- if (typescript_1.default.isIdentifier(declaration.name)) {
60
- variables.set(declaration.name.text, declaration.initializer);
61
- declarations.set(declaration.name.text, declaration.name);
62
- if (module === 'typescript-eslint')
63
- namespaces.add(declaration.name.text);
64
- }
65
- else if (typescript_1.default.isObjectBindingPattern(declaration.name)) {
66
- for (const element of declaration.name.elements) {
67
- const name = element.propertyName ?? element.name;
68
- if (!typescript_1.default.isIdentifier(name) || !typescript_1.default.isIdentifier(element.name))
69
- continue;
70
- if ((module === 'eslint/config' && name.text === 'defineConfig') ||
71
- (module === 'typescript-eslint' && name.text === 'config'))
72
- helpers.add(element.name.text);
73
- }
74
- }
75
- }
76
- }
77
- if (typescript_1.default.isExportAssignment(statement) && !statement.isExportEquals)
78
- exports.push(statement.expression);
79
- if (typescript_1.default.isExpressionStatement(statement) && typescript_1.default.isBinaryExpression(statement.expression)) {
80
- const { left, right, operatorToken } = statement.expression;
81
- if (operatorToken.kind === typescript_1.default.SyntaxKind.EqualsToken &&
82
- typescript_1.default.isPropertyAccessExpression(left) &&
83
- typescript_1.default.isIdentifier(left.expression) &&
84
- left.expression.text === 'module' &&
85
- left.name.text === 'exports') {
86
- exports.push(right);
87
- exportTargets.add(left);
88
- }
89
- }
90
- }
91
- function unwrap(node, seen = new Set()) {
92
- if (typescript_1.default.isParenthesizedExpression(node))
93
- return unwrap(node.expression, seen);
94
- if (typescript_1.default.isIdentifier(node) && !seen.has(node.text)) {
95
- const value = variables.get(node.text);
96
- if (value) {
97
- resolvedAliases.add(node.text);
98
- aliasReferences.add(node);
99
- seen.add(node.text);
100
- return unwrap(value, seen);
101
- }
102
- }
103
- return node;
104
- }
105
- const root = exports.length === 1 ? unwrap(exports[0]) : undefined;
106
- // const 只固定变量绑定,不能保证数组不被 pop/push 或传给其他函数修改。
107
- // 有额外引用时保留原配置,避免改变初始化数组影响用户后续操作。
108
- function checkReferences(node) {
109
- if (typescript_1.default.isIdentifier(node) &&
110
- resolvedAliases.has(node.text) &&
111
- declarations.get(node.text) !== node &&
112
- !aliasReferences.has(node))
113
- return true;
114
- // module["exports"]、(module).exports、module 别名也可能修改导出数组。
115
- if (typescript_1.default.isIdentifier(node) &&
116
- node.text === 'module' &&
117
- !(typescript_1.default.isPropertyAccessExpression(node.parent) &&
118
- node.parent.expression === node &&
119
- exportTargets.has(node.parent)))
120
- return true;
121
- return typescript_1.default.forEachChild(node, (child) => checkReferences(child) || undefined) ?? false;
122
- }
123
- const unsafeReference = checkReferences(file);
124
- const isHelper = (node) => {
125
- const callee = node.expression;
126
- return typescript_1.default.isIdentifier(callee)
127
- ? helpers.has(callee.text)
128
- : typescript_1.default.isPropertyAccessExpression(callee) &&
129
- typescript_1.default.isIdentifier(callee.expression) &&
130
- namespaces.has(callee.expression.text) &&
131
- callee.name.text === 'config';
132
- };
133
- const entries = root && typescript_1.default.isArrayLiteralExpression(root)
134
- ? root.elements
135
- : root && typescript_1.default.isCallExpression(root) && isHelper(root)
136
- ? root.arguments
137
- : undefined;
138
- if (!entries || unsafeReference) {
139
- return {
140
- changed: false,
141
- warning: `Cannot safely migrate ${filename}; add ${JSON.stringify(pattern)} to its global ignores manually.`,
142
- };
143
- }
144
- // 只认真正的全局 ignores;带 files/rules 的局部忽略不能阻止服务端配置匹配 UI。
145
- // 仅检查最后一个配置,确保此前的否定规则或动态配置无法撤销新增忽略。
146
- const last = entries.at(-1);
147
- if (last && typescript_1.default.isObjectLiteralExpression(last)) {
148
- const props = last.properties;
149
- const nameOf = (p) => p.name && (typescript_1.default.isIdentifier(p.name) || typescript_1.default.isStringLiteral(p.name)) ? p.name.text : undefined;
150
- if (new Set(props.map(nameOf)).size === props.length &&
151
- props.every((p) => typescript_1.default.isPropertyAssignment(p) && ['ignores', 'name'].includes(nameOf(p) ?? ''))) {
152
- const ignores = props.find((p) => nameOf(p) === 'ignores');
153
- if (ignores &&
154
- typescript_1.default.isPropertyAssignment(ignores) &&
155
- typescript_1.default.isArrayLiteralExpression(ignores.initializer)) {
156
- const patterns = ignores.initializer.elements;
157
- if (patterns.length &&
158
- patterns.every(typescript_1.default.isStringLiteral) &&
159
- patterns[patterns.length - 1].text === pattern)
160
- return { changed: false };
161
- }
162
- }
163
- }
164
- const eol = source.includes('\r\n') ? '\r\n' : '\n';
165
- const position = last ? last.end : entries.pos;
166
- const addition = `${last ? ',' : ''}${eol} { ignores: [${JSON.stringify(pattern)}] }`;
167
- node_fs_1.default.writeFileSync(filename, source.slice(0, position) + addition + source.slice(position));
168
- return { changed: true };
169
- }
@@ -1,123 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // 叶子检查入口:不得调用 lint 或聚合 type:check npm script。
4
- const path = require('node:path');
5
- const { spawn } = require('node:child_process');
6
- const fs = require('node:fs');
7
- const cwd = process.cwd();
8
-
9
- function getBinName(name) {
10
- return process.platform === 'win32' ? `${name}.cmd` : name;
11
- }
12
-
13
- function runCommand(command, args) {
14
- return new Promise((resolve) => {
15
- const child = spawn(command, args, {
16
- cwd,
17
- stdio: 'inherit',
18
- shell: false,
19
- });
20
-
21
- child.on('close', (code) => resolve(code ?? 1));
22
- child.on('error', () => resolve(1));
23
- });
24
- }
25
-
26
- // 串行依次执行每个任务,全部跑完后再聚合退出码:
27
- // 降低并发资源占用、让各任务输出按顺序清晰可读,同时保留“一次暴露所有 lint 问题”的行为。
28
- async function runTasksSerially(taskSpecs) {
29
- let exitCode = 0;
30
- for (const [command, args] of taskSpecs) {
31
- const code = await runCommand(command, args);
32
- if (code !== 0) {
33
- exitCode = 1;
34
- }
35
- }
36
- return exitCode;
37
- }
38
-
39
- function isEslintTarget(filePath) { return /\.(c|m)?(j|t)sx?$/.test(filePath); }
40
-
41
- const UI_DIR = 'server/mcp/ui';
42
-
43
- function uiSourceFiles() {
44
- const files = [];
45
- function visit(relative) {
46
- const absolute = path.join(cwd, relative);
47
- if (!fs.existsSync(absolute)) return;
48
- if (fs.lstatSync(absolute).isSymbolicLink()) {
49
- throw new Error(`MCP UI 检查不支持符号链接: ${relative}`);
50
- }
51
- for (const entry of fs.readdirSync(absolute, { withFileTypes: true })) {
52
- if (['node_modules', 'dist', '.git'].includes(entry.name)) continue;
53
- const file = `${relative}/${entry.name}`;
54
- if (entry.isSymbolicLink()) throw new Error(`MCP UI 检查不支持符号链接: ${file}`);
55
- if (entry.isDirectory()) visit(file);
56
- else if (isEslintTarget(file) && !/\.d\.(?:ts|mts|cts)$/.test(file)) files.push(file);
57
- }
58
- }
59
- visit(UI_DIR);
60
- return files.sort();
61
- }
62
-
63
- function uiTasks({ types = true, eslint = true } = {}) {
64
- const sources = uiSourceFiles();
65
- if (!sources.length) return [];
66
- const tasks = [];
67
- if (types) {
68
- const config = `${UI_DIR}/tsconfig.json`;
69
- if (!fs.existsSync(path.join(cwd, config))) {
70
- throw new Error(`MCP UI 缺少 ${config},请按 mcp-guide 补齐独立浏览器 TypeScript 配置。`);
71
- }
72
- const ts = require('typescript');
73
- const parsed = ts.getParsedCommandLineOfConfigFile(path.join(cwd, config), {}, {
74
- ...ts.sys,
75
- onUnRecoverableConfigFileDiagnostic: d => { throw new Error(ts.flattenDiagnosticMessageText(d.messageText, '\n')); },
76
- });
77
- if (!parsed || parsed.errors.length) throw new Error(`MCP UI TypeScript 配置无效: ${config}`);
78
- const included = new Set(parsed.fileNames.map(file => path.resolve(file)));
79
- const missing = sources.filter(file => !included.has(path.resolve(cwd, file)));
80
- if (missing.length) throw new Error(`MCP UI TypeScript 配置未覆盖源码: ${missing.join(', ')};请检查 include/exclude,JS 源码需开启 allowJs。`);
81
- tasks.push([getBinName('npx'), ['--no-install', 'tsc', '--noEmit', '-p', config]]);
82
- }
83
- if (eslint) {
84
- const config = ['eslint.mcp-ui.config.js', 'eslint.mcp-ui.config.cjs', 'eslint.mcp-ui.config.mjs']
85
- .find(file => fs.existsSync(path.join(cwd, file)));
86
- if (!config) throw new Error('MCP UI 缺少独立 ESLint 配置,请按 mcp-guide 创建 eslint.mcp-ui.config.cjs。');
87
- tasks.push([getBinName('npx'), ['--no-install', 'eslint', '--config', config, '--quiet', '--max-warnings', '0', ...sources]]);
88
- }
89
- return tasks;
90
- }
91
-
92
- function packageScripts() {
93
- return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).scripts || {};
94
- }
95
-
96
- async function runTypeCheck() {
97
- const scripts = packageScripts();
98
- const tasks = ['type:check:server', 'type:check:client'].map(name => {
99
- if (!scripts[name]) throw new Error(`缺少 scripts.${name},请保留应用原有类型检查命令。`);
100
- return [getBinName('npm'), ['run', name]];
101
- });
102
- tasks.push(...uiTasks({ eslint: false }));
103
- process.exit(await runTasksSerially(tasks));
104
- }
105
-
106
- async function main(args = process.argv.slice(2)) {
107
- const modes = ['--typecheck', '--typecheck-mcp-ui', '--eslint-mcp-ui'];
108
- if (args.length !== 1 || !modes.includes(args[0])) {
109
- throw new Error('用法: check.cjs --typecheck | --typecheck-mcp-ui | --eslint-mcp-ui');
110
- }
111
- // 自定义 leaf script 意外回到聚合入口时立即失败,避免生成递归进程树。
112
- if (process.env.MIAODA_TYPECHECK_ACTIVE === '1') {
113
- throw new Error('检测到检查命令递归,请检查 type:check:server/client,不能回调 lint 或 type:check。');
114
- }
115
- process.env.MIAODA_TYPECHECK_ACTIVE = '1';
116
- if (args[0] === '--typecheck') return runTypeCheck();
117
- process.exit(await runTasksSerially(uiTasks(args[0] === '--typecheck-mcp-ui' ? { eslint: false } : { types: false })));
118
- }
119
-
120
- module.exports = { getBinName, runTasksSerially, uiTasks, packageScripts, main };
121
- if (require.main === module) {
122
- main().catch(error => { console.error(`[check] ${error.message}`); process.exit(1); });
123
- }