@open-dy/vibecoding-cli 0.1.6-beta.0 → 0.1.6-beta.1
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/README.md +2 -2
- package/package.json +3 -3
- package/release-provenance.json +4 -4
- package/src/cli.mjs +8 -3
- package/src/commands/build.mjs +3 -7
- package/src/commands/db.mjs +0 -1
- package/src/commands/lint.mjs +11 -14
- package/src/commands/validate.mjs +5 -2
- package/src/commands/verify.mjs +11 -23
- package/src/core/args.mjs +2 -1
- package/src/core/runner.mjs +6 -27
- package/src/validators/feed-card.cjs +1 -1
- package/src/validators/package-size.mjs +248 -0
- package/src/validators/routes.cjs +3 -4
- package/src/validators/server-artifact.mjs +2 -4
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ vibecoding verify --target taro --auto-start --full
|
|
|
16
16
|
- `taro`:Taro 主应用、Server,以及项目中存在时的 OpenCard。
|
|
17
17
|
- `opencard`:独立 OpenCard 项目。
|
|
18
18
|
|
|
19
|
-
主要命令:`dev`、`build`、`lint`、`verify`、`pack`、`docs`、`turbo`、`init-mysql-db`,以及 `validate feed-card|routes|empty|opencard-structure`。
|
|
19
|
+
主要命令:`dev`、`build`、`lint`、`verify`、`pack`、`docs`、`turbo`、`init-mysql-db`,以及 `validate feed-card|routes|empty|opencard-structure|package-size`。
|
|
20
20
|
|
|
21
21
|
完整 SDK API 文档随 SDK npm 包和 Skill 交付,优先按 `${SKILL_DIR}/sdk-docs/versions.json` 核对版本,再查对应端的 `llms.txt`。`vibecoding docs --target taro`(OpenCard 使用 `--target opencard`,可指定 `--cwd <工程目录>`)只读定位实际安装的 SDK,输出 JSON:依赖键、版本、包位置、包内文档索引及公开类型路径。`ready` 表示包内存在文档索引;`docs-missing` 表示已安装的包没有内置索引,继续查 Skill 文档,版本不一致或信息不足时按 `exports` / `types` 查公开入口。业务导入使用 `imports`,npm 别名以依赖键为前缀,不能换成包的 `name`。包尚不可用时最多等待 `--timeout` 秒(默认 90),超时返回 `SDK_NOT_INSTALLED`,应检查工程安装结果。命令不会安装、重建 SDK 或启动服务。
|
|
22
22
|
|
|
@@ -40,7 +40,7 @@ CLI 内置 Schema 与通用小程序模板支持可选 `BgMusicUrl`;填写时
|
|
|
40
40
|
|
|
41
41
|
服务包仅包含 `dist`、生成的 `Dockerfile`、`package.json`、`run.sh` 以及可选的 `sql`、`scripts`。生成 Dockerfile 时按原脚本规则为每条 `COPY package.json` 追加存在的目录,项目源文件不改写。构建/zip 失败保留退出码,临时目录在失败时也会清理。
|
|
42
42
|
|
|
43
|
-
`verify` 的静态阶段在构建通过后,校验主工程 `server.zip` 的文件数(阈值 `1000`,超限失败),不生成 ZIP
|
|
43
|
+
`verify` 的静态阶段在构建通过后,校验主工程 `server.zip` 的文件数(阈值 `1000`,超限失败),不生成 ZIP 或计算服务端压缩包大小。随后对 tt 产物跑 `tma project-size`:落地页 `dist-tt`,存在 OpenCard 时再查 `opencard/dist`,独立 `--target opencard` 查该工程 `dist`。限额为总包 16MB。`tma` 不在 PATH 时安装 `tt-ide-cli@0.1.34`。单独复检用 `vibecoding validate package-size --target taro`,要求产物已存在;`--json` 输出判定结果,`--tma <path>` 指定二进制。标准 CLI 模板与打包共享黑名单:任意层级过滤依赖、缓存、测试目录、环境文件和私钥,保留其余业务资源。自定义 `scripts/pack.sh` 支持 `REQUIRED_SERVER_PATHS` / `SERVER_PACK_EXCLUDES` / `SERVER_PACK_EXCLUDE_GLOBS`,兼容旧的 `REQUIRED_SERVER_INCLUDES` / `OPTIONAL_SERVER_INCLUDES`;脚本缺失或无法解析、服务目录或必需文件缺失均失败。`pack` 对普通和封闭域服务应用同一规则,并重写各自的 `.dockerignore`,使 ZIP 与镜像包含相同资源。静态检查不等待 dev 的安装成功日志;自动启动预览后仍等待安装与端口就绪。
|
|
44
44
|
|
|
45
45
|
浏览器验收优先使用 `agent-browser-real`,不可用时使用 `agent-browser`。浏览器命令只在验收阶段查找;CDP / 浏览器基础设施失败会报告 `infra_browser` 和 `runtime_status=unverified`,继续以非零退出码结束,页面与业务错误仍按实际错误判定。
|
|
46
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-dy/vibecoding-cli",
|
|
3
|
-
"version": "0.1.6-beta.
|
|
3
|
+
"version": "0.1.6-beta.1",
|
|
4
4
|
"description": "Command orchestration, validation and packaging for Douyin vibecoding Taro and OpenCard projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"ajv": "^8.18.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@open-dy/vibecoding-sdk-taro": "0.1.
|
|
47
|
-
"@open-dy/vibecoding-sdk-opencard": "0.1.
|
|
46
|
+
"@open-dy/vibecoding-sdk-taro": "0.1.0-beta.0",
|
|
47
|
+
"@open-dy/vibecoding-sdk-opencard": "0.1.3-beta.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
|
50
50
|
"@open-dy/vibecoding-sdk-taro": {
|
package/release-provenance.json
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"producer": "vibecoding-cli",
|
|
4
4
|
"packageName": "@open-dy/vibecoding-cli",
|
|
5
|
-
"version": "0.1.6-beta.
|
|
5
|
+
"version": "0.1.6-beta.1",
|
|
6
6
|
"source": {
|
|
7
|
-
"digest": "sha256-
|
|
8
|
-
"files":
|
|
7
|
+
"digest": "sha256-ff1ee2d57d7028ec10a7b6e38ff6fa605a076d665d8667ee0c168564b3f7d57e",
|
|
8
|
+
"files": 88
|
|
9
9
|
},
|
|
10
|
-
"payload": "sha256-
|
|
10
|
+
"payload": "sha256-fdb8c0900115ffe7138745c6b71545dca209f4a2053a881de779fe2da78f1912",
|
|
11
11
|
"toolchain": {
|
|
12
12
|
"node": "v22.23.1",
|
|
13
13
|
"packageManager": null
|
package/src/cli.mjs
CHANGED
|
@@ -20,7 +20,7 @@ Commands:
|
|
|
20
20
|
build Build the selected target
|
|
21
21
|
lint Type-check, lint and validate source structure
|
|
22
22
|
verify Run static and/or runtime verification
|
|
23
|
-
validate <name> Run feed-card, routes, empty or
|
|
23
|
+
validate <name> Run feed-card, routes, empty, opencard-structure or package-size
|
|
24
24
|
init-mysql-db Initialize the Taro server database
|
|
25
25
|
pack Build deployment archives
|
|
26
26
|
docs Locate the selected project's installed SDK reference
|
|
@@ -41,7 +41,9 @@ Options:
|
|
|
41
41
|
--ports-only Verify listening ports only
|
|
42
42
|
--auto-start Start dev once when verify finds missing ports
|
|
43
43
|
--timeout <seconds> Dev startup / docs dependency wait timeout (default: 90)
|
|
44
|
-
--frontend-url <url> Override the primary runtime-check URL
|
|
44
|
+
--frontend-url <url> Override the primary runtime-check URL
|
|
45
|
+
--json Emit JSON from validate package-size
|
|
46
|
+
--tma <path> Use an explicit tma binary for package-size`
|
|
45
47
|
|
|
46
48
|
function assertNoExtra(positionals, count, command) {
|
|
47
49
|
if (positionals.length > count) throw new UsageError(`Unexpected argument for ${command}: ${positionals[count]}`)
|
|
@@ -101,7 +103,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
101
103
|
case 'init-mysql-db': assertNoExtra(positionals, 1, command); return runDatabaseInit(context)
|
|
102
104
|
case 'validate':
|
|
103
105
|
assertNoExtra(positionals, 2, command)
|
|
104
|
-
return runValidate(context, positionals[1]
|
|
106
|
+
return runValidate(context, positionals[1], {
|
|
107
|
+
json: Boolean(flags.json),
|
|
108
|
+
tmaBin: flags.tma,
|
|
109
|
+
})
|
|
105
110
|
default: throw new UsageError(`Unknown command: ${command}\n\n${HELP}`)
|
|
106
111
|
}
|
|
107
112
|
}
|
package/src/commands/build.mjs
CHANGED
|
@@ -24,7 +24,6 @@ export async function buildTaroFrontend(context, type, options = {}) {
|
|
|
24
24
|
cwd: context.taroRoot,
|
|
25
25
|
env: taroEnvironment(context.taroRoot, options.env),
|
|
26
26
|
label: `taro ${type} build`,
|
|
27
|
-
quiet: options.quiet,
|
|
28
27
|
})
|
|
29
28
|
if (type === 'tt' && !options.watch) {
|
|
30
29
|
writeWebConfig(path.join(context.taroRoot, 'dist-tt'))
|
|
@@ -46,7 +45,6 @@ export async function buildOpenCard(context, options = {}) {
|
|
|
46
45
|
...cardEnvironment,
|
|
47
46
|
}),
|
|
48
47
|
label: 'opencard tt build',
|
|
49
|
-
quiet: options.quiet,
|
|
50
48
|
})
|
|
51
49
|
if (!options.watch) {
|
|
52
50
|
writeWebConfig(path.join(root, options.env?.TT_OUTPUT_ROOT?.trim() || 'dist'))
|
|
@@ -64,7 +62,6 @@ export function buildServer(context, options = {}) {
|
|
|
64
62
|
cwd: context.taroRoot,
|
|
65
63
|
env: options.env,
|
|
66
64
|
label: 'server build',
|
|
67
|
-
quiet: options.quiet,
|
|
68
65
|
})
|
|
69
66
|
}
|
|
70
67
|
|
|
@@ -73,7 +70,6 @@ export function buildClosedDomainServer(context, options = {}) {
|
|
|
73
70
|
cwd: context.taroRoot,
|
|
74
71
|
env: options.env,
|
|
75
72
|
label: 'closed-domain-server build',
|
|
76
|
-
quiet: options.quiet,
|
|
77
73
|
})
|
|
78
74
|
}
|
|
79
75
|
|
|
@@ -94,7 +90,7 @@ export async function buildPackInputs(context, options = {}) {
|
|
|
94
90
|
if (context.hasOpenCard) {
|
|
95
91
|
tasks.push({ name: 'opencard', run: () => buildOpenCard(context, options) })
|
|
96
92
|
}
|
|
97
|
-
await runTasks(tasks, { label: 'pack build'
|
|
93
|
+
await runTasks(tasks, { label: 'pack build' })
|
|
98
94
|
}
|
|
99
95
|
|
|
100
96
|
export async function runBuild(context, options = {}) {
|
|
@@ -140,6 +136,6 @@ export async function runBuild(context, options = {}) {
|
|
|
140
136
|
if (context.hasOpenCard) {
|
|
141
137
|
tasks.push({ name: 'opencard', run: () => buildOpenCard(context, buildOptions) })
|
|
142
138
|
}
|
|
143
|
-
await runTasks(tasks, { label: 'build'
|
|
144
|
-
validateRoutes(context
|
|
139
|
+
await runTasks(tasks, { label: 'build' })
|
|
140
|
+
validateRoutes(context)
|
|
145
141
|
}
|
package/src/commands/db.mjs
CHANGED
package/src/commands/lint.mjs
CHANGED
|
@@ -106,21 +106,21 @@ export async function lintOpenCard(context, options = {}) {
|
|
|
106
106
|
{ name: 'opencard-structure', run: async () => validateOpenCardStructure(validationContext) },
|
|
107
107
|
{ name: 'opencard-widget-shell', run: async () => validateWidgetShell(validationContext) },
|
|
108
108
|
{ name: 'opencard-lynx-css', run: () => runLynxCss(root, options.lintProject) },
|
|
109
|
-
{ name: 'opencard-tsc', run: () => runProcess(tsc, ['--noEmit'], { cwd: root
|
|
109
|
+
{ name: 'opencard-tsc', run: () => runProcess(tsc, ['--noEmit'], { cwd: root }) },
|
|
110
110
|
]
|
|
111
111
|
if (eslintSelection.runEslint) {
|
|
112
112
|
tasks.push({
|
|
113
113
|
name: 'opencard-eslint',
|
|
114
|
-
run: () => runProcess(eslint, eslintArgs, { cwd: root
|
|
114
|
+
run: () => runProcess(eslint, eslintArgs, { cwd: root }),
|
|
115
115
|
})
|
|
116
116
|
}
|
|
117
|
-
await runTasks(tasks, { label: 'opencard lint'
|
|
117
|
+
await runTasks(tasks, { label: 'opencard lint' })
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
export async function runLint(context, options = {}) {
|
|
121
121
|
const incremental = options.incremental || process.env.LINT_INCREMENTAL === '1'
|
|
122
122
|
if (context.target === 'opencard') {
|
|
123
|
-
await lintOpenCard(context, { incremental
|
|
123
|
+
await lintOpenCard(context, { incremental })
|
|
124
124
|
return
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -142,8 +142,8 @@ export async function runLint(context, options = {}) {
|
|
|
142
142
|
const tasks = []
|
|
143
143
|
if (full || changed.some((file) => file.startsWith('src/'))) {
|
|
144
144
|
tasks.push(
|
|
145
|
-
{ name: 'taro-tsc', run: () => runProcess(tsc, ['--noEmit'], { cwd: context.root
|
|
146
|
-
{ name: 'taro-eslint', run: () => runProcess(eslint, eslintArgs, { cwd: context.root
|
|
145
|
+
{ name: 'taro-tsc', run: () => runProcess(tsc, ['--noEmit'], { cwd: context.root }) },
|
|
146
|
+
{ name: 'taro-eslint', run: () => runProcess(eslint, eslintArgs, { cwd: context.root }) },
|
|
147
147
|
)
|
|
148
148
|
}
|
|
149
149
|
if (full || serverChanged || closedDomainChanged) {
|
|
@@ -156,7 +156,7 @@ export async function runLint(context, options = {}) {
|
|
|
156
156
|
tasks.push({
|
|
157
157
|
name: 'server-lint',
|
|
158
158
|
run: () => runTasks([
|
|
159
|
-
{ name: 'server-tsc', run: () => runProcess(tsc, ['--noEmit'], { cwd: context.serverRoot
|
|
159
|
+
{ name: 'server-tsc', run: () => runProcess(tsc, ['--noEmit'], { cwd: context.serverRoot }) },
|
|
160
160
|
{
|
|
161
161
|
name: 'server-eslint',
|
|
162
162
|
run: () => runProcess(eslint, [
|
|
@@ -164,16 +164,13 @@ export async function runLint(context, options = {}) {
|
|
|
164
164
|
'--cache-location',
|
|
165
165
|
'node_modules/.cache/eslint/server-src',
|
|
166
166
|
'src/**/*.{js,jsx,ts,tsx}',
|
|
167
|
-
], { cwd: context.serverRoot
|
|
167
|
+
], { cwd: context.serverRoot }),
|
|
168
168
|
},
|
|
169
|
-
], { label: 'server lint'
|
|
169
|
+
], { label: 'server lint' }),
|
|
170
170
|
})
|
|
171
171
|
}
|
|
172
172
|
if (context.hasOpenCard && (full || changed.some((file) => file.startsWith('opencard/')))) {
|
|
173
|
-
tasks.push({
|
|
174
|
-
name: 'opencard-lint',
|
|
175
|
-
run: () => lintOpenCard(context, { incremental: !full, quiet: options.quiet }),
|
|
176
|
-
})
|
|
173
|
+
tasks.push({ name: 'opencard-lint', run: () => lintOpenCard(context, { incremental: !full }) })
|
|
177
174
|
}
|
|
178
|
-
await runTasks(tasks, { label: 'lint'
|
|
175
|
+
await runTasks(tasks, { label: 'lint' })
|
|
179
176
|
}
|
|
@@ -2,9 +2,10 @@ import { UsageError } from '../core/errors.mjs'
|
|
|
2
2
|
import { validateFeedCard } from '../validators/feed-card.mjs'
|
|
3
3
|
import { validateNonEmptyTaro } from '../validators/empty-project.mjs'
|
|
4
4
|
import { validateOpenCardStructure } from '../validators/opencard-structure.mjs'
|
|
5
|
+
import { validatePackageSize } from '../validators/package-size.mjs'
|
|
5
6
|
import { validateRoutes } from '../validators/routes.mjs'
|
|
6
7
|
|
|
7
|
-
export async function runValidate(context, validator) {
|
|
8
|
+
export async function runValidate(context, validator, options = {}) {
|
|
8
9
|
switch (validator) {
|
|
9
10
|
case 'feed-card':
|
|
10
11
|
validateFeedCard(context)
|
|
@@ -18,9 +19,11 @@ export async function runValidate(context, validator) {
|
|
|
18
19
|
case 'opencard-structure':
|
|
19
20
|
validateOpenCardStructure(context)
|
|
20
21
|
return
|
|
22
|
+
case 'package-size':
|
|
23
|
+
return validatePackageSize(context, options)
|
|
21
24
|
default:
|
|
22
25
|
throw new UsageError(
|
|
23
|
-
`Unknown validator: ${validator || '<missing>'}\nAvailable validators: feed-card, routes, empty, opencard-structure`,
|
|
26
|
+
`Unknown validator: ${validator || '<missing>'}\nAvailable validators: feed-card, routes, empty, opencard-structure, package-size`,
|
|
24
27
|
)
|
|
25
28
|
}
|
|
26
29
|
}
|
package/src/commands/verify.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { runBuild } from './build.mjs'
|
|
|
13
13
|
import { runDatabaseInit } from './db.mjs'
|
|
14
14
|
import { runLint } from './lint.mjs'
|
|
15
15
|
import { validateServerArtifactFileCount } from '../validators/server-artifact.mjs'
|
|
16
|
+
import { validatePackageSize } from '../validators/package-size.mjs'
|
|
16
17
|
|
|
17
18
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
18
19
|
const devLauncher = fileURLToPath(new URL('../../bin/vibecoding', import.meta.url))
|
|
@@ -33,23 +34,22 @@ async function runStatic(context) {
|
|
|
33
34
|
validateDependencies(context)
|
|
34
35
|
const env = await readRuntimeEnvironment(context.root)
|
|
35
36
|
const tasks = [
|
|
36
|
-
{ name: 'lint', run: () => runLint(context
|
|
37
|
-
{ name: 'build', run: () => runBuild(context, { frontendTarget: 'tt'
|
|
37
|
+
{ name: 'lint', run: () => runLint(context) },
|
|
38
|
+
{ name: 'build', run: () => runBuild(context, { frontendTarget: 'tt' }) },
|
|
38
39
|
]
|
|
39
40
|
if (context.target === 'taro') {
|
|
40
41
|
tasks.push(
|
|
41
|
-
{ name: 'database', run: () => runDatabaseInit(context, { env
|
|
42
|
-
{ name: 'feed-card', run: async () => validateFeedCard(context
|
|
42
|
+
{ name: 'database', run: () => runDatabaseInit(context, { env }) },
|
|
43
|
+
{ name: 'feed-card', run: async () => validateFeedCard(context) },
|
|
43
44
|
)
|
|
44
45
|
}
|
|
45
|
-
await runTasks(tasks, { label: 'static verification'
|
|
46
|
+
await runTasks(tasks, { label: 'static verification' })
|
|
46
47
|
if (context.target === 'taro') {
|
|
47
48
|
validateServerArtifactFileCount(context.serverRoot ?? path.join(context.root, 'server'), {
|
|
48
49
|
packScriptPath: path.join(context.root, 'scripts/pack.sh'),
|
|
49
|
-
quiet: true,
|
|
50
50
|
})
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
await validatePackageSize(context)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export function startDevDetached(context, spawnProcess = spawn) {
|
|
@@ -625,31 +625,19 @@ export async function runVerify(context, options = {}) {
|
|
|
625
625
|
options = { timeoutSeconds: 90, ...options, full }
|
|
626
626
|
const frontendUrl = resolveFrontendUrl(context, options)
|
|
627
627
|
parseHttpUrl(frontendUrl, 'frontend')
|
|
628
|
-
if (full || options.staticOnly)
|
|
629
|
-
|
|
630
|
-
}
|
|
631
|
-
if (options.staticOnly) {
|
|
632
|
-
console.log('[verify] --static-only: done.')
|
|
633
|
-
return
|
|
634
|
-
}
|
|
628
|
+
if (full || options.staticOnly) await runStatic(context)
|
|
629
|
+
if (options.staticOnly) return
|
|
635
630
|
|
|
636
631
|
await verifyPorts(context, options, frontendUrl)
|
|
637
|
-
if (options.portsOnly)
|
|
638
|
-
|
|
639
|
-
return
|
|
640
|
-
}
|
|
641
|
-
if (full && context.target === 'taro') {
|
|
642
|
-
await verifyFeedApi(context)
|
|
643
|
-
}
|
|
632
|
+
if (options.portsOnly) return
|
|
633
|
+
if (full && context.target === 'taro') await verifyFeedApi(context)
|
|
644
634
|
if (context.target === 'taro' &&
|
|
645
635
|
existsSync(path.resolve(context.root, '../sar_game/package.json'))) {
|
|
646
636
|
console.log('[vibecoding:verify] SAR game project detected; skipping browser runtime checks')
|
|
647
|
-
console.log('[verify] All checks passed.')
|
|
648
637
|
return
|
|
649
638
|
}
|
|
650
639
|
await browserCheck(context.root, frontendUrl, context.target)
|
|
651
640
|
if (context.target === 'taro' && context.hasOpenCard) {
|
|
652
641
|
await browserCheck(context.opencardRoot, 'http://localhost:10087/', 'opencard')
|
|
653
642
|
}
|
|
654
|
-
console.log('[verify] All checks passed.')
|
|
655
643
|
}
|
package/src/core/args.mjs
CHANGED
|
@@ -9,10 +9,11 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
9
9
|
'skip-install',
|
|
10
10
|
'skip-root-install',
|
|
11
11
|
'incremental',
|
|
12
|
+
'json',
|
|
12
13
|
'help',
|
|
13
14
|
])
|
|
14
15
|
|
|
15
|
-
const VALUE_FLAGS = new Set(['target', 'cwd', 'timeout', 'frontend-url', 'scope'])
|
|
16
|
+
const VALUE_FLAGS = new Set(['target', 'cwd', 'timeout', 'frontend-url', 'scope', 'tma'])
|
|
16
17
|
|
|
17
18
|
export function parseArgs(argv) {
|
|
18
19
|
const positionals = []
|
package/src/core/runner.mjs
CHANGED
|
@@ -12,49 +12,28 @@ export function resolveSdkTaroEntry(root) {
|
|
|
12
12
|
return createRequire(path.join(root, 'package.json')).resolve('@open-dy/vibecoding-sdk-opencard/build/taro-toolchain')
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function withCapturedOutput(message, stdout, stderr) {
|
|
16
|
-
if (!stdout && !stderr) return message
|
|
17
|
-
const separator = stdout && stderr && !stdout.endsWith('\n') ? '\n' : ''
|
|
18
|
-
return `${message}\n${stdout}${separator}${stderr}`
|
|
19
|
-
}
|
|
20
|
-
|
|
21
15
|
export function runProcess(command, args = [], options = {}) {
|
|
22
16
|
const label = options.label ?? path.basename(command)
|
|
23
17
|
return new Promise((resolve, reject) => {
|
|
24
|
-
const captureOutput = options.quiet && options.stdio === undefined
|
|
25
18
|
const child = spawn(command, args, {
|
|
26
19
|
cwd: options.cwd,
|
|
27
20
|
env: { ...process.env, ...options.env },
|
|
28
|
-
stdio:
|
|
21
|
+
stdio: options.stdio ?? 'inherit',
|
|
29
22
|
detached: options.detached ?? false,
|
|
30
23
|
shell: options.shell ?? false,
|
|
31
24
|
})
|
|
32
|
-
let stdout = ''
|
|
33
|
-
let stderr = ''
|
|
34
|
-
if (captureOutput) {
|
|
35
|
-
child.stdout.setEncoding('utf8')
|
|
36
|
-
child.stderr.setEncoding('utf8')
|
|
37
|
-
child.stdout.on('data', (chunk) => { stdout += chunk })
|
|
38
|
-
child.stderr.on('data', (chunk) => { stderr += chunk })
|
|
39
|
-
}
|
|
40
25
|
|
|
41
26
|
child.on('error', (error) => {
|
|
42
|
-
reject(new CliError(
|
|
43
|
-
`${label} failed to start: ${error.message}`,
|
|
44
|
-
stdout,
|
|
45
|
-
stderr,
|
|
46
|
-
)))
|
|
27
|
+
reject(new CliError(`${label} failed to start: ${error.message}`))
|
|
47
28
|
})
|
|
48
29
|
child.on('exit', (code, signal) => {
|
|
49
30
|
if (code === 0) {
|
|
50
31
|
resolve({ code: 0, signal, child })
|
|
51
32
|
return
|
|
52
33
|
}
|
|
53
|
-
reject(new CliError(
|
|
34
|
+
reject(new CliError(
|
|
54
35
|
`${label} failed${signal ? ` with signal ${signal}` : ` with exit code ${code}`}`,
|
|
55
|
-
|
|
56
|
-
stderr,
|
|
57
|
-
)))
|
|
36
|
+
))
|
|
58
37
|
})
|
|
59
38
|
})
|
|
60
39
|
}
|
|
@@ -99,9 +78,9 @@ export function runCaptured(command, args = [], options = {}) {
|
|
|
99
78
|
export async function runTasks(tasks, options = {}) {
|
|
100
79
|
const results = await Promise.allSettled(
|
|
101
80
|
tasks.map(async ({ name, run }) => {
|
|
102
|
-
|
|
81
|
+
console.log(`[vibecoding] starting ${name}`)
|
|
103
82
|
await run()
|
|
104
|
-
|
|
83
|
+
console.log(`[vibecoding] completed ${name}`)
|
|
105
84
|
return name
|
|
106
85
|
}),
|
|
107
86
|
)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { accessSync, constants, existsSync, mkdirSync, statSync } from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { CliError } from '../core/errors.mjs'
|
|
6
|
+
import { runCaptured } from '../core/runner.mjs'
|
|
7
|
+
|
|
8
|
+
const MB = 1024 * 1024
|
|
9
|
+
export const PACKAGE_SIZE_LIMITS = Object.freeze({
|
|
10
|
+
totalMax: 16 * MB,
|
|
11
|
+
})
|
|
12
|
+
export const TT_IDE_CLI_SPEC = 'tt-ide-cli@0.1.34'
|
|
13
|
+
const INSTALL_TIMEOUT_MS = 180_000
|
|
14
|
+
const TMA_TIMEOUT_MS = 60_000
|
|
15
|
+
|
|
16
|
+
export function formatPackageBytes(bytes) {
|
|
17
|
+
const value = Number(bytes) || 0
|
|
18
|
+
if (value < 1024) return `${value} B`
|
|
19
|
+
if (value < MB) return `${(value / 1024).toFixed(1)} KB`
|
|
20
|
+
return `${(value / MB).toFixed(2)} MB`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function extractJsonObject(text) {
|
|
24
|
+
const raw = String(text || '')
|
|
25
|
+
const start = raw.indexOf('{')
|
|
26
|
+
const end = raw.lastIndexOf('}')
|
|
27
|
+
if (start < 0 || end <= start) return null
|
|
28
|
+
return raw.slice(start, end + 1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseProjectSizeOutput(stdout, stderr = '') {
|
|
32
|
+
const combined = `${stdout || ''}\n${stderr || ''}`
|
|
33
|
+
if (/Bad Project Type/i.test(combined)) {
|
|
34
|
+
throw new CliError(
|
|
35
|
+
'tma project-size reported Bad Project Type: run it on tt build artifacts (dist-tt or opencard/dist), not source',
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
const jsonText = extractJsonObject(stdout) || extractJsonObject(stderr)
|
|
39
|
+
if (!jsonText) {
|
|
40
|
+
const preview = combined.replace(/\s+/g, ' ').trim().slice(0, 240)
|
|
41
|
+
throw new CliError(`Unable to parse tma project-size --json output${preview ? `: ${preview}` : ''}`)
|
|
42
|
+
}
|
|
43
|
+
let report
|
|
44
|
+
try {
|
|
45
|
+
report = JSON.parse(jsonText)
|
|
46
|
+
} catch {
|
|
47
|
+
throw new CliError(`Invalid tma project-size JSON: ${jsonText.slice(0, 240)}`)
|
|
48
|
+
}
|
|
49
|
+
if (!report || typeof report !== 'object' || typeof report.total !== 'number') {
|
|
50
|
+
throw new CliError(`tma project-size JSON is missing total: ${jsonText.slice(0, 240)}`)
|
|
51
|
+
}
|
|
52
|
+
return report
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function evaluateSizeReport(report, limits = PACKAGE_SIZE_LIMITS) {
|
|
56
|
+
const total = Number(report?.total) || 0
|
|
57
|
+
const failures = []
|
|
58
|
+
if (total > limits.totalMax) {
|
|
59
|
+
failures.push(`total package ${formatPackageBytes(total)} exceeds ${formatPackageBytes(limits.totalMax)}`)
|
|
60
|
+
}
|
|
61
|
+
return { ok: failures.length === 0, failures, total }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function collectPackageSizeTargets(context) {
|
|
65
|
+
if (context.target === 'opencard') {
|
|
66
|
+
return [{
|
|
67
|
+
id: 'opencard',
|
|
68
|
+
label: 'OpenCard',
|
|
69
|
+
artifactRel: 'dist',
|
|
70
|
+
artifactDir: path.join(context.root, 'dist'),
|
|
71
|
+
}]
|
|
72
|
+
}
|
|
73
|
+
const targets = [{
|
|
74
|
+
id: 'landing-page',
|
|
75
|
+
label: 'landing page',
|
|
76
|
+
artifactRel: 'dist-tt',
|
|
77
|
+
artifactDir: path.join(context.root, 'dist-tt'),
|
|
78
|
+
}]
|
|
79
|
+
if (context.hasOpenCard && context.opencardRoot) {
|
|
80
|
+
targets.push({
|
|
81
|
+
id: 'interest-card',
|
|
82
|
+
label: 'interest card',
|
|
83
|
+
artifactRel: 'opencard/dist',
|
|
84
|
+
artifactDir: path.join(context.opencardRoot, 'dist'),
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
return targets
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findOnPath(name, env = process.env) {
|
|
91
|
+
const executableName = process.platform === 'win32' ? `${name}.cmd` : name
|
|
92
|
+
for (const directory of (env.PATH || '').split(path.delimiter)) {
|
|
93
|
+
const executable = path.resolve(directory || '.', executableName)
|
|
94
|
+
try {
|
|
95
|
+
accessSync(executable, constants.X_OK)
|
|
96
|
+
if (statSync(executable).isFile()) return executable
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
return ''
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function resolveCacheRoot(env = process.env) {
|
|
103
|
+
if (existsSync('/workspace')) {
|
|
104
|
+
const workspaceTmp = '/workspace/tmp'
|
|
105
|
+
mkdirSync(workspaceTmp, { recursive: true })
|
|
106
|
+
return workspaceTmp
|
|
107
|
+
}
|
|
108
|
+
return env.TMPDIR || os.tmpdir()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveNpmBin() {
|
|
112
|
+
const sibling = path.join(path.dirname(process.execPath), process.platform === 'win32' ? 'npm.cmd' : 'npm')
|
|
113
|
+
if (existsSync(sibling)) return sibling
|
|
114
|
+
return findOnPath('npm') || 'npm'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function resolveTmaBin(options = {}) {
|
|
118
|
+
if (options.tmaBin) {
|
|
119
|
+
if (!existsSync(options.tmaBin)) {
|
|
120
|
+
throw new CliError(`--tma path does not exist: ${options.tmaBin}`)
|
|
121
|
+
}
|
|
122
|
+
return options.tmaBin
|
|
123
|
+
}
|
|
124
|
+
const fromPath = findOnPath('tma', options.env)
|
|
125
|
+
if (fromPath) return fromPath
|
|
126
|
+
|
|
127
|
+
const cacheRoot = options.cacheRoot ?? resolveCacheRoot(options.env)
|
|
128
|
+
const prefix = path.join(cacheRoot, 'tt-ide-cli')
|
|
129
|
+
const bin = path.join(prefix, 'node_modules', '.bin', process.platform === 'win32' ? 'tma.cmd' : 'tma')
|
|
130
|
+
if (existsSync(bin)) return bin
|
|
131
|
+
|
|
132
|
+
mkdirSync(prefix, { recursive: true })
|
|
133
|
+
const npmCache = path.join(cacheRoot, 'npm-cache')
|
|
134
|
+
mkdirSync(npmCache, { recursive: true })
|
|
135
|
+
const env = {
|
|
136
|
+
...process.env,
|
|
137
|
+
...options.env,
|
|
138
|
+
npm_config_cache: npmCache,
|
|
139
|
+
npm_config_fund: 'false',
|
|
140
|
+
npm_config_audit: 'false',
|
|
141
|
+
npm_config_update_notifier: 'false',
|
|
142
|
+
}
|
|
143
|
+
const registries = [
|
|
144
|
+
env.NPM_CONFIG_REGISTRY,
|
|
145
|
+
env.npm_config_registry,
|
|
146
|
+
'https://bnpm.byted.org',
|
|
147
|
+
'https://registry.npmmirror.com/',
|
|
148
|
+
].filter((value, index, list) => value && list.indexOf(value) === index)
|
|
149
|
+
const npmBin = resolveNpmBin()
|
|
150
|
+
const errors = []
|
|
151
|
+
const runCommand = options.runCommand ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions))
|
|
152
|
+
for (const registry of registries) {
|
|
153
|
+
const result = runCommand(npmBin, ['install', TT_IDE_CLI_SPEC, '--prefix', prefix, '--registry', registry], {
|
|
154
|
+
env,
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
157
|
+
})
|
|
158
|
+
if ((result.status === 0) && existsSync(bin)) return bin
|
|
159
|
+
const detail = String(result.stderr || result.stdout || `exit ${result.status}`).trim().slice(0, 400)
|
|
160
|
+
errors.push(`${registry}: ${detail}`)
|
|
161
|
+
}
|
|
162
|
+
throw new CliError(
|
|
163
|
+
`Unable to install ${TT_IDE_CLI_SPEC} for tma project-size. Confirm npm registry access, or put tma on PATH / pass --tma.\n${errors.join('\n')}`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function artifactReady(artifactDir) {
|
|
168
|
+
return existsSync(path.join(artifactDir, 'app.json')) && (
|
|
169
|
+
existsSync(path.join(artifactDir, 'app.js')) || existsSync(path.join(artifactDir, 'app.ts'))
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function defaultRunTma(tmaBin, artifactDir) {
|
|
174
|
+
return runCaptured(tmaBin, ['project-size', '--json', artifactDir], {
|
|
175
|
+
allowFailure: true,
|
|
176
|
+
timeoutMs: TMA_TIMEOUT_MS,
|
|
177
|
+
label: 'tma project-size',
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @desc 校验 tt 构建产物包体积,必须对 dist-tt / opencard/dist 执行,不能对源码目录执行。
|
|
183
|
+
* @params {object} context CLI 工程上下文。
|
|
184
|
+
* @params {object} [options] 可选 tma 路径、JSON 输出和测试注入。
|
|
185
|
+
* @return {Promise<object>} 各产物的体积判定;超限或缺产物时抛出 CliError。
|
|
186
|
+
* @example await validatePackageSize(context)
|
|
187
|
+
*/
|
|
188
|
+
export async function validatePackageSize(context, options = {}) {
|
|
189
|
+
const targets = collectPackageSizeTargets(context)
|
|
190
|
+
const tmaBin = options.resolveTma
|
|
191
|
+
? await options.resolveTma()
|
|
192
|
+
: resolveTmaBin({ tmaBin: options.tmaBin, env: options.env, runCommand: options.runCommand })
|
|
193
|
+
const runTma = options.runTma ?? defaultRunTma
|
|
194
|
+
const results = []
|
|
195
|
+
|
|
196
|
+
for (const target of targets) {
|
|
197
|
+
const entry = { id: target.id, label: target.label, artifact: target.artifactRel, ok: false }
|
|
198
|
+
try {
|
|
199
|
+
if (!existsSync(target.artifactDir)) {
|
|
200
|
+
throw new CliError(
|
|
201
|
+
`Missing tt artifact directory ${target.artifactRel}. Run pnpm build:tt (or pnpm verify) first.`,
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
if (!artifactReady(target.artifactDir)) {
|
|
205
|
+
throw new CliError(
|
|
206
|
+
`${target.artifactRel} is not a complete tt artifact (missing app.json / app.js). Do not run tma project-size on source or H5 dist.`,
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
const output = await runTma(tmaBin, target.artifactDir)
|
|
210
|
+
const report = parseProjectSizeOutput(output?.stdout, output?.stderr)
|
|
211
|
+
const evaluated = evaluateSizeReport(report)
|
|
212
|
+
entry.total = evaluated.total
|
|
213
|
+
entry.ok = evaluated.ok
|
|
214
|
+
if (!evaluated.ok) {
|
|
215
|
+
entry.failures = evaluated.failures
|
|
216
|
+
throw new CliError(evaluated.failures.join('; '))
|
|
217
|
+
}
|
|
218
|
+
console.log(
|
|
219
|
+
`[vibecoding:package-size] OK ${target.label}: ${formatPackageBytes(evaluated.total)} / ${formatPackageBytes(PACKAGE_SIZE_LIMITS.totalMax)}`,
|
|
220
|
+
)
|
|
221
|
+
} catch (error) {
|
|
222
|
+
entry.ok = false
|
|
223
|
+
entry.message = error instanceof Error ? error.message : String(error)
|
|
224
|
+
console.error(`[vibecoding:package-size] FAIL ${target.label}: ${entry.message}`)
|
|
225
|
+
}
|
|
226
|
+
results.push(entry)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const ok = results.length > 0 && results.every((item) => item.ok)
|
|
230
|
+
const payload = { ok, limits: PACKAGE_SIZE_LIMITS, tma: tmaBin, targets: results }
|
|
231
|
+
if (options.json) {
|
|
232
|
+
console.log(JSON.stringify({
|
|
233
|
+
ok: payload.ok,
|
|
234
|
+
limits: payload.limits,
|
|
235
|
+
targets: payload.targets,
|
|
236
|
+
}, null, 2))
|
|
237
|
+
}
|
|
238
|
+
if (!ok) {
|
|
239
|
+
const details = results
|
|
240
|
+
.filter((item) => !item.ok)
|
|
241
|
+
.map((item) => `${item.label} (${item.artifact}): ${item.message}`)
|
|
242
|
+
.join('\n')
|
|
243
|
+
throw new CliError(
|
|
244
|
+
`Package size check failed.\n${details}\nMove large images/audio/static assets to CDN, rebuild tt artifacts, then re-run.`,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
return payload
|
|
248
|
+
}
|
|
@@ -3,7 +3,6 @@ const path = require('path');
|
|
|
3
3
|
|
|
4
4
|
function validateRoutes(context, options = {}) {
|
|
5
5
|
if (context.target !== 'taro') return;
|
|
6
|
-
const quiet = Boolean(options.quiet);
|
|
7
6
|
const ROOT = context.root;
|
|
8
7
|
const routesDir = options.routesDir || '/tmp/routes';
|
|
9
8
|
const CLIENT_FILE = path.join(routesDir, 'client-routes.json');
|
|
@@ -403,7 +402,7 @@ function main() {
|
|
|
403
402
|
const closedDomainServerRoutes = loadClosedDomainServerRoutes();
|
|
404
403
|
const cardRoutingConfig = loadCardRoutingConfig();
|
|
405
404
|
|
|
406
|
-
|
|
405
|
+
console.log('=== 前后端路由一致性检测报告 ===\n');
|
|
407
406
|
|
|
408
407
|
assertOpenDomainServerDoesNotCallClosedDomainOpenAPI();
|
|
409
408
|
|
|
@@ -452,7 +451,7 @@ function main() {
|
|
|
452
451
|
}
|
|
453
452
|
}
|
|
454
453
|
|
|
455
|
-
|
|
454
|
+
console.log('--- 检测结果 ---\n');
|
|
456
455
|
|
|
457
456
|
if (mismatched.length > 0) {
|
|
458
457
|
console.error(`未匹配(${mismatched.length} 条):以下前端请求路径未匹配到所选服务域的服务端路由,请检查前端请求路径、服务域配置,或确认服务端已注册对应路由`);
|
|
@@ -461,7 +460,7 @@ function main() {
|
|
|
461
460
|
}
|
|
462
461
|
throw new Error('前后端路由一致性检测失败');
|
|
463
462
|
} else {
|
|
464
|
-
|
|
463
|
+
console.log('所有前端静态请求路径均已在服务端注册对应路由,检测通过。');
|
|
465
464
|
}
|
|
466
465
|
}
|
|
467
466
|
|
|
@@ -63,7 +63,7 @@ function countFiles(target, policy) {
|
|
|
63
63
|
(policy.excludes && isServerPackExcluded(name, policy) ? 0 : countFiles(path.join(target, name), policy)), 0)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
export function validateServerArtifactFileCount(serverRoot, { packScriptPath
|
|
66
|
+
export function validateServerArtifactFileCount(serverRoot, { packScriptPath } = {}) {
|
|
67
67
|
if (!existsSync(serverRoot)) throw new CliError(`Server directory is missing: ${serverRoot}`)
|
|
68
68
|
let policy = DEFAULT_POLICY
|
|
69
69
|
if (packScriptPath) {
|
|
@@ -84,8 +84,6 @@ export function validateServerArtifactFileCount(serverRoot, { packScriptPath, qu
|
|
|
84
84
|
if (count > MAX_FILE_COUNT) {
|
|
85
85
|
throw new CliError(`Packed server artifact ${serverRoot} would contain ${count} files, exceeding the limit ${MAX_FILE_COUNT}. This count follows the server pack policy. Reduce files included by pack before submission.`)
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
console.log(`[verify] static:server-artifact-file-count → OK (${count}/${MAX_FILE_COUNT}, ${serverRoot}).`)
|
|
89
|
-
}
|
|
87
|
+
console.log(`[verify] static:server-artifact-file-count → OK (${count}/${MAX_FILE_COUNT}, ${serverRoot}).`)
|
|
90
88
|
return { skipped: false, count, missing: [] }
|
|
91
89
|
}
|