@dev-workflow/skill 0.1.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/bin/cli.mjs +9 -0
  4. package/package.json +39 -0
  5. package/skills/artifact/README.md +23 -0
  6. package/skills/execution/devFlow/SKILL.md +341 -0
  7. package/skills/execution/devFlow/agents/openai.yaml +7 -0
  8. package/skills/execution/devFlow/domains/backend/references/api-tech.md +173 -0
  9. package/skills/execution/devFlow/domains/backend/scripts/check_api_tech_doc.mjs +170 -0
  10. package/skills/execution/devFlow/domains/backend/scripts/check_api_tech_doc.test.mjs +89 -0
  11. package/skills/execution/devFlow/domains/backend/templates/api-tech.md +164 -0
  12. package/skills/execution/devFlow/domains/frontend/references/contract-check.md +270 -0
  13. package/skills/execution/devFlow/domains/frontend/references/foundation-freeze.md +92 -0
  14. package/skills/execution/devFlow/domains/frontend/references/lightweight-flow.md +55 -0
  15. package/skills/execution/devFlow/domains/frontend/references/page-build.md +268 -0
  16. package/skills/execution/devFlow/domains/frontend/references/page-tech.md +414 -0
  17. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/field-common.md +69 -0
  18. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-complex.md +105 -0
  19. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-id-code.md +44 -0
  20. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-number-date.md +106 -0
  21. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-relation.md +144 -0
  22. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-selectors.md +102 -0
  23. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-text.md +130 -0
  24. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/index.md +42 -0
  25. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/interaction.md +61 -0
  26. package/skills/execution/devFlow/domains/frontend/scripts/check_page_tech_doc.mjs +82 -0
  27. package/skills/execution/devFlow/domains/frontend/scripts/contract_check_static.mjs +145 -0
  28. package/skills/execution/devFlow/domains/frontend/scripts/generate_foundation_summary.mjs +224 -0
  29. package/skills/execution/devFlow/domains/frontend/templates/contract-report.md +66 -0
  30. package/skills/execution/devFlow/domains/frontend/templates/mini-plan-l0.md +7 -0
  31. package/skills/execution/devFlow/domains/frontend/templates/mini-plan-l1.md +9 -0
  32. package/skills/execution/devFlow/domains/frontend/templates/page-build/component.tsx.tpl +8 -0
  33. package/skills/execution/devFlow/domains/frontend/templates/page-build/constants.ts.tpl +5 -0
  34. package/skills/execution/devFlow/domains/frontend/templates/page-build/index.ts.tpl +1 -0
  35. package/skills/execution/devFlow/domains/frontend/templates/page-build/route.tsx.tpl +11 -0
  36. package/skills/execution/devFlow/domains/frontend/templates/page-build/service.ts.tpl +17 -0
  37. package/skills/execution/devFlow/domains/frontend/templates/page-build/types.ts.tpl +11 -0
  38. package/skills/execution/devFlow/domains/frontend/templates/page-tech.md +103 -0
  39. package/skills/execution/devFlow/domains/requirement/references/prd-review.md +129 -0
  40. package/skills/execution/devFlow/domains/requirement/templates/prd-review.md +57 -0
  41. package/skills/execution/figmaSync/SKILL.md +349 -0
  42. package/skills/execution/figmaSync/chapters/apply.md +177 -0
  43. package/skills/execution/figmaSync/chapters/plan.md +359 -0
  44. package/skills/execution/figmaSync/chapters/prepare.md +47 -0
  45. package/skills/execution/figmaSync/examples/native-css.md +129 -0
  46. package/skills/execution/figmaSync/examples/plan-doc-sample.md +39 -0
  47. package/skills/execution/figmaSync/scripts/_shared/session-log.mjs +82 -0
  48. package/skills/execution/figmaSync/scripts/_shared/token-resolver.mjs +132 -0
  49. package/skills/execution/figmaSync/scripts/_shared/token-source.mjs +145 -0
  50. package/skills/execution/figmaSync/scripts/figma-sync-report.mjs +448 -0
  51. package/skills/execution/figmaSync/scripts/icon-inventory.mjs +165 -0
  52. package/skills/execution/figmaSync/scripts/lookup-var.mjs +184 -0
  53. package/skills/execution/figmaSync/scripts/match-token.mjs +210 -0
  54. package/skills/execution/figmaSync/scripts/prepare-check.mjs +290 -0
  55. package/skills/execution/figmaSync/scripts/verify-plan.mjs +458 -0
  56. package/skills/execution/figmaSync/snapshots/README.md +13 -0
  57. package/skills/execution/figmaSync/snapshots/last-sync.md +1538 -0
  58. package/skills/execution/figmaSync/templates/PLAN.md.tpl +227 -0
  59. package/skills/review/consistency-checker/SKILL.md +112 -0
  60. package/skills/review/consistency-checker/rules/README.md +16 -0
  61. package/src/cli/agents.mjs +16 -0
  62. package/src/cli/copy.mjs +95 -0
  63. package/src/cli/install.mjs +131 -0
  64. package/src/cli/prompts.mjs +66 -0
  65. package/tools/lark/README.md +78 -0
  66. package/tools/lark/references/lark-doc.md +156 -0
  67. package/tools/lark/references/lark-read.md +235 -0
  68. package/tools/lark/references/prepare.md +147 -0
  69. package/tools/lark/scripts/lark_api.mjs +404 -0
  70. package/tools/lark/scripts/lark_api.test.mjs +63 -0
  71. package/tools/lark/scripts/lark_check_permissions.mjs +52 -0
  72. package/tools/lark/scripts/lark_publish_doc.mjs +299 -0
  73. package/tools/lark/scripts/lark_read_docx.mjs +328 -0
  74. package/tools/lark/scripts/markdown_to_lark_blocks.mjs +397 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Lookup Var (Fast Path 1)
3
+ *
4
+ * 把 CSS 变量名映射到 /theme 路径——专为「设计稿 CSS 已含 var() 引用」的快路径。
5
+ * 不做反向哈希搜索,只做查表。
6
+ *
7
+ * 用法:
8
+ * node .agent/skills/figmaSync/scripts/lookup-var.mjs "--fill-neutral-primary"
9
+ * node .agent/skills/figmaSync/scripts/lookup-var.mjs "var(--fill-neutral-primary, #090A0B)"
10
+ * node .agent/skills/figmaSync/scripts/lookup-var.mjs "--apex-spacing-8"
11
+ *
12
+ * 支持的变量命名:
13
+ * --apex-fill-neutral-primary (apex-ui 实际产出)
14
+ * --fill-neutral-primary (设计师在 Figma 里常写的简写)
15
+ * var(--xxx, fallback) (整段 CSS 表达式也接受)
16
+ *
17
+ * 输出(命中):
18
+ * {
19
+ * "success": true,
20
+ * "cssVariable": "--fill-neutral-primary", // ✅ 直接写入 CSS: var(--fill-neutral-primary)
21
+ * "themePath": "foundation.fill.neutral.primary", // ✅ 写入 PLAN.md 供审核
22
+ * "cssValue": "{grey.14}",
23
+ * "usage": ["color"],
24
+ * "$type": "color",
25
+ * "layer": "foundation",
26
+ * "shortRef": "fill.neutral.primary"
27
+ * }
28
+ *
29
+ * 输出(未命中):
30
+ * { "success": false, "message": "...", "candidates": [...top-5 模糊建议...] }
31
+ */
32
+ import { appendSessionLog } from './_shared/session-log.mjs'
33
+ import { loadCurrentTokenRecords } from './_shared/token-source.mjs'
34
+
35
+ const ALIAS_LAYER_ORDER = ['foundation', 'baseValue', 'palette']
36
+
37
+ function parseArg(raw) {
38
+ if (!raw) return null
39
+ const varMatch = raw.match(/var\(\s*(--[a-zA-Z0-9_-]+)/)
40
+ if (varMatch) return varMatch[1]
41
+ const trimmed = raw.trim()
42
+ if (trimmed.startsWith('--')) return trimmed
43
+ return null
44
+ }
45
+
46
+ function buildVarIndex(records) {
47
+ // 一个变量记录同时支持两种 var 写法(带/不带 apex- 前缀),并支持 path 后缀命中
48
+ const map = new Map()
49
+ for (const rec of records) {
50
+ const segments = rec.path
51
+ const dashJoined = segments.join('-').toLowerCase()
52
+ const variants = new Set([
53
+ `--${dashJoined}`,
54
+ `--apex-${dashJoined}`,
55
+ // 部分设计师会把 setName 也写进 var 名(罕见,但兼容)
56
+ `--${rec.setName.toLowerCase()}-${dashJoined}`
57
+ ])
58
+ if (rec.cssVar) variants.add(rec.cssVar.toLowerCase())
59
+ for (const v of variants) {
60
+ if (!map.has(v)) map.set(v, [])
61
+ map.get(v).push(rec)
62
+ }
63
+ }
64
+ return map
65
+ }
66
+
67
+ function pickRecommended(records) {
68
+ // var 命中可能多 set 同名(foundation 与 component 都叫 fill.neutral.primary 很少见,但允许)
69
+ // 推荐顺序:foundation > baseValue > component > componentUsage > palette
70
+ for (const layer of ALIAS_LAYER_ORDER) {
71
+ const hit = records.find((r) => r.setName === layer)
72
+ if (hit) return hit
73
+ }
74
+ return records[0]
75
+ }
76
+
77
+ function fuzzyCandidates(varName, varIndex, limit = 5) {
78
+ const clean = varName.replace(/^(--apex-|--)/, '')
79
+ const keys = [...varIndex.keys()].map((k) => k.replace(/^(--apex-|--)/, ''))
80
+ // 简单子串排序
81
+ const ranked = keys
82
+ .map((k) => ({ k, score: similarity(clean, k) }))
83
+ .filter((r) => r.score > 0)
84
+ .sort((a, b) => b.score - a.score)
85
+ .slice(0, limit)
86
+ return ranked.map((r) => `--${r.k}`)
87
+ }
88
+
89
+ function similarity(a, b) {
90
+ if (a === b) return 1
91
+ if (b.includes(a) || a.includes(b)) return 0.7
92
+ const tokensA = a.split('-')
93
+ const tokensB = b.split('-')
94
+ const common = tokensA.filter((t) => tokensB.includes(t)).length
95
+ return common / Math.max(tokensA.length, tokensB.length)
96
+ }
97
+
98
+ function main() {
99
+ const arg = process.argv[2]
100
+ const varName = parseArg(arg)
101
+ if (!varName) {
102
+ console.log(
103
+ JSON.stringify({
104
+ success: false,
105
+ message: 'Provide a CSS variable like "--fill-neutral-primary" or "var(--xxx, fallback)"'
106
+ })
107
+ )
108
+ process.exit(0)
109
+ }
110
+
111
+ let records
112
+ try {
113
+ records = loadRecords()
114
+ } catch (err) {
115
+ console.log(JSON.stringify({ success: false, message: err.message }))
116
+ process.exit(0)
117
+ }
118
+
119
+ const varIndex = buildVarIndex(records)
120
+
121
+ const hits = varIndex.get(varName.toLowerCase())
122
+ if (!hits || hits.length === 0) {
123
+ appendSessionLog({ script: 'lookup-var', input: varName, hit: false })
124
+ console.log(
125
+ JSON.stringify({
126
+ success: false,
127
+ message: `No token mapped to ${varName}. If the value is hardcoded, run match-token.mjs instead.`,
128
+ candidates: fuzzyCandidates(varName, varIndex)
129
+ })
130
+ )
131
+ process.exit(0)
132
+ }
133
+
134
+ const recommended = pickRecommended(hits)
135
+ const themePath = `${recommended.setName}.${recommended.pathStr}`
136
+ const cssVariable = recommended.cssVar ?? toCssVariable(recommended.path)
137
+ const usage = collectUsage(recommended)
138
+ appendSessionLog({
139
+ script: 'lookup-var',
140
+ input: varName,
141
+ hit: true,
142
+ token: themePath,
143
+ cssVariable,
144
+ layer: recommended.setName,
145
+ type: recommended.$type
146
+ })
147
+ console.log(
148
+ JSON.stringify({
149
+ success: true,
150
+ cssVariable,
151
+ themePath,
152
+ cssValue: recommended.$value,
153
+ usage,
154
+ $type: recommended.$type,
155
+ layer: recommended.setName,
156
+ shortRef: recommended.pathStr
157
+ })
158
+ )
159
+ }
160
+
161
+ function loadRecords() {
162
+ return loadCurrentTokenRecords().records
163
+ }
164
+
165
+ function collectUsage(record) {
166
+ if (record.$type === 'color')
167
+ return ['color', 'background-color', 'border-color', 'fill', 'stroke']
168
+ if (record.$type === 'number') {
169
+ const first = record.path[0]
170
+ if (first === 'spacing') return ['gap', 'padding', 'margin', 'inset']
171
+ if (first === 'radius') return ['border-radius']
172
+ if (first === 'border') return ['border-width']
173
+ if (first === 'size') return ['width', 'height', 'min-width', 'min-height']
174
+ if (first?.startsWith('font'))
175
+ return ['font-size', 'font-weight', 'line-height', 'letter-spacing']
176
+ }
177
+ return []
178
+ }
179
+
180
+ function toCssVariable(pathParts) {
181
+ return `--${pathParts.join('-')}`
182
+ }
183
+
184
+ main()
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Match Token
3
+ *
4
+ * 用法:
5
+ * node .agent/skills/figmaSync/scripts/match-token.mjs "<value>" [--type=color|number]
6
+ *
7
+ * 行为:
8
+ * 1. 加载 theme/dark CSS token 源
9
+ * 2. 反向查找:哪些 /theme CSS variables 最终解析后等于 <value>
10
+ * 3. 按层级优先级排序:foundation > baseValue > palette
11
+ * 4. 默认推荐 foundation 层(语义稳定 + 跨组件可复用)
12
+ *
13
+ * 推荐策略(按 $type 分流):
14
+ * - color : foundation > palette
15
+ * (foundation 语义稳定 + 跨组件复用;palette 是最差选择)
16
+ * - number : baseValue
17
+ * (baseValue 的 spacing.8 / radius.4 已是语义化最稳定的层)
18
+ *
19
+ * 输出示例(命中):
20
+ * {
21
+ * "success": true,
22
+ * "matches": [...],
23
+ * "recommend": {
24
+ * "layer": "foundation",
25
+ * "themePath": "foundation.fill.neutral.tertiary",
26
+ * "cssVariable": "--fill-neutral-tertiary",
27
+ * "cssValue": "{grey.12}"
28
+ * },
29
+ * "recommendReason": "color variables default to foundation layer..."
30
+ * }
31
+ *
32
+ * 输出示例(未命中):
33
+ * { "success": false, "message": "No match found.", "originalValue": "..." }
34
+ */
35
+ import { appendSessionLog } from './_shared/session-log.mjs'
36
+ import { createResolver, findTokensByValue } from './_shared/token-resolver.mjs'
37
+ import { loadCurrentTokenRecords } from './_shared/token-source.mjs'
38
+
39
+ const RECOMMEND_LAYER_ORDER_BY_TYPE = {
40
+ color: ['foundation', 'component', 'componentUsage', 'palette'],
41
+ number: ['baseValue', 'componentUsage', 'component']
42
+ }
43
+
44
+ const RECOMMEND_REASON_BY_TYPE = {
45
+ color:
46
+ 'color variables default to foundation layer for semantic stability and cross-component reuse',
47
+ number:
48
+ 'number variables default to baseValue layer (spacing/radius/size) as the most stable semantic source'
49
+ }
50
+
51
+ function parseArgs(argv) {
52
+ const args = argv.slice(2)
53
+ const out = { value: null, type: null }
54
+ for (const arg of args) {
55
+ if (arg.startsWith('--type=')) {
56
+ out.type = arg.slice('--type='.length)
57
+ } else if (!out.value) {
58
+ out.value = arg
59
+ }
60
+ }
61
+ return out
62
+ }
63
+
64
+ /**
65
+ * 把用户输入规范化为可比对的字面量:
66
+ * - "8px" / "8.5px" → "8" / "8.5"(字符串,与 CSS token 记录输出对齐)
67
+ * - "0.8" / "8" → 同上
68
+ * - "#abcdef" / "rgb(...)" 保留小写
69
+ * - 其余原样返回
70
+ * 返回值始终是 string,避免 sonar 多类型返回告警,也方便日志去重。
71
+ */
72
+ function normalizeQuery(raw) {
73
+ if (raw === undefined || raw === null) return ''
74
+ const trimmed = String(raw).trim()
75
+ const pxMatch = /^(-?\d+(?:\.\d+)?)px$/i.exec(trimmed)
76
+ if (pxMatch) return pxMatch[1]
77
+ const numMatch = /^(-?\d+(?:\.\d+)?)$/.exec(trimmed)
78
+ if (numMatch) return numMatch[1]
79
+ return trimmed.toLowerCase()
80
+ }
81
+
82
+ function isNumericQuery(value) {
83
+ return /^-?\d+(?:\.\d+)?$/.test(String(value))
84
+ }
85
+
86
+ function main() {
87
+ const { value, type } = parseArgs(process.argv)
88
+
89
+ if (!value) {
90
+ console.log(
91
+ JSON.stringify({
92
+ success: false,
93
+ message: 'Missing value argument. Usage: node match-token.mjs <value> [--type=color|number]'
94
+ })
95
+ )
96
+ process.exit(0)
97
+ }
98
+
99
+ let loaded
100
+ try {
101
+ loaded = loadTokenRecords()
102
+ } catch (err) {
103
+ console.log(JSON.stringify({ success: false, message: err.message }))
104
+ process.exit(0)
105
+ }
106
+
107
+ const records = loaded.records
108
+ const resolver = createResolver(records, loaded.order)
109
+
110
+ const normalized = normalizeQuery(value)
111
+ let matches = findTokensByValue(resolver, normalized)
112
+ if (type) {
113
+ matches = matches.filter((m) => m.$type === type)
114
+ } else if (isNumericQuery(normalized)) {
115
+ const numberMatches = matches.filter((m) => m.$type === 'number')
116
+ if (numberMatches.length > 0) matches = numberMatches
117
+ }
118
+
119
+ if (matches.length === 0) {
120
+ appendSessionLog({ script: 'match-token', input: value, hit: false })
121
+ console.log(
122
+ JSON.stringify({
123
+ success: false,
124
+ message: 'No match found. Proceed with Fallback Levels (Level 1/2/3).',
125
+ originalValue: value,
126
+ normalizedValue: normalized
127
+ })
128
+ )
129
+ process.exit(0)
130
+ }
131
+
132
+ const simplifiedMatches = matches.map((m) => ({
133
+ layer: m.setName,
134
+ themePath: `${m.setName}.${m.path}`,
135
+ cssVariable: m.cssVar ?? toCssVariable(m.path.split('.')),
136
+ cssValue: m.$value,
137
+ resolvedValue: m.resolvedValue,
138
+ $type: m.$type
139
+ }))
140
+
141
+ const primaryType = simplifiedMatches[0].$type
142
+ const layerOrder = RECOMMEND_LAYER_ORDER_BY_TYPE[primaryType] ?? ['foundation']
143
+ let recommend = pickRecommendedMatch(simplifiedMatches, primaryType)
144
+ for (const layer of layerOrder) {
145
+ const hit = simplifiedMatches.find((m) => m.layer === layer)
146
+ if (hit) {
147
+ recommend = pickRecommendedMatch(
148
+ simplifiedMatches.filter((m) => m.layer === layer),
149
+ primaryType
150
+ )
151
+ break
152
+ }
153
+ }
154
+
155
+ appendSessionLog({
156
+ script: 'match-token',
157
+ input: value,
158
+ hit: true,
159
+ token: recommend.themePath,
160
+ cssVariable: recommend.cssVariable,
161
+ layer: recommend.layer,
162
+ type: primaryType
163
+ })
164
+
165
+ console.log(
166
+ JSON.stringify({
167
+ success: true,
168
+ matches: simplifiedMatches,
169
+ recommend,
170
+ recommendReason:
171
+ RECOMMEND_REASON_BY_TYPE[primaryType] ?? 'no recommendation rule for this token type'
172
+ })
173
+ )
174
+ }
175
+
176
+ function pickRecommendedMatch(matches, primaryType) {
177
+ if (primaryType !== 'number') return matches[0]
178
+
179
+ const pathPriority = [
180
+ 'spacing.',
181
+ 'radius.',
182
+ 'size.',
183
+ 'border.',
184
+ 'icon.',
185
+ 'fontsize.',
186
+ 'fontheight.',
187
+ 'fontspacing.'
188
+ ]
189
+ return [...matches].sort((a, b) => {
190
+ const aPriority = priorityForPath(a.themePath, pathPriority)
191
+ const bPriority = priorityForPath(b.themePath, pathPriority)
192
+ return aPriority - bPriority
193
+ })[0]
194
+ }
195
+
196
+ function priorityForPath(path, prefixes) {
197
+ const shortPath = path.split('.').slice(1).join('.')
198
+ const index = prefixes.findIndex((prefix) => shortPath.startsWith(prefix))
199
+ return index === -1 ? prefixes.length : index
200
+ }
201
+
202
+ function loadTokenRecords() {
203
+ return loadCurrentTokenRecords()
204
+ }
205
+
206
+ function toCssVariable(pathParts) {
207
+ return `--${pathParts.join('-')}`
208
+ }
209
+
210
+ main()
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Prepare Check
3
+ *
4
+ * 检查 figmaSync 原生 CSS 工作流依赖的技术栈与资源。分级输出:
5
+ * - blocker: 缺失会阻塞 plan/apply
6
+ * - warning: 不影响运行但建议修复
7
+ * - info: 诊断信息
8
+ *
9
+ * 用法:
10
+ * pnpm figma:prepare
11
+ * pnpm figma:prepare --json
12
+ */
13
+ import { execSync } from 'node:child_process'
14
+ import fs from 'node:fs'
15
+ import path from 'node:path'
16
+
17
+ const ROOT = process.cwd()
18
+
19
+ const CHECKS = [
20
+ checkNodeVersion,
21
+ checkPackageManager,
22
+ checkRequiredDeps,
23
+ checkRequiredDevDeps,
24
+ checkThemeCss,
25
+ checkPackageScripts,
26
+ checkGitignore,
27
+ checkApexUiAvailable,
28
+ checkCommonComponentSources,
29
+ checkAssetSources,
30
+ checkRouterConvention,
31
+ checkFigmaMcpHint
32
+ ]
33
+
34
+ function main() {
35
+ const json = process.argv.includes('--json')
36
+ const results = []
37
+ for (const fn of CHECKS) {
38
+ try {
39
+ const r = fn()
40
+ if (Array.isArray(r)) results.push(...r)
41
+ else if (r) results.push(r)
42
+ } catch (err) {
43
+ results.push({
44
+ level: 'blocker',
45
+ name: fn.name,
46
+ message: `check threw: ${err.message}`
47
+ })
48
+ }
49
+ }
50
+
51
+ const blockers = results.filter((r) => r.level === 'blocker')
52
+ const warnings = results.filter((r) => r.level === 'warning')
53
+ const infos = results.filter((r) => r.level === 'info')
54
+
55
+ if (json) {
56
+ console.log(JSON.stringify({ blockers, warnings, infos }, null, 2))
57
+ } else {
58
+ printHuman(results)
59
+ console.log()
60
+ console.log(
61
+ `[figma:prepare] ${blockers.length} blocker · ${warnings.length} warning · ${infos.length} info`
62
+ )
63
+ }
64
+
65
+ process.exit(blockers.length > 0 ? 1 : 0)
66
+ }
67
+
68
+ function printHuman(results) {
69
+ const groups = { blocker: '❌', warning: '⚠️ ', info: 'ℹ️ ' }
70
+ for (const level of ['blocker', 'warning', 'info']) {
71
+ const items = results.filter((r) => r.level === level)
72
+ if (items.length === 0) continue
73
+ console.log(`\n${groups[level]} ${level.toUpperCase()}`)
74
+ for (const r of items) {
75
+ console.log(` · [${r.name}] ${r.message}`)
76
+ if (r.fix) console.log(` fix → ${r.fix}`)
77
+ }
78
+ }
79
+ if (results.every((r) => r.level === 'info')) {
80
+ console.log('\n✅ All checks passed.')
81
+ }
82
+ }
83
+
84
+ function checkNodeVersion() {
85
+ const v = process.versions.node
86
+ const major = Number(v.split('.')[0])
87
+ if (major < 18) {
88
+ return {
89
+ level: 'blocker',
90
+ name: 'node-version',
91
+ message: `Node ${v} 太旧;要求 ≥ 18`,
92
+ fix: '升级 Node 到 18+'
93
+ }
94
+ }
95
+ return { level: 'info', name: 'node-version', message: `Node ${v} OK` }
96
+ }
97
+
98
+ function checkPackageManager() {
99
+ try {
100
+ const v = execSync('pnpm -v', { encoding: 'utf-8' }).trim()
101
+ return { level: 'info', name: 'pnpm', message: `pnpm ${v} OK` }
102
+ } catch {
103
+ return {
104
+ level: 'blocker',
105
+ name: 'pnpm',
106
+ message: 'pnpm 未安装',
107
+ fix: 'corepack enable && corepack prepare pnpm@latest --activate'
108
+ }
109
+ }
110
+ }
111
+
112
+ function readPackageJson() {
113
+ const p = path.join(ROOT, 'package.json')
114
+ if (!fs.existsSync(p)) throw new Error('package.json missing in project root')
115
+ return JSON.parse(fs.readFileSync(p, 'utf-8'))
116
+ }
117
+
118
+ function checkRequiredDeps() {
119
+ const pkg = readPackageJson()
120
+ const deps = pkg.dependencies ?? {}
121
+ const required = ['@frontend/apex-ui--react']
122
+ return required.map((name) =>
123
+ deps[name]
124
+ ? { level: 'info', name: `dep:${name}`, message: `${name}@${deps[name]} OK` }
125
+ : {
126
+ level: 'blocker',
127
+ name: `dep:${name}`,
128
+ message: `缺少 dependency ${name}`,
129
+ fix: `pnpm add ${name}`
130
+ }
131
+ )
132
+ }
133
+
134
+ function checkRequiredDevDeps() {
135
+ const pkg = readPackageJson()
136
+ const deps = pkg.devDependencies ?? {}
137
+ const required = ['@babel/parser', '@babel/traverse', 'globby']
138
+ return required.map((name) =>
139
+ deps[name]
140
+ ? { level: 'info', name: `devDep:${name}`, message: `${name}@${deps[name]} OK` }
141
+ : {
142
+ level: 'warning',
143
+ name: `devDep:${name}`,
144
+ message: `缺少 devDependency ${name},部分报告能力可能受影响`,
145
+ fix: `pnpm add -D ${name}`
146
+ }
147
+ )
148
+ }
149
+
150
+ function checkThemeCss() {
151
+ const srcDir = path.join(ROOT, 'theme', 'dark')
152
+ const requiredCss = ['primitives.css', 'aliases.css']
153
+ const missingCss = requiredCss.filter((file) => !fs.existsSync(path.join(srcDir, file)))
154
+ if (missingCss.length > 0) {
155
+ return {
156
+ level: 'blocker',
157
+ name: 'theme-css',
158
+ message: `theme/dark 缺少:${missingCss.join(', ')}`,
159
+ fix: '先补齐 /theme CSS variables 源文件'
160
+ }
161
+ }
162
+ return { level: 'info', name: 'theme-css', message: 'theme/dark CSS variables 源 OK' }
163
+ }
164
+
165
+ const REQUIRED_SCRIPTS = ['figma:report', 'figma:verify-plan']
166
+
167
+ function checkPackageScripts() {
168
+ const pkg = readPackageJson()
169
+ const scripts = pkg.scripts ?? {}
170
+ return REQUIRED_SCRIPTS.map((name) =>
171
+ scripts[name]
172
+ ? { level: 'info', name: `script:${name}`, message: `script "${name}" OK` }
173
+ : {
174
+ level: 'blocker',
175
+ name: `script:${name}`,
176
+ message: `package.json scripts 缺少 "${name}"`,
177
+ fix: '参考 figmaSync SKILL.md 速查表补齐 scripts'
178
+ }
179
+ )
180
+ }
181
+
182
+ const REQUIRED_GITIGNORE = [
183
+ '.agent/skills/figmaSync/.session-log.jsonl',
184
+ '.agent/skills/figmaSync/session-report-plan.md',
185
+ '.agent/skills/figmaSync/session-report-apply.md'
186
+ ]
187
+
188
+ function checkGitignore() {
189
+ const p = path.join(ROOT, '.gitignore')
190
+ if (!fs.existsSync(p)) {
191
+ return {
192
+ level: 'warning',
193
+ name: 'gitignore',
194
+ message: '.gitignore 不存在',
195
+ fix: '至少忽略 figmaSync session 文件'
196
+ }
197
+ }
198
+ const text = fs.readFileSync(p, 'utf-8')
199
+ const missing = REQUIRED_GITIGNORE.filter((line) => !text.includes(line))
200
+ if (missing.length > 0) {
201
+ return {
202
+ level: 'warning',
203
+ name: 'gitignore',
204
+ message: `.gitignore 缺项:${missing.join(', ')}`,
205
+ fix: '把缺失行追加到 .gitignore'
206
+ }
207
+ }
208
+ return { level: 'info', name: 'gitignore', message: '.gitignore OK' }
209
+ }
210
+
211
+ function checkApexUiAvailable() {
212
+ const llms = path.join(ROOT, 'node_modules/@frontend/apex-ui--react/dist/llms.txt')
213
+ if (!fs.existsSync(llms)) {
214
+ return {
215
+ level: 'warning',
216
+ name: 'apex-ui-llms',
217
+ message: 'apex-ui llms.txt 找不到,组件复用判断会受影响',
218
+ fix: 'pnpm install 重新安装 @frontend/apex-ui--react'
219
+ }
220
+ }
221
+ return { level: 'info', name: 'apex-ui-llms', message: 'apex-ui llms.txt 可用' }
222
+ }
223
+
224
+ function checkCommonComponentSources() {
225
+ const commonDir = path.join(ROOT, 'src', 'common')
226
+ if (!fs.existsSync(commonDir)) {
227
+ return {
228
+ level: 'warning',
229
+ name: 'common-components',
230
+ message: 'src/common 不存在,plan 阶段无法扫描 common component 复用机会'
231
+ }
232
+ }
233
+ return { level: 'info', name: 'common-components', message: 'src/common 可扫描' }
234
+ }
235
+
236
+ function checkAssetSources() {
237
+ const candidates = [
238
+ 'assets',
239
+ 'icons',
240
+ 'public',
241
+ path.join('src', 'assets'),
242
+ path.join('src', 'icons')
243
+ ]
244
+ const existing = candidates.filter((item) => fs.existsSync(path.join(ROOT, item)))
245
+ if (existing.length === 0) {
246
+ return {
247
+ level: 'warning',
248
+ name: 'assets',
249
+ message: '未发现常见 assets/icons 目录,icon 匹配可能全部进入待确认'
250
+ }
251
+ }
252
+ return { level: 'info', name: 'assets', message: `可扫描资产目录:${existing.join(', ')}` }
253
+ }
254
+
255
+ function checkRouterConvention() {
256
+ const pkg = readPackageJson()
257
+ const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }
258
+ const detected = []
259
+ if (deps['@tanstack/react-router']) detected.push('TanStack Router')
260
+ if (deps['react-router-dom'] || deps['react-router']) detected.push('React Router')
261
+ if (deps.next) detected.push('Next.js App Router')
262
+
263
+ if (detected.length === 0) {
264
+ return {
265
+ level: 'info',
266
+ name: 'router',
267
+ message: '未识别到路由库;PLAN.md 中 targetFile 由用户决定'
268
+ }
269
+ }
270
+ const router = detected.join(' + ')
271
+ let hint = ''
272
+ if (detected.includes('TanStack Router')) {
273
+ hint = ' → file-based 路由:targetFile 用 `src/routes/<feature>/route.tsx`'
274
+ } else if (detected.includes('Next.js App Router')) {
275
+ hint = ' → App Router:targetFile 用 `app/<feature>/page.tsx`'
276
+ } else if (detected.includes('React Router')) {
277
+ hint = ' → 常见约定 `src/pages/<feature>/index.tsx`'
278
+ }
279
+ return { level: 'info', name: 'router', message: `检测到 ${router}${hint}` }
280
+ }
281
+
282
+ function checkFigmaMcpHint() {
283
+ return {
284
+ level: 'info',
285
+ name: 'figma-mcp',
286
+ message: 'Figma MCP 可用性由 AI 在 plan 阶段调用 whoami 试探,shell 无法判断'
287
+ }
288
+ }
289
+
290
+ main()