@clawos-dev/clawd 0.2.547 → 0.2.549
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 +40 -37
- package/dist/cli.cjs +488 -657
- package/dist/dispatch/mcp-server.cjs +5 -2
- package/dist/persona-defaults/persona-app-builder/.mcp.json +17 -2
- package/dist/persona-defaults/persona-app-builder/CLAUDE.md +8 -9
- package/dist/persona-defaults/persona-app-builder/extension-kit/README.md +6 -7
- package/dist/persona-defaults/persona-app-builder/extension-kit/contract/s.yaml.tmpl +3 -2
- package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-credentials.sh +96 -0
- package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/publish.sh +3 -3
- package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/remove-extension.sh +2 -2
- package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/mcp-server.cjs +25400 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.mcp.json +14 -1
- package/dist/persona-defaults/persona-dataclaw-builder/CLAUDE.md +1 -1
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/README.md +6 -7
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/config.env +1 -1
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/contract/s.yaml.tmpl +3 -2
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/contract/bootstrap +22 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-credentials.sh +96 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-toolchain.sh +65 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/new-extension.sh +71 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/publish.sh +205 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/remove-extension.sh +89 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/verify.sh +49 -0
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/mcp-server.cjs +25400 -0
- package/dist/share-md-viewer-error.html +1 -1
- package/dist/share-md-viewer.html +1 -1
- package/dist/share-ui/assets/{guest-LyTPPmtK.js → guest-BG2kYWjt.js} +132 -132
- package/dist/share-ui/assets/guest-BHSs9s97.css +32 -0
- package/dist/share-ui/guest.html +2 -2
- package/package.json +1 -1
- package/dist/deploy-kit/.secrets/aliyun.env.example +0 -7
- package/dist/deploy-kit/.secrets/aliyun.env.local.example +0 -12
- package/dist/deploy-kit/scripts/ensure-credentials.sh +0 -111
- package/dist/deploy-kit/scripts/ensure-credentials.spec.ts +0 -438
- package/dist/deploy-kit/scripts/publish.spec.ts +0 -278
- package/dist/share-ui/assets/guest-DVW7Ohf0.css +0 -32
- /package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/contract/bootstrap +0 -0
- /package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/ensure-toolchain.sh +0 -0
- /package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/new-extension.sh +0 -0
- /package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/verify.sh +0 -0
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { execFileSync } from 'node:child_process'
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, statSync, cpSync, rmSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { tmpdir } from 'node:os'
|
|
6
|
-
|
|
7
|
-
// ensure-credentials.sh 的契约测试。核心不变量:**发布/清理全程不碰用户全局 ~/.s**。
|
|
8
|
-
// 老实现 `cat > ~/.s/access.yaml` 会把用户自己的 Serverless Devs default 凭证顶掉且不可恢复
|
|
9
|
-
// (所有装了 clawd 的用户点一次「发布上线」就中招),这些用例就是钉死那个回归。
|
|
10
|
-
//
|
|
11
|
-
// 真脚本 + 真 bash 跑,只把两个外部 CLI(aliyun / s)换成记录 argv 的 shim:
|
|
12
|
-
// 断言的是脚本自己的行为(写哪、写什么、传什么 flag),没有 mock 掉被测逻辑。
|
|
13
|
-
|
|
14
|
-
const SCRIPTS_DIR = __dirname
|
|
15
|
-
|
|
16
|
-
interface Sandbox {
|
|
17
|
-
root: string
|
|
18
|
-
kit: string
|
|
19
|
-
home: string
|
|
20
|
-
bin: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let box: Sandbox
|
|
24
|
-
|
|
25
|
-
/** 用户自己的 ~/.s/access.yaml —— 全程必须一个字节都不变。 */
|
|
26
|
-
const USER_ACCESS_YAML = `default:
|
|
27
|
-
AccountID: '1999888877776666'
|
|
28
|
-
AccessKeyID: LTAI-USER-OWN-KEY
|
|
29
|
-
AccessKeySecret: user-own-secret
|
|
30
|
-
`
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* aliyun CLI shim —— **照真 CLI 的凭证优先级建模**(实测 aliyun 3.3.17,`--mode AK`):
|
|
34
|
-
*
|
|
35
|
-
* - `--access-key-id` / `--access-key-secret` / `--region` 三个 **flag 缺一不可**;
|
|
36
|
-
* - `ALIBABA_CLOUD_*` 环境变量在这个模式下**根本不被消费**——没有 `~/.aliyun/config.json`
|
|
37
|
-
* 的干净机器上只会得到 `region can't be empty`(rc=3),有 profile 的机器上则**静默走用户
|
|
38
|
-
* 自己的 profile**(等于拿用户身份部署)。
|
|
39
|
-
*
|
|
40
|
-
* 所以 shim 只认 flag、不认 env:脚本要是退回去依赖 env 优先级,这里当场红。
|
|
41
|
-
*/
|
|
42
|
-
function writeAliyunShim(bin: string, accountId = '5566778899001122', argvLog?: string): void {
|
|
43
|
-
writeFileSync(
|
|
44
|
-
join(bin, 'aliyun'),
|
|
45
|
-
`#!/usr/bin/env bash
|
|
46
|
-
${argvLog ? `printf '%s\\n' "$*" >> "${argvLog}"` : ''}
|
|
47
|
-
argv=" $* "
|
|
48
|
-
for f in --access-key-id --access-key-secret --region; do
|
|
49
|
-
case "$argv" in
|
|
50
|
-
*" $f "*) ;;
|
|
51
|
-
*) echo "ERROR: region can't be empty" >&2; exit 3 ;;
|
|
52
|
-
esac
|
|
53
|
-
done
|
|
54
|
-
case "$argv" in
|
|
55
|
-
*" sts "*) printf '{"AccountId":"${accountId}","Arn":"acs:ram::${accountId}:user/clawd-fc-developer"}\\n' ;;
|
|
56
|
-
*) printf '{"customDomains":[]}\\n' ;;
|
|
57
|
-
esac
|
|
58
|
-
`,
|
|
59
|
-
{ mode: 0o755 },
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** s shim:把每次调用的 argv 追加到 $S_ARGV_LOG,供断言 --access 传对了没。 */
|
|
64
|
-
function writeSShim(bin: string, log: string): void {
|
|
65
|
-
writeFileSync(
|
|
66
|
-
join(bin, 's'),
|
|
67
|
-
`#!/usr/bin/env bash\nprintf '%s\\n' "$*" >> "${log}"\n`,
|
|
68
|
-
{ mode: 0o755 },
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
beforeEach(() => {
|
|
73
|
-
const root = mkdtempSync(join(tmpdir(), 'ensure-creds-'))
|
|
74
|
-
const kit = join(root, 'deploy-kit')
|
|
75
|
-
const home = join(root, 'home')
|
|
76
|
-
const bin = join(root, 'bin')
|
|
77
|
-
mkdirSync(join(kit, '.secrets'), { recursive: true })
|
|
78
|
-
mkdirSync(join(home, '.s'), { recursive: true })
|
|
79
|
-
mkdirSync(bin, { recursive: true })
|
|
80
|
-
|
|
81
|
-
// 真脚本进沙箱 kit。.secrets 按生产形状:bundle 只带 *.example(2026-09-18 起 AK 不再随包
|
|
82
|
-
// 分发,见 specs/2026-09-18-aliyun-ak-leak-rotate-design.md),本机凭证是使用者自己写的 .local。
|
|
83
|
-
cpSync(SCRIPTS_DIR, join(kit, 'scripts'), { recursive: true })
|
|
84
|
-
writeFileSync(
|
|
85
|
-
join(kit, '.secrets', 'aliyun.env.local'),
|
|
86
|
-
'ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI-MY-OWN\nALIBABA_CLOUD_ACCESS_KEY_SECRET=my-own-secret\n',
|
|
87
|
-
)
|
|
88
|
-
writeFileSync(join(home, '.s', 'access.yaml'), USER_ACCESS_YAML)
|
|
89
|
-
writeAliyunShim(bin)
|
|
90
|
-
|
|
91
|
-
box = { root, kit, home, bin }
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
afterEach(() => {
|
|
95
|
-
rmSync(box.root, { recursive: true, force: true })
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
/** 在沙箱里 source ensure-credentials.sh 并跑一段 bash,返回 stdout。 */
|
|
99
|
-
function runInKit(body: string): string {
|
|
100
|
-
const script = `
|
|
101
|
-
set -euo pipefail
|
|
102
|
-
KIT_DIR="${box.kit}"
|
|
103
|
-
REGION="\${REGION:-cn-hangzhou}"
|
|
104
|
-
source "$KIT_DIR/scripts/ensure-credentials.sh"
|
|
105
|
-
${body}
|
|
106
|
-
`
|
|
107
|
-
return execFileSync('bash', ['-c', script], {
|
|
108
|
-
env: { ...process.env, HOME: box.home, PATH: `${box.bin}:${process.env.PATH}` },
|
|
109
|
-
encoding: 'utf8',
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function userAccessYaml(): string {
|
|
114
|
-
return readFileSync(join(box.home, '.s', 'access.yaml'), 'utf8')
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function kitAccessYaml(): string {
|
|
118
|
-
return readFileSync(join(box.kit, '.s-home', '.s', 'access.yaml'), 'utf8')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
describe('ensure_credentials', () => {
|
|
122
|
-
it('不碰用户全局 ~/.s/access.yaml', () => {
|
|
123
|
-
runInKit('ensure_credentials')
|
|
124
|
-
expect(userAccessYaml()).toBe(USER_ACCESS_YAML)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('凭证写进 kit 私有 s-home', () => {
|
|
128
|
-
runInKit('ensure_credentials')
|
|
129
|
-
const yaml = kitAccessYaml()
|
|
130
|
-
expect(yaml).toContain("AccountID: '5566778899001122'")
|
|
131
|
-
expect(yaml).toContain('AccessKeyID: LTAI-MY-OWN')
|
|
132
|
-
expect(yaml).toContain('AccessKeySecret: my-own-secret')
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('profile 名是 clawd-fc 而不是 default', () => {
|
|
136
|
-
// 隔离万一失效(某版本 s 不认 serverless_devs_config_home)时,s 会去用户
|
|
137
|
-
// ~/.s/access.yaml 找 clawd-fc —— 找不到就硬报错,而不是默默拿用户的 default
|
|
138
|
-
// 凭证把 extension 部署进别人的账号。
|
|
139
|
-
runInKit('ensure_credentials')
|
|
140
|
-
expect(kitAccessYaml()).toMatch(/^clawd-fc:$/m)
|
|
141
|
-
expect(kitAccessYaml()).not.toMatch(/^default:$/m)
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('把 Serverless Devs 的家目录指向 kit 私有目录', () => {
|
|
145
|
-
const out = runInKit('ensure_credentials; echo "S_HOME=$serverless_devs_config_home"')
|
|
146
|
-
expect(out).toContain(`S_HOME=${box.kit}/.s-home`)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('access.yaml 权限 600(含 SK,不给同机其它用户读)', () => {
|
|
150
|
-
runInKit('ensure_credentials')
|
|
151
|
-
const mode = statSync(join(box.kit, '.s-home', '.s', 'access.yaml')).mode & 0o777
|
|
152
|
-
expect(mode.toString(8)).toBe('600')
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
// 存量机器:OTA 停发 aliyun.env 之后 refreshDeployKit 不再覆盖也不删它,旧文件会留在盘上。
|
|
156
|
-
// 有 .local 时 .local 必须盖过它(否则老用户配了自己的 AK 还在用那把已停用的)。
|
|
157
|
-
it('legacy aliyun.env 仍被读,但 .local 覆盖它', () => {
|
|
158
|
-
writeFileSync(
|
|
159
|
-
join(box.kit, '.secrets', 'aliyun.env'),
|
|
160
|
-
'ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI-STALE-OTA\nALIBABA_CLOUD_ACCESS_KEY_SECRET=stale-ota-secret\n',
|
|
161
|
-
)
|
|
162
|
-
runInKit('ensure_credentials')
|
|
163
|
-
expect(kitAccessYaml()).toContain('AccessKeyID: LTAI-MY-OWN')
|
|
164
|
-
expect(kitAccessYaml()).not.toContain('LTAI-STALE-OTA')
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('只有 aliyun.env 没有 .local 也能用(使用者直接写 aliyun.env 的老习惯)', () => {
|
|
168
|
-
rmSync(join(box.kit, '.secrets', 'aliyun.env.local'))
|
|
169
|
-
writeFileSync(
|
|
170
|
-
join(box.kit, '.secrets', 'aliyun.env'),
|
|
171
|
-
'ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI-PLAIN-FILE\nALIBABA_CLOUD_ACCESS_KEY_SECRET=plain-file-secret\n',
|
|
172
|
-
)
|
|
173
|
-
runInKit('ensure_credentials')
|
|
174
|
-
expect(kitAccessYaml()).toContain('AccessKeyID: LTAI-PLAIN-FILE')
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it('调 aliyun 时显式传 AK 和 region,不靠环境变量的隐式优先级', () => {
|
|
178
|
-
// 真 CLI 在 `--mode AK` 下不消费 ALIBABA_CLOUD_*:干净机器上报 `region can't be empty`
|
|
179
|
-
// 直接跑不起来;有 ~/.aliyun/config.json 的机器上更坏——静默走用户自己的 profile,
|
|
180
|
-
// 等于拿用户的阿里云身份去部署。两种都必须由显式 flag 堵死。
|
|
181
|
-
const log = join(box.root, 'aliyun-argv.log')
|
|
182
|
-
writeAliyunShim(box.bin, '5566778899001122', log)
|
|
183
|
-
runInKit('ensure_credentials')
|
|
184
|
-
const argv = readFileSync(log, 'utf8')
|
|
185
|
-
expect(argv).toContain('--access-key-id LTAI-MY-OWN')
|
|
186
|
-
expect(argv).toContain('--access-key-secret my-own-secret')
|
|
187
|
-
expect(argv).toContain('--region cn-hangzhou')
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('机器上没有 aliyun profile 也能拿到 AccountId', () => {
|
|
191
|
-
// 全新用户机器:ensure_toolchain 只装 CLI、从不跑 `aliyun configure`,所以 HOME 下
|
|
192
|
-
// 没有 .aliyun/config.json。严格 shim 复现真 CLI 的这条路径。
|
|
193
|
-
rmSync(join(box.home, '.aliyun'), { recursive: true, force: true })
|
|
194
|
-
runInKit('ensure_credentials')
|
|
195
|
-
expect(kitAccessYaml()).toContain("AccountID: '5566778899001122'")
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('aliyun.env / .local 都没有、env 里也没有 AK 时 fail-loud,文案指到 .local', () => {
|
|
199
|
-
rmSync(join(box.kit, '.secrets', 'aliyun.env.local'))
|
|
200
|
-
expect(() => runInKit('ensure_credentials')).toThrow(/aliyun\.env\.local/)
|
|
201
|
-
expect(existsSync(join(box.kit, '.s-home', '.s', 'access.yaml'))).toBe(false)
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
// 云上分支(persona 上云 M1b Step 9):容器里没有 `.secrets/`——`build-image.mjs` 刻意把它
|
|
205
|
-
// 剔出镜像层(镜像要分发,凭据进层洗不掉)。AK 由 gateway 经 **MCP server 的 per-server env**
|
|
206
|
-
// 交到调用本脚本的那个子进程里,不落文件:写成文件的话 agent 一个 `cat` 就看见了。
|
|
207
|
-
it('没有 .secrets/ 凭证文件但 env 里有 AK 时用 env(云上容器那条路)', () => {
|
|
208
|
-
rmSync(join(box.kit, '.secrets', 'aliyun.env.local'))
|
|
209
|
-
const out = execFileSync(
|
|
210
|
-
'bash',
|
|
211
|
-
[
|
|
212
|
-
'-c',
|
|
213
|
-
`set -euo pipefail
|
|
214
|
-
KIT_DIR="${box.kit}"
|
|
215
|
-
REGION=cn-hangzhou
|
|
216
|
-
source "$KIT_DIR/scripts/ensure-credentials.sh"
|
|
217
|
-
ensure_credentials
|
|
218
|
-
printf 'ok %s\\n' "$ALIBABA_CLOUD_ACCESS_KEY_ID"`,
|
|
219
|
-
],
|
|
220
|
-
{
|
|
221
|
-
env: {
|
|
222
|
-
...process.env,
|
|
223
|
-
HOME: box.home,
|
|
224
|
-
PATH: `${box.bin}:${process.env.PATH}`,
|
|
225
|
-
ALIBABA_CLOUD_ACCESS_KEY_ID: 'LTAI-FROM-ENV',
|
|
226
|
-
ALIBABA_CLOUD_ACCESS_KEY_SECRET: 'secret-from-env',
|
|
227
|
-
},
|
|
228
|
-
encoding: 'utf8',
|
|
229
|
-
},
|
|
230
|
-
)
|
|
231
|
-
expect(out).toContain('ok LTAI-FROM-ENV')
|
|
232
|
-
// 走 env 分支照样把凭证落进 kit 私有 s-home(publish 用的是那份 profile),仍不碰用户全局
|
|
233
|
-
expect(kitAccessYaml()).toContain('LTAI-FROM-ENV')
|
|
234
|
-
expect(userAccessYaml()).toContain('LTAI-USER-OWN-KEY')
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
it('文件在就优先用文件(云上那条只是兜底,不能反过来盖掉本机 .local 凭证)', () => {
|
|
238
|
-
const out = execFileSync(
|
|
239
|
-
'bash',
|
|
240
|
-
[
|
|
241
|
-
'-c',
|
|
242
|
-
`set -euo pipefail
|
|
243
|
-
KIT_DIR="${box.kit}"
|
|
244
|
-
REGION=cn-hangzhou
|
|
245
|
-
source "$KIT_DIR/scripts/ensure-credentials.sh"
|
|
246
|
-
ensure_aliyun_env
|
|
247
|
-
printf 'id=%s\\n' "$ALIBABA_CLOUD_ACCESS_KEY_ID"`,
|
|
248
|
-
],
|
|
249
|
-
{
|
|
250
|
-
env: {
|
|
251
|
-
...process.env,
|
|
252
|
-
HOME: box.home,
|
|
253
|
-
PATH: `${box.bin}:${process.env.PATH}`,
|
|
254
|
-
ALIBABA_CLOUD_ACCESS_KEY_ID: 'LTAI-FROM-ENV',
|
|
255
|
-
ALIBABA_CLOUD_ACCESS_KEY_SECRET: 'secret-from-env',
|
|
256
|
-
},
|
|
257
|
-
encoding: 'utf8',
|
|
258
|
-
},
|
|
259
|
-
)
|
|
260
|
-
expect(out).toContain('id=LTAI-MY-OWN')
|
|
261
|
-
expect(out).not.toContain('LTAI-FROM-ENV')
|
|
262
|
-
})
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
describe('publish.sh', () => {
|
|
266
|
-
/** 最小 persona kit + 项目目录;模板只留跟本文件断言相关的占位符。 */
|
|
267
|
-
function publishFixture(): { ext: string; persona: string } {
|
|
268
|
-
// 严格 shim 的自定义域名查询回空 → publish.sh 在 verify 段 exit 1。deploy 之后的事,
|
|
269
|
-
// 不影响本用例要断言的凭证 / 渲染行为,省掉 curl shim。
|
|
270
|
-
writeAliyunShim(box.bin, '5566778899001122', join(box.root, 'aliyun-argv.log'))
|
|
271
|
-
const ext = join(box.root, 'proj')
|
|
272
|
-
const persona = join(box.root, 'persona')
|
|
273
|
-
mkdirSync(join(ext, 'server'), { recursive: true })
|
|
274
|
-
mkdirSync(join(persona, 'extension-kit', 'contract'), { recursive: true })
|
|
275
|
-
mkdirSync(join(box.kit, 'contract'), { recursive: true })
|
|
276
|
-
writeFileSync(join(box.kit, 'contract', 'bootstrap'), '#!/usr/bin/env bash\n')
|
|
277
|
-
writeFileSync(join(ext, 'ext.conf'), 'APP_NAME=demo\nSLUG=ab12\nCODE_DIR=./server\n')
|
|
278
|
-
writeFileSync(join(persona, 'extension-kit', 'config.env'), 'REGION=cn-hangzhou\n')
|
|
279
|
-
writeFileSync(
|
|
280
|
-
join(persona, 'extension-kit', 'contract', 's.yaml.tmpl'),
|
|
281
|
-
'name: __DEPLOY_NAME__-app\naccess: clawd-fc\nenv: __APP_ENV__\nprefix: __TABLE_PREFIX__\n',
|
|
282
|
-
)
|
|
283
|
-
return { ext, persona }
|
|
284
|
-
}
|
|
285
|
-
function runPublish(ext: string, persona: string, env: string): void {
|
|
286
|
-
expect(() =>
|
|
287
|
-
execFileSync('bash', [join(box.kit, 'scripts', 'publish.sh'), ext, persona, env], {
|
|
288
|
-
env: { ...process.env, HOME: box.home, PATH: `${box.bin}:${process.env.PATH}` },
|
|
289
|
-
encoding: 'utf8',
|
|
290
|
-
stdio: 'pipe',
|
|
291
|
-
}),
|
|
292
|
-
).toThrow() // verify 段无域名 → exit 1,deploy 已经跑过了
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
it('用 clawd-fc profile 调 s deploy,不碰用户全局 ~/.s', () => {
|
|
296
|
-
const log = join(box.root, 's-argv.log')
|
|
297
|
-
writeSShim(box.bin, log)
|
|
298
|
-
const { ext, persona } = publishFixture()
|
|
299
|
-
runPublish(ext, persona, 'integration')
|
|
300
|
-
expect(readFileSync(log, 'utf8')).toContain('deploy -y -t s.int.yaml --access clawd-fc')
|
|
301
|
-
expect(userAccessYaml()).toBe(USER_ACCESS_YAML)
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
// 双环境(spec 2026-09-15-app-builder-integration-env-design §3.2):同一个项目两个 env 各渲各的
|
|
305
|
-
// s 文件、各自的 DEPLOY_NAME / TABLE_PREFIX,prod 那份跟双环境之前的值一字不差(老地址不动)。
|
|
306
|
-
it('integration → s.int.yaml(-int / _int_),prod → s.prod.yaml(老值),两份并存互不覆盖', () => {
|
|
307
|
-
const log = join(box.root, 's-argv.log')
|
|
308
|
-
writeSShim(box.bin, log)
|
|
309
|
-
const { ext, persona } = publishFixture()
|
|
310
|
-
runPublish(ext, persona, 'integration')
|
|
311
|
-
runPublish(ext, persona, 'prod')
|
|
312
|
-
expect(readFileSync(join(ext, 's.int.yaml'), 'utf8')).toBe('name: demo-ab12-int-app\naccess: clawd-fc\nenv: integration\nprefix: demo_ab12_int_\n')
|
|
313
|
-
expect(readFileSync(join(ext, 's.prod.yaml'), 'utf8')).toBe('name: demo-ab12-app\naccess: clawd-fc\nenv: prod\nprefix: demo_ab12_\n')
|
|
314
|
-
expect(existsSync(join(ext, 's.yaml'))).toBe(false)
|
|
315
|
-
const argv = readFileSync(log, 'utf8')
|
|
316
|
-
expect(argv).toContain('deploy -y -t s.int.yaml --access clawd-fc')
|
|
317
|
-
expect(argv).toContain('deploy -y -t s.prod.yaml --access clawd-fc')
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
// 存量项目的 .gitignore 是 scaffold 时拷的一次性文件,只有 `s.yaml`:s.int.yaml 渲下去会被当成源码推上 GitHub
|
|
321
|
-
//(里面有 supabase key)。publish.sh 像 bake SLUG 那样幂等地补一行,模板项目已有的不重复。
|
|
322
|
-
it('项目 .gitignore 没有 s.*.yaml 就补上(幂等);没有 .gitignore 也建', () => {
|
|
323
|
-
writeSShim(box.bin, join(box.root, 's-argv.log'))
|
|
324
|
-
const { ext, persona } = publishFixture()
|
|
325
|
-
writeFileSync(join(ext, '.gitignore'), 'node_modules/\ns.yaml\n')
|
|
326
|
-
runPublish(ext, persona, 'integration')
|
|
327
|
-
expect(readFileSync(join(ext, '.gitignore'), 'utf8')).toBe('node_modules/\ns.yaml\ns.*.yaml\n')
|
|
328
|
-
runPublish(ext, persona, 'prod')
|
|
329
|
-
expect(readFileSync(join(ext, '.gitignore'), 'utf8')).toBe('node_modules/\ns.yaml\ns.*.yaml\n')
|
|
330
|
-
rmSync(join(ext, '.gitignore'))
|
|
331
|
-
runPublish(ext, persona, 'integration')
|
|
332
|
-
expect(readFileSync(join(ext, '.gitignore'), 'utf8')).toBe('s.*.yaml\n')
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
it('缺 env / 非法 env:渲染和 deploy 都不跑', () => {
|
|
336
|
-
const log = join(box.root, 's-argv.log')
|
|
337
|
-
writeSShim(box.bin, log)
|
|
338
|
-
const { ext, persona } = publishFixture()
|
|
339
|
-
for (const args of [[ext, persona], [ext, persona, 'staging']]) {
|
|
340
|
-
expect(() =>
|
|
341
|
-
execFileSync('bash', [join(box.kit, 'scripts', 'publish.sh'), ...args], {
|
|
342
|
-
env: { ...process.env, HOME: box.home, PATH: `${box.bin}:${process.env.PATH}` },
|
|
343
|
-
encoding: 'utf8',
|
|
344
|
-
stdio: 'pipe',
|
|
345
|
-
}),
|
|
346
|
-
).toThrow(/integration\|prod/)
|
|
347
|
-
}
|
|
348
|
-
expect(existsSync(log)).toBe(false)
|
|
349
|
-
expect(existsSync(join(ext, 's.int.yaml'))).toBe(false)
|
|
350
|
-
})
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
describe('remove-extension.sh', () => {
|
|
354
|
-
function removeFixture(opts: { sFiles: string[]; tables?: string; names?: Record<string, string> }): { ext: string; persona: string } {
|
|
355
|
-
const ext = join(box.root, 'proj')
|
|
356
|
-
const persona = join(box.root, 'persona')
|
|
357
|
-
mkdirSync(ext, { recursive: true })
|
|
358
|
-
mkdirSync(join(persona, 'extension-kit'), { recursive: true })
|
|
359
|
-
writeFileSync(join(ext, 'ext.conf'), `APP_NAME=demo\nSLUG=ab12\n${opts.tables !== undefined ? `SUPABASE_TABLES="${opts.tables}"\n` : ''}`)
|
|
360
|
-
writeFileSync(join(persona, 'extension-kit', 'config.env'), 'REGION=cn-hangzhou\n')
|
|
361
|
-
for (const f of opts.sFiles) writeFileSync(join(ext, f), `name: ${opts.names?.[f] ?? 'x'}\n`)
|
|
362
|
-
return { ext, persona }
|
|
363
|
-
}
|
|
364
|
-
function runRemove(ext: string, persona: string): string {
|
|
365
|
-
return execFileSync('bash', [join(box.kit, 'scripts', 'remove-extension.sh'), ext, persona], {
|
|
366
|
-
env: { ...process.env, HOME: box.home, PATH: `${box.bin}:${process.env.PATH}` },
|
|
367
|
-
encoding: 'utf8',
|
|
368
|
-
})
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
it('用 clawd-fc profile 调 s remove,不碰用户全局 ~/.s', () => {
|
|
372
|
-
const log = join(box.root, 's-argv.log')
|
|
373
|
-
writeSShim(box.bin, log)
|
|
374
|
-
const { ext, persona } = removeFixture({ sFiles: ['s.int.yaml'] })
|
|
375
|
-
runRemove(ext, persona)
|
|
376
|
-
expect(readFileSync(log, 'utf8')).toContain('remove -y -t s.int.yaml --access clawd-fc')
|
|
377
|
-
expect(userAccessYaml()).toBe(USER_ACCESS_YAML)
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
// 双环境:两个 env 各 s remove 一次(哪份 s 文件在删哪份;legacy s.yaml 也认),DROP 打两套前缀
|
|
381
|
-
it('s.int.yaml / s.prod.yaml 各 remove 一次(legacy s.yaml 与 s.prod.yaml 同指 prod 函数,有后者就不再对前者 remove);基础表名展开成 _int_ 与无标签两套 DROP', () => {
|
|
382
|
-
const log = join(box.root, 's-argv.log')
|
|
383
|
-
writeSShim(box.bin, log)
|
|
384
|
-
const { ext, persona } = removeFixture({ sFiles: ['s.int.yaml', 's.prod.yaml', 's.yaml'], tables: 'visits notes' })
|
|
385
|
-
const out = runRemove(ext, persona)
|
|
386
|
-
const argv = readFileSync(log, 'utf8')
|
|
387
|
-
expect(argv).toContain('remove -y -t s.int.yaml --access clawd-fc')
|
|
388
|
-
expect(argv).toContain('remove -y -t s.prod.yaml --access clawd-fc')
|
|
389
|
-
expect(argv).not.toContain('-t s.yaml ')
|
|
390
|
-
for (const t of ['demo_ab12_int_visits', 'demo_ab12_visits', 'demo_ab12_int_notes', 'demo_ab12_notes']) {
|
|
391
|
-
expect(out).toContain(`DROP TABLE IF EXISTS public.${t} CASCADE;`)
|
|
392
|
-
}
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
it('双环境之前的完整表名(带完整前缀 <APP_NAME>_<SLUG>_)只打一条;别的前缀一律按基础名展开,DROP 永远落在自己前缀下', () => {
|
|
396
|
-
const log = join(box.root, 's-argv.log')
|
|
397
|
-
writeSShim(box.bin, log)
|
|
398
|
-
const { ext, persona } = removeFixture({ sFiles: ['s.yaml'], tables: 'demo_ab12_visits' })
|
|
399
|
-
const out = runRemove(ext, persona)
|
|
400
|
-
expect(readFileSync(log, 'utf8')).toContain('remove -y -t s.yaml --access clawd-fc')
|
|
401
|
-
expect(out.match(/DROP TABLE/g)).toHaveLength(1)
|
|
402
|
-
expect(out).toContain('DROP TABLE IF EXISTS public.demo_ab12_visits CASCADE;')
|
|
403
|
-
writeFileSync(join(ext, 'ext.conf'), 'APP_NAME=demo\nSLUG=ab12\nSUPABASE_TABLES="demo_other_visits"\n')
|
|
404
|
-
const out2 = runRemove(ext, persona)
|
|
405
|
-
expect(out2).not.toContain('public.demo_other_visits ')
|
|
406
|
-
expect(out2.match(/DROP TABLE IF EXISTS public\.demo_ab12_(int_)?demo_other_visits CASCADE;/g)).toHaveLength(2)
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
// dataclaw 双环境之前 functionName 不带 slug:老 s.yaml 与 s.prod.yaml 指**不同**函数(name: 不同),两份都要拆
|
|
410
|
-
it('老 s.yaml 的 name 与 s.prod.yaml 不同(指不同函数):两份都 remove', () => {
|
|
411
|
-
const log = join(box.root, 's-argv.log')
|
|
412
|
-
writeSShim(box.bin, log)
|
|
413
|
-
const { ext, persona } = removeFixture({ sFiles: ['s.prod.yaml', 's.yaml'], names: { 's.yaml': 'demo-app', 's.prod.yaml': 'demo-ab12-app' } })
|
|
414
|
-
runRemove(ext, persona)
|
|
415
|
-
const argv = readFileSync(log, 'utf8')
|
|
416
|
-
expect(argv).toContain('remove -y -t s.prod.yaml --access clawd-fc')
|
|
417
|
-
expect(argv).toContain('remove -y -t s.yaml --access clawd-fc')
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
// #4:基础名恰好以 <APP_NAME>_ 开头(shop 的 shop_items)不能被当成 legacy 完整名——legacy 判据是完整前缀 <APP_NAME>_<SLUG>_
|
|
421
|
-
it('基础名以 <APP_NAME>_ 开头(shop_items)仍按基础名展开两套', () => {
|
|
422
|
-
writeSShim(box.bin, join(box.root, 's-argv.log'))
|
|
423
|
-
const { ext, persona } = removeFixture({ sFiles: ['s.prod.yaml'], tables: 'demo_items' })
|
|
424
|
-
const out = runRemove(ext, persona)
|
|
425
|
-
expect(out).toContain('DROP TABLE IF EXISTS public.demo_ab12_int_demo_items CASCADE;')
|
|
426
|
-
expect(out).toContain('DROP TABLE IF EXISTS public.demo_ab12_demo_items CASCADE;')
|
|
427
|
-
expect(out.match(/DROP TABLE/g)).toHaveLength(2)
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
it('从未发布过(没有任何 s 文件):不调 s remove,退出 0', () => {
|
|
431
|
-
const log = join(box.root, 's-argv.log')
|
|
432
|
-
writeSShim(box.bin, log)
|
|
433
|
-
const { ext, persona } = removeFixture({ sFiles: [] })
|
|
434
|
-
const out = runRemove(ext, persona)
|
|
435
|
-
expect(existsSync(log)).toBe(false)
|
|
436
|
-
expect(out).toContain('从未发布过')
|
|
437
|
-
})
|
|
438
|
-
})
|