@clawos-dev/clawd 0.2.546 → 0.2.548
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 +35 -35
- package/dist/cli.cjs +518 -663
- 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 +25397 -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 +25397 -0
- 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/{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,278 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { execFileSync } from 'node:child_process'
|
|
3
|
-
import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync, readdirSync } from 'node:fs'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { tmpdir } from 'node:os'
|
|
6
|
-
|
|
7
|
-
// 复刻 publish.sh 渲染段(generic placeholder substitution)的最小脚本,锁定占位符替换契约。
|
|
8
|
-
// 实现若改 publish.sh 的扫描循环,本片段须同步。
|
|
9
|
-
const RENDER_SNIPPET = `
|
|
10
|
-
set -euo pipefail
|
|
11
|
-
TMPL="$1"; OUT="$2"
|
|
12
|
-
sed_args=()
|
|
13
|
-
for ph in $(grep -oE '__[A-Z0-9_]+__' "$TMPL" | sort -u); do
|
|
14
|
-
var="\${ph#__}"; var="\${var%__}"
|
|
15
|
-
if ! declare -p "$var" >/dev/null 2>&1; then
|
|
16
|
-
echo "MISSING:$ph" >&2; exit 1
|
|
17
|
-
fi
|
|
18
|
-
val="\${!var}"
|
|
19
|
-
val="\${val//\\\\/\\\\\\\\}"
|
|
20
|
-
val="\${val//|/\\\\|}"
|
|
21
|
-
val="\${val//&/\\\\&}"
|
|
22
|
-
sed_args+=(-e "s|$ph|$val|g")
|
|
23
|
-
done
|
|
24
|
-
sed "\${sed_args[@]}" "$TMPL" > "$OUT"
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
function render(tmpl: string, env: Record<string, string>): { ok: boolean; out?: string; err?: string } {
|
|
28
|
-
const dir = mkdtempSync(join(tmpdir(), 'pub-'))
|
|
29
|
-
const tmplPath = join(dir, 's.yaml.tmpl')
|
|
30
|
-
const outPath = join(dir, 's.yaml')
|
|
31
|
-
writeFileSync(tmplPath, tmpl)
|
|
32
|
-
try {
|
|
33
|
-
execFileSync('bash', ['-c', RENDER_SNIPPET, 'render', tmplPath, outPath], {
|
|
34
|
-
env: { ...process.env, ...env },
|
|
35
|
-
})
|
|
36
|
-
return { ok: true, out: readFileSync(outPath, 'utf8') }
|
|
37
|
-
} catch (e) {
|
|
38
|
-
return { ok: false, err: (e as { stderr?: Buffer }).stderr?.toString() ?? '' }
|
|
39
|
-
} finally {
|
|
40
|
-
rmSync(dir, { recursive: true, force: true })
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
describe('publish.sh 占位符替换', () => {
|
|
45
|
-
it('定义量正常替换', () => {
|
|
46
|
-
const r = render('url: __FOO__\nkey: __BAR__\n', { FOO: 'http://x', BAR: 'abc' })
|
|
47
|
-
expect(r.ok).toBe(true)
|
|
48
|
-
expect(r.out).toContain('url: http://x')
|
|
49
|
-
expect(r.out).toContain('key: abc')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('未定义量 fail-loud', () => {
|
|
53
|
-
const r = render('x: __NOPE__\n', {})
|
|
54
|
-
expect(r.ok).toBe(false)
|
|
55
|
-
expect(r.err).toContain('MISSING:__NOPE__')
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('显式空字符串视为已定义、替换为空', () => {
|
|
59
|
-
const r = render('x: "__OPT__"\n', { OPT: '' })
|
|
60
|
-
expect(r.ok).toBe(true)
|
|
61
|
-
expect(r.out).toContain('x: ""')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('值含 sed 特殊字符(| & \\)按字面替换不损坏', () => {
|
|
65
|
-
const r = render('secret: __SEC__\n', { SEC: 'a|b&c\\d' })
|
|
66
|
-
expect(r.ok).toBe(true)
|
|
67
|
-
expect(r.out).toContain('secret: a|b&c\\d')
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// 双环境(spec 2026-09-15-app-builder-integration-env-design §3.2):env 是脚本第 3 位置参数,
|
|
73
|
-
// 环境名 integration / prod 是人读的;进资源名的是短标签 ENV_TAG(int / prod 为空保持老地址)。
|
|
74
|
-
// 复刻 publish.sh 的派生段;实现若改脚本,本片段须同步。
|
|
75
|
-
const DERIVE_SNIPPET = `
|
|
76
|
-
set -euo pipefail
|
|
77
|
-
APP_NAME="$1"; SLUG="$2"
|
|
78
|
-
APP_ENV="\${3:?缺 env(integration|prod)}"
|
|
79
|
-
case "$APP_ENV" in integration|prod) ;; *) echo "❌ env 只能是 integration|prod,收到:$APP_ENV" >&2; exit 1;; esac
|
|
80
|
-
BASE_NAME="\${APP_NAME}-\${SLUG}"
|
|
81
|
-
if [ "$APP_ENV" = integration ]; then
|
|
82
|
-
ENV_TAG=int; DEPLOY_NAME="\${BASE_NAME}-int"; TABLE_PREFIX="\${APP_NAME}_\${SLUG}_int_"
|
|
83
|
-
else
|
|
84
|
-
ENV_TAG=prod; DEPLOY_NAME="\${BASE_NAME}"; TABLE_PREFIX="\${APP_NAME}_\${SLUG}_"
|
|
85
|
-
fi
|
|
86
|
-
S_FILE="s.\${ENV_TAG}.yaml"
|
|
87
|
-
printf '%s\\n%s\\n%s\\n%s\\n' "$ENV_TAG" "$DEPLOY_NAME" "$TABLE_PREFIX" "$S_FILE"
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
function derive(args: string[]): { ok: boolean; lines?: string[]; err?: string } {
|
|
91
|
-
try {
|
|
92
|
-
const out = execFileSync('bash', ['-c', DERIVE_SNIPPET, 'derive', ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
|
|
93
|
-
return { ok: true, lines: out.trim().split('\n') }
|
|
94
|
-
} catch (e) {
|
|
95
|
-
return { ok: false, err: (e as { stderr?: string }).stderr ?? '' }
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
describe('publish.sh 按 env 派生资源名', () => {
|
|
100
|
-
it('integration → ENV_TAG=int,DEPLOY_NAME / TABLE_PREFIX 带 int 标签,渲到 s.int.yaml', () => {
|
|
101
|
-
expect(derive(['demo', 'ab12', 'integration'])).toEqual({ ok: true, lines: ['int', 'demo-ab12-int', 'demo_ab12_int_', 's.int.yaml'] })
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('prod → 不带标签:DEPLOY_NAME / TABLE_PREFIX 与双环境之前完全一致(老地址不动),渲到 s.prod.yaml', () => {
|
|
105
|
-
expect(derive(['demo', 'ab12', 'prod'])).toEqual({ ok: true, lines: ['prod', 'demo-ab12', 'demo_ab12_', 's.prod.yaml'] })
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('缺 env / 非法 env:退 1,不猜默认', () => {
|
|
109
|
-
expect(derive(['demo', 'ab12']).ok).toBe(false)
|
|
110
|
-
const bad = derive(['demo', 'ab12', 'staging'])
|
|
111
|
-
expect(bad.ok).toBe(false)
|
|
112
|
-
expect(bad.err).toContain('integration|prod')
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('真脚本里的派生段与本片段一致(用法注释 + 第 3 参 + s deploy -t)', () => {
|
|
116
|
-
const real = readFileSync(join(__dirname, 'publish.sh'), 'utf8')
|
|
117
|
-
for (const line of [
|
|
118
|
-
'APP_ENV="${3:?',
|
|
119
|
-
'case "$APP_ENV" in integration|prod)',
|
|
120
|
-
'DEPLOY_NAME="${BASE_NAME}-int"',
|
|
121
|
-
'TABLE_PREFIX="${APP_NAME}_${SLUG}_int_"',
|
|
122
|
-
'S_FILE="s.${ENV_TAG}.yaml"',
|
|
123
|
-
's deploy -y -t "$S_FILE"',
|
|
124
|
-
]) expect(real, line).toContain(line)
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
// M1b Step 5c:账号级资源(自定义域名 + 证书)按履约切换
|
|
130
|
-
//
|
|
131
|
-
// 自定义域名与通配符证书都绑在**我们自己的**阿里云账号上,所以它只在「部署者供给阿里云 key」
|
|
132
|
-
// 这种履约下成立。使用者自带 key 时发布落在他自己的账号里,那里既没有我们的 DNS 也没有我们的
|
|
133
|
-
// 证书,硬渲染出来只会在 fc3-domain 那步失败——所以整段不渲染,用 FC 默认域名。
|
|
134
|
-
//
|
|
135
|
-
// 下面两组用例跑的是**真的 config.env**(不是手编样例):一组验它作为「可被 env 覆盖的默认值表」
|
|
136
|
-
// 的行为,一组验按同一个判据拼出来的 s.yaml 差异。
|
|
137
|
-
const DEFAULTS = join(__dirname, '..', '..', 'persona', 'defaults')
|
|
138
|
-
const KIT = join(DEFAULTS, 'persona-app-builder', 'extension-kit')
|
|
139
|
-
|
|
140
|
-
/** 复刻 publish.sh 的「source config.env → 打印结果」段。实现若改 publish.sh,本片段须同步。 */
|
|
141
|
-
function sourceConfigEnv(env: Record<string, string>): Record<string, string> {
|
|
142
|
-
const out = execFileSync(
|
|
143
|
-
'bash',
|
|
144
|
-
['-c', `set -euo pipefail; . "$1"; for v in APP_DOMAIN_SUFFIX CERT_ID REGION FC_CPU; do printf '%s=%s\\n' "$v" "\${!v}"; done`, 'cfg', join(KIT, 'config.env')],
|
|
145
|
-
{ env: { ...process.env, ...env }, encoding: 'utf8' },
|
|
146
|
-
)
|
|
147
|
-
return Object.fromEntries(out.trim().split('\n').map((l) => {
|
|
148
|
-
const i = l.indexOf('=')
|
|
149
|
-
return [l.slice(0, i), l.slice(i + 1)]
|
|
150
|
-
}))
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* 复刻 publish.sh 的「按 kit 有没有 s.yaml.domain.tmpl + APP_DOMAIN_SUFFIX 拼模板 → 渲染」段。
|
|
155
|
-
* 同上,改脚本须同步。**判据的第一层是文件存在**——没有那个文件的 persona 压根不参与这套切换。
|
|
156
|
-
*/
|
|
157
|
-
function renderSYaml(env: Record<string, string>): string {
|
|
158
|
-
const cfg = sourceConfigEnv(env)
|
|
159
|
-
const base = readFileSync(join(KIT, 'contract', 's.yaml.tmpl'), 'utf8')
|
|
160
|
-
const domTmpl = join(KIT, 'contract', 's.yaml.domain.tmpl')
|
|
161
|
-
const tmpl = existsSync(domTmpl) && cfg.APP_DOMAIN_SUFFIX
|
|
162
|
-
? base + readFileSync(domTmpl, 'utf8')
|
|
163
|
-
: base
|
|
164
|
-
const r = render(tmpl, {
|
|
165
|
-
...cfg,
|
|
166
|
-
...env,
|
|
167
|
-
APP_NAME: 'demo', DEPLOY_NAME: 'demo-ab12', CODE_DIR: './server', FC_ENTRY: 'dist/main.js',
|
|
168
|
-
FC_RUNTIME: 'custom.debian12', NODE_LAYER: 'layer', FC_MEMORY: '512', FC_TIMEOUT: '60',
|
|
169
|
-
SUPABASE_URL: 'http://sb', SUPABASE_KEY: 'anon',
|
|
170
|
-
APP_ENV: 'integration', TABLE_PREFIX: 'demo_ab12_int_',
|
|
171
|
-
})
|
|
172
|
-
expect(r.err ?? '').toBe('')
|
|
173
|
-
return r.out!
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
describe('config.env 作为「可被 env 覆盖的默认值表」', () => {
|
|
177
|
-
it('env 里没有这几个变量 → 部署级默认(品牌域名 + 我们账号的 certId)', () => {
|
|
178
|
-
const clean = JSON.parse(
|
|
179
|
-
execFileSync('bash', ['-c',
|
|
180
|
-
`unset APP_DOMAIN_SUFFIX CERT_ID REGION; . "$1"; printf '{"d":"%s","c":"%s","r":"%s"}' "$APP_DOMAIN_SUFFIX" "$CERT_ID" "$REGION"`,
|
|
181
|
-
'cfg', join(KIT, 'config.env')], { encoding: 'utf8' }),
|
|
182
|
-
)
|
|
183
|
-
expect(clean).toEqual({ d: 'app.clawos.chat', c: '25612343', r: 'cn-hangzhou' })
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('env 给了就以 env 为准(source 不覆盖上游注入的值)', () => {
|
|
187
|
-
const cfg = sourceConfigEnv({ APP_DOMAIN_SUFFIX: 'app.example.com', CERT_ID: '999', REGION: 'cn-beijing' })
|
|
188
|
-
expect(cfg.APP_DOMAIN_SUFFIX).toBe('app.example.com')
|
|
189
|
-
expect(cfg.CERT_ID).toBe('999')
|
|
190
|
-
expect(cfg.REGION).toBe('cn-beijing')
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('APP_DOMAIN_SUFFIX 显式空串保持为空(${VAR-d} 不是 ${VAR:-d})', () => {
|
|
194
|
-
// 这是「使用者自带 key」那条履约的开关:空串必须活下来,被默认值吃掉就等于开关失灵
|
|
195
|
-
expect(sourceConfigEnv({ APP_DOMAIN_SUFFIX: '' }).APP_DOMAIN_SUFFIX).toBe('')
|
|
196
|
-
// 对照:平台常量用 ${VAR:-d},空串没有意义,回落默认
|
|
197
|
-
expect(sourceConfigEnv({ FC_CPU: '' }).FC_CPU).toBe('0.35')
|
|
198
|
-
})
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
describe('两种履约渲染出来的 s.yaml 差异', () => {
|
|
202
|
-
it('部署级:带 fc3-domain 段 + 品牌域名 + certId', () => {
|
|
203
|
-
const out = renderSYaml({})
|
|
204
|
-
expect(out).toContain('component: fc3-domain')
|
|
205
|
-
expect(out).toContain('domainName: demo-ab12.app.clawos.chat')
|
|
206
|
-
expect(out).toContain('certId: 25612343')
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
it('APP_ENV / TABLE_PREFIX 渲进 FC 的 environmentVariables(server 只经它取表名)', () => {
|
|
210
|
-
const out = renderSYaml({})
|
|
211
|
-
expect(out).toContain('APP_ENV: integration')
|
|
212
|
-
expect(out).toContain('TABLE_PREFIX: demo_ab12_int_')
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
it('使用者自带 key:整段不渲染,只剩函数 + httpTrigger(走 FC 默认域名)', () => {
|
|
216
|
-
const out = renderSYaml({ APP_DOMAIN_SUFFIX: '' })
|
|
217
|
-
expect(out).not.toContain('fc3-domain')
|
|
218
|
-
expect(out).not.toContain('certId')
|
|
219
|
-
expect(out).not.toContain('clawos.chat')
|
|
220
|
-
expect(out).toContain('triggerType: http')
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
it('换成使用者自己的域名与证书:两个值都跟着走', () => {
|
|
224
|
-
const out = renderSYaml({ APP_DOMAIN_SUFFIX: 'app.example.com', CERT_ID: '999' })
|
|
225
|
-
expect(out).toContain('domainName: demo-ab12.app.example.com')
|
|
226
|
-
expect(out).toContain('certId: 999')
|
|
227
|
-
})
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
// publish.sh 是 **persona 无关的共享脚本**,persona-dataclaw-builder 也走它。它有自己的一套
|
|
231
|
-
// extension-kit:functionName 不带 slug、自定义域名段写死在它自己的 s.yaml.tmpl 里
|
|
232
|
-
// (`domainName: auto`)、没有 s.yaml.domain.tmpl。5c 的判据必须是「有没有那个文件」而不是
|
|
233
|
-
// 「APP_DOMAIN_SUFFIX 空不空」——只看后者的话,dataclaw 会被当成「使用者自带 key」拖进
|
|
234
|
-
// FC 默认域名那条分支,而它的函数名规则根本对不上,等于给一个没参与的 persona 换了发布路径。
|
|
235
|
-
describe('未参与 5c 的 persona(dataclaw-builder)不受影响', () => {
|
|
236
|
-
const DC_KIT = join(DEFAULTS, 'persona-dataclaw-builder', 'extension-kit')
|
|
237
|
-
|
|
238
|
-
it('没有 s.yaml.domain.tmpl —— 判据的第一层就把它排除在外', () => {
|
|
239
|
-
expect(existsSync(join(DC_KIT, 'contract', 's.yaml.domain.tmpl'))).toBe(false)
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('它的自定义域名段仍在自己的 s.yaml.tmpl 里,不依赖 APP_DOMAIN_SUFFIX', () => {
|
|
243
|
-
const text = readFileSync(join(DC_KIT, 'contract', 's.yaml.tmpl'), 'utf8')
|
|
244
|
-
expect(text).toContain('fc3-domain')
|
|
245
|
-
expect(text).not.toContain('__APP_DOMAIN_SUFFIX__')
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
it('functionName / 资源 key 走 __DEPLOY_NAME__,双环境的 -int 标签才跟得上(不然 integration 会覆盖 prod 函数)', () => {
|
|
249
|
-
const text = readFileSync(join(DC_KIT, 'contract', 's.yaml.tmpl'), 'utf8')
|
|
250
|
-
expect(text).toContain('functionName: __DEPLOY_NAME__')
|
|
251
|
-
expect(text).not.toContain('functionName: __APP_NAME__')
|
|
252
|
-
expect(text).toContain('APP_ENV: __APP_ENV__')
|
|
253
|
-
expect(text).toContain('TABLE_PREFIX: __TABLE_PREFIX__')
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('它的 config.env 里没有 APP_DOMAIN_SUFFIX(所以那个变量对它恒空)', () => {
|
|
257
|
-
expect(readFileSync(join(DC_KIT, 'config.env'), 'utf8')).not.toContain('APP_DOMAIN_SUFFIX')
|
|
258
|
-
})
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
// deploy-kit 的脚本 macOS(BSD userland)和 Linux(GNU coreutils,= M1b 的 Pod)都要跑,
|
|
262
|
-
// 两套 userland 语义有分歧的写法就是定时炸弹:`mktemp -t prefix` BSD 自己补随机后缀,
|
|
263
|
-
// GNU 要求模板以 X 结尾,后者直接 `too few X's in template` 退 1(coreutils 9.1 实测),
|
|
264
|
-
// `set -e` 下当场打死脚本 —— 这条在 2026-09-03 的复审里真炸过一次。
|
|
265
|
-
// 本机跑测试只覆盖得到 BSD 那半,所以用静态形态守:写全 `XXXXXX` 模板两边都过。
|
|
266
|
-
// 只看非注释行 —— 注释里为了讲清楚这个坑就得写出那个形态。
|
|
267
|
-
describe('deploy-kit 脚本的 BSD / GNU 可移植性', () => {
|
|
268
|
-
it('不用 `mktemp -t`(GNU 要求模板带 X)', () => {
|
|
269
|
-
const hits: string[] = []
|
|
270
|
-
for (const f of readdirSync(__dirname).filter((n) => n.endsWith('.sh'))) {
|
|
271
|
-
for (const line of readFileSync(join(__dirname, f), 'utf8').split('\n')) {
|
|
272
|
-
if (line.trim().startsWith('#')) continue
|
|
273
|
-
if (/\bmktemp\s+-t\b/.test(line)) hits.push(`${f}: ${line.trim()}`)
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
expect(hits).toEqual([])
|
|
277
|
-
})
|
|
278
|
-
})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|