@manohub/kit 0.6.0

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 (36) hide show
  1. package/CONTRACT.md +597 -0
  2. package/README.md +158 -0
  3. package/bin/kit.mjs +118 -0
  4. package/dist/composables/use-client-pagination.d.ts +43 -0
  5. package/dist/composables/use-client-pagination.js +28 -0
  6. package/dist/entry/create-query-client.d.ts +8 -0
  7. package/dist/entry/create-query-client.js +14 -0
  8. package/dist/entry/create-sub-app.d.ts +81 -0
  9. package/dist/entry/create-sub-app.js +111 -0
  10. package/dist/entry/index.d.ts +4 -0
  11. package/dist/entry/index.js +13 -0
  12. package/dist/entry/initial-guard.d.ts +12 -0
  13. package/dist/entry/initial-guard.js +23 -0
  14. package/dist/index.d.ts +19 -0
  15. package/dist/index.js +7 -0
  16. package/dist/providers/locale-detection.d.ts +35 -0
  17. package/dist/providers/locale-detection.js +44 -0
  18. package/dist/providers/setup-i18n.d.ts +50 -0
  19. package/dist/providers/setup-i18n.js +42 -0
  20. package/dist/services/app-container.d.ts +31 -0
  21. package/dist/services/app-container.js +22 -0
  22. package/dist/services/app-context.d.ts +4 -0
  23. package/dist/services/app-context.js +11 -0
  24. package/dist/services/index.d.ts +7 -0
  25. package/package.json +68 -0
  26. package/skills/README.md +78 -0
  27. package/skills/install.mjs +299 -0
  28. package/skills/kit/SKILL.md +85 -0
  29. package/skills/kit/references/adoption.md +170 -0
  30. package/skills/kit/references/contract-index.md +76 -0
  31. package/skills/kit-dev/SKILL.md +117 -0
  32. package/skills/kit-dev/references/page-recipes.md +350 -0
  33. package/skills/kit-dev/references/style-rules.md +47 -0
  34. package/skills/kit-migrate/SKILL.md +125 -0
  35. package/skills/kit-migrate/references/migration-map.md +294 -0
  36. package/skills/kit-migrate/references/migration-playbook.md +188 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * 技能安装器:把随本包分发的 AI 技能落到消费方的技能目录。
3
+ *
4
+ * 用法(消费方工程根执行):
5
+ * pnpm exec kit install
6
+ * pnpm exec kit install --also-claude
7
+ * pnpm exec kit install --target .x/skills
8
+ * pnpm exec kit install --dry-run
9
+ *
10
+ * 设计取舍:
11
+ *
12
+ * 1. **幂等**:每个技能「先删同名目录再整体复制」。包升级后重跑一次即刷新到新版内容,
13
+ * 不存在「旧文件残留」或「同名合并」的中间态。
14
+ * 2. **越界保护**:只允许操作 `SKILL_NAMES` 白名单内的目录名,且目标必须是白名单目录的**直接子路径**。
15
+ * 删除动作因此永远不可能落到用户自己的技能或其它文件上。
16
+ * 3. **零依赖 + 纯函数**:判定与计划部分抽成纯函数,落盘动作只在直接执行时发生。
17
+ * 改这里的判定逻辑后**必须手工验证**:造一个越界目标(白名单外的目录名)与一个非空目标,
18
+ * 确认都被拒绝(安装器本身没有自测,见仓根 `docs/testing.md`「已放弃的守卫」)。
19
+ * 4. 只复制**技能目录**,跳过本文件与 `README.md`(那是给包维护者看的),也不复制 `__tests__`。
20
+ * 5. **落盘前先算差异**:告诉消费方「这次刷新了什么」——包升级后技能内容有没有变,光看命令输出
21
+ * 是看不出来的;没有差异就是「已是最新」,有差异则列出新增 / 更新 / 删除的文件数。
22
+ */
23
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from 'node:fs'
24
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
25
+ import { fileURLToPath, pathToFileURL } from 'node:url'
26
+
27
+ /** 本技能包管理的技能目录名(白名单:只有这些名字允许被创建或清理) */
28
+ export const SKILL_NAMES = ['kit', 'kit-migrate', 'kit-dev']
29
+
30
+ /** 缺省的技能目录(相对消费方工程根) */
31
+ export const TARGET_CODEXBUDDY = ['.codebuddy', 'skills']
32
+ export const TARGET_CLAUDE = ['.claude', 'skills']
33
+
34
+ /** 把源目录里「名字在白名单内、且含 SKILL.md」的目录识别为可安装技能 */
35
+ export function listSourceSkills(sourceDir) {
36
+ return SKILL_NAMES.filter((name) => {
37
+ const dir = join(sourceDir, name)
38
+ return existsSync(dir) && statSync(dir).isDirectory() && existsSync(join(dir, 'SKILL.md'))
39
+ })
40
+ }
41
+
42
+ /** 目标目录安全校验:不允许写到文件系统根(`/`、`C:\`) */
43
+ export function assertSafeTarget(targetDir) {
44
+ const resolved = resolve(targetDir)
45
+ if (resolved === dirname(resolved)) {
46
+ throw new Error(`拒绝写入文件系统根目录:${resolved}`)
47
+ }
48
+ return resolved
49
+ }
50
+
51
+ /** 越界保护:`path` 必须是 `targetDir` 的直接子路径,且名字在白名单内 */
52
+ export function assertManagedPath(targetDir, path) {
53
+ const target = assertSafeTarget(targetDir)
54
+ const name = basename(path)
55
+ if (!SKILL_NAMES.includes(name)) {
56
+ throw new Error(`拒绝操作白名单外的目录:${path}`)
57
+ }
58
+ if (dirname(resolve(path)) !== target) {
59
+ throw new Error(`拒绝操作目标目录之外的路径:${path}`)
60
+ }
61
+ return join(target, name)
62
+ }
63
+
64
+ /**
65
+ * 解析命令行参数。未知参数直接报错 —— 静默忽略会让人以为选项生效了。
66
+ * (不参与单测的 IO,纯字符串处理)
67
+ */
68
+ export function parseArgs(argv) {
69
+ const opts = { target: null, alsoClaude: false, dryRun: false, help: false }
70
+ for (let i = 0; i < argv.length; i += 1) {
71
+ const arg = argv[i]
72
+ if (arg === '--target') {
73
+ const value = argv[i + 1]
74
+ if (!value || value.startsWith('--')) throw new Error('--target 需要一个目录参数')
75
+ opts.target = value
76
+ i += 1
77
+ } else if (arg === '--also-claude') {
78
+ opts.alsoClaude = true
79
+ } else if (arg === '--dry-run') {
80
+ opts.dryRun = true
81
+ } else if (arg === '--help' || arg === '-h') {
82
+ opts.help = true
83
+ } else {
84
+ throw new Error(`未知参数:${arg}(用 --help 看用法)`)
85
+ }
86
+ }
87
+ return opts
88
+ }
89
+
90
+ /** 计算落盘目标目录:默认 `.codebuddy/skills`,`--target` 优先,`--also-claude` 追加 */
91
+ export function resolveTargets({ cwd, target = null, alsoClaude = false }) {
92
+ if (target) {
93
+ const resolved = isAbsolute(target) ? target : join(cwd, target)
94
+ return [assertSafeTarget(resolved)]
95
+ }
96
+ const targets = [assertSafeTarget(join(cwd, ...TARGET_CODEXBUDDY))]
97
+ if (alsoClaude) targets.push(assertSafeTarget(join(cwd, ...TARGET_CLAUDE)))
98
+ return targets
99
+ }
100
+
101
+ /** 统计目录下的文件数(用于落盘摘要) */
102
+ export function countFiles(dir) {
103
+ let total = 0
104
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
105
+ total += entry.isDirectory() ? countFiles(join(dir, entry.name)) : 1
106
+ }
107
+ return total
108
+ }
109
+
110
+ /** 递归列出目录下的文件(绝对路径) */
111
+ function walkFiles(dir) {
112
+ const out = []
113
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
114
+ const full = join(dir, entry.name)
115
+ if (entry.isDirectory()) out.push(...walkFiles(full))
116
+ else out.push(full)
117
+ }
118
+ return out
119
+ }
120
+
121
+ /**
122
+ * 读一份目录快照:`相对路径(posix)→ 文件内容`;目录不存在时返回 `null`
123
+ * (`null` 表示「环境里还没有这份技能」,与「有目录但内容全空」区分开)。
124
+ */
125
+ export function readDirSnapshot(dir) {
126
+ if (!existsSync(dir)) return null
127
+ const snapshot = new Map()
128
+ for (const file of walkFiles(dir)) {
129
+ snapshot.set(relative(dir, file).split('\\').join('/'), readFileSync(file, 'utf8'))
130
+ }
131
+ return snapshot
132
+ }
133
+
134
+ /**
135
+ * 比较「包内技能」与「已落盘技能」的内容差异,用于告诉消费方这次刷新了什么。
136
+ *
137
+ * 比"版本号比对"更直接:技能的版本号与包版本没有绑定关系,而内容一变就是要重跑的信号。
138
+ */
139
+ export function diffSkillDirs(from, to) {
140
+ const after = readDirSnapshot(from)
141
+ const before = readDirSnapshot(to)
142
+ if (after === null) throw new Error(`技能源目录不存在:${from}`)
143
+ if (before === null) {
144
+ return { firstInstall: true, added: [...after.keys()].sort(), changed: [], removed: [] }
145
+ }
146
+ const added = []
147
+ const changed = []
148
+ for (const [rel, content] of after) {
149
+ if (!before.has(rel)) added.push(rel)
150
+ else if (before.get(rel) !== content) changed.push(rel)
151
+ }
152
+ const removed = [...before.keys()].filter((rel) => !after.has(rel))
153
+ return { firstInstall: false, added: added.sort(), changed: changed.sort(), removed: removed.sort() }
154
+ }
155
+
156
+ /** 生成安装计划(不落盘);源目录里一个技能都没有时直接报错 */
157
+ export function planInstall({ sourceDir, targetDirs }) {
158
+ if (!existsSync(sourceDir)) throw new Error(`技能源目录不存在:${sourceDir}`)
159
+ const skills = listSourceSkills(sourceDir)
160
+ if (skills.length === 0) {
161
+ throw new Error(`技能源目录里没有可安装的技能(期望 ${SKILL_NAMES.join(' / ')}):${sourceDir}`)
162
+ }
163
+ if (targetDirs.length === 0) throw new Error('没有解析出任何目标目录')
164
+
165
+ const actions = []
166
+ for (const target of targetDirs) {
167
+ for (const skill of skills) {
168
+ const from = join(sourceDir, skill)
169
+ const to = join(assertSafeTarget(target), skill)
170
+ assertManagedPath(target, to)
171
+ actions.push({ skill, from, to, files: countFiles(from) })
172
+ }
173
+ }
174
+ return { sourceDir, targetDirs, skills, actions }
175
+ }
176
+
177
+ /**
178
+ * 落盘(`dryRun` 时只返回计划结果,不写文件系统)。
179
+ *
180
+ * 差异必须**在删除之前**读出来(删完就比对不出来了),所以先快照再清理。
181
+ */
182
+ export function applyInstall(plan, { dryRun = false } = {}) {
183
+ const results = []
184
+ for (const action of plan.actions) {
185
+ const diff = diffSkillDirs(action.from, action.to)
186
+ if (!dryRun) {
187
+ mkdirSync(dirname(action.to), { recursive: true })
188
+ // 先清理再复制:保证与包内内容完全一致(幂等)
189
+ rmSync(action.to, { recursive: true, force: true })
190
+ cpSync(action.from, action.to, { recursive: true })
191
+ }
192
+ results.push({ ...action, diff, dryRun })
193
+ }
194
+ return results
195
+ }
196
+
197
+ /**
198
+ * 是否「被直接执行」(而非被 `import`)。
199
+ *
200
+ * **必须比对 realpath**:pnpm 把包实体放在 `node_modules/.pnpm/<pkg>@<ver>/...`,
201
+ * 消费方引用的是软链路径 `node_modules/@manohub/kit/...`;Node 解析 ESM 时会取真实路径,
202
+ * 于是 `import.meta.url` 与 `process.argv[1]` 字面不等 —— 直接比较会让安装器**静默不执行**
203
+ * (真实踩过:在 pnpm 消费方里跑安装器零输出、技能也没落盘,而这正是最主要的使用场景)。
204
+ *
205
+ * `realpath` 可注入,便于单测覆盖「软链」「路径不存在」两种输入。
206
+ */
207
+ export function isDirectRun(argv1, moduleUrl, realpath = (p) => realpathSync(p)) {
208
+ if (!argv1) return false
209
+ if (moduleUrl === pathToFileURL(argv1).href) return true
210
+ try {
211
+ return moduleUrl === pathToFileURL(realpath(argv1)).href
212
+ } catch {
213
+ return false
214
+ }
215
+ }
216
+
217
+ export function usage() {
218
+ return [
219
+ '把 @manohub/kit 随包的 AI 技能安装到本工程的技能目录。',
220
+ '',
221
+ '用法:',
222
+ ' pnpm exec kit install [选项] (npm 消费方:npx kit install)',
223
+ ' node node_modules/@manohub/kit/skills/install.mjs [选项] 等价写法(老脚本/钩子里可用)',
224
+ '',
225
+ '选项:',
226
+ ' --target <dir> 指定目标目录(缺省 <cwd>/.codebuddy/skills)',
227
+ ' --also-claude 同时安装到 <cwd>/.claude/skills',
228
+ ' --dry-run 只打印将要落盘的内容,不写文件系统',
229
+ ' --help, -h 显示本帮助',
230
+ '',
231
+ `管理的技能:${SKILL_NAMES.join(' / ')}(安装时先清理同名目录再复制,可反复执行)`,
232
+ '每次执行都会报告技能内容有没有变化:显示「已是包内最新」说明不必重跑,',
233
+ '显示「更新 N」说明这次升级改了技能内容、本次刷新已生效。',
234
+ ].join('\n')
235
+ }
236
+
237
+ /** 把一次安装的差异描述成一句人话;无差异时明确说「已是最新」 */
238
+ export function describeDiff(diff, files = 0) {
239
+ if (!diff) return ''
240
+ if (diff.firstInstall) return `首次安装(${files} 个文件)`
241
+ const parts = []
242
+ if (diff.changed.length > 0) parts.push(`更新 ${diff.changed.length}`)
243
+ if (diff.added.length > 0) parts.push(`新增 ${diff.added.length}`)
244
+ if (diff.removed.length > 0) parts.push(`删除 ${diff.removed.length}`)
245
+ if (parts.length === 0) return `已是包内最新(内容无变化,共 ${files} 个文件)`
246
+ return `${parts.join(' / ')}(共 ${files} 个文件)`
247
+ }
248
+
249
+ /** 一次安装是否什么都没变(用于把「无需刷新」说清楚) */
250
+ function isUnchanged(diff) {
251
+ return Boolean(diff) && !diff.firstInstall && diff.changed.length + diff.added.length + diff.removed.length === 0
252
+ }
253
+
254
+ function printSummary(results, plan, dryRun) {
255
+ const lines = ['[kit 技能安装]', ` 源:${plan.sourceDir}`, ` 技能:${plan.skills.join('、')}`]
256
+ for (const target of plan.targetDirs) {
257
+ lines.push(` 目标:${target}`)
258
+ }
259
+ if (dryRun) lines.push(' dry-run:以下为将落盘的结果(未写文件系统)')
260
+ for (const item of results) {
261
+ lines.push(` ${item.to} —— ${describeDiff(item.diff, item.files)}`)
262
+ }
263
+ if (dryRun) {
264
+ lines.push(' 去掉 --dry-run 执行实际安装')
265
+ } else if (results.length > 0 && results.every((item) => isUnchanged(item.diff))) {
266
+ lines.push(' 完成:技能内容与包内一致,无需刷新(说明本包这次升级没有改动技能包)')
267
+ } else {
268
+ lines.push(' 完成:技能目录已与包内内容对齐(包升级后重跑本命令即刷新)')
269
+ }
270
+ console.log(lines.join('\n'))
271
+ }
272
+
273
+ async function main() {
274
+ const opts = parseArgs(process.argv.slice(2))
275
+ if (opts.help) {
276
+ console.log(usage())
277
+ return
278
+ }
279
+
280
+ const sourceDir = dirname(fileURLToPath(import.meta.url))
281
+ const targetDirs = resolveTargets({ cwd: process.cwd(), target: opts.target, alsoClaude: opts.alsoClaude })
282
+
283
+ let plan
284
+ try {
285
+ plan = planInstall({ sourceDir, targetDirs })
286
+ } catch (error) {
287
+ console.error(`[kit 技能安装] 失败:${error.message}`)
288
+ process.exitCode = 1
289
+ return
290
+ }
291
+
292
+ const results = applyInstall(plan, { dryRun: opts.dryRun })
293
+ printSummary(results, plan, opts.dryRun)
294
+ }
295
+
296
+ // 仅在被直接执行时落盘;被测试 import 时只取纯函数
297
+ if (isDirectRun(process.argv[1], import.meta.url)) {
298
+ await main()
299
+ }
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: kit
3
+ version: 1.0.0
4
+ description: 子应用骨架层 @manohub/kit 的入口技能:识别意图并编排到 kit-migrate(存量应用改造)或 kit-dev(日常页面开发),并承载两个子技能共用的全局硬约束。触发条件:用户提到 kit / 骨架层 / 接入契约本身、询问该怎么接入或改造但尚未落到具体页面或文件、不确定该用哪个 kit 技能、或需要接入步骤/升级口径等跨场景事项时使用;一旦意图明确落到「迁移存量应用」或「写/改页面」,由对应子技能承接。
5
+ ---
6
+
7
+ # kit:骨架层入口编排
8
+
9
+ 把「用骨架层做事」收敛到一条路径:先判意图,再交给对应子技能。
10
+ 本技能不设计页面、不改代码,只做路由与硬约束。
11
+
12
+ ## 一、先判意图
13
+
14
+ | 用户意图信号 | 走向 |
15
+ |---|---|
16
+ | 应用还没接本包;或页面里还有 `App*` 组件名 / `.ak-*` 样式 / 直连底层组件库;或出现「迁移」「改造」「接入」「违规盘点」「收口」 | `Skill('kit-migrate')` |
17
+ | 应用已接入,要新增或修改页面、组件;或要求「按规范写」「用 Page / Panel / Table…」 | `Skill('kit-dev')` |
18
+ | 两者交织(边接入边改页面) | 先跑 `kit-migrate` 的接入阶段(契约已落到应用文档),再进 `kit-dev` |
19
+ | 应用还不存在,要从零建一个子应用 | 按 `references/adoption.md` «9. 新应用从零搭建» 走,建成后按上面的表继续 |
20
+ | 问的是组件本身的 prop / 用法(不是页面结构) | 读 `node_modules/@manohub/ui/README.md` 与类型声明;本包的契约只管**页面怎么搭** |
21
+ | 与骨架层无关(纯后端问题、非 Vue 前端工程) | 不使用本技能 |
22
+
23
+ 意图不明时先问一句「是要把存量页面改造过来,还是新写页面」,不要猜。
24
+
25
+ ## 二、先读契约(两个子技能都适用)
26
+
27
+ **合规判据只有一个地方**:`node_modules/@manohub/kit/CONTRACT.md`。每条条款都是闭集,
28
+ §7 是配套的自检清单。**本技能与两个子技能都不复述条款** —— 需要判据时读契约,
29
+ 不要凭记忆或凭本文件的措辞判断。
30
+
31
+ 按顺序读这五节,一次读完,别边写边查:
32
+
33
+ | # | 去哪读 | 为什么它要排这么前 |
34
+ |---|---|---|
35
+ | 1 | **§0 权威源表** | 值 / 件名 / 成员 / 词表 / 图标名的读取路径全在这里 —— 写任何东西之前先读它,否则你会去抄一份会过期的副本 |
36
+ | 2 | **§2 L0 入口与作用域** | 漏了它,下面三层同时失效(典型症状:组件有框有距,颜色却是浏览器默认灰蓝);**这一层不可豁免** |
37
+ | 3 | **§3 L1 值** | 样式纪律的全部判据;属性白名单是闭集,逐字照用 |
38
+ | 4 | **§5 L2 结构** | 骨架怎么用(20 条):页头、成员归位、两级滚动、操作位、分页都在这里 |
39
+ | 5 | **§6 L3 件与词表** | 件 / 成员 / prop 的用法;**件名与成员以 `.d.ts` 为准,不要抄** |
40
+
41
+ 收工前过 **§7 自检清单**(24 问,五层各一组)。答不上来的那一条就是你要回去读的地方。
42
+
43
+ **冲突时的优先级**:包内 `CONTRACT.md` > `@manohub/ui` 的 README 与类型声明里的 `@example`
44
+ > 消费仓 `AGENTS.md` > 其它文档。发现 `@example` 与契约冲突,按契约写,并把该 `@example` 当缺陷处理。
45
+
46
+ > 本包 **0.6.0 起不再发布消费侧机器规则**(原先的三条护栏已下线)。合规靠
47
+ > 「契约条款 + §7 自检清单」在写作与评审时把关,不再有自动拦断。
48
+
49
+ ## 三、固定命令(在应用包根执行)
50
+
51
+ ```bash
52
+ pnpm exec kit install # 把技能包刷到本地技能目录(幂等,包升级后重跑)
53
+ pnpm exec vue-tsc --noEmit # 类型检查
54
+ pnpm build # 生产构建
55
+ ```
56
+
57
+ - `kit` 是本包的命令入口(`package.json` 的 `bin`);npm 消费方把 `pnpm exec` 换成 `npx`。
58
+ 本包**只有 `install` 一个子命令**。
59
+ - 技能包内容更新后重新落盘(幂等,包升级后重跑即刷新):
60
+
61
+ ```bash
62
+ pnpm exec kit install # 等价:node node_modules/@manohub/kit/skills/install.mjs
63
+ ```
64
+
65
+ ## 四、失败处理
66
+
67
+ | 现象 | 处理 |
68
+ |---|---|
69
+ | 找不到 `node_modules/@manohub/kit` | 在应用包根执行 `pnpm add @manohub/kit @manohub/ui @manohub/theme`(三个包在 npm 官方仓公开) |
70
+ | 页面报 `App* is not exported` | 0.6.0 已移除 `App*` 组件名 —— 走 `kit-migrate` 按对照表替换 |
71
+ | 命令式提示样式全丢 | 提示没落回应用容器:确认入口走的是 `createSubApp`(它无条件写 `data-manohub-ui`),别自己手写 `createApp` |
72
+ | 组件有框有距但颜色全是浏览器默认灰蓝 | 容器缺 `data-manohub-ui` 属性(自建容器的场景),或漏引 `@manohub/theme/default.css` |
73
+ | 调子技能报「技能不存在」 | 技能未安装或未刷新,跑上面的 `pnpm exec kit install` |
74
+ | 终端里敲 `kit` 报 command not found | 用 `pnpm exec kit …`(或 npm 的 `npx kit …`) |
75
+ | 类型检查报「无法解析 `*.css`」 | 消费方 tsconfig 的 `types` 需包含 `vite/client`,见 `references/adoption.md` |
76
+ | 想找「某个写法合不合规」的机器判据 | 没有了 —— 读 `CONTRACT.md` 对应层 + §7 自检清单;确需全仓盘点时按 `kit-migrate` 的条款级盘点流程人工过 |
77
+
78
+ ## 五、参考
79
+
80
+ - `references/adoption.md`:接入 SOP(安装、样式三行、入口、类型配置、验收清单、故障对照、**新应用从零搭建**、升级口径)
81
+ - `references/contract-index.md`:契约速查索引(按主题定位 `CONTRACT.md` 章节,不复制条款)
82
+ - 迁移查表:`../kit-migrate/references/migration-map.md`
83
+ - 开发配方:`../kit-dev/references/page-recipes.md`
84
+ - 组件 API:`node_modules/@manohub/ui/README.md`
85
+ - 自检清单:`node_modules/@manohub/kit/CONTRACT.md` §7
@@ -0,0 +1,170 @@
1
+ # 接入 SOP
2
+
3
+ 把一个新的(或存量的)子应用接到「`@manohub/kit` 入口编排 + `@manohub/theme` 令牌
4
+ + `@manohub/ui` 组件」上。
5
+ 存量改造的逐文件口径另见 `../../kit-migrate/references/migration-map.md`。
6
+
7
+ 契约原文:`node_modules/@manohub/kit/CONTRACT.md`。**本文件是操作步骤,条款以契约为准。**
8
+
9
+ ---
10
+
11
+ ## 0. 前置
12
+
13
+ - Node / pnpm 版本与消费仓要求一致;
14
+ - 消费仓已有 vite + Vue 3 + TypeScript;
15
+ - 知道自己是不是 micro-app 子应用(影响路由 history 模式与挂载协议,§3)。
16
+
17
+ ## 1. 安装
18
+
19
+ ```bash
20
+ pnpm add @manohub/kit @manohub/ui @manohub/theme
21
+ pnpm add vue vue-router pinia vue-i18n @tanstack/vue-query # peer,由应用提供
22
+ ```
23
+
24
+ - 三个包**并列安装**:kit 管入口编排与契约,ui 管组件与服务,theme 管全局令牌(值)。
25
+ - `@manohub/ui` 自带 `@manohub/icon` 依赖,不必单独声明。
26
+ - **每个引入 kit 的应用都必须声明 `vue-i18n`**,哪怕不传 `i18n` 选项 ——
27
+ `createSubApp` 所在模块静态引入它,漏装会在构建期报 `Rollup failed to resolve import "vue-i18n"`。
28
+
29
+ 依赖自查(消费仓根执行):
30
+
31
+ ```bash
32
+ for p in apps/*/package.json packages/*/package.json; do
33
+ grep -q '"@manohub/kit"' "$p" && ! grep -q '"vue-i18n"' "$p" && echo "MISS $p"
34
+ done
35
+ ```
36
+
37
+ ## 2. 样式:两行 + 应用自己一行(顺序即契约)
38
+
39
+ **`@manohub/kit` 不发布任何样式** —— 样式两行直接引主题包与组件库的公开入口。
40
+
41
+ 应用入口 CSS(如 `src/style.css`):
42
+
43
+ ```css
44
+ @import "@manohub/theme/default.css"; /* ① 令牌(值)—— 换主题只换这一行 */
45
+ @import "@manohub/ui/styles.css"; /* ② 组件面(类 + 组件令牌基础值) */
46
+ @import "./app.css"; /* ③ 应用自身(只写布局) */
47
+ ```
48
+
49
+ - **顺序不可换**:先值(令牌)后面(组件面)—— 反过来的话组件面里的 `var(--ui-*)` 全是空值。
50
+ - 换非兜底主题时把 ① 换成对应主题入口(如 `@import "@manohub/theme/farris.css";`),
51
+ 并在入口给 `createSubApp({ theme: 'farris' })`。
52
+ - **reset 与富文本排版归应用自己**:本包不再提供 `reset.css` 与 `.app-markdown` 预设
53
+ (0.6.0 起 `src/styles/` 整体删除)。应用侧要写时注意仍受样式纪律约束(契约 §3 L1)。
54
+
55
+ ## 3. 入口:走 createSubApp
56
+
57
+ ```ts
58
+ // src/main.ts
59
+ import { createSubApp } from '@manohub/kit/entry'
60
+ import Root from './root'
61
+ import { routes } from './router'
62
+
63
+ export const { mount, unmount } = createSubApp({
64
+ rootComponent: Root,
65
+ routes,
66
+ i18n: { messages: { zh, en } }, // 纯 messages,无 i18next 的 translation 包装层
67
+ // history: 'hash', // 宿主不支持深路径 fallback 时
68
+ // theme: 'farris', // 换主题(要同时引那条主题样式)
69
+ // rootPathAliases: ['/subapp/'], // 宿主兼容前缀
70
+ })
71
+ ```
72
+
73
+ 工厂带给你的(**不要自己再写**):`div.app-container` 包裹与 `data-manohub-ui` / `data-theme` 属性、
74
+ pinia / vue-router / vue-query 装配、宿主挂载协议(`window.mount/unmount` + `microApp.*`)、
75
+ 宿主语言同步、首帧路由重置。
76
+
77
+ > **自建容器(不走 `createSubApp`)必须自己带 `data-manohub-ui`**:主题令牌、组件令牌、
78
+ > 组件库服务层宿主解析全锚它一个。只写类名 `app-container` 已不再被任何一方识别(0.6.0 起)。
79
+
80
+ 单例收敛(三项都要,缺一项会在某台机器上出怪问题):
81
+
82
+ | 项 | 怎么做 | 漏了会怎样 |
83
+ |---|---|---|
84
+ | `vue` 单实例 | 消费仓 vite `resolve.dedupe: ['vue']`(monorepo 常见配置) | 组件与业务拿到两套响应式,`provide/inject` 失效 |
85
+ | `vue-i18n` 单实例 | 消费仓 vite `resolve.dedupe: ['vue-i18n']` | **文案全丢且不报错** |
86
+ | `@manohub/kit` / `@manohub/ui` / `@manohub/theme` 唯一副本 | 不要 `link:` 目录链接(会引入第二份 vue 类型) | `vue-tsc` 全量报错 |
87
+
88
+ ## 4. 命名空间表(本仓自建)
89
+
90
+ 自 0.6.0 起 `kit lint` 下线,**类名前缀不再由配置文件登记**,改为本仓自己维护一张命名空间表:
91
+ `docs/kit-namespaces.md`(哪个应用占哪个类名前缀)。格式见
92
+ `../../kit-migrate/references/migration-playbook.md` 的模板。
93
+
94
+ - 库锚 `[data-manohub-ui]` 与类名 `.app-container` 恒允许、不必登记(但 `app-container` 不是跨包契约)。
95
+ - 表只解决「类名归谁」这一件事;**样式属性本身合不合规由契约 §3 L1-5 判**,与命名空间无关。
96
+
97
+ ## 5. 技能包
98
+
99
+ ```bash
100
+ pnpm exec kit install # 落到 .codebuddy/skills/
101
+ pnpm exec kit install --also-claude
102
+ pnpm exec kit install --dry-run # 只预览
103
+ ```
104
+
105
+ 包升级后重跑一次即刷新(安装器先清理同名目录再整体复制,幂等)。
106
+
107
+ ## 6. 类型与解析配置
108
+
109
+ - tsconfig 的 `types` 需含 `vite/client`(否则 `.css` 导入报「无法解析」);
110
+ - `@manohub/kit`、`@manohub/ui`、`@manohub/theme` 都是**产物分发**(`exports` 指向 `dist`),
111
+ 不需要额外的 `paths` 映射;若要映射到源码,只映射到包根 `src/index.ts`,别映射到内部文件
112
+ (那等于绕开公开入口,违反契约 §6 L3-1 的 import 源白名单)。
113
+
114
+ ## 7. 验收清单
115
+
116
+ - [ ] `pnpm exec vue-tsc --noEmit` 0 错
117
+ - [ ] `pnpm build` 成功
118
+ - [ ] **人工过一遍契约 §7 自检清单**(24 问;0.6.0 起没有自动扫描兜底)
119
+ - [ ] 应用能挂载:非 micro-app 下自动挂载;micro-app 下宿主能 `mount/unmount` 重挂
120
+ - [ ] 容器带 `data-manohub-ui` 属性(`createSubApp` 无条件写;自建容器要自己写)
121
+ - [ ] `toast('success', 'ok')` 后 DOM 里 `[data-manohub-ui]` 内有 `.mh-toast`(否则样式会丢)
122
+ - [ ] `showLoading()` → `hideLoading()` 能开关自如(引用计数配平)
123
+ - [ ] 语言切换一处生效(宿主下发 / `applyLocale`),业务文案与组件内建文案一起变
124
+ - [ ] `index.html?xxx=1` 类入口已登记 `rootPathAliases`
125
+ - [ ] 命名空间表 `docs/kit-namespaces.md` 已建
126
+
127
+ ## 8. 常见故障对照
128
+
129
+ | 现象 | 原因与处理 |
130
+ |---|---|
131
+ | 文案全部显示 key | `vue-i18n` 双实例(缺 `dedupe`),或应用自己 `createI18n` 抢了注册 |
132
+ | 组件渲染出来但没样式 | 样式两行缺失或顺序错(§2,本包已不转发样式);或浮层落到 `body`(确认入口走 `createSubApp`) |
133
+ | `toast` / `confirm` 的弹层样式全丢 | 同上:服务层宿主解析没落到本应用容器 → 在 `onReady` 里 `configureHost(el)` |
134
+ | `showLoading` 关不掉 | 引用计数没配平:用返回的句柄在 `finally` 里关 |
135
+ | 宿主 `unmount` 后重挂白屏 | 应用侧自己写了 `createApp` / `mount`,与工厂的协议冲突 |
136
+ | 刷新深路径 404 | 宿主不支持 fallback:`history: 'hash'` |
137
+ | 首屏 query 被清空 | `index.html?xxx=1` 入口未登记 `rootPathAliases` |
138
+ | `vue-tsc` 报「两种同名类型不兼容」 | 装了第二份 `vue`(目录 `link:`、或 `.pnpm` 里残留旧副本):重装 + 清 `.vite` 缓存 |
139
+
140
+ ## 9. 新应用从零搭建(最小骨架)
141
+
142
+ ```text
143
+ my-app/
144
+ ├── package.json 依赖:三个包 + 五个 peer
145
+ ├── vite.config.ts dedupe: ['vue', 'vue-i18n']
146
+ ├── tsconfig.json types: ["vite/client"]
147
+ ├── docs/
148
+ │ └── kit-namespaces.md 本仓类名命名空间表(§4)
149
+ └── src/
150
+ ├── main.ts createSubApp
151
+ ├── router/index.ts routes
152
+ ├── root.tsx 根组件(可以有,也可以直接给页面)
153
+ ├── style.css 样式两行 + 应用自身(§2)
154
+ ├── app.css 应用自身布局
155
+ └── views/ 页面(路由页放这里)
156
+ ```
157
+
158
+ `src/main.ts` 见 §3;第一个页面用列表页模板(`../../kit-dev/references/page-recipes.md` 模板 A)。
159
+ 建成后:过一遍契约 §7 自检清单 → 按 §7 验收。
160
+
161
+ ## 10. 升级与破坏性变更
162
+
163
+ - 升级前先读包根 `CONTRACT.md` 的 §12(升级)与包 `README.md` 的破坏性变更提示。
164
+ - **`0.6.0` 是破坏性变更**(上一个是 `@manohub/app-kit@0.4.3`),三个方面**一次做完**:
165
+ ① 本包不再提供组件 —— `App*` 名与 `.ak-*` 样式全部退场、farris 退场;
166
+ ② 主题层独立成 `@manohub/theme`、容器锚改名 `data-app-container` → `data-manohub-ui`、
167
+ kit 不再发布任何样式;③ 消费侧机器规则(`kit lint` 三条护栏)整批下线,合规改为
168
+ 「契约条款 + §7 自检清单」,契约整体重写为五层(L0 / L1 / L1.5 / L2 / L3)。
169
+ `appkit-guardrails.config.json` 不再被读取 —— 类名前缀迁到本仓 `docs/kit-namespaces.md`。
170
+ - 升级后**务必重建产物再联调**(消费侧装的是 `dist`);联调流程见包根 `AGENTS.md`。
@@ -0,0 +1,76 @@
1
+ # 契约速查索引
2
+
3
+ 用法:先在这里定位章节,再读 `node_modules/@manohub/kit/CONTRACT.md` 的对应段落。
4
+ **本文只做索引,不复述条款** —— 口径冲突时一律以契约原文为准。
5
+
6
+ 组件与服务的 **API 细节不在 kit 的契约里**:它们在 `node_modules/@manohub/ui/README.md`
7
+ 与包内类型声明(`dist/**/*.d.ts`);**全局令牌(值)**在 `node_modules/@manohub/theme/dist/` 的分片 CSS 里
8
+ (契约 §0 权威源表列了全部读取路径)。
9
+
10
+ ## 我想知道…
11
+
12
+ | 我想知道 | 看哪里 |
13
+ |---|---|
14
+ | 写任何东西前,该去哪个文件取值 / 查件名 / 查词表 | 契约 **§0 权威源表** |
15
+ | 应用怎么接入(装什么、样式三行、入口) | 契约 §1 接入 |
16
+ | 换主题 / 换掉第一行主题样式 | 契约 §1.2 |
17
+ | 容器上到底哪些名字是跨包约定 | 契约 §1.3 入口 |
18
+ | 目录怎么摆(路由页放哪、可复用件放哪) | 契约 §1.4 目录约定 |
19
+ | 包的设计底线(入口唯一、作用域锚、不重复骨架职责) | 契约 §2 L0(**不可豁免**) |
20
+ | 应用侧 CSS 能写什么、不能写什么 | 契约 §3 L1-5 的属性白名单(闭集,逐字照用) |
21
+ | 想改组件外观 / 局部换肤怎么办 | 契约 §3「局部换肤」(重设**已有**令牌的值,不发明新令牌名) |
22
+ | 图标怎么用(来源、尺寸、颜色、方位) | 契约 §4 L1.5 |
23
+ | 一个新页面该怎么搭 | 契约 §5 L2 + 末节「三种页面模板(起手式)」 |
24
+ | 谁滚、滚在哪一层 | 契约 §5 组 4 · 区域 |
25
+ | 搜索框 / 筛选 / 操作按钮该放哪个位置 | 契约 §5 组 4 · 区域 |
26
+ | 分页放页脚还是面板页脚 | 契约 §5 组 4 · 区域 |
27
+ | 页头怎么用(`title` / `subTitle` / `icon` / `extra`) | 契约 §5 组 3 · 页头 |
28
+ | 成员为什么没按我写的顺序渲染 | 契约 §5 组 2 · 归位 |
29
+ | 加载 / 空 / 错三态、遮罩该用哪个件 | 契约 §5 组 5 · 版式 |
30
+ | 表单行、只读摘要行、表单内分组 | 契约 §5 组 5 · 版式 |
31
+ | 行 / 列怎么排、间距档、栅格列数 | 契约 §5 组 5 · 版式(`Layout`) |
32
+ | 弹窗 / 抽屉 / 命令式提示怎么选 | 契约 §6 L3「件怎么用」 |
33
+ | 区域容器与卡片怎么选 | 契约 §5 组 4 · 区域 + §6 |
34
+ | 侧栏导航、树的受控展开 | 契约 §6 L3 |
35
+ | 我用的件 / 成员 / prop 到底存不存在 | 契约 §0 权威源表给的 `.d.ts` 路径 |
36
+ | 某个写法是不是违规 | 契约对应层的「正误对照」段(§3 / §4 / §5 / §6 各有一组) |
37
+ | 需要的能力包里没有怎么办 | 契约 §8 缺件与新增怎么走 |
38
+ | 收工前该核对什么 | 契约 **§7 自检清单**(五层共 24 问) |
39
+ | 文案与语言切换怎么写 | 契约 §9 国际化 |
40
+ | 某些「看起来不对」的地方是不是 bug | 契约 §10 已知偏差与有意取舍 |
41
+ | 哪些文件是被设计定版豁免人工确认的 | 契约 §11 定版文件清单 |
42
+ | **从 `@manohub/app-kit@0.4.3` 升到 `@manohub/kit@0.6.0`** | 契约 §12.1(三组变化一次做完)与 §12.2(迁移顺序);对照表见 `../../kit-migrate/references/migration-map.md` |
43
+ | 组件有哪些、每个件的 prop 是什么 | `node_modules/@manohub/ui/README.md` + `dist/index.d.ts` |
44
+ | 本仓的类名命名空间归谁 | 本仓 `docs/kit-namespaces.md`(per-app 文件,不在契约里) |
45
+
46
+ ## 注意:0.6.0 起没有自动扫描了
47
+
48
+ `kit lint` 与三条护栏(style / component / structure)**已下线**,本包不再发布消费侧机器规则。
49
+ 合规判据全在 `CONTRACT.md`:每条都是闭集,配合 §7 自检清单在**写作与评审时**把关。
50
+
51
+ - 别再找「跑一条命令看有多少违规」——没有了。
52
+ - 要全仓盘点时,按 `../../kit-migrate/references/migration-playbook.md` 的**条款级盘点口径**人工过。
53
+ - **库侧**的机械校验(`@manohub/ui` 与 `@manohub/theme` 包内的契约测试)**仍然在跑**,
54
+ 那是库自己的守卫,与消费方无关。
55
+
56
+ ## 最容易踩的几条(先记住这些再动手)
57
+
58
+ 1. **禁 `100vh` / `h-screen`**:子应用被注入宿主容器,高度一律 `100%`(契约 §5 组 5)。
59
+ 2. **分页跟承载表格的容器走**:表格在 `Panel` 里 → 分页放 `Panel.Footer`(契约 §5 组 4)。
60
+ 3. **筛选字段放 `Panel.Header.toolbar`、操作放 `actions`**;字段 >3 上提 `Page.Filter`(契约 §5 组 4)。
61
+ 4. **错误态与空态分开**:加载失败给 `QueryState` 的 `error`,不要塞进 `empty`(契约 §5 组 5)。
62
+ 5. **表格不要套 `height:auto` 的 div**:会让整个 Body 滚、表头跟着滚走(契约 §5 组 5)。
63
+ 6. **应用侧 CSS 只写布局**:属性白名单是闭集,不在名单里的一律违规(契约 §3 L1-5)。
64
+ 7. **别覆写组件库的内部类**:要改外观走契约 §3「局部换肤」(重设已有令牌的值)。
65
+ 8. **缺件走建件流程**,不在页面里自绘近似件,也不回去直连底层组件库(契约 §8)。
66
+ 9. **命令式提示要落在应用容器里**:用 `@manohub/ui` 的服务层(`toast` / `confirm`),
67
+ 别自己写 `position: fixed` 的遮罩 —— 它拿不到令牌(契约 §2、§6)。
68
+ 10. **`index.html?xxx=1` 这类入口必须登记 `rootPathAliases`**,否则首屏守卫清空 query。
69
+ 11. **容器上必须有 `data-manohub-ui`**(入口层无条件写):主题令牌、组件令牌、
70
+ 组件库服务层宿主解析全锚它一个属性。自建容器必须自己写;类名 `app-container` 不是契约(契约 §1.3)。
71
+ 12. **`@manohub/kit` 不发布样式**:样式三行(`@manohub/theme` 令牌 → `@manohub/ui` 组件面 →
72
+ 应用自身)由应用自己引;本包不提供 reset 与富文本预设(契约 §1.2、§10)。
73
+ 13. **成员必须是 `Page` 的直接子节点**:用 `<template v-if>` 包一层会编译成 Fragment,
74
+ 归位认不出,那个成员会被当自由内容挪进主体区(契约 §5 组 2)。
75
+ 14. **`Page.Header` 不传 `title` 时 `extra` 静默不渲染**:不传 `title` 是合法的「自定义页头出口」,
76
+ 但此时右侧位没有意义 —— 要放右侧位就必须传 `title`(契约 §5 组 3)。