@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 +35 -75
- package/examples/AGENTS.md +14 -2
- package/package.json +4 -2
- package/scripts/cli.js +28 -1
- package/scripts/gen/doctor.js +293 -0
- package/scripts/gen/fields.js +118 -0
- package/scripts/gen/fixtures/run.js +75 -10
- package/scripts/gen/fixtures/widgets.openapi.json +2 -1
- package/scripts/gen/page.js +202 -43
- package/skills/README.md +16 -3
- package/skills/shared/design-to-page/SKILL.md +3 -3
- package/skills/shared/design-to-page/references/list-page-template.md +13 -3
- package/skills/shared/design-to-page/references/overlays.md +6 -0
- package/skills/shared/design-to-page/references/page-conventions.md +2 -1
- package/skills/shared/project-conventions/references/architecture.md +5 -2
- package/skills/shared/project-conventions/references/component-contract.md +38 -0
- package/skills/shared/project-conventions/references/mfprotable.md +59 -0
- package/skills/shared/project-conventions/references/quality-checklist.md +14 -0
- package/skills/shared/project-conventions/references/review-checklist.md +2 -2
- package/skills/shared/publish-test/SKILL.md +10 -0
- package/skills/shared/review-and-submit/references/ai-review.md +5 -1
- package/skills/shared/review-and-submit/references/verify.md +2 -0
package/README.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# @mingfuyun/star-allinone
|
|
2
2
|
|
|
3
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
|
158
|
-
import {
|
|
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
|
|
607
|
-
- React-DOM
|
|
582
|
+
- React **18.3.1**(`peerDependencies`,与星眼/星链一致)
|
|
583
|
+
- React-DOM 18.3.1
|
|
608
584
|
- Antd >= 5.27.3
|
|
609
|
-
- cron-validator >= 1.4.0
|
|
585
|
+
- cron-validator >= 1.4.0(Cron 表达式验证)
|
|
610
586
|
|
|
611
|
-
|
|
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.
|
|
607
|
+
#### 2. 多个 React 实例
|
|
632
608
|
|
|
633
|
-
|
|
609
|
+
`Invalid hook call` 或 owner stack 报错,通常是 hoist 出了两份 React。用 `pnpm list react` 确认只有 18.3.1,并在 `.npmrc` 中 hoist React:
|
|
634
610
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
|
package/examples/AGENTS.md
CHANGED
|
@@ -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.
|
|
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 };
|