@open-dy/vibecoding-cli 0.0.1-beta.6 → 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 +5 -1
- package/assets/schemas/custom-card-schema.json +1 -1
- package/package.json +12 -11
- package/release-provenance.json +15 -0
- package/src/commands/pack.mjs +3 -3
- package/src/commands/verify.mjs +40 -4
- package/src/core/runner.mjs +11 -1
- package/src/validators/server-artifact.mjs +69 -0
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/
|
|
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
|
|
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
|
|
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 ../
|
|
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 ../
|
|
36
|
-
"publish:bnpm:ppe": "node ../
|
|
37
|
-
"publish:bnpm:ppe:dry-run": "node ../
|
|
38
|
-
"publish:npm:beta": "node ../
|
|
39
|
-
"publish:npm:beta:dry-run": "node ../
|
|
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
|
|
46
|
-
"@open-dy/vibecoding-sdk-opencard": "0.0.1
|
|
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": "
|
|
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
|
+
}
|
package/src/commands/pack.mjs
CHANGED
|
@@ -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
|
|
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 [
|
|
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
|
}
|
package/src/commands/verify.mjs
CHANGED
|
@@ -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
|
|
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
|
|
package/src/core/runner.mjs
CHANGED
|
@@ -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) =>
|
|
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
|
+
}
|