@open-dy/vibecoding-cli 0.0.1-beta.5 → 0.0.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
@@ -32,6 +32,10 @@ Schema 统一使用 `custom-card-schema.json`。`config/card-routing.json` 的 `
32
32
 
33
33
  服务包仅包含 `dist`、生成的 `Dockerfile`、`package.json`、`run.sh` 以及可选的 `sql`、`scripts`。生成 Dockerfile 时按原脚本规则为每条 `COPY package.json` 追加存在的目录,项目源文件不改写。构建/zip 失败保留退出码,临时目录在失败时也会清理。
34
34
 
35
+ `verify` 的静态阶段在构建通过后,保留主工程 `server.zip` 的文件数预检(阈值 `1000`,超限失败)。标准 CLI 模板使用打包共用的清单;自定义 `scripts/pack.sh` 使用其 `REQUIRED_SERVER_INCLUDES` / `OPTIONAL_SERVER_INCLUDES`。脚本缺失或清单无法解析时,warning 并保守统计整个 `server/`;缺少目录或必需文件时明确报告未覆盖范围。检查范围不扩大到封闭域服务,不生成 ZIP,也不验证压缩包大小。静态检查不等待 dev 的安装成功日志;自动启动预览后仍等待安装与端口就绪。
36
+
37
+ 浏览器验收优先使用 `agent-browser-real`,不可用时使用 `agent-browser`。浏览器命令只在验收阶段查找;CDP / 浏览器基础设施失败会报告 `infra_browser` 和 `runtime_status=unverified`,继续以非零退出码结束,页面与业务错误仍按实际错误判定。
38
+
35
39
  CLI 支持用 `ARTIFACTS_DIR` 指定专用产物目录;为避免误删,拒绝项目、用户主目录、临时目录根及其上级目录。模板的 `pack.sh` 不加载 `.env.dev` 或 `.bashrc`,未传入该变量时默认写入 `/tmp/microapp_artifacts`。
36
40
 
37
41
  Taro 构建统一由 CLI 编排,兴趣卡只在业务域规则上扩展:
@@ -46,4 +50,4 @@ CLI 对外命令和校验入口的 JSDoc 使用 `@desc`、`@params`、`@return`
46
50
 
47
51
  ## 准出测试
48
52
 
49
- 在仓库根目录运行 `node packages/scripts/run-release-contracts.mjs`,或在本包目录运行 `pnpm test:release`。该命令构建并验证 SDK、CLI 和隔离消费者;终端进度、JUnit 报告与 push CI 配置见 [packages/TESTING.md](../TESTING.md)。
53
+ 在仓库根目录运行 `node packages/release/verification/run-release-contracts.mjs`,或在本包目录运行 `pnpm test:release`。该命令构建并验证 SDK、CLI 和隔离消费者;终端进度、JUnit 报告与 push CI 配置见 [packages/TESTING.md](../TESTING.md)。
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "CardBackgroundUrl": {
33
33
  "type": "string",
34
- "description": "必须调用图片生成工具获取真实的图片 URL,自动生成符合预期的背景图片。此图片将作为 Feed 流场景的全屏背景底图,画面内容必须延伸至画面边缘,完全铺满整个画布。严禁出现手机设备外框、样机模型、任何形式的边框,严禁将内容仅集中在中间而导致四周留白或黑边。图片宽高比严格为`9:16`竖图。必须完整保留原始 URL 及全部 query 参数,禁止截断、改写或丢弃任何参数(如 x-expires)。",
34
+ "description": "必须调用图片生成工具获取真实的图片 URL,自动生成符合预期的背景图片。此图片将作为 Feed 流场景的全屏背景底图,画面内容必须延伸至画面边缘,完全铺满整个画布。严禁出现手机设备外框、样机模型、任何形式的边框,严禁将内容仅集中在中间而导致四周留白或黑边。图片宽高比严格为`9:16`竖图。",
35
35
  "pattern": "^https://"
36
36
  },
37
37
  "Button": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-dy/vibecoding-cli",
3
- "version": "0.0.1-beta.5",
3
+ "version": "0.0.1",
4
4
  "description": "Command orchestration, validation and packaging for Douyin vibecoding Taro and OpenCard projects.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,26 +24,27 @@
24
24
  "bin",
25
25
  "src",
26
26
  "assets",
27
- "README.md"
27
+ "README.md",
28
+ "release-provenance.json"
28
29
  ],
29
30
  "scripts": {
30
31
  "ci:check": "pnpm run build && pnpm run test",
31
32
  "build": "node ./scripts/check-package-files.mjs",
32
- "test": "node ../scripts/run-package-tests.mjs cli",
33
+ "test": "node ../testing/run-package-tests.mjs cli",
33
34
  "pack:local": "pnpm pack --pack-destination ./artifacts",
34
35
  "pack:ppe": "node ./scripts/pack-ppe.mjs",
35
- "test:release": "node ../scripts/run-release-contracts.mjs",
36
- "publish:bnpm:ppe": "node ../scripts/ppe-release/cli.mjs vibecoding-cli",
37
- "publish:bnpm:ppe:dry-run": "node ../scripts/ppe-release/cli.mjs vibecoding-cli --dry-run",
38
- "publish:npm:beta": "node ../scripts/npm-beta-release/cli.ts vibecoding-cli",
39
- "publish:npm:beta:dry-run": "node ../scripts/npm-beta-release/cli.ts vibecoding-cli --dry-run"
36
+ "test:release": "node ../release/verification/run-release-contracts.mjs",
37
+ "publish:bnpm:ppe": "node ../release/ppe-release/cli.mjs vibecoding-cli",
38
+ "publish:bnpm:ppe:dry-run": "node ../release/ppe-release/cli.mjs vibecoding-cli --dry-run",
39
+ "publish:npm:beta": "node ../release/npm-beta-release/cli.ts vibecoding-cli",
40
+ "publish:npm:beta:dry-run": "node ../release/npm-beta-release/cli.ts vibecoding-cli --dry-run"
40
41
  },
41
42
  "dependencies": {
42
43
  "ajv": "^8.18.0"
43
44
  },
44
45
  "peerDependencies": {
45
- "@open-dy/vibecoding-sdk-taro": "0.0.1-beta.4",
46
- "@open-dy/vibecoding-sdk-opencard": "0.0.1-beta.4"
46
+ "@open-dy/vibecoding-sdk-taro": "0.0.1",
47
+ "@open-dy/vibecoding-sdk-opencard": "0.0.1"
47
48
  },
48
49
  "peerDependenciesMeta": {
49
50
  "@open-dy/vibecoding-sdk-taro": {
@@ -59,7 +60,7 @@
59
60
  "license": "UNLICENSED",
60
61
  "publishConfig": {
61
62
  "registry": "https://registry.npmjs.org/",
62
- "tag": "beta",
63
+ "tag": "latest",
63
64
  "access": "public"
64
65
  }
65
66
  }
@@ -0,0 +1,15 @@
1
+ {
2
+ "schema": 1,
3
+ "producer": "vibecoding-cli",
4
+ "packageName": "@open-dy/vibecoding-cli",
5
+ "version": "0.0.1",
6
+ "source": {
7
+ "digest": "sha256-ba8401764d5a9fc23bda57f557862542fda9f4187adf2fcdd6625d458d623936",
8
+ "files": 83
9
+ },
10
+ "payload": "sha256-30e2e1be2593d5913151c6b0c2d488e3cc07ce685341a3446c6caa93df95e801",
11
+ "toolchain": {
12
+ "node": "v22.22.3",
13
+ "packageManager": null
14
+ }
15
+ }
@@ -17,6 +17,7 @@ import { CliError } from '../core/errors.mjs'
17
17
  import { resolveProjectContext } from '../core/context.mjs'
18
18
  import { buildOpenCard, buildPackInputs } from './build.mjs'
19
19
  import { readCardRouting } from '../build/opencard-environment.mjs'
20
+ import { REQUIRED_SERVER_INCLUDES, OPTIONAL_SERVER_INCLUDES } from '../validators/server-artifact.mjs'
20
21
 
21
22
  const DEFAULT_ARTIFACTS = '/tmp/microapp_artifacts'
22
23
 
@@ -72,13 +73,12 @@ async function zipDirectory(directory, output, { removeExisting = true } = {}) {
72
73
 
73
74
  function createServerStage(server, stage) {
74
75
  if (!isDirectory(server)) throw new CliError(`Server pack input is missing: ${server}`)
75
- const required = ['dist', 'Dockerfile', 'package.json', 'run.sh']
76
- for (const name of required) {
76
+ for (const name of REQUIRED_SERVER_INCLUDES) {
77
77
  if (!existsSync(path.join(server, name))) {
78
78
  throw new CliError(`Server pack input is missing: ${path.join(server, name)}`)
79
79
  }
80
80
  }
81
- for (const name of ['dist', 'package.json', 'run.sh', 'sql', 'scripts']) {
81
+ for (const name of [...REQUIRED_SERVER_INCLUDES, ...OPTIONAL_SERVER_INCLUDES].filter(name => name !== 'Dockerfile')) {
82
82
  const source = path.join(server, name)
83
83
  if (existsSync(source)) cpSync(source, path.join(stage, name), { recursive: true })
84
84
  }
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process'
2
- import { existsSync, readFileSync } from 'node:fs'
2
+ import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { CliError } from '../core/errors.mjs'
@@ -12,6 +12,7 @@ import { readCardRouting } from '../build/opencard-environment.mjs'
12
12
  import { runBuild } from './build.mjs'
13
13
  import { runDatabaseInit } from './db.mjs'
14
14
  import { runLint } from './lint.mjs'
15
+ import { validateServerArtifactFileCount } from '../validators/server-artifact.mjs'
15
16
 
16
17
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
17
18
  const devLauncher = fileURLToPath(new URL('../../bin/vibecoding', import.meta.url))
@@ -42,6 +43,11 @@ async function runStatic(context) {
42
43
  )
43
44
  }
44
45
  await runTasks(tasks, { label: 'static verification' })
46
+ if (context.target === 'taro') {
47
+ validateServerArtifactFileCount(context.serverRoot ?? path.join(context.root, 'server'), {
48
+ packScriptPath: path.join(context.root, 'scripts/pack.sh'),
49
+ })
50
+ }
45
51
  }
46
52
 
47
53
  export function startDevDetached(context, spawnProcess = spawn) {
@@ -522,6 +528,23 @@ export function isIgnorableOpenCardConsoleFinding(value, options) {
522
528
  /Failed to load resource: the server responded with a status of 404/i.test(text)
523
529
  }
524
530
 
531
+ export function resolveAgentBrowserCommand(env = process.env) {
532
+ for (const name of ['agent-browser-real', 'agent-browser']) {
533
+ for (const directory of (env.PATH || '').split(path.delimiter)) {
534
+ const executable = path.resolve(directory || '.', name)
535
+ try {
536
+ accessSync(executable, constants.X_OK)
537
+ if (statSync(executable).isFile()) return executable
538
+ } catch {}
539
+ }
540
+ }
541
+ return 'agent-browser'
542
+ }
543
+
544
+ export function isBrowserInfrastructureError(error) {
545
+ return /All CDP discovery methods failed|(?:CDP|DevTools|WebSocket).*(?:fail|refus|timeout|timed out|closed|unavailable)|Protocol error.*(?:Browser|Page|Target)\.|(?:failed|unable) to (?:launch|start) (?:the )?(?:browser|chrome|chromium)|spawn .*agent-browser.*ENOENT/i.test(textOf(error?.message || error))
546
+ }
547
+
525
548
  async function browserCheck(root, url, label) {
526
549
  if (packageUses(root, '@open-dy/threejs')) {
527
550
  console.log(`[vibecoding:verify] ${label}: skipping browser runtime check for @open-dy/threejs`)
@@ -529,7 +552,19 @@ async function browserCheck(root, url, label) {
529
552
  }
530
553
  parseHttpUrl(url, label)
531
554
  const session = process.env.AGENT_BROWSER_SESSION || `vibecoding-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
532
- const call = (args, allowFailure = false) => runCaptured('agent-browser', ['--session', session, ...args], { allowFailure })
555
+ const command = resolveAgentBrowserCommand()
556
+ const call = async (args, allowFailure = false) => {
557
+ try {
558
+ return await runCaptured(command, ['--session', session, ...args], {
559
+ allowFailure, timeoutMs: args[0] === 'close' ? 5000 : 120_000, label: `agent-browser ${args[0]}`,
560
+ })
561
+ } catch (error) {
562
+ if (isBrowserInfrastructureError(error)) {
563
+ throw new CliError(`infra_browser: Browser infrastructure unavailable; runtime_status=unverified for ${url}. ${error.message}`)
564
+ }
565
+ throw error
566
+ }
567
+ }
533
568
  try {
534
569
  await call(['network', 'requests'], true)
535
570
  await call(['open', url])
@@ -565,7 +600,9 @@ async function browserCheck(root, url, label) {
565
600
  throw new CliError(`${label} runtime check found ${findings.length} issue(s):\n${findings.map((item) => `- ${flatten(item)}`).join('\n')}`)
566
601
  }
567
602
  } finally {
568
- await call(['close'], true)
603
+ await call(['close'], true).catch((error) => {
604
+ console.log(`[vibecoding:verify] browser cleanup failed: ${error.message}`)
605
+ })
569
606
  }
570
607
  }
571
608
 
@@ -586,7 +623,6 @@ export async function runVerify(context, options = {}) {
586
623
  options = { timeoutSeconds: 90, ...options, full }
587
624
  const frontendUrl = resolveFrontendUrl(context, options)
588
625
  parseHttpUrl(frontendUrl, 'frontend')
589
- if (full) await waitForDevInstall(context, options.timeoutSeconds)
590
626
  if (full || options.staticOnly) await runStatic(context)
591
627
  if (options.staticOnly) return
592
628
 
@@ -45,6 +45,8 @@ export function runCaptured(command, args = [], options = {}) {
45
45
  env: { ...process.env, ...options.env },
46
46
  stdio: ['ignore', 'pipe', 'pipe'],
47
47
  shell: false,
48
+ signal: options.timeoutMs > 0 ? AbortSignal.timeout(options.timeoutMs) : undefined,
49
+ killSignal: 'SIGKILL',
48
50
  })
49
51
  let stdout = ''
50
52
  let stderr = ''
@@ -52,7 +54,15 @@ export function runCaptured(command, args = [], options = {}) {
52
54
  child.stderr.setEncoding('utf8')
53
55
  child.stdout.on('data', (chunk) => { stdout += chunk })
54
56
  child.stderr.on('data', (chunk) => { stderr += chunk })
55
- child.on('error', (error) => reject(new CliError(error.message)))
57
+ child.on('error', (error) => {
58
+ if (error.code === 'ABORT_ERR') {
59
+ child.stdout.destroy()
60
+ child.stderr.destroy()
61
+ reject(new CliError(`${options.label ?? path.basename(command)} timed out after ${options.timeoutMs}ms\n${stderr || stdout}`))
62
+ } else {
63
+ reject(new CliError(error.message))
64
+ }
65
+ })
56
66
  child.on('exit', (code) => {
57
67
  if (code === 0 || options.allowFailure) {
58
68
  resolve({ code: code ?? 1, stdout, stderr })
@@ -0,0 +1,69 @@
1
+ import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { CliError } from '../core/errors.mjs'
4
+
5
+ // Shared with pack: count only paths copied into each server archive.
6
+ export const REQUIRED_SERVER_INCLUDES = ['dist', 'Dockerfile', 'package.json', 'run.sh']
7
+ export const OPTIONAL_SERVER_INCLUDES = ['sql', 'scripts']
8
+ const MAX_FILE_COUNT = 1000
9
+ const CLI_PACK_WRAPPER = [
10
+ 'set -Eeuo pipefail',
11
+ 'ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"',
12
+ 'cd "$ROOT"',
13
+ `CLI_ROOT="$(node -p 'require("node:path").dirname(require.resolve("@open-dy/vibecoding-cli/package.json"))')"`,
14
+ 'exec node "$CLI_ROOT/src/scripts/pack.mjs" "$ROOT"',
15
+ ].join('\n')
16
+
17
+ function readPackIncludes(packScriptPath) {
18
+ const script = readFileSync(packScriptPath, 'utf8')
19
+ const normalized = script.split(/\r?\n/).map(line => line.trim())
20
+ .filter(line => line && !line.startsWith('#')).join('\n')
21
+ if (normalized === CLI_PACK_WRAPPER) {
22
+ return { required: REQUIRED_SERVER_INCLUDES, optional: OPTIONAL_SERVER_INCLUDES }
23
+ }
24
+ const parse = name => {
25
+ const match = script.match(new RegExp(`^\\s*${name}=\\(([^)]*)\\)`, 'm'))
26
+ if (!match) throw new Error(`scripts/pack.sh does not define ${name}`)
27
+ return (match[1].match(/(?:"[^"]*"|'[^']*'|\S)+/g) || [])
28
+ .map(item => item.replace(/^['"]|['"]$/g, ''))
29
+ }
30
+ return { required: parse('REQUIRED_SERVER_INCLUDES'), optional: parse('OPTIONAL_SERVER_INCLUDES') }
31
+ }
32
+
33
+ function countFiles(target) {
34
+ if (!lstatSync(target).isDirectory()) return 1
35
+ return readdirSync(target).reduce((total, name) => total + countFiles(path.join(target, name)), 0)
36
+ }
37
+
38
+ export function validateServerArtifactFileCount(serverRoot, { packScriptPath } = {}) {
39
+ if (!existsSync(serverRoot)) {
40
+ console.warn(`[verify] static:server-artifact-file-count skipped missing server directory: ${serverRoot}.`)
41
+ return { skipped: true }
42
+ }
43
+ let includes = { required: REQUIRED_SERVER_INCLUDES, optional: OPTIONAL_SERVER_INCLUDES }
44
+ let countSource = 'the CLI server archive includes'
45
+ if (packScriptPath) {
46
+ try {
47
+ includes = readPackIncludes(packScriptPath)
48
+ countSource = `${packScriptPath} server.zip includes: ${[...includes.required, ...includes.optional].join(', ')}`
49
+ } catch (error) {
50
+ includes = null
51
+ countSource = 'the entire server directory'
52
+ console.warn(`[verify] static:server-artifact-file-count could not parse ${packScriptPath}; conservatively counting ${serverRoot}: ${error.message}.`)
53
+ }
54
+ }
55
+ const missing = includes?.required.filter(name => !existsSync(path.join(serverRoot, name))) ?? []
56
+ const count = includes
57
+ ? [...includes.required, ...includes.optional]
58
+ .filter(name => existsSync(path.join(serverRoot, name)))
59
+ .reduce((total, name) => total + countFiles(path.join(serverRoot, name)), 0)
60
+ : countFiles(serverRoot)
61
+ if (count > MAX_FILE_COUNT) {
62
+ throw new CliError(`Packed server artifact ${serverRoot} would contain ${count} files, exceeding the limit ${MAX_FILE_COUNT}. This count follows ${countSource}. Reduce files included by pack before submission.`)
63
+ }
64
+ if (missing.length) {
65
+ console.warn(`[verify] static:server-artifact-file-count skipped missing required paths in ${serverRoot}: ${missing.join(', ')}.`)
66
+ }
67
+ console.log(`[verify] static:server-artifact-file-count → ${missing.length ? 'PARTIAL' : 'OK'} (${count}/${MAX_FILE_COUNT}, ${serverRoot}).`)
68
+ return { skipped: false, count, missing }
69
+ }