@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 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 或计算压缩包大小。标准 CLI 模板与打包共享黑名单:任意层级过滤依赖、缓存、测试目录、环境文件和私钥,保留其余业务资源。自定义 `scripts/pack.sh` 支持 `REQUIRED_SERVER_PATHS` / `SERVER_PACK_EXCLUDES` / `SERVER_PACK_EXCLUDE_GLOBS`,兼容旧的 `REQUIRED_SERVER_INCLUDES` / `OPTIONAL_SERVER_INCLUDES`;脚本缺失或无法解析、服务目录或必需文件缺失均失败。`pack` 对普通和封闭域服务应用同一规则,并重写各自的 `.dockerignore`,使 ZIP 与镜像包含相同资源。静态检查不等待 dev 的安装成功日志;自动启动预览后仍等待安装与端口就绪。
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.0",
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.1-beta.0",
47
- "@open-dy/vibecoding-sdk-opencard": "0.1.5-beta.0"
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": {
@@ -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.0",
5
+ "version": "0.1.6-beta.1",
6
6
  "source": {
7
- "digest": "sha256-f2018ad1f7d7656392315c81128e717ce478aeb75dedecdec706fe0035cb026d",
8
- "files": 87
7
+ "digest": "sha256-ff1ee2d57d7028ec10a7b6e38ff6fa605a076d665d8667ee0c168564b3f7d57e",
8
+ "files": 88
9
9
  },
10
- "payload": "sha256-e85c65882947a652f0d0ec0b05ad00afe0a9106143ff44467c80bb045b86879c",
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 opencard-structure
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
  }
@@ -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', quiet: options.quiet })
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', quiet: buildOptions.quiet })
144
- validateRoutes(context, { quiet: buildOptions.quiet })
139
+ await runTasks(tasks, { label: 'build' })
140
+ validateRoutes(context)
145
141
  }
@@ -10,6 +10,5 @@ export async function runDatabaseInit(context, options = {}) {
10
10
  cwd: context.serverRoot,
11
11
  env: options.env,
12
12
  label: 'database initialization',
13
- quiet: options.quiet,
14
13
  })
15
14
  }
@@ -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, quiet: options.quiet }) },
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, quiet: options.quiet }),
114
+ run: () => runProcess(eslint, eslintArgs, { cwd: root }),
115
115
  })
116
116
  }
117
- await runTasks(tasks, { label: 'opencard lint', quiet: options.quiet })
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, quiet: options.quiet })
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, quiet: options.quiet }) },
146
- { name: 'taro-eslint', run: () => runProcess(eslint, eslintArgs, { cwd: context.root, quiet: options.quiet }) },
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, quiet: options.quiet }) },
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, quiet: options.quiet }),
167
+ ], { cwd: context.serverRoot }),
168
168
  },
169
- ], { label: 'server lint', quiet: options.quiet }),
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', quiet: options.quiet })
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
  }
@@ -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, { quiet: true }) },
37
- { name: 'build', run: () => runBuild(context, { frontendTarget: 'tt', quiet: true }) },
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, quiet: true }) },
42
- { name: 'feed-card', run: async () => validateFeedCard(context, { quiet: true }) },
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', quiet: true })
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
- console.log('[verify] Static phase passed (all parallel tasks OK).')
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
- await runStatic(context)
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
- console.log('[verify] --ports-only: done.')
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 = []
@@ -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: captureOutput ? ['ignore', 'pipe', 'pipe'] : (options.stdio ?? 'inherit'),
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(withCapturedOutput(
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(withCapturedOutput(
34
+ reject(new CliError(
54
35
  `${label} failed${signal ? ` with signal ${signal}` : ` with exit code ${code}`}`,
55
- stdout,
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
- if (!options.quiet) console.log(`[vibecoding] starting ${name}`)
81
+ console.log(`[vibecoding] starting ${name}`)
103
82
  await run()
104
- if (!options.quiet) console.log(`[vibecoding] completed ${name}`)
83
+ console.log(`[vibecoding] completed ${name}`)
105
84
  return name
106
85
  }),
107
86
  )
@@ -149,7 +149,7 @@ function validateFeedCard(context, options = {}) {
149
149
  }
150
150
  }
151
151
 
152
- if (!options.quiet) console.log("All validations passed!");
152
+ console.log("All validations passed!");
153
153
  return { skipped: false };
154
154
  }
155
155
 
@@ -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
- if (!quiet) console.log('=== 前后端路由一致性检测报告 ===\n');
405
+ console.log('=== 前后端路由一致性检测报告 ===\n');
407
406
 
408
407
  assertOpenDomainServerDoesNotCallClosedDomainOpenAPI();
409
408
 
@@ -452,7 +451,7 @@ function main() {
452
451
  }
453
452
  }
454
453
 
455
- if (!quiet) console.log('--- 检测结果 ---\n');
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
- if (!quiet) console.log('所有前端静态请求路径均已在服务端注册对应路由,检测通过。');
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, quiet = false } = {}) {
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
- if (!quiet) {
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
  }