@mingfuyun/star-allinone 0.3.2 → 0.3.3

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
@@ -1,6 +1,18 @@
1
1
  # @mingfuyun/star-allinone
2
2
 
3
- 星系运维前端能力包 - 包含Cron表达式生成器等实用组件
3
+ 星系运维前端能力包。运行时只导出 Cron / CustomDrawer / BaseModal / HocModal / ErrorBoundary。表格、布局、权限包装在**业务仓**,不在本包。
4
+
5
+ **导入合同(写错会被 `doctor` 拦住):**
6
+
7
+ | 能力 | 从哪里 import | 不要 |
8
+ | --- | --- | --- |
9
+ | 弹层 overlays | `import { CustomDrawer, BaseModal, HocModal } from '@mingfuyun/star-allinone'` | 不要从 `@/components/CustomDrawer` 抄一份 |
10
+ | 错误边界 | `import { ErrorBoundary } from '@mingfuyun/star-allinone'` | — |
11
+ | Cron | `import { Cron } from '@mingfuyun/star-allinone'` | — |
12
+ | 管理列表表格 | `import MFProTable from '@/components/CommonTable/MFProTable'` | **没有** `MFTablePro`。不要从本包 import 表格 |
13
+ | 请求 | `import Service from '@/services'`(内部 `MFRequest`) | 页面不要直接 axios |
14
+
15
+ Peer 是 **React 18.3.1**,不是 React 19。
4
16
 
5
17
  ## 📦 安装
6
18
 
@@ -35,9 +47,10 @@ pnpm exec star-allinone skills sync --dry-run
35
47
  pnpm exec star-allinone skills sync
36
48
  pnpm exec star-allinone gen service --operation "GET /api/v1/devices/"
37
49
  pnpm exec star-allinone gen page --feature Widgets --route /widgets --title 组件管理 --list-operation "GET /api/v1/widgets/"
50
+ pnpm exec star-allinone doctor
38
51
  ```
39
52
 
40
- `gen service` 按 OpenAPI/Swagger 写一条 `MFRequest` 方法和对应类型。`gen page` 生成标准列表页骨架,并可先生成相关 Service。
53
+ `gen service` 按 OpenAPI/Swagger 写一条 `MFRequest` 方法和对应类型。`gen page` 按 schema 生成列类型、必填和 enum 表单,探测到 `src/components/Page/Container/config.tsx` 时注册 Tab。`doctor` 检查配置、OpenAPI、`MFProTable` 路径、Skill 是否过期、以及错误 import。
41
54
 
42
55
  `list` 查看包内公共 skill。`sync` 把它们写入当前项目的 `.agents/skills/shared-*`,会整体替换过期的 `shared-*`,但不会改 `custom-*`。仓库根没有 `AGENTS.md` 时,会从 `examples/AGENTS.md` 补一份,已有文件不覆盖。
43
56
 
@@ -132,52 +145,15 @@ pnpm install
132
145
 
133
146
  ### 在 Umi 项目中使用
134
147
 
135
- 如果您的项目使用 **Umi** 框架,组件库会自动从您的项目中获取 `@umijs/max` 的能力(如 `useAccess`)。
136
-
137
- **确保安装 peerDependencies:**
138
-
139
- ```bash
140
- # Umi 项目通常已经安装了 @umijs/max,如果没有,请安装:
141
- npm install @umijs/max
142
- # 或
143
- yarn add @umijs/max
144
- # 或
145
- pnpm add @umijs/max
146
- ```
147
-
148
- **工作原理:**
149
-
150
- 1. 组件库通过 `webpack externals` 配置,不会打包 `@umijs/max`
151
- 2. 运行时,组件库会从业务项目的 `node_modules` 中动态获取 `@umijs/max`
152
- 3. 组件库中的权限相关功能(如 `MFTablePro` 的权限过滤)会自动使用您项目中的 `useAccess` Hook
153
-
154
- **示例:在 Umi 项目中使用带权限的组件**
148
+ 本包 **不导出表格**。Umi Max 管理端列表用业务仓自己的 `MFProTable`(权限过滤走项目里的 `useAccess` / `withAuth`):
155
149
 
156
150
  ```tsx
157
- import React from 'react';
158
- import { MFTablePro } from '@mingfuyun/star-allinone';
159
- import { useAccess } from '@umijs/max';
160
-
161
- function TablePage() {
162
- // 组件库会自动使用您项目中的 useAccess
163
- // 无需手动传递,组件库内部会自动获取
164
- return (
165
- <MFTablePro
166
- columns={[
167
- {
168
- title: '操作',
169
- dataIndex: 'action',
170
- render: (_, record) => (
171
- <Button permission="edit">编辑</Button>
172
- ),
173
- },
174
- ]}
175
- // ... 其他配置
176
- />
177
- );
178
- }
151
+ import MFProTable from '@/components/CommonTable/MFProTable';
152
+ import { CustomDrawer, HocModal } from '@mingfuyun/star-allinone';
179
153
  ```
180
154
 
155
+ 完整 props 合同见同步后的 `.agents/skills/shared-project-conventions/references/mfprotable.md`。
156
+
181
157
  ## 🚀 快速开始
182
158
 
183
159
  ### 基础使用
@@ -603,12 +579,12 @@ function Page() {
603
579
 
604
580
  ## 📋 依赖要求
605
581
 
606
- - React >= 19.1.1
607
- - React-DOM >= 19.1.1
582
+ - React **18.3.1**(`peerDependencies`,与星眼/星链一致)
583
+ - React-DOM 18.3.1
608
584
  - Antd >= 5.27.3
609
- - cron-validator >= 1.4.0 (用于Cron表达式验证)
585
+ - cron-validator >= 1.4.0(Cron 表达式验证)
610
586
 
611
- **重要提示**:组件库使用 `peerDependencies` 管理 React 和 Antd,确保你的项目中已安装这些依赖。
587
+ **重要提示**:不要把本包升到 React 19。业务仓和脚手架都是 React 18。
612
588
 
613
589
  ### 常见问题排查
614
590
 
@@ -628,35 +604,19 @@ function Page() {
628
604
  public-hoist-pattern[]=*react*
629
605
  ```
630
606
 
631
- #### 2. "Cannot read properties of undefined (reading 'recentlyCreatedOwnerStacks')" 错误
607
+ #### 2. 多个 React 实例
632
608
 
633
- 这是 React 19 的一个已知问题,可能的原因和解决方案:
609
+ `Invalid hook call` 或 owner stack 报错,通常是 hoist 出了两份 React。用 `pnpm list react` 确认只有 18.3.1,并在 `.npmrc` 中 hoist React:
634
610
 
635
- 1. **React 版本问题**:确保使用 React 19.0.0 或更高版本(推荐 19.1.1+)
636
- 2. **多个 React 实例**:确保项目中只有一个 React 实例
637
- ```bash
638
- # 检查是否有多个 React 实例
639
- npm ls react
640
- # 或
641
- pnpm list react
642
- ```
643
- 3. **pnpm 配置**:如果使用 pnpm,在 `.npmrc` 中添加:
644
- ```ini
645
- shamefully-hoist=true
646
- public-hoist-pattern[]=*react*
647
- public-hoist-pattern[]=*react-dom*
648
- ```
649
- 4. **清除缓存**:尝试清除构建缓存和 node_modules
650
- ```bash
651
- rm -rf node_modules .next dist build
652
- npm install
653
- # 或
654
- pnpm install
655
- ```
656
- 5. **降级到 React 18**:如果问题持续,可以考虑暂时使用 React 18:
657
- ```bash
658
- npm install react@^18.0.0 react-dom@^18.0.0
659
- ```
611
+ ```ini
612
+ shamefully-hoist=true
613
+ public-hoist-pattern[]=*react*
614
+ public-hoist-pattern[]=*react-dom*
615
+ ```
616
+
617
+ ## 云效门禁
618
+
619
+ 业务仓流水线没有仓库内 YAML。建议在云效构建步骤里显式跑 `pnpm typecheck && pnpm lint`。本包的 `npm test` 只跑 codegen fixtures。发版前可用 `ANALYZE=1` 看 chunk。xlsx / 编辑器 / 图表必须 `import()`,不要写进 `app.ts`。
660
620
 
661
621
  ## 🔧 开发
662
622
 
@@ -4,23 +4,35 @@ This is a Umi Max + Ant Design 5 PC admin app. Shared skills live in `.agents/sk
4
4
 
5
5
  Always-on instructions are this file. Do not add `.cursor/rules`.
6
6
 
7
+ Peer React is **18.3.1**. The npm package does not export a table.
8
+
9
+ ## Import contract
10
+
11
+ - Lists: `import MFProTable from '@/components/CommonTable/MFProTable'`. There is no `MFTablePro`. Never import a table from `@mingfuyun/star-allinone`.
12
+ - Overlays: `import { CustomDrawer, BaseModal, HocModal } from '@mingfuyun/star-allinone'`. Do not add or keep `src/components/CustomDrawer` (or local BaseModal / HocModal).
13
+ - Package runtime exports only: `Cron`, `CustomDrawer`, `BaseModal`, `HocModal`, `ErrorBoundary`.
14
+ - Pages: `import Service from '@/services'`. Do not call axios or `src/services/request.tsx` from pages.
15
+
16
+ Props for `MFProTable` are in `.agents/skills/shared-project-conventions/references/mfprotable.md` after skills sync.
17
+
7
18
  ## Default workflow
8
19
 
9
20
  User-facing shared entries only:
10
21
 
11
22
  - One endpoint: `pnpm exec star-allinone gen service --operation "METHOD /path/"` or `$shared-api-to-service`.
12
23
  - Pages: `$shared-design-to-page`. Standard lists run `pnpm exec star-allinone gen page` inside that skill.
24
+ - Project health: `pnpm exec star-allinone doctor`.
13
25
  - Review and commit: `$shared-review-and-submit`. Wait for confirmation.
14
26
  - Test publish: `$shared-publish-test` after explicit pipeline confirmation.
15
27
 
16
28
  ## Hard rules
17
29
 
18
30
  - For non-trivial changes, read `.agents/skills/shared-project-conventions/references/architecture.md`.
19
- - Pages use `import Service from '@/services'`. Do not call axios or `src/services/request.tsx` from pages.
20
- - Lists: `MFProTable`. Forms: `ProForm`. Overlays: `CustomDrawer`, `BaseModal`, `HocModal` from `@mingfuyun/star-allinone`.
21
31
  - Keep columns and actions in `const.tsx`. Empty cells render as `-`.
22
32
  - Keep DTOs in the domain `typings.d.ts` namespace. Preserve backend snake_case.
23
33
  - Do not edit `src/services/request.tsx` for a single endpoint.
24
34
  - Every `await` uses try/catch. Do not use `Modal.confirm`.
25
35
  - After TypeScript changes run `pnpm typecheck`.
36
+ - Do not treat `pnpm test` as unit tests unless that script actually runs a test runner.
37
+ - Heavy modules (`xlsx`, editors, charts) must be `import()`. Do not add them to `src/app.ts`.
26
38
  - If `.agents/project/conventions.md` or `.agents/project/review-rules.md` exists, apply them as additions only.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mingfuyun/star-allinone",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "星系运维前端能力包",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -40,7 +40,9 @@
40
40
  "publish:dev": "node scripts/publish.js --dev",
41
41
  "skills:sync": "node scripts/cli.js skills sync",
42
42
  "skills:list": "node scripts/cli.js skills list",
43
+ "doctor": "node scripts/cli.js doctor",
43
44
  "prepublishOnly": "npm run build",
45
+ "typecheck": "tsc --noEmit",
44
46
  "test": "node scripts/gen/fixtures/run.js",
45
47
  "dev": "webpack --mode=none --watch",
46
48
  "clean": "rm -rf dist"
@@ -76,7 +78,7 @@
76
78
  "webpack-cli": "^5.1.4"
77
79
  },
78
80
  "peerDependencies": {
79
- "react": "18.3.1"
81
+ "react": "^18.3.1"
80
82
  },
81
83
  "publishConfig": {
82
84
  "access": "public",
package/scripts/cli.js CHANGED
@@ -17,6 +17,7 @@ function usage() {
17
17
  console.log(`Usage:
18
18
  star-allinone skills list
19
19
  star-allinone skills sync [--target <dir>] [--dry-run]
20
+ star-allinone doctor [--target <dir>]
20
21
  star-allinone gen service --operation "GET /api/v1/devices/" [--spec <file>] [--domain Devices] [--mock-file] [--dry-run]
21
22
  star-allinone gen page --feature Widgets --route /widgets --title 组件管理 --list-operation "GET /api/v1/widgets/" [--create-operation ...] [--update-operation ...] [--delete-operation ...] [--dry-run]
22
23
 
@@ -26,8 +27,10 @@ skills sync writes package skills/shared/* to <project>/.agents/skills/shared-*.
26
27
  It never writes ~/.codex/skills and never modifies custom-* skills.
27
28
  If <project>/AGENTS.md is missing, it copies examples/AGENTS.md once.
28
29
 
30
+ doctor checks config, OpenAPI, MFRequest, MFProTable, skill sync, and bad imports.
31
+
29
32
  gen service writes one MFRequest method and types from OpenAPI/Swagger.
30
- gen page scaffolds a list + drawer + confirm page and can generate services first.`);
33
+ gen page scaffolds a list + drawer + confirm page from OpenAPI schema and can generate services first.`);
31
34
  process.exitCode = 1;
32
35
  }
33
36
 
@@ -174,10 +177,28 @@ function syncProject(options) {
174
177
  copySkill(path.join(skillsRoot, skillName), destination, skillName, prefix, variables, options);
175
178
  }
176
179
  pruneStaleShared(destination, prefix, syncedNames, options);
180
+ writeSyncStamp(destination, prefix, syncedNames, options);
177
181
  seedAgentsMarkdown(target, options);
178
182
  console.log(`${options.dryRun ? 'Would sync' : 'Synced'} shared skills -> ${destination}`);
179
183
  }
180
184
 
185
+ function writeSyncStamp(destination, prefix, syncedNames, options) {
186
+ const pkg = readJson(path.join(root, 'package.json'));
187
+ const file = path.join(destination, '.star-allinone-sync.json');
188
+ const payload = `${JSON.stringify(
189
+ {
190
+ packageName: pkg.name,
191
+ packageVersion: pkg.version,
192
+ publicSkillPrefix: prefix,
193
+ skills: syncedNames,
194
+ },
195
+ null,
196
+ 2,
197
+ )}\n`;
198
+ console.log(`${options.dryRun ? '[dry-run] ' : ''}${path.relative(path.dirname(destination), file)}`);
199
+ if (!options.dryRun) fs.writeFileSync(file, payload);
200
+ }
201
+
181
202
  function seedAgentsMarkdown(target, options) {
182
203
  const dest = path.join(target, 'AGENTS.md');
183
204
  const source = path.join(root, 'examples', 'AGENTS.md');
@@ -195,6 +216,12 @@ function main() {
195
216
  if (positional.length) return usage();
196
217
  return syncProject(options);
197
218
  }
219
+ if (group === 'doctor') {
220
+ const args = [command, ...rest].filter((item) => typeof item === 'string');
221
+ const { options, positional } = parseArgs(args);
222
+ if (positional.length) return usage();
223
+ return require('./gen/doctor').runDoctor(process.cwd(), options);
224
+ }
198
225
  if (group === 'gen') {
199
226
  const { options, positional } = parseArgs(rest);
200
227
  if (positional.length) return usage();
@@ -0,0 +1,293 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const PACKAGE_ROOT = path.resolve(__dirname, '../..');
6
+ const SHARED_SKILLS = path.join(PACKAGE_ROOT, 'skills', 'shared');
7
+ const DEFAULT_PREFIX = 'shared-';
8
+ const LOCAL_OVERLAYS = ['CustomDrawer', 'BaseModal', 'HocModal'];
9
+ const REQUIRED_CONFIG_KEYS = ['project', 'serviceDir'];
10
+ const KNOWN_CONFIG_KEYS = new Set([
11
+ 'project',
12
+ 'publicSkillPrefix',
13
+ 'serviceDir',
14
+ 'openapi',
15
+ 'publishPipelineId',
16
+ 'sharedSkills',
17
+ 'variables',
18
+ ]);
19
+ const IGNORE_DIRS = new Set([
20
+ 'node_modules',
21
+ 'dist',
22
+ '.git',
23
+ '.umi',
24
+ 'coverage',
25
+ '.turbo',
26
+ ]);
27
+
28
+ function readJson(file) {
29
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
30
+ }
31
+
32
+ function existsAny(root, relatives) {
33
+ return relatives.find((relative) => fs.existsSync(path.join(root, relative)));
34
+ }
35
+
36
+ function walkFiles(dir, results = []) {
37
+ if (!fs.existsSync(dir)) return results;
38
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
39
+ if (IGNORE_DIRS.has(entry.name)) continue;
40
+ const full = path.join(dir, entry.name);
41
+ if (entry.isDirectory()) {
42
+ walkFiles(full, results);
43
+ continue;
44
+ }
45
+ if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) results.push(full);
46
+ }
47
+ return results;
48
+ }
49
+
50
+ const TEMPLATE_EXT = new Set(['.md', '.json', '.yml', '.yaml', '.txt']);
51
+
52
+ function render(content, variables) {
53
+ return content.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (_, key) => {
54
+ const value = key.split('.').reduce((current, part) => current?.[part], variables);
55
+ return value == null ? `{{${key}}}` : String(value);
56
+ });
57
+ }
58
+
59
+ function hashTree(dir, variables) {
60
+ if (!fs.existsSync(dir)) return '';
61
+ const files = [];
62
+ const walk = (current, relative) => {
63
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
64
+ const nextRel = relative ? `${relative}/${entry.name}` : entry.name;
65
+ const full = path.join(current, entry.name);
66
+ if (entry.isDirectory()) {
67
+ walk(full, nextRel);
68
+ continue;
69
+ }
70
+ let content = fs.readFileSync(full);
71
+ if (variables && TEMPLATE_EXT.has(path.extname(entry.name).toLowerCase())) {
72
+ content = Buffer.from(render(content.toString('utf8'), variables));
73
+ }
74
+ files.push({ rel: nextRel, content });
75
+ }
76
+ };
77
+ walk(dir, '');
78
+ files.sort((a, b) => a.rel.localeCompare(b.rel));
79
+ const digest = crypto.createHash('sha256');
80
+ for (const file of files) {
81
+ digest.update(file.rel);
82
+ digest.update('\0');
83
+ digest.update(file.content);
84
+ digest.update('\0');
85
+ }
86
+ return digest.digest('hex');
87
+ }
88
+
89
+ function listPackageSkills() {
90
+ if (!fs.existsSync(SHARED_SKILLS)) return [];
91
+ return fs.readdirSync(SHARED_SKILLS, { withFileTypes: true })
92
+ .filter((entry) => entry.isDirectory())
93
+ .map((entry) => entry.name)
94
+ .sort();
95
+ }
96
+
97
+ function check(ok, level, message) {
98
+ return { ok, level, message };
99
+ }
100
+
101
+ function runDoctor(cwd, options = {}) {
102
+ const root = path.resolve(options.target || cwd);
103
+ const findings = [];
104
+ const configFile = path.join(root, '.star-allinone.json');
105
+
106
+ if (!fs.existsSync(configFile)) {
107
+ findings.push(check(false, 'error', `Missing ${path.relative(root, configFile)}`));
108
+ printReport(root, findings);
109
+ return findings;
110
+ }
111
+
112
+ let config;
113
+ try {
114
+ config = readJson(configFile);
115
+ findings.push(check(true, 'ok', '.star-allinone.json'));
116
+ } catch (error) {
117
+ findings.push(check(false, 'error', `.star-allinone.json is not valid JSON: ${error.message}`));
118
+ printReport(root, findings);
119
+ return findings;
120
+ }
121
+
122
+ const unknownKeys = Object.keys(config).filter((key) => !KNOWN_CONFIG_KEYS.has(key));
123
+ if (unknownKeys.length) {
124
+ findings.push(check(false, 'warn', `Unknown config keys: ${unknownKeys.join(', ')}`));
125
+ }
126
+ for (const key of REQUIRED_CONFIG_KEYS) {
127
+ if (!config[key]) {
128
+ findings.push(check(false, 'error', `Config missing required "${key}"`));
129
+ }
130
+ }
131
+
132
+ const serviceDir = config.serviceDir || 'src/services';
133
+ if (!fs.existsSync(path.join(root, serviceDir))) {
134
+ findings.push(check(false, 'error', `serviceDir not found: ${serviceDir}`));
135
+ } else {
136
+ findings.push(check(true, 'ok', `serviceDir ${serviceDir}`));
137
+ }
138
+
139
+ const requestFile = existsAny(root, [
140
+ `${serviceDir}/request.tsx`,
141
+ `${serviceDir}/request.ts`,
142
+ ]);
143
+ if (!requestFile) {
144
+ findings.push(check(false, 'error', `MFRequest helper missing under ${serviceDir}/request.(ts|tsx)`));
145
+ } else {
146
+ const content = fs.readFileSync(path.join(root, requestFile), 'utf8');
147
+ if (!content.includes('MFRequest')) {
148
+ findings.push(check(false, 'error', `${requestFile} does not define MFRequest`));
149
+ } else {
150
+ findings.push(check(true, 'ok', requestFile));
151
+ }
152
+ }
153
+
154
+ if (config.openapi) {
155
+ if (fs.existsSync(path.join(root, config.openapi))) {
156
+ findings.push(check(true, 'ok', `openapi ${config.openapi}`));
157
+ } else {
158
+ findings.push(check(false, 'error', `openapi file not found: ${config.openapi}`));
159
+ }
160
+ } else {
161
+ findings.push(check(false, 'error', 'Config missing "openapi"'));
162
+ }
163
+
164
+ if (!config.publishPipelineId) {
165
+ findings.push(check(false, 'warn', 'Config missing "publishPipelineId"; $shared-publish-test cannot run'));
166
+ } else {
167
+ findings.push(check(true, 'ok', `publishPipelineId ${config.publishPipelineId}`));
168
+ }
169
+
170
+ const tableFile = existsAny(root, [
171
+ 'src/components/CommonTable/MFProTable.tsx',
172
+ 'src/components/CommonTable/MFProTable.ts',
173
+ ]);
174
+ if (!tableFile) {
175
+ findings.push(check(false, 'error', 'MFProTable missing at src/components/CommonTable/MFProTable.tsx (app-local, not this package)'));
176
+ } else {
177
+ findings.push(check(true, 'ok', tableFile));
178
+ }
179
+
180
+ if (!fs.existsSync(path.join(root, 'AGENTS.md'))) {
181
+ findings.push(check(false, 'warn', 'Missing AGENTS.md; run skills sync once to seed it'));
182
+ } else {
183
+ findings.push(check(true, 'ok', 'AGENTS.md'));
184
+ }
185
+
186
+ const prefix = config.publicSkillPrefix || DEFAULT_PREFIX;
187
+ const skillsDir = path.join(root, '.agents', 'skills');
188
+ const stampFile = path.join(skillsDir, '.star-allinone-sync.json');
189
+ const packageJson = readJson(path.join(PACKAGE_ROOT, 'package.json'));
190
+ if (fs.existsSync(stampFile)) {
191
+ try {
192
+ const stamp = readJson(stampFile);
193
+ if (stamp.packageVersion && stamp.packageVersion !== packageJson.version) {
194
+ findings.push(check(false, 'warn', `skills synced from ${stamp.packageVersion}, package is ${packageJson.version}. Run skills sync`));
195
+ } else {
196
+ findings.push(check(true, 'ok', `skills sync stamp ${stamp.packageVersion || packageJson.version}`));
197
+ }
198
+ } catch (error) {
199
+ findings.push(check(false, 'warn', `.star-allinone-sync.json is not valid JSON: ${error.message}`));
200
+ }
201
+ } else if (fs.existsSync(skillsDir)) {
202
+ findings.push(check(false, 'warn', 'Missing .agents/skills/.star-allinone-sync.json; run skills sync'));
203
+ }
204
+ const packageSkills = listPackageSkills();
205
+ if (!fs.existsSync(skillsDir)) {
206
+ findings.push(check(false, 'error', 'Missing .agents/skills. Run pnpm exec star-allinone skills sync'));
207
+ for (const skill of packageSkills) {
208
+ findings.push(check(false, 'error', `Shared skill not synced: ${prefix}${skill}. Run pnpm exec star-allinone skills sync`));
209
+ }
210
+ } else {
211
+ for (const skill of packageSkills) {
212
+ const publicName = `${prefix}${skill}`;
213
+ const dest = path.join(skillsDir, publicName);
214
+ const source = path.join(SHARED_SKILLS, skill);
215
+ if (!fs.existsSync(dest)) {
216
+ findings.push(check(false, 'error', `Shared skill not synced: ${publicName}. Run pnpm exec star-allinone skills sync`));
217
+ continue;
218
+ }
219
+ const variables = {
220
+ ...config,
221
+ ...config.variables,
222
+ sharedSkillName: publicName,
223
+ project: { ...config, ...config.variables },
224
+ };
225
+ if (hashTree(source, variables) !== hashTree(dest)) {
226
+ findings.push(check(false, 'warn', `${publicName} is stale vs this package. Run pnpm exec star-allinone skills sync`));
227
+ } else {
228
+ findings.push(check(true, 'ok', publicName));
229
+ }
230
+ }
231
+ }
232
+
233
+ const pkgFile = path.join(root, 'package.json');
234
+ if (fs.existsSync(pkgFile)) {
235
+ const pkg = readJson(pkgFile);
236
+ const dep = pkg.dependencies?.['@mingfuyun/star-allinone']
237
+ || pkg.devDependencies?.['@mingfuyun/star-allinone'];
238
+ if (!dep) {
239
+ findings.push(check(false, 'warn', 'package.json does not depend on @mingfuyun/star-allinone'));
240
+ } else {
241
+ findings.push(check(true, 'ok', `@mingfuyun/star-allinone ${dep}`));
242
+ }
243
+ const testScript = pkg.scripts?.test || '';
244
+ if (/\bmax\s+dev\b/.test(testScript)) {
245
+ findings.push(check(false, 'warn', 'package.json "test" runs max dev. Rename it to start:test / dev:test so agents do not treat it as unit tests'));
246
+ }
247
+ }
248
+
249
+ for (const overlay of LOCAL_OVERLAYS) {
250
+ const overlayDir = path.join(root, 'src/components', overlay);
251
+ if (fs.existsSync(overlayDir)) {
252
+ findings.push(check(false, 'error', `Local ${overlay} at src/components/${overlay} should be removed; import from @mingfuyun/star-allinone`));
253
+ }
254
+ }
255
+
256
+ const files = walkFiles(path.join(root, 'src'));
257
+ const badTable = [];
258
+ const localOverlay = [];
259
+ const tableFromPackage = /import\s+(?:type\s+)?(?:\{[^}]*\bMF(?:TablePro|ProTable)\b[^}]*\}|MF(?:TablePro|ProTable))\s+from\s+['"]@mingfuyun\/star-allinone['"]/;
260
+ const localDrawer = /from\s+['"]@\/components\/(?:CustomDrawer|BaseModal|HocModal)(?:\/[^'"]*)?['"]/;
261
+ for (const file of files) {
262
+ const content = fs.readFileSync(file, 'utf8');
263
+ const relative = path.relative(root, file);
264
+ if (tableFromPackage.test(content)) badTable.push(relative);
265
+ if (localDrawer.test(content)) localOverlay.push(relative);
266
+ }
267
+ if (badTable.length) {
268
+ findings.push(check(false, 'error', `MFProTable/MFTablePro imported from the npm package in ${badTable.join(', ')}. Use @/components/CommonTable/MFProTable`));
269
+ } else {
270
+ findings.push(check(true, 'ok', 'No table imports from @mingfuyun/star-allinone'));
271
+ }
272
+ if (localOverlay.length) {
273
+ findings.push(check(false, 'error', `Local overlay imports (use @mingfuyun/star-allinone): ${localOverlay.join(', ')}`));
274
+ } else {
275
+ findings.push(check(true, 'ok', 'Overlay imports use @mingfuyun/star-allinone or none found'));
276
+ }
277
+
278
+ printReport(root, findings);
279
+ return findings;
280
+ }
281
+
282
+ function printReport(root, findings) {
283
+ console.log(`star-allinone doctor (${root})`);
284
+ for (const item of findings) {
285
+ const tag = item.level === 'ok' ? 'OK ' : item.level === 'warn' ? 'WARN' : 'ERR ';
286
+ console.log(`${tag} ${item.message}`);
287
+ }
288
+ const errors = findings.filter((item) => item.level === 'error').length;
289
+ const warnings = findings.filter((item) => item.level === 'warn').length;
290
+ console.log(`\n${errors} error(s), ${warnings} warning(s)`);
291
+ }
292
+
293
+ module.exports = { runDoctor, hashTree };