@clawos-dev/clawd 0.2.515 → 0.2.517
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 +228 -284
- package/dist/cli.cjs +10871 -7110
- package/dist/dispatch/mcp-server.cjs +28 -4
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/SKILL.md +77 -0
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +116 -0
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +33 -0
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/record-push.mjs +49 -0
- package/dist/persona-defaults/persona-app-builder/.mcp.json +8 -1
- package/dist/persona-defaults/persona-app-builder/CLAUDE.md +27 -24
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/SKILL.md +77 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +116 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +33 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/record-push.mjs +49 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.mcp.json +10 -2
- package/dist/persona-defaults/persona-dataclaw-builder/CLAUDE.md +15 -13
- package/dist/share-ui/assets/{guest-CGrg1A9a.js → guest-fV-R3Vmz.js} +125 -125
- package/dist/share-ui/guest.html +1 -1
- package/package.json +3 -1
|
@@ -21429,6 +21429,12 @@ var DeployRepoSchema = external_exports.object({
|
|
|
21429
21429
|
/** 本地领先远端几个提交 */
|
|
21430
21430
|
ahead: external_exports.number()
|
|
21431
21431
|
}).strict();
|
|
21432
|
+
var DeployGithubStatusSchema = external_exports.discriminatedUnion("status", [
|
|
21433
|
+
external_exports.object({ status: external_exports.literal("authorized"), login: external_exports.string().min(1), source: external_exports.enum(["git", "file"]) }).strict(),
|
|
21434
|
+
external_exports.object({ status: external_exports.literal("missing") }).strict(),
|
|
21435
|
+
external_exports.object({ status: external_exports.literal("invalid"), message: external_exports.string().min(1) }).strict(),
|
|
21436
|
+
external_exports.object({ status: external_exports.literal("pending"), authUrl: external_exports.string().url() }).strict()
|
|
21437
|
+
]);
|
|
21432
21438
|
var DeploySecretPresenceSchema = external_exports.object({
|
|
21433
21439
|
env: external_exports.string().min(1),
|
|
21434
21440
|
setAt: external_exports.number().optional()
|
|
@@ -21458,13 +21464,15 @@ var DeployRunSchema = external_exports.object({
|
|
|
21458
21464
|
}).strict();
|
|
21459
21465
|
var DeployStateSchema = external_exports.object({
|
|
21460
21466
|
personaId: external_exports.string().min(1),
|
|
21467
|
+
/** 本机 GitHub 身份。daemon 每次现算;老 daemon 不给(可选只为兼容),UI 缺席时按未知处理 */
|
|
21468
|
+
github: DeployGithubStatusSchema.optional(),
|
|
21461
21469
|
repo: DeployRepoSchema.optional(),
|
|
21462
21470
|
file: DeployFileSchema.optional(),
|
|
21463
21471
|
scan: DeployScanSchema.optional(),
|
|
21464
21472
|
/** 没仓库时缺席(查不了) */
|
|
21465
21473
|
secrets: external_exports.array(DeploySecretPresenceSchema).optional(),
|
|
21466
21474
|
/**
|
|
21467
|
-
*
|
|
21475
|
+
* 最新那次流水线走到哪了。**缺席是常态**:没推过、没授权 GitHub、GitHub 查不到、仓库还没有 run,
|
|
21468
21476
|
* 都不该让整页打不开——同 `secrets` 的处理。
|
|
21469
21477
|
*/
|
|
21470
21478
|
run: DeployRunSchema.optional(),
|
|
@@ -21502,6 +21510,14 @@ var DeployPutSecretArgsSchema = external_exports.object({
|
|
|
21502
21510
|
// 环境变量名 → 值。key 必须是该 secret 声明过的 env 之一,handler 校验。
|
|
21503
21511
|
values: external_exports.record(external_exports.string(), external_exports.string())
|
|
21504
21512
|
}).strict();
|
|
21513
|
+
var DeployGithubAuthorizeArgsSchema = external_exports.object({
|
|
21514
|
+
personaId: external_exports.string().min(1),
|
|
21515
|
+
token: external_exports.string().min(1)
|
|
21516
|
+
}).strict();
|
|
21517
|
+
var DeployGithubOauthStartArgsSchema = external_exports.object({
|
|
21518
|
+
personaId: external_exports.string().min(1),
|
|
21519
|
+
returnTo: external_exports.string().url().optional()
|
|
21520
|
+
}).strict();
|
|
21505
21521
|
var DeployRerunJobArgsSchema = external_exports.object({
|
|
21506
21522
|
personaId: external_exports.string().min(1),
|
|
21507
21523
|
/** GitHub 的 job databaseId。**是数字**——字符串会在拼命令时静默出错 */
|
|
@@ -23869,7 +23885,7 @@ var METHOD_DOCS = {
|
|
|
23869
23885
|
},
|
|
23870
23886
|
// ---- deploy:*(persona 上云:persona 目录 = git 仓库工作树,发布 = 推送触发仓库流水线)----
|
|
23871
23887
|
"deploy:start": {
|
|
23872
|
-
summary: "\u5BF9\u67D0\u4E2A persona \u8D77\u4E00\u6B21\u4E0A\u4E91\u626B\u63CF\u3002**\u6CA1\u4ED3\u5E93\u5148\u5EFA\u4ED3**\
|
|
23888
|
+
summary: "\u5BF9\u67D0\u4E2A persona \u8D77\u4E00\u6B21\u4E0A\u4E91\u626B\u63CF\u3002**\u6CA1\u4ED3\u5E93\u5148\u5EFA\u4ED3**\uFF08GitHub REST \u5EFA\u5728\u7EC4\u7EC7\u4E0B\u3001\u79C1\u6709\uFF0Cgit \u63A8\u9996\u4E2A\u63D0\u4EA4\uFF1Btoken \u6765\u81EA\u672C\u673A git \u7684\u8EAB\u4EFD\uFF0C\u6CA1\u6388\u6743\u5148\u62D2\uFF09\uFF0C\u518D\u8D77 persona-butler \u7684\u626B\u63CF\u4F1A\u8BDD\uFF0C\u7ED3\u679C\u5199\u8FDB persona \u76EE\u5F55\u7684 .clawd/deploy.json \uFF08\u73AF\u5883\u6E05\u5355\uFF0C\u8FDB\u4ED3\u5E93\uFF09\u4E0E .clawd/deploy-scan.json\uFF08\u8BC1\u636E\uFF0C\u4E0D\u8FDB\u4ED3\u5E93\uFF09\u3002\u5DF2\u7ECF\u5728\u626B\u7684\u8BDD\u539F\u6837\u8FD4\u56DE\u3001\u4E0D\u8D77\u7B2C\u4E8C\u4E2A\u626B\u63CF\u4F1A\u8BDD\uFF1Bowner-only",
|
|
23873
23889
|
args: DeployPersonaArgsSchema
|
|
23874
23890
|
},
|
|
23875
23891
|
"deploy:get": {
|
|
@@ -23881,7 +23897,7 @@ var METHOD_DOCS = {
|
|
|
23881
23897
|
args: DeployPatchArgsSchema
|
|
23882
23898
|
},
|
|
23883
23899
|
"deploy:putSecret": {
|
|
23884
|
-
summary: "\u628A\u4E00\u6761\u51ED\u636E\u7684\u503C\u5199\u8FDB\u4ED3\u5E93\u7684 GitHub Secrets\
|
|
23900
|
+
summary: "\u628A\u4E00\u6761\u51ED\u636E\u7684\u503C\u5199\u8FDB\u4ED3\u5E93\u7684 GitHub Secrets\uFF08GitHub REST\uFF0C\u503C\u7528\u4ED3\u5E93\u516C\u94A5\u5C01\u7BB1\u540E\u4E0A\u4F20\uFF09\u3002**\u8FD9\u662F\u503C\u552F\u4E00\u7684\u5165\u53E3**\uFF1A\u5165\u53C2\u4E0D\u8FDB\u65E5\u5FD7\u3001\u54CD\u5E94\u4E0D\u56DE\u663E\u503C\u3001\u672C\u673A\u4E0D\u843D\u76D8\u3002env \u540D\u5FC5\u987B\u662F\u8BE5\u51ED\u636E\u5728\u6E05\u5355\u91CC\u58F0\u660E\u8FC7\u7684\uFF1Bowner-only",
|
|
23885
23901
|
args: DeployPutSecretArgsSchema
|
|
23886
23902
|
},
|
|
23887
23903
|
"deploy:reportFinding": {
|
|
@@ -23897,9 +23913,17 @@ var METHOD_DOCS = {
|
|
|
23897
23913
|
args: DeployPersonaArgsSchema
|
|
23898
23914
|
},
|
|
23899
23915
|
"deploy:rerunJob": {
|
|
23900
|
-
summary: "\u539F\u6837\u91CD\u8DD1\u6700\u65B0\u90A3\u6761\u6D41\u6C34\u7EBF\u91CC\u5931\u8D25\u7684\u67D0\u4E00\u6BB5\
|
|
23916
|
+
summary: "\u539F\u6837\u91CD\u8DD1\u6700\u65B0\u90A3\u6761\u6D41\u6C34\u7EBF\u91CC\u5931\u8D25\u7684\u67D0\u4E00\u6BB5\uFF08GitHub REST rerun job\uFF09\u3002**\u540C\u4E00\u4E2A commit\u3001\u540C\u4E00\u4EFD\u8F93\u5165**\uFF0C\u4E0D\u4EA7\u751F\u65B0\u63D0\u4EA4\u2014\u2014\u6539\u4E86 persona \u5185\u5BB9\u6216\u51ED\u636E\u8981\u8D70 deploy:release\u3002jobId \u5FC5\u987B\u5C5E\u4E8E\u6700\u65B0\u90A3\u6761 run\uFF1Bowner-only",
|
|
23901
23917
|
args: DeployRerunJobArgsSchema
|
|
23902
23918
|
},
|
|
23919
|
+
"deploy:githubAuthorize": {
|
|
23920
|
+
summary: "\u628A\u7528\u6237\u7C98\u7684 GitHub token \u5B58\u6210\u672C\u673A git \u7684\u8EAB\u4EFD\uFF1A\u5148 GET /user \u9A8C\u8BC1\uFF0C\u518D git credential approve \u4EA4\u7ED9 git \u7684 helper \u5B58\uFF08git \u6CA1\u914D helper \u65F6\u9000\u5230\u672C\u673A\u51ED\u636E\u8868\uFF09\u3002\u4E0A\u4E91\u7684\u5EFA\u4ED3 / \u63A8\u9001 / \u5199 Secrets / \u67E5\u6D41\u6C34\u7EBF\u90FD\u7528\u5B83\u3002\u5165\u53C2\u4E0D\u8FDB\u65E5\u5FD7\u3001\u54CD\u5E94\u4E0D\u56DE\u663E token\uFF1Bowner-only",
|
|
23921
|
+
args: DeployGithubAuthorizeArgsSchema
|
|
23922
|
+
},
|
|
23923
|
+
"deploy:githubOauthStart": {
|
|
23924
|
+
summary: "\u4E0A\u4E91\u9875\u300C\u7528 GitHub \u6388\u6743\u300D\uFF1Adaemon \u5E26\u672C\u673A\u767B\u5F55\u51ED\u8BC1\u5411\u4E2D\u5FC3\u8981\u4E00\u6761 GitHub OAuth \u6388\u6743\u94FE\u63A5\u5E76\u56DE\u5728 DeployState.github\uFF08pending\uFF09\u91CC\uFF0C\u9875\u9762\u6253\u5F00\u5B83\uFF1B\u7528\u6237\u5728 GitHub \u70B9\u540C\u610F\u540E\u56DE\u8DF3\u4E2D\u5FC3\uFF0C\u4E2D\u5FC3\u6362\u597D token \u5B58\u4E00\u4EFD\uFF0Cdaemon \u5728\u4E4B\u540E\u7684 deploy:get \u91CC\u9886\u56DE\u6765\u5B58\u8FDB\u672C\u673A git\u3002\u4E2D\u5FC3\u5DF2\u6709\u672C\u4EBA\u7684 token \u65F6\u4E0D\u8DF3 GitHub\uFF0C\u76F4\u63A5\u9886\uFF1B\u8981\u767B\u5F55\u98DE\u4E66\uFF1Bowner-only",
|
|
23925
|
+
args: DeployGithubOauthStartArgsSchema
|
|
23926
|
+
},
|
|
23903
23927
|
// ---- cloud:connectors:*(云 persona 卡片上的连接器;design 2026-09-11-mcp-connector §3.8)----
|
|
23904
23928
|
"cloud:connectors:list": {
|
|
23905
23929
|
summary: "\u4E91\u4E0A persona \u5361\u7247\u4E0A\u7684\u8FDE\u63A5\u5668\u533A\uFF1A\u8FD9\u4E2A persona \u58F0\u660E\u7684\u3001**\u8981\u4F7F\u7528\u8005\u672C\u4EBA\u6388\u6743\u7684**\u8FDE\u63A5\u5668\u5404\u81EA\u8FDE\u6CA1\u8FDE\u4E0A\uFF08connected / missing / reauth\uFF09\u3001\u6309\u94AE\u662F\u5173\u8054\uFF08OAuth\uFF0C\u5E26\u73B0\u7B97\u7684 authUrl\uFF09\u8FD8\u662F\u7C98\u8D34 API key\uFF08\u5E26\u8981\u586B\u7684\u53D8\u91CF\uFF09\u3002daemon \u62FF gateway \u767B\u8BB0\u884C + \u672C\u673A\u767B\u5F55\u51ED\u8BC1\u5411\u4E2D\u5FC3\u4EE3\u67E5\uFF0C\u51ED\u8BC1\u4E0D\u51FA ws\uFF1Bowner-only",
|
package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/SKILL.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-builder-projects
|
|
3
|
+
description: Use when creating, reopening, changing, pushing to GitHub, publishing, or removing an app managed by this persona — an app is projects/<name>/ in the user work dir plus a private ottin4ttc GitHub repo kept through the github MCP.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# app-builder-projects
|
|
7
|
+
|
|
8
|
+
分工:本 skill 定流程;`clawd-app-builder` MCP 管**必须持宿主凭据**的三件事(`createProject` 本地 scaffold 并渲 `server/.env`、`publish`、`removeProject`);`github` MCP 管仓库与提交;`supabase` MCP 管数据。脚本在本文件旁 `scripts/`,只碰当前用户工作区,不需要 `gh`、不需要本地 git。
|
|
9
|
+
|
|
10
|
+
## 路径
|
|
11
|
+
|
|
12
|
+
- `<workdir>`:连接上下文里「你的文件工作目录是 …」那个绝对路径。项目在 `<workdir>/projects/<name>/`
|
|
13
|
+
- `<skill>`:本文件所在目录(你的 cwd 就是 persona 目录,所以是 `<cwd>/.claude/skills/app-builder-projects`)
|
|
14
|
+
- 不从 `$HOME`、dispatcher 的「建议路径」或别的 persona 目录推路径;项目目录一律以 `createProject` / `list-projects.mjs` 返回的 `projectDir` 为准
|
|
15
|
+
|
|
16
|
+
## 列项目
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node <skill>/scripts/list-projects.mjs --root <workdir>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
输出 `{ projects: [{ name, projectDir, createdAt, repoUrl?, pushedCommit?, prodUrl?, publishedAt?, publishedCommit? }] }`,按名字排序。用户不记得名字就把列表念给他。
|
|
23
|
+
|
|
24
|
+
## 新建一个 app
|
|
25
|
+
|
|
26
|
+
1. 问用途(一句话),再问 kebab-case 的项目名(不自己起名)
|
|
27
|
+
2. `list-projects.mjs` 查重名;重名让用户换
|
|
28
|
+
3. `createProject({ name })`(clawd-app-builder MCP):建目录 + scaffold(含把 supabase 凭据渲进 `server/.env`)。返回 `{ name, projectDir, slug, repoName }`,`repoName` 就是仓库名(`<name>-<slug>`;slug 读不到时退成 `<name>`,以返回值为准)。失败已整个回滚,把原因告诉用户修好再来
|
|
29
|
+
4. github MCP `create_repository({ name: "<repoName>", organization: "ottin4ttc", private: true, autoInit: false })`。仓库地址是 `https://github.com/ottin4ttc/<repoName>`
|
|
30
|
+
5. 首次提交:按下面「提交到 GitHub」走一遍,`record-push.mjs` 带上 `--repo-url`
|
|
31
|
+
6. `cd <projectDir>`,`server/` 和 `web/` 各跑一次 `pnpm install`,开始开发;建表走 supabase MCP
|
|
32
|
+
7. 发布:见「发布」
|
|
33
|
+
|
|
34
|
+
## 提交到 GitHub(每完成一段就做一次)
|
|
35
|
+
|
|
36
|
+
1. `node <skill>/scripts/collect-files.mjs --project <projectDir>` → `{ files: [{ path, content }], skipped: [{ path, reason }], treeHash }`。**每次推之前都要跑**,`treeHash` 下一步要用。按项目根 `.gitignore` 过滤(`s.yaml` / `.env` / `node_modules` / `dist` 等不会出现),`.clawd-project.json` 也不进仓库,跳过二进制和超过 512KB 的文件。`skipped` 非空时看一眼里面有没有该进仓库的东西
|
|
37
|
+
2. 推:
|
|
38
|
+
- 首次、或改动面大:github MCP `push_files({ owner: "ottin4ttc", repo: "<repoName>", branch: "main", message: "<说清这次改了什么>", files })`,`files` 就是上一步的整份数组
|
|
39
|
+
- 只改了几个文件:`push_files` 只传改过的那几项(自己从 `files` 里挑),或单文件 `create_or_update_file`(已存在的文件要先 `get_file_contents` 拿 `sha`)
|
|
40
|
+
- 删文件:`delete_file`
|
|
41
|
+
- `content` 传原文,不要 base64
|
|
42
|
+
3. 记账:`push_files` 返回的 ref 里 `object.sha` 就是远端 commit:
|
|
43
|
+
```bash
|
|
44
|
+
node <skill>/scripts/record-push.mjs --project <projectDir> --commit <sha> --tree-hash <第 1 步的 treeHash> [--repo-url https://github.com/ottin4ttc/<repoName>]
|
|
45
|
+
```
|
|
46
|
+
首次带 `--repo-url`,之后带 `--commit` 和 `--tree-hash`。`publish` 会重算 `treeHash` 跟这里记的比:不一致就拒绝发布,所以推完记账之前别再改文件;改了就再推一次
|
|
47
|
+
4. 不跑 `gh`、不 `git push`、不往项目里 `git init`
|
|
48
|
+
|
|
49
|
+
## 改一个已有的 app
|
|
50
|
+
|
|
51
|
+
`list-projects.mjs` 找到 `projectDir` → `cd` 进去改 → 「提交到 GitHub」→ 「发布」。
|
|
52
|
+
|
|
53
|
+
- 目录里**有 `.git`** 的是 GitHub MCP 之前建的项目:按原来的本地 git 流程(`git add -A && git commit && git push`),`publish` 会拒脏工作区、记本地 HEAD;不要给它改成 `push_files`
|
|
54
|
+
- `repoUrl` 为空又没 `.git` 的是更老的项目:当普通目录改,改完直接 `publish`,不补仓库(用户要它进仓库会明说)
|
|
55
|
+
|
|
56
|
+
## 发布
|
|
57
|
+
|
|
58
|
+
先按「提交到 GitHub」把改动推上去并 `record-push.mjs`,再 `publish({ name })`(clawd-app-builder MCP)。**它会拒绝有没推改动的项目**(重算文件树哈希跟 `record-push` 记的比;有 `.git` 的老项目看 `git status`),报「有改动还没推到仓库」就回去推一遍再发。成功返回 `{ prodUrl, commit, publishedAt }`,回复里给用户可点的链接。失败读项目目录下 `.publish.log`,修 → 推 → 记账 → 再 `publish`。**不要自己 bash 跑 `publish.sh`**:凭据只在 tool 进程的 env 里。
|
|
59
|
+
|
|
60
|
+
## 删一个 app
|
|
61
|
+
|
|
62
|
+
用户明确说删才动,先确认一次,再 `removeProject({ name })`。GitHub 仓库保留,用户想删自己去 GitHub 归档。
|
|
63
|
+
|
|
64
|
+
## GitHub 授权不可用时
|
|
65
|
+
|
|
66
|
+
github MCP 报 401 / 未授权、或 tool 列表里根本没有 `create_repository` / `push_files`:
|
|
67
|
+
|
|
68
|
+
- **本机**:让用户把一个有 `repo` 权限、账号在 `ottin4ttc` 组织里的 GitHub PAT 写进 `~/.clawd/secrets/github.env`(一行 `GITHUB_TOKEN=…`),然后**新开一条对话**(凭据在会话起的时候渲进去)
|
|
69
|
+
- **云上**:让用户在 persona 卡片上「关联」GitHub(或「填写」token),完成后**新开一条对话**
|
|
70
|
+
|
|
71
|
+
不要猜别的凭据、不要退回 `gh`、不要把 token 写进任何文件或命令行。
|
|
72
|
+
|
|
73
|
+
## 红线
|
|
74
|
+
|
|
75
|
+
- 只在 `<workdir>/projects/` 下建项目;别的用户的工作区碰不得
|
|
76
|
+
- 凭据(supabase key、阿里云 AK、GitHub token)不进项目代码、不进提交、不进回复
|
|
77
|
+
- `createProject` / `publish` / `removeProject` 只走 clawd-app-builder MCP 的 tool,脚本不自己跑
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 把项目目录里该进仓库的文件收成 GitHub MCP `push_files` 要的 `[{ path, content }]`。
|
|
3
|
+
//
|
|
4
|
+
// 过滤规则 = 项目根 `.gitignore`(模板那份:node_modules/ dist/ .env .publish.log s.yaml server/bootstrap .s/ …)
|
|
5
|
+
// + 永远排掉 `.git` 和 `.clawd-project.json`(插件的记账文件:publish / record-push 都会改它,进了仓库或哈希就会
|
|
6
|
+
// 「每推一次都变一次」,而且里面的 prodUrl / pushedCommit 也不是源码)。**不要**在这里另写一份排除表:publish.sh 渲进项目目录的 s.yaml 含 supabase key,
|
|
7
|
+
// 它进不进仓库只该由 .gitignore 一处决定。
|
|
8
|
+
// 二进制(前 8KB 含 NUL)和超过 512KB 的文件跳过并列进 `skipped`——push_files 只收文本,
|
|
9
|
+
// 而且每个字节都要经模型上下文走一遍;agent 看到 skipped 里有该进仓库的东西要自己判断。
|
|
10
|
+
//
|
|
11
|
+
// 用法:collect-files.mjs --project <项目目录绝对路径>
|
|
12
|
+
// 输出:{ files: [{ path, content }], skipped: [{ path, reason }], treeHash }
|
|
13
|
+
// treeHash = files 按 path 排序后 sha256(path\0content\0…)。record-push.mjs 把它记进元数据,
|
|
14
|
+
// publish tool 再跑一次本脚本比对——不一致就是「改了没推」,拒绝发布。skipped 里的文件不进哈希。
|
|
15
|
+
import { createHash } from 'node:crypto'
|
|
16
|
+
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
17
|
+
import { join, relative, sep } from 'node:path'
|
|
18
|
+
|
|
19
|
+
const MAX_BYTES = 512 * 1024
|
|
20
|
+
const PROBE_BYTES = 8 * 1024
|
|
21
|
+
|
|
22
|
+
function arg(name) {
|
|
23
|
+
const i = process.argv.indexOf(name)
|
|
24
|
+
return i >= 0 ? process.argv[i + 1] : undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const root = arg('--project')
|
|
28
|
+
if (!root) {
|
|
29
|
+
process.stderr.write('usage: collect-files.mjs --project <project-dir>\n')
|
|
30
|
+
process.exit(2)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 极简 .gitignore:够模板那份用。支持
|
|
35
|
+
* - `name`:任一路径段等于 name(`*` / `?` 单段通配)
|
|
36
|
+
* - `name/`:只匹配目录(及其下所有文件)
|
|
37
|
+
* - 含 `/` 的:相对项目根的路径前缀(`server/bootstrap`、`/dist`)
|
|
38
|
+
* 不支持 `!` 取反和 `**`——模板没用到;真要用先加测试。
|
|
39
|
+
*/
|
|
40
|
+
function parseGitignore(text) {
|
|
41
|
+
const rules = []
|
|
42
|
+
for (const raw of text.split('\n')) {
|
|
43
|
+
let line = raw.trim()
|
|
44
|
+
if (!line || line.startsWith('#')) continue
|
|
45
|
+
const dirOnly = line.endsWith('/')
|
|
46
|
+
if (dirOnly) line = line.slice(0, -1)
|
|
47
|
+
const anchored = line.includes('/')
|
|
48
|
+
if (anchored && line.startsWith('/')) line = line.slice(1)
|
|
49
|
+
if (!line) continue
|
|
50
|
+
const re = new RegExp(`^${line.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]')}$`)
|
|
51
|
+
rules.push({ re, dirOnly, anchored })
|
|
52
|
+
}
|
|
53
|
+
return rules
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** rel 用 `/` 分隔;isDir 说 rel 本身是不是目录。 */
|
|
57
|
+
function ignored(rel, isDir, rules) {
|
|
58
|
+
const segments = rel.split('/')
|
|
59
|
+
for (const rule of rules) {
|
|
60
|
+
if (rule.anchored) {
|
|
61
|
+
// 前缀匹配:rel 等于它、或在它下面
|
|
62
|
+
for (let i = 1; i <= segments.length; i++) {
|
|
63
|
+
const head = segments.slice(0, i).join('/')
|
|
64
|
+
if (rule.re.test(head) && (!rule.dirOnly || isDir || i < segments.length)) return true
|
|
65
|
+
}
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
for (let i = 0; i < segments.length; i++) {
|
|
69
|
+
if (!rule.re.test(segments[i])) continue
|
|
70
|
+
// dirOnly:命中的这一段得是目录——要么它不是最后一段(后面还有东西),要么 rel 本身就是目录
|
|
71
|
+
if (!rule.dirOnly || i < segments.length - 1 || isDir) return true
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let rules = []
|
|
78
|
+
try {
|
|
79
|
+
rules = parseGitignore(await readFile(join(root, '.gitignore'), 'utf8'))
|
|
80
|
+
} catch {
|
|
81
|
+
// 没有 .gitignore:只排 .git
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const files = []
|
|
85
|
+
const skipped = []
|
|
86
|
+
|
|
87
|
+
async function walk(dir) {
|
|
88
|
+
for (const name of (await readdir(dir)).sort()) {
|
|
89
|
+
if (name === '.git' || (dir === root && name === '.clawd-project.json')) continue
|
|
90
|
+
const abs = join(dir, name)
|
|
91
|
+
const rel = relative(root, abs).split(sep).join('/')
|
|
92
|
+
const info = await stat(abs)
|
|
93
|
+
if (info.isDirectory()) {
|
|
94
|
+
if (!ignored(rel, true, rules)) await walk(abs)
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
if (!info.isFile() || ignored(rel, false, rules)) continue
|
|
98
|
+
if (info.size > MAX_BYTES) {
|
|
99
|
+
skipped.push({ path: rel, reason: `too large (${info.size} bytes > ${MAX_BYTES})` })
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
const buf = await readFile(abs)
|
|
103
|
+
if (buf.subarray(0, PROBE_BYTES).includes(0)) {
|
|
104
|
+
skipped.push({ path: rel, reason: 'binary' })
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
files.push({ path: rel, content: buf.toString('utf8') })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await walk(root)
|
|
112
|
+
// 按字节序排,不用 localeCompare:不同机器 locale 不同会让同一棵树排出两种顺序
|
|
113
|
+
files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
114
|
+
const hash = createHash('sha256')
|
|
115
|
+
for (const f of files) hash.update(f.path).update('\0').update(f.content).update('\0')
|
|
116
|
+
process.stdout.write(`${JSON.stringify({ files, skipped, treeHash: hash.digest('hex') })}\n`)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
function arg(name) {
|
|
6
|
+
const i = process.argv.indexOf(name)
|
|
7
|
+
return i >= 0 ? process.argv[i + 1] : undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const root = arg('--root')
|
|
11
|
+
if (!root) {
|
|
12
|
+
process.stderr.write('usage: list-projects.mjs --root <user-workdir>\n')
|
|
13
|
+
process.exit(2)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const projectsRoot = join(root, 'projects')
|
|
17
|
+
let names = []
|
|
18
|
+
try { names = await readdir(projectsRoot) } catch (error) {
|
|
19
|
+
if (error?.code !== 'ENOENT') throw error
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const projects = []
|
|
23
|
+
for (const dirName of names) {
|
|
24
|
+
try {
|
|
25
|
+
const value = JSON.parse(await readFile(join(projectsRoot, dirName, '.clawd-project.json'), 'utf8'))
|
|
26
|
+
if (value?.name !== dirName || typeof value.createdAt !== 'string') continue
|
|
27
|
+
projects.push({ ...value, projectDir: join(projectsRoot, dirName) })
|
|
28
|
+
} catch {
|
|
29
|
+
// User-owned directories without valid app-builder metadata are not projects.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
projects.sort((a, b) => a.name.localeCompare(b.name))
|
|
33
|
+
process.stdout.write(`${JSON.stringify({ projects })}\n`)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 经 GitHub MCP 建仓 / push 之后记账:把 repoUrl、远端 commit sha、push 那一刻的文件树哈希写进项目的 .clawd-project.json。
|
|
3
|
+
// publish tool 对没有本地 git 的项目:重算文件树哈希与 pushedTreeHash 比,不一致拒绝发布;一致就记 pushedCommit
|
|
4
|
+
// 当 publishedCommit(「线上 = 仓库里的哪个 commit」)。
|
|
5
|
+
//
|
|
6
|
+
// 用法:record-push.mjs --project <项目目录绝对路径> --commit <push_files 返回的 object.sha> --tree-hash <collect-files.mjs 输出的 treeHash> [--repo-url <https://github.com/ottin4ttc/...>]
|
|
7
|
+
// 输出:写回后的元数据 JSON
|
|
8
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
function arg(name) {
|
|
12
|
+
const i = process.argv.indexOf(name)
|
|
13
|
+
return i >= 0 ? process.argv[i + 1] : undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const project = arg('--project')
|
|
17
|
+
const commit = arg('--commit')
|
|
18
|
+
const treeHash = arg('--tree-hash')
|
|
19
|
+
const repoUrl = arg('--repo-url')
|
|
20
|
+
if (!project || !commit || !treeHash) {
|
|
21
|
+
process.stderr.write('usage: record-push.mjs --project <project-dir> --commit <sha> --tree-hash <sha256 from collect-files.mjs> [--repo-url <url>]\n')
|
|
22
|
+
process.exit(2)
|
|
23
|
+
}
|
|
24
|
+
if (!/^[0-9a-f]{64}$/i.test(treeHash)) {
|
|
25
|
+
process.stderr.write(`invalid tree hash "${treeHash}": expected the 64-hex treeHash printed by collect-files.mjs\n`)
|
|
26
|
+
process.exit(2)
|
|
27
|
+
}
|
|
28
|
+
if (!/^[0-9a-f]{7,64}$/i.test(commit)) {
|
|
29
|
+
process.stderr.write(`invalid commit sha "${commit}": expected hex, 7-64 chars (push_files result → object.sha)\n`)
|
|
30
|
+
process.exit(2)
|
|
31
|
+
}
|
|
32
|
+
if (repoUrl !== undefined && !/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+$/.test(repoUrl)) {
|
|
33
|
+
process.stderr.write(`invalid repo url "${repoUrl}": expected https://github.com/<owner>/<repo>\n`)
|
|
34
|
+
process.exit(2)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const metaPath = join(project, '.clawd-project.json')
|
|
38
|
+
let meta
|
|
39
|
+
try {
|
|
40
|
+
meta = JSON.parse(await readFile(metaPath, 'utf8'))
|
|
41
|
+
} catch (error) {
|
|
42
|
+
process.stderr.write(`not an app-builder project (cannot read ${metaPath}): ${error instanceof Error ? error.message : String(error)}\n`)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
if (repoUrl) meta.repoUrl = repoUrl
|
|
46
|
+
meta.pushedCommit = commit.toLowerCase()
|
|
47
|
+
meta.pushedTreeHash = treeHash.toLowerCase()
|
|
48
|
+
await writeFile(metaPath, `${JSON.stringify(meta, null, 2)}\n`)
|
|
49
|
+
process.stdout.write(`${JSON.stringify(meta)}\n`)
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "preinstall ship 进 daemon defaults,daemon 启动时 refreshDaemonManagedDirs 把这文件同步到 ~/.clawd*/personas/persona-app-builder/.mcp.json
|
|
2
|
+
"_comment": "preinstall ship 进 daemon defaults,daemon 启动时 refreshDaemonManagedDirs 把这文件同步到 ~/.clawd*/personas/persona-app-builder/.mcp.json。**这里只有声明,没有值**:`${VAR}` 占位由 daemon 在 spawn 时从凭据表渲进 per-server env / headers(见 daemon/src/persona/mcp-secret-render.ts),渲染产物是另一份 0600 文件、同名 server 压过本文件,所以 agent 在 persona 目录里读不到任何 key。supabase:用 @aliyun-rds/supabase-mcp-server Mode 2 单实例直连(不再用 aliyun AK),底下接阿里云托管 supabase。这三个 env 名不是我们发明的——该包的 commander option 默认值就取 process.env.SUPABASE_URL / SUPABASE_ANON_KEY / SUPABASE_SERVICE_ROLE_KEY,所以不再传 --supabase-* CLI 参数(参数会进 ps 输出,等于没藏)。**不要往这里加 --jwt-secret / SUPABASE_AUTH_JWT_SECRET**:jwt-secret 是该实例的 JWT 签发密钥,拿到它可以自签任意 role/任意 user 的 token(含 service_role),下发出去等于把 anon/service key 的轮换能力一起交出去;而 MCP server 里它唯一的用处是 verify_jwt_secret 这个自检工具报个 found/preview。github:GitHub 官方远程 MCP(spec 2026-09-14-app-builder-github-mcp-design),建仓 / 提交文件都由 agent 按 .claude/skills/app-builder-projects 走它,本机不再依赖 gh CLI。Authorization 走 `${GITHUB_TOKEN}` 占位而不是留空让 Claude Code 自己 OAuth:没占位的 http server 不进 daemon 的渲染产物,只剩 cwd 里这份项目级声明,daemon 起的 headless 会话过不了 Claude Code 的「项目 MCP 待批准」和交互式 OAuth 两关(2026-09-14 实测)。本机把 PAT 放 ~/.clawd/secrets/github.env(`GITHUB_TOKEN=…`);云上按连接器规则由部署者选「关联」(OAuth,中心注入 Bearer)或「填写」。值本身随 OTA 下发的边界与收敛方向见 doc/glossary/persona-cloud.md。",
|
|
3
3
|
"mcpServers": {
|
|
4
|
+
"github": {
|
|
5
|
+
"type": "http",
|
|
6
|
+
"url": "https://api.githubcopilot.com/mcp/",
|
|
7
|
+
"headers": {
|
|
8
|
+
"Authorization": "Bearer ${GITHUB_TOKEN}"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
4
11
|
"supabase": {
|
|
5
12
|
"type": "stdio",
|
|
6
13
|
"command": "npx",
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
你是 clawd 里的「全栈应用搭建师」。一句话定位:**从零做一个前后端项目,用 Supabase 当后端数据/服务层,用 deploy-kit 一键部署到阿里云 FC(serverless)让它公网跑起来。** 你是 clawd 的一个 persona,不是独立产品;你做出来的项目是独立的,但你这个角色活在 clawd 里。
|
|
4
4
|
|
|
5
|
-
## 一个 app =
|
|
5
|
+
## 一个 app = 用户工作区里的一个项目 + 一个 GitHub 仓库
|
|
6
6
|
|
|
7
|
-
核心形态(spec 2026-09-14-app-builder-git-project-design):
|
|
7
|
+
核心形态(spec 2026-09-14-app-builder-git-project-design + 2026-09-14-app-builder-github-mcp-design):
|
|
8
8
|
|
|
9
|
-
- **每个 app 就是当前用户工作区下的 `projects/<name
|
|
10
|
-
- **会话不绑项目**。一个会话里可以先改 A 再改 B;隔几天新开会话改旧 app 也行——用 `
|
|
9
|
+
- **每个 app 就是当前用户工作区下的 `projects/<name>/`**,配一个 GitHub 组织 `ottin4ttc` 下的私有仓库(名字 `<name>-<slug>`,slug 是 scaffold 时 bake 进 `ext.conf` 的 4 位散列,跟 FC 函数名 / 子域名同源)。仓库由你经 **`github` MCP** 建和提交,本机不需要 `gh`、项目目录里不需要 git
|
|
10
|
+
- **会话不绑项目**。一个会话里可以先改 A 再改 B;隔几天新开会话改旧 app 也行——用 `app-builder-projects` skill 的 `list-projects.mjs` 找到目录接着干
|
|
11
11
|
- **没有右栏预览、没有 dev server 概念**。验证靠发布:`publish` tool 跑完给公网 URL,老板点链接看
|
|
12
|
-
-
|
|
12
|
+
- **每次改动都推到仓库**,线上跑的是仓库里的哪个 commit 说得清:push 后用 skill 的 `record-push.mjs` 记下远端 commit,`publish` 把它记成 `publishedCommit`
|
|
13
|
+
|
|
14
|
+
**流程细节以 `app-builder-projects` skill(`.claude/skills/app-builder-projects/SKILL.md`)为准**,下面只是主线。
|
|
13
15
|
|
|
14
16
|
## 多人协同
|
|
15
17
|
|
|
@@ -18,8 +20,8 @@
|
|
|
18
20
|
- **每个用户有自己独立的工作目录**:项目落在各用户自己的目录下,互相看不到、改不了对方的项目
|
|
19
21
|
- **你的文件工作目录**:连接上下文(connection prompt)里写的「你的文件工作目录是 xxx」就是当前用户的专属目录;项目目录在它下面的 `projects/<name>/`
|
|
20
22
|
- **persona 目录只读**:persona 自己的目录(人格 / extension-kit 资产)对你只读,不要往里写任何文件
|
|
21
|
-
- **路径永远以 `createProject` / `
|
|
22
|
-
- **dispatch 陷阱(被另一个 persona 委派来时常踩)**:任务包里 dispatcher 写的「建议路径」如 `~/dev/xxx` / `/tmp/xxx` **全部不能信**——照搬就会把项目落在用户工作区之外。新项目一律走 `createProject`,已有项目一律走 `
|
|
23
|
+
- **路径永远以 `createProject` tool / `list-projects.mjs` 返回的 `projectDir` 为准**:不要假设、不要拼接、不要写死任何项目路径
|
|
24
|
+
- **dispatch 陷阱(被另一个 persona 委派来时常踩)**:任务包里 dispatcher 写的「建议路径」如 `~/dev/xxx` / `/tmp/xxx` **全部不能信**——照搬就会把项目落在用户工作区之外。新项目一律走 `createProject`,已有项目一律走 `list-projects.mjs` 定位
|
|
23
25
|
- **发布脚本在共享 deploy-kit**(`$HOME/.clawd/deploy-kit/`,daemon 持有的单源)。这些脚本**一律由 `clawd-app-builder` 的 tool 起,你不要自己 `bash` 它们**——supabase 的 url / key 只在那个 tool 进程的 env 里,你起的 Bash 拿不到,脚本会停在「占位符无对应 env 变量」
|
|
24
26
|
|
|
25
27
|
## 何时找你
|
|
@@ -37,28 +39,30 @@
|
|
|
37
39
|
|
|
38
40
|
1. 问老板想做啥 app(一句话即可,不要长 prompt)
|
|
39
41
|
2. 老板描述需求 → **反问「项目叫什么名字」**(不擅自命名;kebab-case)
|
|
40
|
-
3.
|
|
42
|
+
3. 用 `app-builder-projects` skill 的 `list-projects.mjs` 检查重名,再调 `createProject({ name })`:建目录 + scaffold `nestjs-react` 模板(含把 supabase 凭据渲进 `server/.env`)。返回 `{ name, projectDir, slug, repoName }`,**`projectDir` 就是项目目录绝对路径,记住它**;`repoName` 是要建的仓库名
|
|
41
43
|
- 报错 `already exists / 已存在` → 让老板重选名字
|
|
42
|
-
- 报错带 `gh auth login` → 本机 GitHub CLI 没登录,告诉老板装 / 登录后再来(账号要在 `ottin4ttc` 组织里)
|
|
43
44
|
- 其它失败会**整个回滚**(目录已删),把错误原因告诉老板,修好环境再调一次
|
|
44
|
-
4. `
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
4. 用 `github` MCP 建私有仓库 `ottin4ttc/<repoName>`(`create_repository`),把 skill 的 `collect-files.mjs` 收出来的文件 `push_files` 上去,再用 `record-push.mjs` 记下 `repoUrl` 和远端 commit。**不调 `gh`、不 `git push`**
|
|
46
|
+
- github MCP 报 401 / 未授权、或根本没有这些 tool → 本机让老板把 GitHub PAT 写进 `~/.clawd/secrets/github.env`(`GITHUB_TOKEN=…`),云上让老板在 persona 卡片关联 GitHub;两种都要**新开一条对话**才生效(账号要在 `ottin4ttc` 组织里)
|
|
47
|
+
5. `cd <projectDir>`,装依赖:`server/` 和 `web/` 各跑一次 `pnpm install`
|
|
48
|
+
6. 开发:写业务、用 supabase MCP 建表(见下文「后端」)。复杂任务走 superpowers 节奏,简单任务 TodoWrite 跟踪(分级见下)
|
|
49
|
+
7. **每完成一段就按 skill 推一次**:`collect-files.mjs` → `push_files`(或单文件 `create_or_update_file` / `delete_file`)→ `record-push.mjs`
|
|
50
|
+
8. 发布:调 `publish({ name })`,拿到 `prodUrl` 后在回复里给老板可点的链接
|
|
51
|
+
9. 老板看链接提意见 → 回到第 6 步,同一个会话一直往下聊
|
|
49
52
|
|
|
50
53
|
### 改一个已有的 app
|
|
51
54
|
|
|
52
|
-
1.
|
|
53
|
-
2. `cd <projectDir>`
|
|
54
|
-
3.
|
|
55
|
+
1. 运行 `list-projects.mjs`,按名字找到它的 `projectDir`(老板不记得名字就把列表念给他)
|
|
56
|
+
2. `cd <projectDir>` 直接改;改完按 skill 推到仓库 + `record-push.mjs`;`publish` 给链接
|
|
57
|
+
3. 目录里**有 `.git`** 的是 GitHub MCP 之前建的项目:照原来的本地 git 流程(`git add -A && git commit && git push`),`publish` 会拒脏工作区、记本地 HEAD,不要改成 `push_files`
|
|
58
|
+
4. `repoUrl` 为空又没 `.git` 的是更老的项目。**不要给它补仓库**:就当普通目录改,改完直接 `publish`(tool 不记 commit)。老板要它进仓库会明说
|
|
55
59
|
|
|
56
60
|
### 发布
|
|
57
61
|
|
|
58
62
|
**只走 `publish` tool,不手跑 `publish.sh`。** 它同步跑完 build → deploy → verify,成功返回 `{ prodUrl, commit, publishedAt }`;失败返回失败阶段 + stderr 末尾 + `.publish.log` 路径。
|
|
59
63
|
|
|
60
|
-
-
|
|
61
|
-
- 失败时:读项目目录下的 `.publish.log` 完整日志 → 定位并修(改源码 / 改 `ext.conf` / 看凭证)→
|
|
64
|
+
- **发布前先把改动推到仓库并 `record-push.mjs`**:`publish` 会重算文件树哈希跟记账的比,有没推的改动直接拒(有 `.git` 的老项目看 `git status`);线上必须对应仓库里的一个 commit
|
|
65
|
+
- 失败时:读项目目录下的 `.publish.log` 完整日志 → 定位并修(改源码 / 改 `ext.conf` / 看凭证)→ 推 → 再调一次 `publish`,直到拿到公网 URL
|
|
62
66
|
- 看到 `❌ s.yaml.tmpl 占位符 __SUPABASE_URL__ 无对应 env 变量` 这类报错,**不要去找 key 手动 export、也不要往 `config.env` 里写值**——那是你自己 bash 跑了脚本,改回调 tool 就好
|
|
63
67
|
- 一个环境:公网地址固定是 `https://<name>-<slug>.app.clawos.chat`,再发就是覆盖
|
|
64
68
|
|
|
@@ -72,16 +76,15 @@
|
|
|
72
76
|
|
|
73
77
|
## 可用 tool 完整清单(`clawd-app-builder` MCP)
|
|
74
78
|
|
|
75
|
-
|
|
79
|
+
**宿主侧只有以下 3 个 tool**——都是必须持宿主凭据的事。它们是这条流水线自己的后端,不是通用 daemon RPC——**别经 `clawd-rpc` 的 `call` 去找 `appBuilder:*`**。列项目走 skill 脚本;GitHub 建仓 / 提交走 `github` MCP,不依赖本机 `gh` CLI。
|
|
76
80
|
|
|
77
81
|
| tool | 入参 | 用途 |
|
|
78
82
|
|---|---|---|
|
|
79
|
-
| `createProject` | `{name}` | 建目录 + scaffold
|
|
80
|
-
| `listProjects` | `{}` | 列当前用户的项目:name / projectDir / repoUrl / prodUrl / publishedAt / publishedCommit |
|
|
83
|
+
| `createProject` | `{name}` | 建目录 + scaffold(渲 `server/.env`)。返回 `{ name, projectDir, slug, repoName }`,不碰 GitHub |
|
|
81
84
|
| `publish` | `{name}` | 跑发布流水线(build → deploy → verify),同步等结束,返回公网 URL 或失败详情 |
|
|
82
85
|
| `removeProject` | `{name}` | 拆掉已发布的资源 + 删本地目录(仓库保留)。老板确认后才调 |
|
|
83
86
|
|
|
84
|
-
**红线:不在此列就不要瞎试。** 不存在 `startDevServer` / `reportStage` / `setProdUrl` / `updateProjectPort` 这些老 tool,也没有任何 `appBuilder:*` RPC。遇到清单外的能力需求 → 跟老板说,让他评估要不要加。
|
|
87
|
+
**红线:不在此列就不要瞎试。** 不存在 `listProjects` / `startDevServer` / `reportStage` / `setProdUrl` / `updateProjectPort` 这些老 tool,也没有任何 `appBuilder:*` RPC。遇到清单外的能力需求 → 跟老板说,让他评估要不要加。
|
|
85
88
|
|
|
86
89
|
## 后端:supabase MCP
|
|
87
90
|
|
|
@@ -145,7 +148,7 @@ s deploy -y --access "$S_ACCESS_ALIAS" # s 一律显式带 profile
|
|
|
145
148
|
- 在 supabase MCP 上 drop / alter clawos 已有的表,或在不确认归属时改共享库结构
|
|
146
149
|
- 不打招呼就在阿里云上新建 / 删除有成本或不可逆的资源(FC 函数、自定义域名、OSS 等);`removeProject` 前必须确认
|
|
147
150
|
- 自己 `bash` 跑 `publish.sh` / `new-extension.sh` / `remove-extension.sh`——只走 tool
|
|
148
|
-
- 把密钥(Supabase token、阿里云 AK/SK)写进项目代码或提交记录;`s.yaml` / `.env` / `.publish.log` 已在 `.gitignore
|
|
151
|
+
- 把密钥(Supabase token、阿里云 AK/SK、GitHub token)写进项目代码或提交记录;`s.yaml` / `.env` / `.publish.log` 已在 `.gitignore`,`collect-files.mjs` 就按它过滤,别去掉、也别绕过脚本手拼 `push_files` 的文件清单
|
|
149
152
|
- `--no-verify` 绕 hook / 禁用测试代替修测试 / 提交编译不过的代码 / 凭假设动手
|
|
150
153
|
- 在用户工作区之外建项目目录
|
|
151
154
|
|
package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/SKILL.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-builder-projects
|
|
3
|
+
description: Use when creating, reopening, changing, pushing to GitHub, publishing, or removing an app managed by this persona — an app is projects/<name>/ in the user work dir plus a private ottin4ttc GitHub repo kept through the github MCP.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# app-builder-projects
|
|
7
|
+
|
|
8
|
+
分工:本 skill 定流程;`clawd-app-builder` MCP 管**必须持宿主凭据**的三件事(`createProject` 本地 scaffold 并渲 `server/.env`、`publish`、`removeProject`);`github` MCP 管仓库与提交;`supabase` MCP 管数据。脚本在本文件旁 `scripts/`,只碰当前用户工作区,不需要 `gh`、不需要本地 git。
|
|
9
|
+
|
|
10
|
+
## 路径
|
|
11
|
+
|
|
12
|
+
- `<workdir>`:连接上下文里「你的文件工作目录是 …」那个绝对路径。项目在 `<workdir>/projects/<name>/`
|
|
13
|
+
- `<skill>`:本文件所在目录(你的 cwd 就是 persona 目录,所以是 `<cwd>/.claude/skills/app-builder-projects`)
|
|
14
|
+
- 不从 `$HOME`、dispatcher 的「建议路径」或别的 persona 目录推路径;项目目录一律以 `createProject` / `list-projects.mjs` 返回的 `projectDir` 为准
|
|
15
|
+
|
|
16
|
+
## 列项目
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node <skill>/scripts/list-projects.mjs --root <workdir>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
输出 `{ projects: [{ name, projectDir, createdAt, repoUrl?, pushedCommit?, prodUrl?, publishedAt?, publishedCommit? }] }`,按名字排序。用户不记得名字就把列表念给他。
|
|
23
|
+
|
|
24
|
+
## 新建一个 app
|
|
25
|
+
|
|
26
|
+
1. 问用途(一句话),再问 kebab-case 的项目名(不自己起名)
|
|
27
|
+
2. `list-projects.mjs` 查重名;重名让用户换
|
|
28
|
+
3. `createProject({ name })`(clawd-app-builder MCP):建目录 + scaffold(含把 supabase 凭据渲进 `server/.env`)。返回 `{ name, projectDir, slug, repoName }`,`repoName` 就是仓库名(`<name>-<slug>`;slug 读不到时退成 `<name>`,以返回值为准)。失败已整个回滚,把原因告诉用户修好再来
|
|
29
|
+
4. github MCP `create_repository({ name: "<repoName>", organization: "ottin4ttc", private: true, autoInit: false })`。仓库地址是 `https://github.com/ottin4ttc/<repoName>`
|
|
30
|
+
5. 首次提交:按下面「提交到 GitHub」走一遍,`record-push.mjs` 带上 `--repo-url`
|
|
31
|
+
6. `cd <projectDir>`,`server/` 和 `web/` 各跑一次 `pnpm install`,开始开发;建表走 supabase MCP
|
|
32
|
+
7. 发布:见「发布」
|
|
33
|
+
|
|
34
|
+
## 提交到 GitHub(每完成一段就做一次)
|
|
35
|
+
|
|
36
|
+
1. `node <skill>/scripts/collect-files.mjs --project <projectDir>` → `{ files: [{ path, content }], skipped: [{ path, reason }], treeHash }`。**每次推之前都要跑**,`treeHash` 下一步要用。按项目根 `.gitignore` 过滤(`s.yaml` / `.env` / `node_modules` / `dist` 等不会出现),`.clawd-project.json` 也不进仓库,跳过二进制和超过 512KB 的文件。`skipped` 非空时看一眼里面有没有该进仓库的东西
|
|
37
|
+
2. 推:
|
|
38
|
+
- 首次、或改动面大:github MCP `push_files({ owner: "ottin4ttc", repo: "<repoName>", branch: "main", message: "<说清这次改了什么>", files })`,`files` 就是上一步的整份数组
|
|
39
|
+
- 只改了几个文件:`push_files` 只传改过的那几项(自己从 `files` 里挑),或单文件 `create_or_update_file`(已存在的文件要先 `get_file_contents` 拿 `sha`)
|
|
40
|
+
- 删文件:`delete_file`
|
|
41
|
+
- `content` 传原文,不要 base64
|
|
42
|
+
3. 记账:`push_files` 返回的 ref 里 `object.sha` 就是远端 commit:
|
|
43
|
+
```bash
|
|
44
|
+
node <skill>/scripts/record-push.mjs --project <projectDir> --commit <sha> --tree-hash <第 1 步的 treeHash> [--repo-url https://github.com/ottin4ttc/<repoName>]
|
|
45
|
+
```
|
|
46
|
+
首次带 `--repo-url`,之后带 `--commit` 和 `--tree-hash`。`publish` 会重算 `treeHash` 跟这里记的比:不一致就拒绝发布,所以推完记账之前别再改文件;改了就再推一次
|
|
47
|
+
4. 不跑 `gh`、不 `git push`、不往项目里 `git init`
|
|
48
|
+
|
|
49
|
+
## 改一个已有的 app
|
|
50
|
+
|
|
51
|
+
`list-projects.mjs` 找到 `projectDir` → `cd` 进去改 → 「提交到 GitHub」→ 「发布」。
|
|
52
|
+
|
|
53
|
+
- 目录里**有 `.git`** 的是 GitHub MCP 之前建的项目:按原来的本地 git 流程(`git add -A && git commit && git push`),`publish` 会拒脏工作区、记本地 HEAD;不要给它改成 `push_files`
|
|
54
|
+
- `repoUrl` 为空又没 `.git` 的是更老的项目:当普通目录改,改完直接 `publish`,不补仓库(用户要它进仓库会明说)
|
|
55
|
+
|
|
56
|
+
## 发布
|
|
57
|
+
|
|
58
|
+
先按「提交到 GitHub」把改动推上去并 `record-push.mjs`,再 `publish({ name })`(clawd-app-builder MCP)。**它会拒绝有没推改动的项目**(重算文件树哈希跟 `record-push` 记的比;有 `.git` 的老项目看 `git status`),报「有改动还没推到仓库」就回去推一遍再发。成功返回 `{ prodUrl, commit, publishedAt }`,回复里给用户可点的链接。失败读项目目录下 `.publish.log`,修 → 推 → 记账 → 再 `publish`。**不要自己 bash 跑 `publish.sh`**:凭据只在 tool 进程的 env 里。
|
|
59
|
+
|
|
60
|
+
## 删一个 app
|
|
61
|
+
|
|
62
|
+
用户明确说删才动,先确认一次,再 `removeProject({ name })`。GitHub 仓库保留,用户想删自己去 GitHub 归档。
|
|
63
|
+
|
|
64
|
+
## GitHub 授权不可用时
|
|
65
|
+
|
|
66
|
+
github MCP 报 401 / 未授权、或 tool 列表里根本没有 `create_repository` / `push_files`:
|
|
67
|
+
|
|
68
|
+
- **本机**:让用户把一个有 `repo` 权限、账号在 `ottin4ttc` 组织里的 GitHub PAT 写进 `~/.clawd/secrets/github.env`(一行 `GITHUB_TOKEN=…`),然后**新开一条对话**(凭据在会话起的时候渲进去)
|
|
69
|
+
- **云上**:让用户在 persona 卡片上「关联」GitHub(或「填写」token),完成后**新开一条对话**
|
|
70
|
+
|
|
71
|
+
不要猜别的凭据、不要退回 `gh`、不要把 token 写进任何文件或命令行。
|
|
72
|
+
|
|
73
|
+
## 红线
|
|
74
|
+
|
|
75
|
+
- 只在 `<workdir>/projects/` 下建项目;别的用户的工作区碰不得
|
|
76
|
+
- 凭据(supabase key、阿里云 AK、GitHub token)不进项目代码、不进提交、不进回复
|
|
77
|
+
- `createProject` / `publish` / `removeProject` 只走 clawd-app-builder MCP 的 tool,脚本不自己跑
|