diff --git a/skills/plan/db-design-gen/SKILL.md b/skills/plan/db-design-gen/SKILL.md index ea01120..5db6668 100644 --- a/skills/plan/db-design-gen/SKILL.md +++ b/skills/plan/db-design-gen/SKILL.md @@ -47,7 +47,7 @@ allowed-tools: Read Write Edit Grep Glob AskUserQuestion ### C. 渲染 docs/03 -1. 读取 `${CLAUDE_SKILL_DIR}/templates/docs-03-header-template.md`,填充 `schema_name`(从 `.env.local` 读 `DB_SCHEMA`,无则填 `【人工填写:DB_SCHEMA】`)、`er_overview`(纯文本 ER 概览),以及步骤 A0 确认的 `{{PK_CONVENTION}}` / `{{TENANT_COLS}}` / `{{COL_PREFIX_RULE}}`。 +1. 读取 `${CLAUDE_SKILL_DIR}/templates/docs-03-header-template.md`,填充 `schema_name`(从 `config-vars.yaml` 读 `database.schema`,无则填 `【人工填写:database.schema】`)、`er_overview`(纯文本 ER 概览),以及步骤 A0 确认的 `{{PK_CONVENTION}}` / `{{TENANT_COLS}}` / `{{COL_PREFIX_RULE}}`。 2. 渲染「表清单」:对每张表:读取并填充 `${CLAUDE_SKILL_DIR}/templates/docs-03-table-template.md`,其中标准列区块用步骤 A0 确认的 `{{PK_CONVENTION}}` / `{{TENANT_COLS}}` 展开(`TENANT_COLS` = 「无」时不输出租户列行)。 3. 写入 `docs/03-数据库设计文档.md`。 diff --git a/skills/plan/db-init/SKILL.md b/skills/plan/db-init/SKILL.md index cca2ba4..f0df304 100644 --- a/skills/plan/db-init/SKILL.md +++ b/skills/plan/db-init/SKILL.md @@ -1,6 +1,6 @@ --- name: db-init -description: A4 DB 初始化——LLM 解析 docs/03-数据库设计文档.md → 生成 sql/migrations/V1__initial_schema.sql(DDL only,Flyway 初始 migration)→ 用 lib/validate-ddl.mjs 全量校验 DDL ↔ docs/03 一致性 → 验证 MySQL 连接 → 调 scripts/setup-test-db.mjs 复用三层防护并 DROP+CREATE 空库 → 用 lib/apply-ddl.mjs apply V1。 +description: A4 DB 初始化——LLM 解析 docs/03-数据库设计文档.md → 生成 sql/migrations/V1__initial_schema.sql(DDL only,Flyway 初始 migration)→ 用 lib/validate-ddl.mjs 全量校验 DDL ↔ docs/03 一致性 → 验证 MySQL 连接 → 调 scripts/setup-test-db.mjs DROP+CREATE 空库 → 用 lib/apply-ddl.mjs apply V1。 user-invocable: false allowed-tools: Read Write Edit Glob Skill Bash(node *) --- @@ -56,30 +56,39 @@ node "${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs" \ ### B. 数据库环境检查 -用 `Glob` 确认 `.env.local` 存在(不存在 → 提示重跑 A2 `skeleton-gen` 并停下)。用 `Read` 逐行解析 `KEY=VALUE`(跳过空行 / `#` 注释,**不做 shell-source / 变量展开**),校验 `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_SCHEMA` 5 项均非空——任一缺失 → 打印缺失字段并停下。 +用 `Glob` 确认 `config-vars.yaml` 存在(不存在 → 提示重跑 A1 `scope-lock` 并停下)。用 `Read` 读其 `database:` 段,校验 `host` / `port` / `user` / `password` / `schema` 5 项均非空且非 `【人工填写` 占位——任一缺失 → 打印缺失字段并停下。 -用解析出的值跑连通性自检。必须用 Node `spawnSync('mysql', args, {shell:false})`,不要把密码拼进 shell 命令;空密码也要传 `--password=`,避免 `mysql -p` 进入交互式等待。 +用解析出的值跑连通性自检。必须用 Node `spawnSync('mysql', args, {shell:false})`,不要把密码拼进 shell 命令;空密码也要传 `--password=`,避免 `mysql -p` 进入交互式等待。下面的 `node -e` 内联了与 `lib/yaml-config.mjs` 同规则的极简 YAML 读取(2 层 map + 标量),仅读 `database:` 段: ```bash node -e ' const { spawnSync } = require("node:child_process"); const { readFileSync } = require("node:fs"); -const env = {}; -for (const raw of readFileSync(".env.local", "utf8").split(/\r?\n/)) { - const line = raw.trim(); - if (!line || line.startsWith("#")) continue; - const eq = line.indexOf("="); - if (eq < 0) continue; - const key = line.slice(0, eq).trim(); - let value = line.slice(eq + 1).trim(); - if (value.length >= 2 && ((value[0] === "\"" && value.at(-1) === "\"") || (value[0] === "'"'"'" && value.at(-1) === "'"'"'"))) value = value.slice(1, -1); - env[key] = value; +const SQ = String.fromCharCode(39), DQ = String.fromCharCode(34); +function parseScalar(raw) { + let s = String(raw).trim(); + if (s === "" || s[0] === "#") return ""; + const q = s[0]; + if (q === SQ || q === DQ) { const e = s.indexOf(q, 1); if (e !== -1) return s.slice(1, e); } + const h = s.indexOf(" #"); if (h !== -1) s = s.slice(0, h).trim(); + return s; } +const cfg = {}; let sec = null; +for (const raw of readFileSync("config-vars.yaml", "utf8").split(/\r?\n/)) { + const t = raw.trim(); if (!t || t[0] === "#") continue; + const c = raw.indexOf(":"); if (c < 0) continue; + const k = raw.slice(0, c).trim(); if (!k) continue; + const ind = raw.length - raw.replace(/^\s+/, "").length; + const v = parseScalar(raw.slice(c + 1)); + if (ind === 0) { if (v === "") { sec = {}; cfg[k] = sec; } else { cfg[k] = v; sec = null; } } + else if (sec) { sec[k] = v; } +} +const db = cfg.database || {}; const args = [ - `--host=${env.DB_HOST}`, - `--port=${env.DB_PORT || "3306"}`, - `--user=${env.DB_USER}`, - `--password=${env.DB_PASSWORD || ""}`, + `--host=${db.host}`, + `--port=${db.port || "3306"}`, + `--user=${db.user}`, + `--password=${db.password || ""}`, "-e", "SELECT 1;" ]; @@ -90,7 +99,7 @@ process.exit(r.status === null ? 1 : r.status); 成功 → 进入步骤 C;失败 → 打印具体错误(认证 / 主机不可达 / 端口拒接)并停下。 -勾选:` - [ ] .env.local 凭据已验证(mysql -e "SELECT 1" OK)` +勾选:` - [ ] config-vars.yaml DB 凭据已验证(mysql -e "SELECT 1" OK)` ### C. 自动导入 MySQL @@ -102,10 +111,10 @@ node scripts/setup-test-db.mjs #### C.2 把 V1 灌入已清空的 schema -调 `${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs`:它用纯 JS 解析 `.env.local`(**不** shell-source,消除注入),复用 host 白名单 + schema 名安全闸,再经 mysql2 把 DDL 灌入 schema。 +调 `${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs`:它用纯 JS 解析 `config-vars.yaml` 的 `database:` 段(**不** shell-source,消除注入),再经 mysql2 把 DDL 灌入 schema。 ```bash -node "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" .env.local sql/migrations/V1__initial_schema.sql +node "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" config-vars.yaml sql/migrations/V1__initial_schema.sql ``` 退出码与处理: @@ -113,7 +122,7 @@ node "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" .env.local sql/migrations/V1__ini - `1` → 失败:打印 stderr 并停下 - `2` → 用法错(路径找不到),打印路径并停下 -勾选:` - [ ] setup-test-db.mjs 防护通过 + DROP+CREATE + apply V1 已执行` +勾选:` - [ ] setup-test-db.mjs DROP+CREATE + apply V1 已执行` ### D. 勾选 docs/08 进度 + 进入 A5 @@ -126,7 +135,8 @@ node "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" .env.local sql/migrations/V1__ini ## 参考 - `${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs`(A.3 docs/03 ↔ V1.sql 5 维一致性校验,跨平台纯 Node) -- `${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs`(C.2 安全解析 .env.local + mysql2 灌入 DDL,不 shell-source) +- `${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs`(C.2 安全解析 config-vars.yaml 的 database: 段 + mysql2 灌入 DDL,不 shell-source) +- `${CLAUDE_PLUGIN_ROOT}/lib/yaml-config.mjs`(apply-ddl 依赖的极简 YAML 读取) - `docs/03-数据库设计文档.md`(DDL 翻译输入,SSoT) -- `.env.local`(DB 凭据) +- `config-vars.yaml`(DB 凭据,A1 产出) - 产物:`sql/migrations/V1__initial_schema.sql`(由 Flyway 在 Spring Boot 启动时验证 / apply) diff --git a/skills/plan/scope-lock/templates/config-vars-template.yaml b/skills/plan/scope-lock/templates/config-vars-template.yaml index 6711bce..75ba973 100644 --- a/skills/plan/scope-lock/templates/config-vars-template.yaml +++ b/skills/plan/scope-lock/templates/config-vars-template.yaml @@ -1,21 +1,29 @@ -# config-vars.yaml — non-sensitive project config; rules live in docs/07. -# Sensitive values go in .env.local; only list key names under secrets_ref. +# config-vars.yaml — 项目全部配置(含敏感凭据)。随项目提交,内部 git 传播。 +# 工具脚本(apply-ddl / setup-test-db)运行时按 2 层 map 解析此文件。 +# 值含 : / # / 空格 / $ / 引号等特殊字符时,用单引号包裹整个值:password: 'p@ss: w0rd#1' backend: - base_package: 【人工填写:后端根包名 / 命名空间,如 com.acme.erp】 + base_package: com.xly.erp http_port: 【人工填写:后端 HTTP 端口,默认 8080】 frontend: - pkg_name: 【人工填写:前端包名,如 acme-erp-web】 + pkg_name: xly-erp-web dev_port: 【人工填写:前端开发服务器端口,默认 5173】 +database: + host: 【人工填写:MySQL host,推荐 localhost】 + port: 【人工填写:MySQL port,默认 3306】 + user: 【人工填写:开发账号名】 + password: 【人工填写:对应密码,含特殊字符时用单引号包裹】 + schema: 【人工填写:schema 名,推荐含 test/_dev/_local,例如 erp_dev】 + admin_init: - username: 【人工填写:超级管理员初始账号,如 admin】 - # 初始密码属敏感 → 见 .env.local 的 ADMIN_INIT_PASSWORD + username: admin + password: 666666 -secrets_ref: - - DB_PASSWORD # 数据库密码 - - JWT_SECRET # JWT / 令牌签名密钥 - # - REDIS_PASSWORD # 缓存 / 会话(用 Redis 时) - # - ADMIN_INIT_PASSWORD # 超级管理员初始密码(有初始账号时) - # - OSS_ACCESS_KEY_SECRET / SMS_API_SECRET ... # 第三方凭证按需添加 +secrets: + jwt_secret: 【人工填写:JWT 签名密钥,256+ bit 随机串】 + # 项目专属凭据按需取消注释 / 追加,直接填真实值: + # redis_password: 【人工填写:Redis 密码(用 Redis 时)】 + # oss_access_key_secret: 【人工填写:对象存储密钥】 + # sms_api_secret: 【人工填写:短信网关密钥】 diff --git a/skills/plan/skeleton-gen/templates/docs-07-env-template.md b/skills/plan/skeleton-gen/templates/docs-07-env-template.md index 2a3da65..b187012 100644 --- a/skills/plan/skeleton-gen/templates/docs-07-env-template.md +++ b/skills/plan/skeleton-gen/templates/docs-07-env-template.md @@ -6,14 +6,15 @@ ## 三、配置与凭据规则 -项目配置分两处存放,**本文档只记规则、不记具体值**: +项目**全部配置**(含敏感凭据)统一存放在仓库根 `config-vars.yaml`,结构化 YAML,随项目提交(内部 git 传播)。**本文档只记规则、不记具体值**: -- **非敏感、项目级配置**(根包名 / 命名空间、应用端口、前端包名、管理员初始账号等)→ 仓库根 `config-vars.yaml`,结构化 YAML,随项目提交。 -- **敏感凭据**(数据库密码、JWT / 签名密钥、Redis 密码、第三方 key/secret、管理员初始密码等)→ 仓库根 `.env.local`,入 `.gitignore`,**不提交**;`config-vars.yaml` 末尾 `secrets_ref` 只登记键名作引用。 +- **非敏感、项目级配置**(根包名 / 命名空间、应用端口、前端包名、管理员初始账号等)→ `config-vars.yaml` 对应段。 +- **敏感凭据**(数据库密码、JWT / 签名密钥、Redis 密码、第三方 key/secret、管理员初始密码等)→ `config-vars.yaml` 的 `database` / `admin_init.password` / `secrets` 段,直接填真实值。 规则: - 根包名 / 命名空间一经在 `config-vars.yaml` 锁定,全项目复用,不得各模块各写。 - 端口遵循 § 二 约定;调整时改 `config-vars.yaml`,本文档不写具体端口。 -- 任何敏感值不得出现在 `config-vars.yaml`、docs、源码或日志中——只允许出现在 `.env.local`。 +- 任何配置值(含敏感值)只允许出现在 `config-vars.yaml`,不得散落在 docs、源码或日志中。 +- 工具脚本(apply-ddl / setup-test-db)运行时按 2 层 map 解析 `config-vars.yaml`;值含特殊字符时用单引号包裹。 ## 四、常用命令 diff --git a/skills/plan/skeleton-gen/templates/env-local-template b/skills/plan/skeleton-gen/templates/env-local-template deleted file mode 100644 index 015c5d5..0000000 --- a/skills/plan/skeleton-gen/templates/env-local-template +++ /dev/null @@ -1,12 +0,0 @@ -# Local dev credentials — gitignored. -# Quote values containing $/space/!/backtick with single quotes: DB_PASSWORD='p@ss$w0rd!' - -DB_HOST=【人工填写:MySQL host,推荐 localhost】 -DB_PORT=【人工填写:MySQL port,默认 3306】 -DB_USER=【人工填写:开发账号名】 -DB_PASSWORD=【人工填写:对应密码,含特殊字符时用单引号包裹】 -DB_SCHEMA=【人工填写:schema 名,推荐含 test/_dev/_local,例如 erp_dev】 -JWT_SECRET=【人工填写:JWT 签名密钥,256+ bit 随机串】 - -# 可选:额外允许 DROP CREATE 的远程 host(空格或逗号分隔)。 -TEST_DB_ALLOWED_HOSTS= diff --git a/skills/plan/skeleton-gen/templates/gitignore-append-template b/skills/plan/skeleton-gen/templates/gitignore-append-template index ac39aa6..d11b14b 100644 --- a/skills/plan/skeleton-gen/templates/gitignore-append-template +++ b/skills/plan/skeleton-gen/templates/gitignore-append-template @@ -1,7 +1,5 @@ # ==== ERP 插件推荐忽略项(skeleton-gen 追加) ==== -# 本地运行时配置(含真实凭据,严禁入库) -.env.local -.env.*.local +# 注:项目配置(含凭据)统一在 config-vars.yaml,随项目提交(内部 git 传播),不在此忽略。 # Java / Maven target/ diff --git a/skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs b/skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs index 838cdc5..dd53ee4 100644 --- a/skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs +++ b/skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node -// scripts/setup-test-db.mjs — DROP + CREATE 空测试库。 +// scripts/setup-test-db.mjs — DROP + CREATE 测试库。 // 由 coding.mjs 的 test-gate 调用;schema 由 Flyway 在 Spring Boot 启动时重放。 -// 只允许本地 host(或 TEST_DB_ALLOWED_HOSTS 白名单内的 host)+ 测试库名(含 test/_dev/_local/_ci)。 +// DB 凭据从仓库根 config-vars.yaml 的 database: 段读取(host / schema 完全信任该文件,无额外校验)。 import { spawnSync } from 'node:child_process' import { existsSync, readFileSync } from 'node:fs' @@ -9,86 +9,70 @@ import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) -const ENV_FILE = join(SCRIPT_DIR, '..', '.env.local') +const CONFIG_FILE = join(SCRIPT_DIR, '..', 'config-vars.yaml') -function parseEnv(text) { - const env = {} - for (const rawLine of text.split(/\r?\n/)) { - const line = rawLine.trim() - if (line === '' || line.startsWith('#')) continue - const eq = line.indexOf('=') - if (eq === -1) continue - const key = line.slice(0, eq).trim() - if (!key) continue - let value = line.slice(eq + 1).trim() - if ( - value.length >= 2 && - ((value.startsWith("'") && value.endsWith("'")) || - (value.startsWith('"') && value.endsWith('"'))) - ) { - value = value.slice(1, -1) +// 极简 YAML 读取(2 层 map + 标量;与插件 lib/yaml-config.mjs 同规则,内联以免运行时依赖)。 +function parseScalar(raw) { + let s = String(raw).trim() + if (s === '' || s[0] === '#') return '' + const q = s[0] + if (q === '"' || q === "'") { + const end = s.indexOf(q, 1) + if (end !== -1) return s.slice(1, end) + } + const hash = s.indexOf(' #') + if (hash !== -1) s = s.slice(0, hash).trim() + return s +} +function parseYamlConfig(text) { + const root = {} + let section = null + for (const rawLine of text.split('\n')) { + const line = rawLine.replace(/\r$/, '') + const trimmed = line.trim() + if (trimmed === '' || trimmed[0] === '#') continue + const colon = line.indexOf(':') + if (colon === -1) continue + const key = line.slice(0, colon).trim() + if (key === '') continue + const indent = line.length - line.replace(/^\s+/, '').length + const value = parseScalar(line.slice(colon + 1)) + if (indent === 0) { + if (value === '') { + section = {} + root[key] = section + } else { + root[key] = value + section = null + } + } else if (section) { + section[key] = value + } else { + root[key] = value } - env[key] = value } - return env + return root } -if (!existsSync(ENV_FILE)) { - console.error(`[setup-test-db] .env.local 不存在(${ENV_FILE})`) +if (!existsSync(CONFIG_FILE)) { + console.error(`[setup-test-db] config-vars.yaml 不存在(${CONFIG_FILE})`) process.exit(1) } -const env = parseEnv(readFileSync(ENV_FILE, 'utf8')) +const db = parseYamlConfig(readFileSync(CONFIG_FILE, 'utf8')).database || {} -const DB_HOST = env.DB_HOST ?? '' -const DB_PORT = env.DB_PORT ?? '3306' -const DB_USER = env.DB_USER ?? '' -const DB_PASSWORD = env.DB_PASSWORD ?? '' -const DB_SCHEMA = env.DB_SCHEMA ?? '' +const DB_HOST = db.host ?? '' +const DB_PORT = db.port ?? '3306' +const DB_USER = db.user ?? '' +const DB_PASSWORD = db.password ?? '' +const DB_SCHEMA = db.schema ?? '' if (!/^\d+$/.test(DB_PORT) || Number(DB_PORT) <= 0 || Number(DB_PORT) > 65535) { - console.error(`[setup-test-db] DB_PORT 非法: ${DB_PORT}(必须是 1..65535 的整数)`) - process.exit(1) -} - -if (!/^[A-Za-z0-9_]+$/.test(DB_SCHEMA)) { - console.error(`[setup-test-db] DB_SCHEMA 只能包含字母、数字、下划线,当前为: ${DB_SCHEMA}`) - process.exit(1) -} - -// 防护 1:默认只允许本地 host(localhost / 127.0.0.1 / ::1)。 -// 额外允许的远程 host 在 .env.local 的 TEST_DB_ALLOWED_HOSTS 中(空格或逗号分隔)。 -const extraHosts = (env.TEST_DB_ALLOWED_HOSTS ?? '') - .split(/[\s,]+/) - .filter(Boolean) -const allowedHosts = ['localhost', '127.0.0.1', '::1', ...extraHosts] -if (!allowedHosts.includes(DB_HOST)) { - console.error(`[setup-test-db] 拒绝在非白名单 host (${DB_HOST}) 上执行 DROP DATABASE`) - console.error(` 当前白名单:${allowedHosts.join(' ')}`) - console.error(' 加入 host:在 .env.local 追加 TEST_DB_ALLOWED_HOSTS=" "') - process.exit(1) -} - -// 防护 2:schema 名需像测试/开发库(含 test / _dev / _local / _ci),否则拒绝。 -const schemaLooksLikeTest = - /test/.test(DB_SCHEMA) || /_dev$/.test(DB_SCHEMA) || /_local$/.test(DB_SCHEMA) || /_ci$/.test(DB_SCHEMA) -if (!schemaLooksLikeTest) { - console.error( - `[setup-test-db] schema '${DB_SCHEMA}' 不像测试库(期望命名含 test / _dev / _local / _ci)` - ) + console.error(`[setup-test-db] database.port 非法: ${DB_PORT}(必须是 1..65535 的整数)`) process.exit(1) } console.log(`[setup-test-db] 即将 DROP + CREATE \`${DB_SCHEMA}\` on ${DB_HOST}:${DB_PORT}`) -if (!['localhost', '127.0.0.1', '::1'].includes(DB_HOST)) { - console.log( - '[setup-test-db] 目标是 **远程** host(已在 TEST_DB_ALLOWED_HOSTS 白名单中,每次 test.mjs 都会 DROP)' - ) - console.log(`[setup-test-db] 当前白名单: ${allowedHosts.join(' ')}`) - console.log( - '[setup-test-db] 若不希望每次自动 DROP,从 .env.local 的 TEST_DB_ALLOWED_HOSTS 删掉此 host' - ) -} const sql = `DROP DATABASE IF EXISTS \`${DB_SCHEMA}\`; ` +