@clawos-dev/clawd 0.2.519 → 0.2.521
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/dist/app-builder-plugin/mcp-server.cjs +65 -19
- package/dist/cli.cjs +1 -1
- package/dist/deploy-kit/scripts/ensure-credentials.spec.ts +139 -18
- package/dist/deploy-kit/scripts/new-extension.sh +6 -2
- package/dist/deploy-kit/scripts/publish.sh +32 -9
- package/dist/deploy-kit/scripts/publish.spec.ts +72 -0
- package/dist/deploy-kit/scripts/remove-extension.sh +36 -9
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/SKILL.md +20 -4
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +2 -2
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +4 -1
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/render-sql.mjs +64 -0
- package/dist/persona-defaults/persona-app-builder/CLAUDE.md +19 -14
- package/dist/persona-defaults/persona-app-builder/extension-kit/README.md +1 -1
- package/dist/persona-defaults/persona-app-builder/extension-kit/contract/s.yaml.tmpl +8 -3
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/ext.conf +3 -1
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/.env.example +4 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/.fcignore +1 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/db/schema.sql +15 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/main.ts +3 -1
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/table-name.ts +19 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/SKILL.md +20 -4
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +2 -2
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +4 -1
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/render-sql.mjs +64 -0
- package/dist/persona-defaults/persona-dataclaw-builder/CLAUDE.md +4 -2
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/README.md +1 -1
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/contract/s.yaml.tmpl +15 -6
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# ============================================================
|
|
3
|
-
# 清理一个 extension:删 FC 资源(
|
|
3
|
+
# 清理一个 extension:删 FC 资源(函数+触发器+域名,integration / prod 两套)+ 可选删它的 Supabase 表(通用,persona 无关)
|
|
4
4
|
# 用法: remove-extension.sh <extension目录> <persona根目录>
|
|
5
|
-
# 删哪些表:读 ext.conf 的 SUPABASE_TABLES(extension
|
|
5
|
+
# 删哪些表:读 ext.conf 的 SUPABASE_TABLES(extension 自己声明的**基础名**,如 visits;非 supabase persona 留空即跳过)。
|
|
6
|
+
# 每个基础名打两套 DROP:<APP_NAME>_<SLUG>_int_<t>(integration)与 <APP_NAME>_<SLUG>_<t>(prod)。
|
|
7
|
+
# 值若已带完整前缀 <APP_NAME>_<SLUG>_ 视为双环境之前声明的完整表名,只打一条。
|
|
6
8
|
# 安全:① 校验 <APP_NAME>_<SLUG>_ 前缀防误删 clawos / 同名 app 别人副本 ② 列出 DROP 让 agent 用 supabase MCP 接力 ③ 数据不可逆
|
|
7
9
|
# ============================================================
|
|
8
10
|
set -euo pipefail
|
|
@@ -18,8 +20,9 @@ PERSONA_KIT="$PERSONA_ROOT/extension-kit"
|
|
|
18
20
|
APP_NAME="${APP_NAME:-$(basename "$EXT_DIR")}"
|
|
19
21
|
SUPABASE_TABLES="${SUPABASE_TABLES:-}"
|
|
20
22
|
# SLUG 由 new-extension.sh scaffold 时 bake;空 = 老项目(本改动前 scaffold)走 legacy 表前缀。
|
|
21
|
-
#
|
|
23
|
+
# prod 表名形如 ${APP_NAME}_${SLUG}_<table>,integration 多一段 int_;老项目走旧的 ${APP_NAME}_<table>。
|
|
22
24
|
TABLE_PREFIX="${APP_NAME}_${SLUG:+${SLUG}_}"
|
|
25
|
+
TABLE_PREFIX_INT="${TABLE_PREFIX}int_"
|
|
23
26
|
|
|
24
27
|
# 确保工具链就绪(缺 s / aliyun 则装,已装跳过)—— 下面 s remove 依赖它
|
|
25
28
|
source "$KIT_DIR/scripts/ensure-toolchain.sh"
|
|
@@ -29,16 +32,40 @@ ensure_toolchain
|
|
|
29
32
|
source "$KIT_DIR/scripts/ensure-credentials.sh"
|
|
30
33
|
ensure_credentials
|
|
31
34
|
|
|
32
|
-
# 1) 删 FC 侧(函数 + 触发器 + 自定义域名)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
# 1) 删 FC 侧(函数 + 触发器 + 自定义域名):integration / prod 各自有一份 publish.sh 渲的 s 文件
|
|
36
|
+
#(s.int.yaml / s.prod.yaml;双环境之前渲的叫 s.yaml),有哪份删哪份——从未发过的环境没有产物,跳过。
|
|
37
|
+
removed=0
|
|
38
|
+
for s_file in s.int.yaml s.prod.yaml s.yaml; do
|
|
39
|
+
[ -f "$EXT_DIR/$s_file" ] || continue
|
|
40
|
+
# 老 s.yaml 与 s.prod.yaml 可能指同一个 prod 函数(app-builder:DEPLOY_NAME 不带标签,name: 相同):
|
|
41
|
+
# 那就只按 s.prod.yaml 拆,对同一资源 s remove 两次第二次会因「不存在」失败。
|
|
42
|
+
# name: 不同(dataclaw 双环境之前 functionName 不带 slug)= 两个函数,两份都拆。
|
|
43
|
+
if [ "$s_file" = s.yaml ] && [ -f "$EXT_DIR/s.prod.yaml" ] \
|
|
44
|
+
&& [ "$(grep -m1 '^name:' "$EXT_DIR/s.yaml")" = "$(grep -m1 '^name:' "$EXT_DIR/s.prod.yaml")" ]; then
|
|
45
|
+
continue
|
|
46
|
+
fi
|
|
47
|
+
echo "==> 删 FC 资源: $APP_NAME ($s_file)"
|
|
48
|
+
( cd "$EXT_DIR" && s remove -y -t "$s_file" --access "$S_ACCESS_ALIAS" )
|
|
49
|
+
removed=1
|
|
50
|
+
done
|
|
51
|
+
[ "$removed" = 1 ] || echo "==> 没有渲染过的 s 文件(从未发布过),FC 侧无资源可删"
|
|
35
52
|
|
|
36
53
|
# 2) 列出要删的 Supabase 表(脚本不直接调 DB,改打印 DROP 让 agent 用 supabase MCP 跑)。
|
|
37
54
|
# bash 调不到 MCP(MCP 是 LLM 通道),pg 直连要密码 agent 拿不到 —— 最干净:打印 DROP,
|
|
38
55
|
# 让 agent 看到 Bash tool stdout 后用 mcp__supabase__execute_sql 接力。
|
|
39
56
|
if [ -n "$SUPABASE_TABLES" ]; then
|
|
40
|
-
#
|
|
57
|
+
# 基础名(visits)展开成两套完整表名;已带完整前缀 ${TABLE_PREFIX} 的是 legacy 完整表名,原样一条
|
|
58
|
+
#(判据用完整前缀而不是 ${APP_NAME}_:基础名本身以 <APP_NAME>_ 开头——shop 的 shop_items——是合法的)。
|
|
59
|
+
# 安全锁:展开后每个表必须 ${TABLE_PREFIX} 前缀(int 那套自然也满足),否则拒绝
|
|
60
|
+
#(防误伤 clawos / 同名 app 别人副本的表)。
|
|
61
|
+
FULL_TABLES=""
|
|
41
62
|
for t in $SUPABASE_TABLES; do
|
|
63
|
+
case "$t" in
|
|
64
|
+
${TABLE_PREFIX}*) FULL_TABLES="$FULL_TABLES $t" ;;
|
|
65
|
+
*) FULL_TABLES="$FULL_TABLES ${TABLE_PREFIX_INT}${t} ${TABLE_PREFIX}${t}" ;;
|
|
66
|
+
esac
|
|
67
|
+
done
|
|
68
|
+
for t in $FULL_TABLES; do
|
|
42
69
|
case "$t" in
|
|
43
70
|
${TABLE_PREFIX}*) ;;
|
|
44
71
|
*) echo "❌ 表 '$t' 不是 '${TABLE_PREFIX}' 前缀,拒绝输出(可能误伤 clawos / 别人同名 app)"; exit 1 ;;
|
|
@@ -46,10 +73,10 @@ if [ -n "$SUPABASE_TABLES" ]; then
|
|
|
46
73
|
done
|
|
47
74
|
|
|
48
75
|
echo ""
|
|
49
|
-
echo "==> ⚠️ 下面是要删的 Supabase
|
|
76
|
+
echo "==> ⚠️ 下面是要删的 Supabase 表(integration 与 prod 两套),脚本不直接调 DB"
|
|
50
77
|
echo " (阿里云自建无 management API + bash 调不到 MCP)"
|
|
51
78
|
echo ""
|
|
52
|
-
for t in $
|
|
79
|
+
for t in $FULL_TABLES; do
|
|
53
80
|
echo " DROP TABLE IF EXISTS public.$t CASCADE;"
|
|
54
81
|
done
|
|
55
82
|
echo ""
|
package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ description: Use when creating, reopening, changing, pushing to GitHub, publishi
|
|
|
19
19
|
node <skill>/scripts/list-projects.mjs --root <workdir>
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
输出 `{ projects: [{ name, projectDir, createdAt, repoUrl?, pushedCommit?,
|
|
22
|
+
输出 `{ projects: [{ name, projectDir, createdAt, repoUrl?, pushedCommit?, deployments?: { integration?: { url, publishedAt, publishedCommit }, prod?: {…} }, prodUrl? }] }`,按名字排序(`prodUrl` = `deployments.prod.url` 的兼容字段)。用户不记得名字就把列表念给他。
|
|
23
23
|
|
|
24
24
|
## 新建一个 app
|
|
25
25
|
|
|
@@ -28,7 +28,7 @@ node <skill>/scripts/list-projects.mjs --root <workdir>
|
|
|
28
28
|
3. `createProject({ name })`(clawd-app-builder MCP):建目录 + scaffold(含把 supabase 凭据渲进 `server/.env`)。返回 `{ name, projectDir, slug, repoName }`,`repoName` 就是仓库名(`<name>-<slug>`;slug 读不到时退成 `<name>`,以返回值为准)。失败已整个回滚,把原因告诉用户修好再来
|
|
29
29
|
4. github MCP `create_repository({ name: "<repoName>", organization: "ottin4ttc", private: true, autoInit: false })`。仓库地址是 `https://github.com/ottin4ttc/<repoName>`
|
|
30
30
|
5. 首次提交:按下面「提交到 GitHub」走一遍,`record-push.mjs` 带上 `--repo-url`
|
|
31
|
-
6. `cd <projectDir>`,`server/` 和 `web/` 各跑一次 `pnpm install
|
|
31
|
+
6. `cd <projectDir>`,`server/` 和 `web/` 各跑一次 `pnpm install`,开始开发;建表见「建表 / 改表」
|
|
32
32
|
7. 发布:见「发布」
|
|
33
33
|
|
|
34
34
|
## 提交到 GitHub(每完成一段就做一次)
|
|
@@ -53,13 +53,29 @@ node <skill>/scripts/list-projects.mjs --root <workdir>
|
|
|
53
53
|
- 目录里**有 `.git`** 的是 GitHub MCP 之前建的项目:按原来的本地 git 流程(`git add -A && git commit && git push`),`publish` 会拒脏工作区、记本地 HEAD;不要给它改成 `push_files`
|
|
54
54
|
- `repoUrl` 为空又没 `.git` 的是更老的项目:当普通目录改,改完直接 `publish`,不补仓库(用户要它进仓库会明说)
|
|
55
55
|
|
|
56
|
+
## 建表 / 改表
|
|
57
|
+
|
|
58
|
+
表两套(integration / prod),DDL 只有一份:
|
|
59
|
+
|
|
60
|
+
1. 表结构写在 `<projectDir>/server/db/schema.sql`,表名用 `__TABLE_PREFIX__visits` 占位,语句写成可重复执行的(`IF NOT EXISTS` / `OR REPLACE`)
|
|
61
|
+
2. 渲 integration 那套并执行:`node <skill>/scripts/render-sql.mjs --project <projectDir> --env integration` → 输出的 SQL 交给 supabase MCP `execute_sql`(阿里云自建实例上 `apply_migration` 不可用)
|
|
62
|
+
3. 在测试地址验过后再渲 prod:`--env prod` → `execute_sql`
|
|
63
|
+
4. 把**基础名**(`visits`,不带前缀)填进 `ext.conf` 的 `SUPABASE_TABLES`(空格分隔)
|
|
64
|
+
5. server 代码里表名只经 `tableName('visits')`(`server/src/table-name.ts`,读 env `TABLE_PREFIX`;本地 `.env` 是 `<name>_<slug>_int_`,线上由 publish 按 env 注入),不写死前缀
|
|
65
|
+
|
|
56
66
|
## 发布
|
|
57
67
|
|
|
58
|
-
先按「提交到 GitHub」把改动推上去并 `record-push.mjs
|
|
68
|
+
先按「提交到 GitHub」把改动推上去并 `record-push.mjs`,再分两步发(clawd-app-builder MCP,`env` 必填、没有默认):
|
|
69
|
+
|
|
70
|
+
1. `publish({ name, env: "integration" })` → 返回 `{ env, url, commit, publishedAt }`,`url` 是 `https://<name>-<slug>-int.app.clawos.chat`,回复里给用户可点的测试链接,等他看
|
|
71
|
+
2. 用户确认没问题 → `publish({ name, env: "prod" })` → `url` 是 `https://<name>-<slug>.app.clawos.chat`。**prod 只接受 integration 发过的同一个 commit**:报「还没发过 integration」说明这个 commit 没在测试地址验过(改了推了没再发 integration),回到第 1 步;没有绕过开关
|
|
72
|
+
3. 用户要改 → 改 → 推 → 记账 → 回到第 1 步(prod 不动)
|
|
73
|
+
|
|
74
|
+
**它会拒绝有没推改动的项目**(重算文件树哈希跟 `record-push` 记的比;有 `.git` 的老项目看 `git status`),报「有改动还没推到仓库」就回去推一遍再发。失败读项目目录下 `.publish.log`,修 → 推 → 记账 → 再 `publish`。**不要自己 bash 跑 `publish.sh`**:凭据只在 tool 进程的 env 里。
|
|
59
75
|
|
|
60
76
|
## 删一个 app
|
|
61
77
|
|
|
62
|
-
用户明确说删才动,先确认一次,再 `removeProject({ name })
|
|
78
|
+
用户明确说删才动,先确认一次,再 `removeProject({ name })`:拆 integration / prod 两个环境的 FC 函数 / 域名,打印两套表的 DROP(用 supabase MCP `execute_sql` 跑完才算删干净)。GitHub 仓库保留,用户想删自己去 GitHub 归档。
|
|
63
79
|
|
|
64
80
|
## GitHub 授权不可用时
|
|
65
81
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// 把项目目录里该进仓库的文件收成 GitHub MCP `push_files` 要的 `[{ path, content }]`。
|
|
3
3
|
//
|
|
4
|
-
// 过滤规则 = 项目根 `.gitignore`(模板那份:node_modules/ dist/ .env .publish.log s.yaml server/bootstrap .s/ …)
|
|
4
|
+
// 过滤规则 = 项目根 `.gitignore`(模板那份:node_modules/ dist/ .env .publish.log s.yaml s.*.yaml server/bootstrap .s/ …)
|
|
5
5
|
// + 永远排掉 `.git` 和 `.clawd-project.json`(插件的记账文件:publish / record-push 都会改它,进了仓库或哈希就会
|
|
6
|
-
// 「每推一次都变一次」,而且里面的
|
|
6
|
+
// 「每推一次都变一次」,而且里面的 deployments / pushedCommit 也不是源码)。**不要**在这里另写一份排除表:publish.sh 渲进项目目录的 s.int.yaml / s.prod.yaml 含 supabase key,
|
|
7
7
|
// 它进不进仓库只该由 .gitignore 一处决定。
|
|
8
8
|
// 二进制(前 8KB 含 NUL)和超过 512KB 的文件跳过并列进 `skipped`——push_files 只收文本,
|
|
9
9
|
// 而且每个字节都要经模型上下文走一遍;agent 看到 skipped 里有该进仓库的东西要自己判断。
|
|
@@ -24,7 +24,10 @@ for (const dirName of names) {
|
|
|
24
24
|
try {
|
|
25
25
|
const value = JSON.parse(await readFile(join(projectsRoot, dirName, '.clawd-project.json'), 'utf8'))
|
|
26
26
|
if (value?.name !== dirName || typeof value.createdAt !== 'string') continue
|
|
27
|
-
|
|
27
|
+
// 双环境后发布记录在 deployments.{integration,prod};prodUrl 保留作兼容字段 = prod 那份的 url。
|
|
28
|
+
// 插件还没读写过的老文件顶层就有 prodUrl,原样带出。
|
|
29
|
+
const prodUrl = value.deployments?.prod?.url
|
|
30
|
+
projects.push({ ...value, ...(prodUrl ? { prodUrl } : {}), projectDir: join(projectsRoot, dirName) })
|
|
28
31
|
} catch {
|
|
29
32
|
// User-owned directories without valid app-builder metadata are not projects.
|
|
30
33
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 把项目的 server/db/schema.sql 渲成某个环境的 SQL 打到 stdout:`__TABLE_PREFIX__` → 该环境的表前缀。
|
|
3
|
+
// 表结构只有这一份源,integration / prod 各渲一次再喂给 supabase MCP 的 execute_sql——两套表结构
|
|
4
|
+
// 不会漂移。前缀口径与 publish.sh / remove-extension.sh 相同:
|
|
5
|
+
// integration → <APP_NAME>_<SLUG>_int_ prod → <APP_NAME>_<SLUG>_
|
|
6
|
+
// ext.conf 没 SLUG 的老项目退成 <APP_NAME>_int_ / <APP_NAME>_。
|
|
7
|
+
import { readFile } from 'node:fs/promises'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
const ENVS = ['integration', 'prod']
|
|
11
|
+
const PLACEHOLDER = '__TABLE_PREFIX__'
|
|
12
|
+
|
|
13
|
+
function arg(name) {
|
|
14
|
+
const i = process.argv.indexOf(name)
|
|
15
|
+
return i >= 0 ? process.argv[i + 1] : undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const projectDir = arg('--project')
|
|
19
|
+
const env = arg('--env')
|
|
20
|
+
if (!projectDir || !env || !ENVS.includes(env)) {
|
|
21
|
+
process.stderr.write('usage: render-sql.mjs --project <projectDir> --env integration|prod\n')
|
|
22
|
+
process.exit(2)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fail(msg) {
|
|
26
|
+
process.stderr.write(`${msg}\n`)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** ext.conf 是 bash 赋值文件;这里只认 `KEY=value` / `KEY="value"` 单行,够 APP_NAME / SLUG 用。 */
|
|
31
|
+
function readConf(text) {
|
|
32
|
+
const out = {}
|
|
33
|
+
for (const line of text.split('\n')) {
|
|
34
|
+
const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line.trim())
|
|
35
|
+
if (!m) continue
|
|
36
|
+
out[m[1]] = m[2].replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1')
|
|
37
|
+
}
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const confPath = join(projectDir, 'ext.conf')
|
|
42
|
+
const schemaPath = join(projectDir, 'server', 'db', 'schema.sql')
|
|
43
|
+
|
|
44
|
+
let conf
|
|
45
|
+
try {
|
|
46
|
+
conf = readConf(await readFile(confPath, 'utf8'))
|
|
47
|
+
} catch (error) {
|
|
48
|
+
fail(`读不到 ${confPath}:${error?.message ?? error}`)
|
|
49
|
+
}
|
|
50
|
+
if (!conf.APP_NAME) fail(`${confPath} 里没有 APP_NAME,算不出表前缀`)
|
|
51
|
+
|
|
52
|
+
let schema
|
|
53
|
+
try {
|
|
54
|
+
schema = await readFile(schemaPath, 'utf8')
|
|
55
|
+
} catch (error) {
|
|
56
|
+
fail(`读不到 ${schemaPath}:${error?.message ?? error}(表结构应写在 server/db/schema.sql,表名用 ${PLACEHOLDER}visits 这种占位)`)
|
|
57
|
+
}
|
|
58
|
+
if (!schema.includes(PLACEHOLDER)) {
|
|
59
|
+
fail(`${schemaPath} 里没有 ${PLACEHOLDER}:表名不要写死,写成 ${PLACEHOLDER}<表名>,两个环境才能各渲各的`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const base = conf.SLUG ? `${conf.APP_NAME}_${conf.SLUG}_` : `${conf.APP_NAME}_`
|
|
63
|
+
const prefix = env === 'integration' ? `${base}int_` : base
|
|
64
|
+
process.stdout.write(schema.replaceAll(PLACEHOLDER, prefix))
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
- **每个 app 就是当前用户工作区下的 `projects/<name>/`**,配一个 GitHub 组织 `ottin4ttc` 下的私有仓库(名字 `<name>-<slug>`,slug 是 scaffold 时 bake 进 `ext.conf` 的 4 位散列,跟 FC 函数名 / 子域名同源)。仓库由你经 **`github` MCP** 建和提交,本机不需要 `gh`、项目目录里不需要 git
|
|
10
10
|
- **会话不绑项目**。一个会话里可以先改 A 再改 B;隔几天新开会话改旧 app 也行——用 `app-builder-projects` skill 的 `list-projects.mjs` 找到目录接着干
|
|
11
11
|
- **没有右栏预览、没有 dev server 概念**。验证靠发布:`publish` tool 跑完给公网 URL,老板点链接看
|
|
12
|
-
- **每次改动都推到仓库**,线上跑的是仓库里的哪个 commit 说得清:push 后用 skill 的 `record-push.mjs` 记下远端 commit,`publish`
|
|
12
|
+
- **每次改动都推到仓库**,线上跑的是仓库里的哪个 commit 说得清:push 后用 skill 的 `record-push.mjs` 记下远端 commit,`publish` 把它记成该环境的 `publishedCommit`
|
|
13
|
+
- **两个环境**:integration(`<name>-<slug>-int.app.clawos.chat`,给老板看的测试地址)和 prod(`<name>-<slug>.app.clawos.chat`)。先发 integration,老板点头再发 prod
|
|
13
14
|
|
|
14
15
|
**流程细节以 `app-builder-projects` skill(`.claude/skills/app-builder-projects/SKILL.md`)为准**,下面只是主线。
|
|
15
16
|
|
|
@@ -47,24 +48,27 @@
|
|
|
47
48
|
5. `cd <projectDir>`,装依赖:`server/` 和 `web/` 各跑一次 `pnpm install`
|
|
48
49
|
6. 开发:写业务、用 supabase MCP 建表(见下文「后端」)。复杂任务走 superpowers 节奏,简单任务 TodoWrite 跟踪(分级见下)
|
|
49
50
|
7. **每完成一段就按 skill 推一次**:`collect-files.mjs` → `push_files`(或单文件 `create_or_update_file` / `delete_file`)→ `record-push.mjs`
|
|
50
|
-
8.
|
|
51
|
-
9. 老板看链接提意见 → 回到第 6
|
|
51
|
+
8. 发布:先 `publish({ name, env: "integration" })`,把返回的测试链接给老板;老板确认没问题再 `publish({ name, env: "prod" })`,给正式链接
|
|
52
|
+
9. 老板看链接提意见 → 回到第 6 步,同一个会话一直往下聊(prod 不动,直到下一轮老板在 integration 上点头)
|
|
52
53
|
|
|
53
54
|
### 改一个已有的 app
|
|
54
55
|
|
|
55
56
|
1. 运行 `list-projects.mjs`,按名字找到它的 `projectDir`(老板不记得名字就把列表念给他)
|
|
56
|
-
2. `cd <projectDir>` 直接改;改完按 skill 推到仓库 + `record-push.mjs`;`publish`
|
|
57
|
+
2. `cd <projectDir>` 直接改;改完按 skill 推到仓库 + `record-push.mjs`;`publish(integration)` 给测试链接,老板点头再 `publish(prod)`
|
|
57
58
|
3. 目录里**有 `.git`** 的是 GitHub MCP 之前建的项目:照原来的本地 git 流程(`git add -A && git commit && git push`),`publish` 会拒脏工作区、记本地 HEAD,不要改成 `push_files`
|
|
58
59
|
4. `repoUrl` 为空又没 `.git` 的是更老的项目。**不要给它补仓库**:就当普通目录改,改完直接 `publish`(tool 不记 commit)。老板要它进仓库会明说
|
|
59
60
|
|
|
60
61
|
### 发布
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
**上线两步走**:先 `publish({ name, env: "integration" })`,把测试链接给老板;老板确认没问题后再 `publish({ name, env: "prod" })`。prod 只接受 integration 发过的同一个 commit,tool 会机械拒绝。Supabase 表两套(`_int_` 前缀 / 无后缀前缀),server 只通过 env `TABLE_PREFIX` 取表名,代码里不写死;建表用 skill 的 `render-sql.mjs` 渲 `server/db/schema.sql` 后经 supabase MCP 执行,先 integration 再 prod。
|
|
64
|
+
|
|
65
|
+
**只走 `publish` tool,不手跑 `publish.sh`。** 它同步跑完 build → deploy → verify,成功返回 `{ env, url, commit, publishedAt }`;失败返回失败阶段 + stderr 末尾 + `.publish.log` 路径。`env` 必填(`integration` | `prod`),没有默认值。
|
|
63
66
|
|
|
64
67
|
- **发布前先把改动推到仓库并 `record-push.mjs`**:`publish` 会重算文件树哈希跟记账的比,有没推的改动直接拒(有 `.git` 的老项目看 `git status`);线上必须对应仓库里的一个 commit
|
|
65
68
|
- 失败时:读项目目录下的 `.publish.log` 完整日志 → 定位并修(改源码 / 改 `ext.conf` / 看凭证)→ 推 → 再调一次 `publish`,直到拿到公网 URL
|
|
66
69
|
- 看到 `❌ s.yaml.tmpl 占位符 __SUPABASE_URL__ 无对应 env 变量` 这类报错,**不要去找 key 手动 export、也不要往 `config.env` 里写值**——那是你自己 bash 跑了脚本,改回调 tool 就好
|
|
67
|
-
-
|
|
70
|
+
- 地址固定:integration 是 `https://<name>-<slug>-int.app.clawos.chat`,prod 是 `https://<name>-<slug>.app.clawos.chat`;同一环境再发就是覆盖
|
|
71
|
+
- 报「还没发过 integration」= 这个 commit 没在测试地址验过:先 `publish(integration)`,老板看过再来。**没有绕过开关**
|
|
68
72
|
|
|
69
73
|
### 本地自测(可选,不做成 UI)
|
|
70
74
|
|
|
@@ -72,7 +76,7 @@
|
|
|
72
76
|
|
|
73
77
|
### 删 app
|
|
74
78
|
|
|
75
|
-
老板明确说要删才动:先确认一次,再调 `removeProject({ name })`——下线 FC 函数 / 触发器 /
|
|
79
|
+
老板明确说要删才动:先确认一次,再调 `removeProject({ name })`——下线 integration / prod 两个环境的 FC 函数 / 触发器 / 域名,打印两套 supabase 表的 DROP 语句(照着用 supabase MCP 跑),退出 0 才删本地目录。**GitHub 仓库保留**,老板想删自己去 GitHub 归档。
|
|
76
80
|
|
|
77
81
|
## 可用 tool 完整清单(`clawd-app-builder` MCP)
|
|
78
82
|
|
|
@@ -81,8 +85,8 @@
|
|
|
81
85
|
| tool | 入参 | 用途 |
|
|
82
86
|
|---|---|---|
|
|
83
87
|
| `createProject` | `{name}` | 建目录 + scaffold(渲 `server/.env`)。返回 `{ name, projectDir, slug, repoName }`,不碰 GitHub |
|
|
84
|
-
| `publish` | `{name}` | 跑发布流水线(build → deploy → verify
|
|
85
|
-
| `removeProject` | `{name}` |
|
|
88
|
+
| `publish` | `{name, env}` | 跑发布流水线(build → deploy → verify)到 `env`(`integration` \| `prod`,必填),同步等结束,返回 `{ env, url, commit, publishedAt }` 或失败详情。prod 只接受 integration 发过的同一个 commit |
|
|
89
|
+
| `removeProject` | `{name}` | 拆掉两个环境已发布的资源 + 删本地目录(仓库保留)。老板确认后才调 |
|
|
86
90
|
|
|
87
91
|
**红线:不在此列就不要瞎试。** 不存在 `listProjects` / `startDevServer` / `reportStage` / `setProdUrl` / `updateProjectPort` 这些老 tool,也没有任何 `appBuilder:*` RPC。遇到清单外的能力需求 → 跟老板说,让他评估要不要加。
|
|
88
92
|
|
|
@@ -92,8 +96,9 @@
|
|
|
92
96
|
|
|
93
97
|
- 读表结构、跑 SQL、看 auth 用户都走这个 MCP,不要凭记忆猜 schema
|
|
94
98
|
- **凭据值你看不到,也不需要看到**:`extension-kit/config.env` 与 `server/.env.example` 里只有变量名(`${SUPABASE_URL}` / `${SUPABASE_KEY}`),值由 daemon 在起 MCP server 时才注进那个子进程。`createProject` 时 scaffold 会把项目的 `server/.env` 渲好,直接 `pnpm dev` 就能连上——**不要**去别处找 key 往文件里抄
|
|
95
|
-
- **红线**:这是共享生产库。建表 / 迁移前先 `list_tables`
|
|
96
|
-
-
|
|
99
|
+
- **红线**:这是共享生产库。建表 / 迁移前先 `list_tables` 看清现状,新项目的表**必须**带 `TABLE_PREFIX` 沉到 `public` schema(integration 是 `${APP_NAME}_${SLUG}_int_`,prod 是 `${APP_NAME}_${SLUG}_`,如 `helloworld_a1b2_int_click_counter` / `helloworld_a1b2_click_counter`;`APP_NAME` 和 `SLUG` 都从项目 `ext.conf` 读,**不要自己另起一套 slug**),避免和 clawos / 其它项目 / 别人同名 app 的表撞名;**绝不** drop / alter clawos 已有的表,不确定哪些是 clawos 的就先问老板
|
|
100
|
+
- **表两套,DDL 一份**:表结构写在 `server/db/schema.sql`,表名用 `__TABLE_PREFIX__visits` 占位;`node <skill>/scripts/render-sql.mjs --project <projectDir> --env integration` 渲出 SQL 用 supabase MCP `execute_sql` 跑(`apply_migration` 在阿里云自建实例上不可用),在测试地址验过再 `--env prod` 跑一次。server 代码里表名只经 `tableName('visits')`(`server/src/table-name.ts`,读 env `TABLE_PREFIX`),不写死
|
|
101
|
+
- 建了表记得把**基础名**(如 `visits`)填进 `ext.conf` 的 `SUPABASE_TABLES`(空格分隔),`removeProject` 据此打印两套清理语句
|
|
97
102
|
|
|
98
103
|
**易踩的坑**:唯一真源是 **daemon 的凭据表**(`~/.clawd/secrets/` 覆盖 clawd 自带的那份)—— supabase MCP 连它,`publish.sh` 也把它渲染进 s.yaml 给线上用,scaffold 渲项目 `.env` 还是它。项目 `.env` 被手改成别的地址,就会 **MCP 建表落一台 / server 通过 supabase-js 查另一台**,两边 PostgREST 的 schema cache 各自独立 → `PGRST205 schema cache 找不到表`。看到 PGRST 系列错码,先看项目 `.env` 的 `SUPABASE_URL` 是不是被人动过。
|
|
99
104
|
|
|
@@ -101,8 +106,8 @@
|
|
|
101
106
|
|
|
102
107
|
- 一律走 **FC(函数计算,serverless)**:免 Docker、闲置缩到 0 按量、适合海量小应用
|
|
103
108
|
- Node web 应用走 `custom.debian12` + 官方 Node 层 + `bootstrap` + `fc3-domain` 自定义域名(否则 fcapp.run 默认 URL 强制下载、浏览器不渲染)
|
|
104
|
-
- **全局唯一身份**:scaffold 时 `new-extension.sh` 生成 4 位 `SLUG` 并连同 `APP_NAME` 一起 bake 进 `ext.conf`,**锁定后稳定**。两种派生:DNS-safe `DEPLOY_NAME=${APP_NAME}-${SLUG}`(FC 函数 / 子域名 / GitHub 仓库名);SQL-safe `${APP_NAME}_${SLUG}_
|
|
105
|
-
- **自定义域名 = `<DEPLOY_NAME>.app.clawos.chat`**(HTTPS
|
|
109
|
+
- **全局唯一身份**:scaffold 时 `new-extension.sh` 生成 4 位 `SLUG` 并连同 `APP_NAME` 一起 bake 进 `ext.conf`,**锁定后稳定**。两种派生:DNS-safe `DEPLOY_NAME=${APP_NAME}-${SLUG}`(FC 函数 / 子域名 / GitHub 仓库名);SQL-safe `${APP_NAME}_${SLUG}_`(表前缀)。integration 环境在两者上各加短标签:`-int` / `int_`(环境名叫 integration / prod,进资源名的是 `int`)
|
|
110
|
+
- **自定义域名 = `<DEPLOY_NAME>.app.clawos.chat`**(HTTPS 直出),integration 就是 `<DEPLOY_NAME>-int.app.clawos.chat`——一眼看域名,带 `-int` 的是测试环境。根域一条泛解析 CNAME + 通配符证书都绑在 clawd 自己的阿里云账号上;使用者自带 key 时 `APP_DOMAIN_SUFFIX` 置空、走 FC 默认域名(`publish.sh` 两条分支同一个判据)。**不要**手动回退 `domainName: auto`——那派的 `xxx.fcapp.run` 阿里云 3 天回收
|
|
106
111
|
- 工具链 `aliyun` CLI + Serverless Devs(`s`)由 `deploy-kit/scripts/ensure-toolchain.sh` 在 publish / remove 前自动检测 + 安装,接管发布失败时**不必再排查「CLI 没装」**
|
|
107
112
|
- 实时推送用 Supabase Realtime(FC 不维持长连接)
|
|
108
113
|
|
|
@@ -121,7 +126,7 @@ source "$HOME/.clawd/personas/persona-app-builder/extension-kit/config.env"
|
|
|
121
126
|
source "$KIT_DIR/scripts/ensure-toolchain.sh" && ensure_toolchain
|
|
122
127
|
source "$KIT_DIR/scripts/ensure-credentials.sh" && ensure_credentials
|
|
123
128
|
aliyun_ak fc GET /2023-03-30/custom-domains # aliyun 一律走 aliyun_ak,别裸调 --mode AK
|
|
124
|
-
s deploy -y --access "$S_ACCESS_ALIAS"
|
|
129
|
+
s deploy -y -t s.int.yaml --access "$S_ACCESS_ALIAS" # s 一律显式带 profile + 指明哪个环境的 s 文件(s.int.yaml / s.prod.yaml)
|
|
125
130
|
```
|
|
126
131
|
|
|
127
132
|
**红线**:绝不写用户全局 `~/.s/access.yaml`(那里是用户自己账号的凭证,盖上去不可恢复);绝不 `echo` / 打印 / 写进日志或提交记录里暴露 SK;绝不把 `.secrets/` 拷进项目代码;shared `aliyun.env` 为空先停下提醒老板。
|
|
@@ -42,7 +42,7 @@ PR="$HOME/.clawd/personas/persona-app-builder"; DK="$HOME/.clawd/deploy-kit"
|
|
|
42
42
|
# 建表后把表名填进 myapp/ext.conf 的 SUPABASE_TABLES,清理时才删得掉
|
|
43
43
|
|
|
44
44
|
# 3) 一键发布到 FC,输出公网地址
|
|
45
|
-
"$DK/scripts/publish.sh" <projDir> "$PR"
|
|
45
|
+
"$DK/scripts/publish.sh" <projDir> "$PR" integration # 第 3 参 env:integration | prod(必填)
|
|
46
46
|
|
|
47
47
|
# 4) 不要了就一键清理(FC 资源 + 它的 Supabase 表)
|
|
48
48
|
"$DK/scripts/remove-extension.sh" <projDir> "$PR"
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
#
|
|
4
4
|
# 占位符约定:
|
|
5
5
|
# __APP_NAME__ = 用户给的项目名(描述/可读用)
|
|
6
|
-
# __DEPLOY_NAME__ = APP_NAME-<4位 slug
|
|
7
|
-
# 保证多用户/多副本同名时不撞 FC 函数 +
|
|
6
|
+
# __DEPLOY_NAME__ = APP_NAME-<4位 slug>[-int],FC functionName / 子域名 / s 项目名/资源 key 走这个
|
|
7
|
+
# 保证多用户/多副本同名时不撞 FC 函数 + 不撞域名;integration 环境带 -int 标签,
|
|
8
|
+
# prod 不带(老地址不变)
|
|
9
|
+
# __APP_ENV__ = integration | prod,server 里 process.env.APP_ENV
|
|
10
|
+
# __TABLE_PREFIX__ = APP_NAME_<slug>_[int_],server 只经 process.env.TABLE_PREFIX 取表名,两个环境两套表
|
|
8
11
|
#
|
|
9
12
|
# **自定义域名段不在本文件里**,在同目录的 s.yaml.domain.tmpl。publish.sh 只在
|
|
10
13
|
# APP_DOMAIN_SUFFIX 非空时把那一段接到本文件后面再渲染 —— 使用者自带阿里云 key 时
|
|
@@ -24,7 +27,7 @@ resources:
|
|
|
24
27
|
props:
|
|
25
28
|
region: ${vars.region}
|
|
26
29
|
functionName: __DEPLOY_NAME__
|
|
27
|
-
description: __APP_NAME__ extension (
|
|
30
|
+
description: __APP_NAME__ extension (__APP_ENV__)
|
|
28
31
|
runtime: __FC_RUNTIME__
|
|
29
32
|
logConfig: auto
|
|
30
33
|
layers:
|
|
@@ -44,6 +47,8 @@ resources:
|
|
|
44
47
|
APP_ENTRY: __FC_ENTRY__
|
|
45
48
|
SUPABASE_URL: __SUPABASE_URL__
|
|
46
49
|
SUPABASE_KEY: __SUPABASE_KEY__
|
|
50
|
+
APP_ENV: __APP_ENV__
|
|
51
|
+
TABLE_PREFIX: __TABLE_PREFIX__
|
|
47
52
|
triggers:
|
|
48
53
|
- triggerName: httpTrigger
|
|
49
54
|
triggerType: http
|
package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/ext.conf
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
CODE_DIR=./server
|
|
2
2
|
FC_ENTRY=dist/main.js
|
|
3
3
|
BUILD_CMD="cd web && pnpm install && pnpm build && cd ../server && pnpm install && pnpm build && mkdir -p dist/public && cp -R ../web/dist/. dist/public/"
|
|
4
|
-
# 本 extension 拥有的 Supabase
|
|
4
|
+
# 本 extension 拥有的 Supabase 表:写**基础名**(空格分隔,如 "visits notes"),不带前缀。
|
|
5
|
+
# 真实表名 = TABLE_PREFIX + 基础名,integration / prod 各一套(<APP_NAME>_<SLUG>_int_visits / <APP_NAME>_<SLUG>_visits);
|
|
6
|
+
# 建表改 server/db/schema.sql 后用 skill 的 render-sql.mjs 渲两套跑,removeProject 据此打两套 DROP。
|
|
5
7
|
SUPABASE_TABLES=""
|
|
@@ -5,4 +5,8 @@
|
|
|
5
5
|
# 要换实例改凭据表,别改这里的值——改了下次 scaffold 就被覆盖回去。
|
|
6
6
|
SUPABASE_URL=${SUPABASE_URL}
|
|
7
7
|
SUPABASE_KEY=${SUPABASE_KEY}
|
|
8
|
+
# 两个环境两套表:server 只通过 TABLE_PREFIX 取表名(`${process.env.TABLE_PREFIX}visits`),代码里不写死。
|
|
9
|
+
# 本地 pnpm dev 一律连 integration 那套(scaffold 渲成 <APP_NAME>_<SLUG>_int_);线上由 publish 按 env 注进 FC。
|
|
10
|
+
APP_ENV=${APP_ENV}
|
|
11
|
+
TABLE_PREFIX=${TABLE_PREFIX}
|
|
8
12
|
PORT=3000
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- 本项目 Supabase 表结构的唯一真源。
|
|
2
|
+
--
|
|
3
|
+
-- 表名一律写 __TABLE_PREFIX__<基础名>,不要写死前缀:integration / prod 两套表由同一份 DDL 渲出来,
|
|
4
|
+
-- node <skill>/scripts/render-sql.mjs --project <projectDir> --env integration # → <APP_NAME>_<SLUG>_int_visits
|
|
5
|
+
-- node <skill>/scripts/render-sql.mjs --project <projectDir> --env prod # → <APP_NAME>_<SLUG>_visits
|
|
6
|
+
-- 输出的 SQL 用 supabase MCP 的 execute_sql 跑(阿里云自建实例上 apply_migration 不可用)。
|
|
7
|
+
-- 先渲 integration 跑、在测试地址验过,再渲 prod 跑。语句请写成可重复执行的(IF NOT EXISTS / OR REPLACE)。
|
|
8
|
+
-- 建了表把基础名填进 ext.conf 的 SUPABASE_TABLES(如 "visits"),removeProject 才知道清哪两套。
|
|
9
|
+
--
|
|
10
|
+
-- 示例(按需改写;模板默认没有业务表,所以下面是注释):
|
|
11
|
+
-- create table if not exists public.__TABLE_PREFIX__visits (
|
|
12
|
+
-- id bigserial primary key,
|
|
13
|
+
-- path text not null,
|
|
14
|
+
-- created_at timestamptz not null default now()
|
|
15
|
+
-- );
|
|
@@ -2,6 +2,7 @@ import './polyfill';
|
|
|
2
2
|
import { NestFactory } from '@nestjs/core';
|
|
3
3
|
import { NestExpressApplication } from '@nestjs/platform-express';
|
|
4
4
|
import { AppModule } from './app.module';
|
|
5
|
+
import { TABLE_PREFIX } from './table-name';
|
|
5
6
|
import * as path from 'node:path';
|
|
6
7
|
|
|
7
8
|
// 两种跑法:
|
|
@@ -31,7 +32,8 @@ async function bootstrap() {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
await app.listen(port, '0.0.0.0');
|
|
34
|
-
|
|
35
|
+
// APP_ENV / TABLE_PREFIX 来自 .env(dev)或 FC 环境变量(线上);import table-name 时缺 TABLE_PREFIX 已经抛了
|
|
36
|
+
console.log(`server listening on :${port} (${isDev ? 'dev' : 'prod'}, APP_ENV=${process.env.APP_ENV ?? '?'}, tables ${TABLE_PREFIX}*)`);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
bootstrap();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Supabase 表名的唯一入口:表名 = TABLE_PREFIX + 基础名。
|
|
2
|
+
// 两个环境(integration / prod)两套表,靠前缀区分:本地 dev 由 server/.env 给 <APP_NAME>_<SLUG>_int_,
|
|
3
|
+
// 线上由 publish 按 env 注进 FC 的环境变量。代码里凡碰表的地方都走 tableName('visits'),不写死前缀。
|
|
4
|
+
// 表结构写在 server/db/schema.sql(同样用 __TABLE_PREFIX__ 占位),见那边注释。
|
|
5
|
+
|
|
6
|
+
const prefix = process.env.TABLE_PREFIX;
|
|
7
|
+
if (!prefix) {
|
|
8
|
+
// fail-fast:没前缀就会读写到别人的表 / 不存在的表,宁可启动即死
|
|
9
|
+
throw new Error(
|
|
10
|
+
'TABLE_PREFIX 未设:本地跑请检查 server/.env(createProject 时 scaffold 会渲进去);线上由 publish tool 按 env 注入',
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const TABLE_PREFIX: string = prefix;
|
|
15
|
+
|
|
16
|
+
/** `tableName('visits')` → `<APP_NAME>_<SLUG>_int_visits`(integration)/ `<APP_NAME>_<SLUG>_visits`(prod) */
|
|
17
|
+
export function tableName(base: string): string {
|
|
18
|
+
return `${TABLE_PREFIX}${base}`;
|
|
19
|
+
}
|
package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ description: Use when creating, reopening, changing, pushing to GitHub, publishi
|
|
|
19
19
|
node <skill>/scripts/list-projects.mjs --root <workdir>
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
输出 `{ projects: [{ name, projectDir, createdAt, repoUrl?, pushedCommit?,
|
|
22
|
+
输出 `{ projects: [{ name, projectDir, createdAt, repoUrl?, pushedCommit?, deployments?: { integration?: { url, publishedAt, publishedCommit }, prod?: {…} }, prodUrl? }] }`,按名字排序(`prodUrl` = `deployments.prod.url` 的兼容字段)。用户不记得名字就把列表念给他。
|
|
23
23
|
|
|
24
24
|
## 新建一个 app
|
|
25
25
|
|
|
@@ -28,7 +28,7 @@ node <skill>/scripts/list-projects.mjs --root <workdir>
|
|
|
28
28
|
3. `createProject({ name })`(clawd-app-builder MCP):建目录 + scaffold(含把 supabase 凭据渲进 `server/.env`)。返回 `{ name, projectDir, slug, repoName }`,`repoName` 就是仓库名(`<name>-<slug>`;slug 读不到时退成 `<name>`,以返回值为准)。失败已整个回滚,把原因告诉用户修好再来
|
|
29
29
|
4. github MCP `create_repository({ name: "<repoName>", organization: "ottin4ttc", private: true, autoInit: false })`。仓库地址是 `https://github.com/ottin4ttc/<repoName>`
|
|
30
30
|
5. 首次提交:按下面「提交到 GitHub」走一遍,`record-push.mjs` 带上 `--repo-url`
|
|
31
|
-
6. `cd <projectDir>`,`server/` 和 `web/` 各跑一次 `pnpm install
|
|
31
|
+
6. `cd <projectDir>`,`server/` 和 `web/` 各跑一次 `pnpm install`,开始开发;建表见「建表 / 改表」
|
|
32
32
|
7. 发布:见「发布」
|
|
33
33
|
|
|
34
34
|
## 提交到 GitHub(每完成一段就做一次)
|
|
@@ -53,13 +53,29 @@ node <skill>/scripts/list-projects.mjs --root <workdir>
|
|
|
53
53
|
- 目录里**有 `.git`** 的是 GitHub MCP 之前建的项目:按原来的本地 git 流程(`git add -A && git commit && git push`),`publish` 会拒脏工作区、记本地 HEAD;不要给它改成 `push_files`
|
|
54
54
|
- `repoUrl` 为空又没 `.git` 的是更老的项目:当普通目录改,改完直接 `publish`,不补仓库(用户要它进仓库会明说)
|
|
55
55
|
|
|
56
|
+
## 建表 / 改表
|
|
57
|
+
|
|
58
|
+
表两套(integration / prod),DDL 只有一份:
|
|
59
|
+
|
|
60
|
+
1. 表结构写在 `<projectDir>/server/db/schema.sql`,表名用 `__TABLE_PREFIX__visits` 占位,语句写成可重复执行的(`IF NOT EXISTS` / `OR REPLACE`)
|
|
61
|
+
2. 渲 integration 那套并执行:`node <skill>/scripts/render-sql.mjs --project <projectDir> --env integration` → 输出的 SQL 交给 supabase MCP `execute_sql`(阿里云自建实例上 `apply_migration` 不可用)
|
|
62
|
+
3. 在测试地址验过后再渲 prod:`--env prod` → `execute_sql`
|
|
63
|
+
4. 把**基础名**(`visits`,不带前缀)填进 `ext.conf` 的 `SUPABASE_TABLES`(空格分隔)
|
|
64
|
+
5. server 代码里表名只经 `tableName('visits')`(`server/src/table-name.ts`,读 env `TABLE_PREFIX`;本地 `.env` 是 `<name>_<slug>_int_`,线上由 publish 按 env 注入),不写死前缀
|
|
65
|
+
|
|
56
66
|
## 发布
|
|
57
67
|
|
|
58
|
-
先按「提交到 GitHub」把改动推上去并 `record-push.mjs
|
|
68
|
+
先按「提交到 GitHub」把改动推上去并 `record-push.mjs`,再分两步发(clawd-app-builder MCP,`env` 必填、没有默认):
|
|
69
|
+
|
|
70
|
+
1. `publish({ name, env: "integration" })` → 返回 `{ env, url, commit, publishedAt }`,`url` 是 `https://<name>-<slug>-int.app.clawos.chat`,回复里给用户可点的测试链接,等他看
|
|
71
|
+
2. 用户确认没问题 → `publish({ name, env: "prod" })` → `url` 是 `https://<name>-<slug>.app.clawos.chat`。**prod 只接受 integration 发过的同一个 commit**:报「还没发过 integration」说明这个 commit 没在测试地址验过(改了推了没再发 integration),回到第 1 步;没有绕过开关
|
|
72
|
+
3. 用户要改 → 改 → 推 → 记账 → 回到第 1 步(prod 不动)
|
|
73
|
+
|
|
74
|
+
**它会拒绝有没推改动的项目**(重算文件树哈希跟 `record-push` 记的比;有 `.git` 的老项目看 `git status`),报「有改动还没推到仓库」就回去推一遍再发。失败读项目目录下 `.publish.log`,修 → 推 → 记账 → 再 `publish`。**不要自己 bash 跑 `publish.sh`**:凭据只在 tool 进程的 env 里。
|
|
59
75
|
|
|
60
76
|
## 删一个 app
|
|
61
77
|
|
|
62
|
-
用户明确说删才动,先确认一次,再 `removeProject({ name })
|
|
78
|
+
用户明确说删才动,先确认一次,再 `removeProject({ name })`:拆 integration / prod 两个环境的 FC 函数 / 域名,打印两套表的 DROP(用 supabase MCP `execute_sql` 跑完才算删干净)。GitHub 仓库保留,用户想删自己去 GitHub 归档。
|
|
63
79
|
|
|
64
80
|
## GitHub 授权不可用时
|
|
65
81
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// 把项目目录里该进仓库的文件收成 GitHub MCP `push_files` 要的 `[{ path, content }]`。
|
|
3
3
|
//
|
|
4
|
-
// 过滤规则 = 项目根 `.gitignore`(模板那份:node_modules/ dist/ .env .publish.log s.yaml server/bootstrap .s/ …)
|
|
4
|
+
// 过滤规则 = 项目根 `.gitignore`(模板那份:node_modules/ dist/ .env .publish.log s.yaml s.*.yaml server/bootstrap .s/ …)
|
|
5
5
|
// + 永远排掉 `.git` 和 `.clawd-project.json`(插件的记账文件:publish / record-push 都会改它,进了仓库或哈希就会
|
|
6
|
-
// 「每推一次都变一次」,而且里面的
|
|
6
|
+
// 「每推一次都变一次」,而且里面的 deployments / pushedCommit 也不是源码)。**不要**在这里另写一份排除表:publish.sh 渲进项目目录的 s.int.yaml / s.prod.yaml 含 supabase key,
|
|
7
7
|
// 它进不进仓库只该由 .gitignore 一处决定。
|
|
8
8
|
// 二进制(前 8KB 含 NUL)和超过 512KB 的文件跳过并列进 `skipped`——push_files 只收文本,
|
|
9
9
|
// 而且每个字节都要经模型上下文走一遍;agent 看到 skipped 里有该进仓库的东西要自己判断。
|
|
@@ -24,7 +24,10 @@ for (const dirName of names) {
|
|
|
24
24
|
try {
|
|
25
25
|
const value = JSON.parse(await readFile(join(projectsRoot, dirName, '.clawd-project.json'), 'utf8'))
|
|
26
26
|
if (value?.name !== dirName || typeof value.createdAt !== 'string') continue
|
|
27
|
-
|
|
27
|
+
// 双环境后发布记录在 deployments.{integration,prod};prodUrl 保留作兼容字段 = prod 那份的 url。
|
|
28
|
+
// 插件还没读写过的老文件顶层就有 prodUrl,原样带出。
|
|
29
|
+
const prodUrl = value.deployments?.prod?.url
|
|
30
|
+
projects.push({ ...value, ...(prodUrl ? { prodUrl } : {}), projectDir: join(projectsRoot, dirName) })
|
|
28
31
|
} catch {
|
|
29
32
|
// User-owned directories without valid app-builder metadata are not projects.
|
|
30
33
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 把项目的 server/db/schema.sql 渲成某个环境的 SQL 打到 stdout:`__TABLE_PREFIX__` → 该环境的表前缀。
|
|
3
|
+
// 表结构只有这一份源,integration / prod 各渲一次再喂给 supabase MCP 的 execute_sql——两套表结构
|
|
4
|
+
// 不会漂移。前缀口径与 publish.sh / remove-extension.sh 相同:
|
|
5
|
+
// integration → <APP_NAME>_<SLUG>_int_ prod → <APP_NAME>_<SLUG>_
|
|
6
|
+
// ext.conf 没 SLUG 的老项目退成 <APP_NAME>_int_ / <APP_NAME>_。
|
|
7
|
+
import { readFile } from 'node:fs/promises'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
const ENVS = ['integration', 'prod']
|
|
11
|
+
const PLACEHOLDER = '__TABLE_PREFIX__'
|
|
12
|
+
|
|
13
|
+
function arg(name) {
|
|
14
|
+
const i = process.argv.indexOf(name)
|
|
15
|
+
return i >= 0 ? process.argv[i + 1] : undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const projectDir = arg('--project')
|
|
19
|
+
const env = arg('--env')
|
|
20
|
+
if (!projectDir || !env || !ENVS.includes(env)) {
|
|
21
|
+
process.stderr.write('usage: render-sql.mjs --project <projectDir> --env integration|prod\n')
|
|
22
|
+
process.exit(2)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fail(msg) {
|
|
26
|
+
process.stderr.write(`${msg}\n`)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** ext.conf 是 bash 赋值文件;这里只认 `KEY=value` / `KEY="value"` 单行,够 APP_NAME / SLUG 用。 */
|
|
31
|
+
function readConf(text) {
|
|
32
|
+
const out = {}
|
|
33
|
+
for (const line of text.split('\n')) {
|
|
34
|
+
const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line.trim())
|
|
35
|
+
if (!m) continue
|
|
36
|
+
out[m[1]] = m[2].replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1')
|
|
37
|
+
}
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const confPath = join(projectDir, 'ext.conf')
|
|
42
|
+
const schemaPath = join(projectDir, 'server', 'db', 'schema.sql')
|
|
43
|
+
|
|
44
|
+
let conf
|
|
45
|
+
try {
|
|
46
|
+
conf = readConf(await readFile(confPath, 'utf8'))
|
|
47
|
+
} catch (error) {
|
|
48
|
+
fail(`读不到 ${confPath}:${error?.message ?? error}`)
|
|
49
|
+
}
|
|
50
|
+
if (!conf.APP_NAME) fail(`${confPath} 里没有 APP_NAME,算不出表前缀`)
|
|
51
|
+
|
|
52
|
+
let schema
|
|
53
|
+
try {
|
|
54
|
+
schema = await readFile(schemaPath, 'utf8')
|
|
55
|
+
} catch (error) {
|
|
56
|
+
fail(`读不到 ${schemaPath}:${error?.message ?? error}(表结构应写在 server/db/schema.sql,表名用 ${PLACEHOLDER}visits 这种占位)`)
|
|
57
|
+
}
|
|
58
|
+
if (!schema.includes(PLACEHOLDER)) {
|
|
59
|
+
fail(`${schemaPath} 里没有 ${PLACEHOLDER}:表名不要写死,写成 ${PLACEHOLDER}<表名>,两个环境才能各渲各的`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const base = conf.SLUG ? `${conf.APP_NAME}_${conf.SLUG}_` : `${conf.APP_NAME}_`
|
|
63
|
+
const prefix = env === 'integration' ? `${base}int_` : base
|
|
64
|
+
process.stdout.write(schema.replaceAll(PLACEHOLDER, prefix))
|