@open-dy/vibecoding-cli 0.0.1-beta.1 → 0.0.1-beta.10
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 +8 -2
- package/assets/schemas/custom-card-schema.json +1 -1
- package/package.json +18 -10
- package/release-provenance.json +15 -0
- package/src/adapters/interest-card/opencard/dev-watch.mjs +3 -20
- package/src/adapters/interest-card/opencard/validate-widget-shell.mjs +8 -7
- package/src/build/opencard-environment.mjs +11 -4
- package/src/cli.mjs +4 -1
- package/src/commands/build.mjs +5 -2
- package/src/commands/dev.mjs +2 -4
- package/src/commands/docs.mjs +91 -0
- package/src/commands/pack.mjs +6 -3
- package/src/commands/verify.mjs +43 -4
- package/src/core/runner.mjs +11 -1
- package/src/preview/preview-runner.mjs +37 -0
- package/src/preview/start-preview.mjs +4 -24
- package/src/validators/server-artifact.mjs +69 -0
- package/src/validators/widget-shell.mjs +20 -72
package/README.md
CHANGED
|
@@ -16,7 +16,9 @@ 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`、`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`。
|
|
20
|
+
|
|
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 或启动服务。
|
|
20
22
|
|
|
21
23
|
Turbo 构建应通过 CLI 包装命令执行,例如 `vibecoding turbo --target opencard run build:raw`。CLI 会读取项目的 `turbo.json`,合并版本化默认配置,并将结果写入项目内的 `.vibecoding/cache/turbo.json`;封闭域状态和 `config/card-routing.json` 也会参与构建环境与缓存输入。`--root-turbo-json` 由 CLI 管理,不能由业务命令覆盖;需要传递其他 Turbo 参数时,放在 `--` 后面。
|
|
22
24
|
|
|
@@ -30,6 +32,10 @@ Schema 统一使用 `custom-card-schema.json`。`config/card-routing.json` 的 `
|
|
|
30
32
|
|
|
31
33
|
服务包仅包含 `dist`、生成的 `Dockerfile`、`package.json`、`run.sh` 以及可选的 `sql`、`scripts`。生成 Dockerfile 时按原脚本规则为每条 `COPY package.json` 追加存在的目录,项目源文件不改写。构建/zip 失败保留退出码,临时目录在失败时也会清理。
|
|
32
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
|
+
|
|
33
39
|
CLI 支持用 `ARTIFACTS_DIR` 指定专用产物目录;为避免误删,拒绝项目、用户主目录、临时目录根及其上级目录。模板的 `pack.sh` 不加载 `.env.dev` 或 `.bashrc`,未传入该变量时默认写入 `/tmp/microapp_artifacts`。
|
|
34
40
|
|
|
35
41
|
Taro 构建统一由 CLI 编排,兴趣卡只在业务域规则上扩展:
|
|
@@ -44,4 +50,4 @@ CLI 对外命令和校验入口的 JSDoc 使用 `@desc`、`@params`、`@return`
|
|
|
44
50
|
|
|
45
51
|
## 准出测试
|
|
46
52
|
|
|
47
|
-
在仓库根目录运行 `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-beta.
|
|
3
|
+
"version": "0.0.1-beta.10",
|
|
4
4
|
"description": "Command orchestration, validation and packaging for Douyin vibecoding Taro and OpenCard projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,24 +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
|
-
"build": "node ./scripts/
|
|
32
|
-
"test": "node ../
|
|
32
|
+
"build": "node ./scripts/check-package-files.mjs",
|
|
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 ../
|
|
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"
|
|
38
41
|
},
|
|
39
42
|
"dependencies": {
|
|
40
43
|
"ajv": "^8.18.0"
|
|
41
44
|
},
|
|
42
45
|
"peerDependencies": {
|
|
43
|
-
"@open-dy/vibecoding-sdk-taro": "0.0.1-beta.
|
|
44
|
-
"@open-dy/vibecoding-sdk-opencard": "0.0.1-beta.
|
|
46
|
+
"@open-dy/vibecoding-sdk-taro": "0.0.1-beta.9",
|
|
47
|
+
"@open-dy/vibecoding-sdk-opencard": "0.0.1-beta.8"
|
|
45
48
|
},
|
|
46
49
|
"peerDependenciesMeta": {
|
|
47
50
|
"@open-dy/vibecoding-sdk-taro": {
|
|
@@ -54,5 +57,10 @@
|
|
|
54
57
|
"engines": {
|
|
55
58
|
"node": ">=18"
|
|
56
59
|
},
|
|
57
|
-
"license": "UNLICENSED"
|
|
60
|
+
"license": "UNLICENSED",
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"registry": "https://registry.npmjs.org/",
|
|
63
|
+
"tag": "beta",
|
|
64
|
+
"access": "public"
|
|
65
|
+
}
|
|
58
66
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"producer": "vibecoding-cli",
|
|
4
|
+
"packageName": "@open-dy/vibecoding-cli",
|
|
5
|
+
"version": "0.0.1-beta.10",
|
|
6
|
+
"source": {
|
|
7
|
+
"digest": "sha256-07a273825a0a8e9dc91058343a3e733044e46875e3315aeabdc1611d73e27e66",
|
|
8
|
+
"files": 83
|
|
9
|
+
},
|
|
10
|
+
"payload": "sha256-0c32f577ee31c8dd4bd2081c6b5b92ed2186a4c40ce5f26b4ff7a49a96940a09",
|
|
11
|
+
"toolchain": {
|
|
12
|
+
"node": "v22.22.3",
|
|
13
|
+
"packageManager": null
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { startRoutingPreview } from '../../../preview/watch-card-routing.mjs';
|
|
4
|
+
import { loadPreviewRunner } from '../../../preview/preview-runner.mjs';
|
|
4
5
|
import fs from 'node:fs';
|
|
5
|
-
import { createRequire } from 'node:module';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { fileURLToPath
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
8
|
|
|
9
9
|
const __dirname = path.join(process.env.VIBECODING_OPENCARD_ROOT || process.cwd(), 'scripts');
|
|
10
10
|
const root = path.resolve(__dirname, '..');
|
|
@@ -17,7 +17,6 @@ const META_PATH = '/opt/gem/ai_coding_app/.meta/ai_coding_app_meta.json';
|
|
|
17
17
|
const READY_TIMEOUT_MS = 5 * 60 * 1000;
|
|
18
18
|
const READY_POLL_MS = 500;
|
|
19
19
|
const DEFAULT_PORT = 10087;
|
|
20
|
-
const AI_CODING_BUILTIN_PACKAGES_DIR = process.env.AI_CODING_BUILTIN_PACKAGES_DIR;
|
|
21
20
|
|
|
22
21
|
const children = [];
|
|
23
22
|
let shuttingDown = false;
|
|
@@ -29,22 +28,6 @@ function log(message) {
|
|
|
29
28
|
console.log(`[opencard-dev-watch] ${message}`);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
async function loadPreviewRunner() {
|
|
33
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
34
|
-
const offlineRunner = manifest.devDependencies?.['@byted/microapp-preview-runner']?.startsWith('file:patches/') === true;
|
|
35
|
-
if (offlineRunner || !AI_CODING_BUILTIN_PACKAGES_DIR) {
|
|
36
|
-
return import(pathToFileURL(createRequire(path.join(root, 'package.json')).resolve('@byted/microapp-preview-runner')).href);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const requireFromTarget = createRequire(
|
|
40
|
-
path.join(path.resolve(AI_CODING_BUILTIN_PACKAGES_DIR), 'package.json'),
|
|
41
|
-
);
|
|
42
|
-
const runnerEntry = requireFromTarget.resolve('@byted/microapp-preview-runner');
|
|
43
|
-
|
|
44
|
-
log(`using preview runner from ${AI_CODING_BUILTIN_PACKAGES_DIR}`);
|
|
45
|
-
return import(pathToFileURL(runnerEntry).href);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
31
|
function readMeta() {
|
|
49
32
|
try {
|
|
50
33
|
return JSON.parse(fs.readFileSync(META_PATH, 'utf-8'));
|
|
@@ -151,7 +134,7 @@ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
|
151
134
|
}
|
|
152
135
|
|
|
153
136
|
async function startCycle(env, signal) {
|
|
154
|
-
const { startRspuckPreview } = await loadPreviewRunner();
|
|
137
|
+
const { startRspuckPreview } = await loadPreviewRunner(root);
|
|
155
138
|
|
|
156
139
|
fs.rmSync(path.join(root, TT_OUTPUT_DIR), { recursive: true, force: true });
|
|
157
140
|
fs.rmSync(RSPUCK_OUTPUT_DIR, { recursive: true, force: true });
|
|
@@ -14,7 +14,6 @@ const __dirname = path.join(process.env.VIBECODING_OPENCARD_ROOT || process.cwd(
|
|
|
14
14
|
const ROOT = path.resolve(__dirname, '..');
|
|
15
15
|
const SRC = path.join(ROOT, 'src');
|
|
16
16
|
const PAGE_ENTRY = path.join(SRC, 'pages/index/index.tsx');
|
|
17
|
-
const require = createRequire(path.join(ROOT, 'scripts', 'package.json'));
|
|
18
17
|
|
|
19
18
|
const SHELL_CLASS = 'widget-root';
|
|
20
19
|
const REQUIRED_RADIUS_PX = 12;
|
|
@@ -332,7 +331,8 @@ function jsxName(el) {
|
|
|
332
331
|
return '';
|
|
333
332
|
}
|
|
334
333
|
|
|
335
|
-
function loadBabel() {
|
|
334
|
+
function loadBabel(root = ROOT) {
|
|
335
|
+
const require = createRequire(path.join(root, 'scripts', 'package.json'));
|
|
336
336
|
try {
|
|
337
337
|
const parser = require('@babel/parser');
|
|
338
338
|
const traverseMod = require('@babel/traverse');
|
|
@@ -384,8 +384,8 @@ function collectPageRootClassSetsRegex(source) {
|
|
|
384
384
|
return [{ type: 'View', classes }];
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
export function collectPageRootClassSets(source) {
|
|
388
|
-
const babel = loadBabel();
|
|
387
|
+
export function collectPageRootClassSets(source, root = ROOT) {
|
|
388
|
+
const babel = loadBabel(root);
|
|
389
389
|
if (!babel) return collectPageRootClassSetsRegex(source);
|
|
390
390
|
|
|
391
391
|
let ast;
|
|
@@ -626,9 +626,10 @@ export function lintWidgetShell({
|
|
|
626
626
|
pageSource,
|
|
627
627
|
pageFile = 'src/pages/index/index.tsx',
|
|
628
628
|
rules,
|
|
629
|
+
root = ROOT,
|
|
629
630
|
}) {
|
|
630
631
|
const findings = [];
|
|
631
|
-
const roots = collectPageRootClassSets(pageSource);
|
|
632
|
+
const roots = collectPageRootClassSets(pageSource, root);
|
|
632
633
|
if (roots.length === 0) {
|
|
633
634
|
findings.push(
|
|
634
635
|
finding(
|
|
@@ -698,9 +699,9 @@ function parseInlineCssFiles(files) {
|
|
|
698
699
|
}).flat();
|
|
699
700
|
}
|
|
700
701
|
|
|
701
|
-
export function lintWidgetShellFromSources({ pageSource, cssFiles }) {
|
|
702
|
+
export function lintWidgetShellFromSources({ pageSource, cssFiles, root = ROOT }) {
|
|
702
703
|
const rules = parseInlineCssFiles(cssFiles);
|
|
703
|
-
return lintWidgetShell({ pageSource, rules });
|
|
704
|
+
return lintWidgetShell({ pageSource, rules, root });
|
|
704
705
|
}
|
|
705
706
|
|
|
706
707
|
function assertSelfTest() {
|
|
@@ -17,8 +17,9 @@ function findRoutingPath(cardRoot) {
|
|
|
17
17
|
|
|
18
18
|
/** Read the host configuration; missing routing retains the legacy open-domain default. */
|
|
19
19
|
export function resolveCardRoutingPath(context) {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const hostRoot = context.taroRoot ?? (context.target === 'taro' ? context.root : undefined)
|
|
21
|
+
return hostRoot
|
|
22
|
+
? path.join(hostRoot, 'config/card-routing.json')
|
|
22
23
|
: findRoutingPath(context.root)
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -37,8 +38,14 @@ export function readCardRouting(context) {
|
|
|
37
38
|
} catch (error) {
|
|
38
39
|
throw new Error(`${file} must contain valid JSON: ${error.message}`)
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const serviceDomain = routing?.cardData?.serviceDomain
|
|
42
|
+
const guidance = '请核对卡片接口所属服务及现有需求后补齐或修正该字段,并重新执行 verify。\n' +
|
|
43
|
+
'证据不明确或存在冲突时请询问用户;不要通过默认填入 open 或 closed 来绕过校验。'
|
|
44
|
+
if (serviceDomain === undefined) {
|
|
45
|
+
throw new Error(`${file} 缺少 cardData.serviceDomain。\n${guidance}`)
|
|
46
|
+
}
|
|
47
|
+
if (!['closed', 'open', 'none'].includes(serviceDomain)) {
|
|
48
|
+
throw new Error(`${file} cardData.serviceDomain must be none, open, or closed; got ${JSON.stringify(serviceDomain)}。\n${guidance}`)
|
|
42
49
|
}
|
|
43
50
|
return routing
|
|
44
51
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { runPack } from './commands/pack.mjs'
|
|
|
10
10
|
import { runValidate } from './commands/validate.mjs'
|
|
11
11
|
import { runVerify } from './commands/verify.mjs'
|
|
12
12
|
import { runTurbo } from './commands/turbo.mjs'
|
|
13
|
+
import { runDocs } from './commands/docs.mjs'
|
|
13
14
|
|
|
14
15
|
const HELP = `Usage: vibecoding <command> [options]
|
|
15
16
|
|
|
@@ -22,6 +23,7 @@ Commands:
|
|
|
22
23
|
validate <name> Run feed-card, routes, empty or opencard-structure
|
|
23
24
|
init-mysql-db Initialize the Taro server database
|
|
24
25
|
pack Build deployment archives
|
|
26
|
+
docs Locate the selected project's installed SDK reference
|
|
25
27
|
turbo <args...> Run Turbo with CLI defaults and project config
|
|
26
28
|
|
|
27
29
|
Required:
|
|
@@ -38,7 +40,7 @@ Options:
|
|
|
38
40
|
--static-only Verify static phase only
|
|
39
41
|
--ports-only Verify listening ports only
|
|
40
42
|
--auto-start Start dev once when verify finds missing ports
|
|
41
|
-
--timeout <seconds> Dev startup timeout (default: 90)
|
|
43
|
+
--timeout <seconds> Dev startup / docs dependency wait timeout (default: 90)
|
|
42
44
|
--frontend-url <url> Override the primary runtime-check URL`
|
|
43
45
|
|
|
44
46
|
function assertNoExtra(positionals, count, command) {
|
|
@@ -95,6 +97,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
95
97
|
case 'lint': assertNoExtra(positionals, 1, command); return runLint(context, options)
|
|
96
98
|
case 'verify': assertNoExtra(positionals, 1, command); return runVerify(context, options)
|
|
97
99
|
case 'pack': assertNoExtra(positionals, 1, command); return runPack(context, options)
|
|
100
|
+
case 'docs': assertNoExtra(positionals, 1, command); return runDocs(context, options)
|
|
98
101
|
case 'init-mysql-db': assertNoExtra(positionals, 1, command); return runDatabaseInit(context)
|
|
99
102
|
case 'validate':
|
|
100
103
|
assertNoExtra(positionals, 2, command)
|
package/src/commands/build.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { openCardBuildEnvironment } from '../build/opencard-environment.mjs'
|
|
1
|
+
import { openCardBuildEnvironment, readCardRouting } from '../build/opencard-environment.mjs'
|
|
2
2
|
import { mkdirSync } from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { readEnvFile } from '../core/environment.mjs'
|
|
@@ -33,6 +33,7 @@ export async function buildTaroFrontend(context, type, options = {}) {
|
|
|
33
33
|
export async function buildOpenCard(context, options = {}) {
|
|
34
34
|
const root = context.target === 'opencard' ? context.root : context.opencardRoot
|
|
35
35
|
if (!root) return
|
|
36
|
+
const cardEnvironment = openCardBuildEnvironment(context)
|
|
36
37
|
const args = ['build', '--type', 'tt']
|
|
37
38
|
if (options.mode) args.push('--mode', options.mode)
|
|
38
39
|
if (options.watch) args.push('--watch')
|
|
@@ -41,7 +42,7 @@ export async function buildOpenCard(context, options = {}) {
|
|
|
41
42
|
env: taroEnvironment(root, {
|
|
42
43
|
...(options.watch ? { TT_OUTPUT_ROOT: 'dist-tt' } : {}),
|
|
43
44
|
...options.env,
|
|
44
|
-
...
|
|
45
|
+
...cardEnvironment,
|
|
45
46
|
}),
|
|
46
47
|
label: 'opencard tt build',
|
|
47
48
|
})
|
|
@@ -78,6 +79,7 @@ export function buildClosedDomainServer(context, options = {}) {
|
|
|
78
79
|
* exposes only the stable `pack.sh` entrypoint.
|
|
79
80
|
*/
|
|
80
81
|
export async function buildPackInputs(context, options = {}) {
|
|
82
|
+
readCardRouting(context)
|
|
81
83
|
const tasks = [
|
|
82
84
|
{ name: 'taro-tt', run: () => buildTaroFrontend(context, 'tt', { ...options, mode: 'prod' }) },
|
|
83
85
|
{ name: 'server', run: () => buildServer(context, { ...options, production: true }) },
|
|
@@ -92,6 +94,7 @@ export async function buildPackInputs(context, options = {}) {
|
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
export async function runBuild(context, options = {}) {
|
|
97
|
+
readCardRouting(context)
|
|
95
98
|
const buildOptions = {
|
|
96
99
|
...options,
|
|
97
100
|
env: resolveBuildEnvironment(context.root, options.env),
|
package/src/commands/dev.mjs
CHANGED
|
@@ -232,8 +232,7 @@ export async function runDev(context, options = {}) {
|
|
|
232
232
|
env,
|
|
233
233
|
onExit: handlePreviewExit,
|
|
234
234
|
})
|
|
235
|
-
|
|
236
|
-
runtimes.push(preview.runtime)
|
|
235
|
+
runtimes.push(preview)
|
|
237
236
|
} else {
|
|
238
237
|
const baseline = env.HAS_CALLED_DATABASE_INIT_TOOL
|
|
239
238
|
await runDatabaseInit(context, { env })
|
|
@@ -268,8 +267,7 @@ export async function runDev(context, options = {}) {
|
|
|
268
267
|
)
|
|
269
268
|
const startManagedPreview = async (previewOptions) => {
|
|
270
269
|
const preview = await startPreview(previewOptions)
|
|
271
|
-
|
|
272
|
-
runtimes.push(preview.runtime)
|
|
270
|
+
runtimes.push(preview)
|
|
273
271
|
}
|
|
274
272
|
const previewTasks = [
|
|
275
273
|
startManagedPreview({ root: context.root, env, onExit: handlePreviewExit }),
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { CliError } from '../core/errors.mjs'
|
|
5
|
+
|
|
6
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
7
|
+
const isFile = file => existsSync(file) && statSync(file).isFile()
|
|
8
|
+
|
|
9
|
+
function targets(value) {
|
|
10
|
+
if (typeof value === 'string') return [value]
|
|
11
|
+
if (!value || typeof value !== 'object') return []
|
|
12
|
+
return Object.values(value).flatMap(targets)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Limit lookup to project/ancestor node_modules, including pnpm hoisting.
|
|
16
|
+
// NODE_PATH and global packages are not evidence of this project's install.
|
|
17
|
+
function installedPackage(root, dependency) {
|
|
18
|
+
const require = createRequire(path.join(root, 'package.json'))
|
|
19
|
+
for (const directory of require.resolve.paths(dependency) ?? []) {
|
|
20
|
+
const relative = path.relative(path.dirname(directory), root)
|
|
21
|
+
if (path.basename(directory) !== 'node_modules' || relative === '..'
|
|
22
|
+
|| relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) continue
|
|
23
|
+
const manifest = path.join(directory, dependency, 'package.json')
|
|
24
|
+
if (isFile(manifest)) return realpathSync(path.dirname(manifest))
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function packageFile(root, target) {
|
|
30
|
+
if (!target.startsWith('./') || target.includes('*')) return null
|
|
31
|
+
const file = path.resolve(root, target)
|
|
32
|
+
const relative = path.relative(root, file)
|
|
33
|
+
return !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) && isFile(file) ? file : null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Resolve the selected project's declared SDK and its installed documentation.
|
|
37
|
+
* This also handles npm aliases and pnpm symlinks without a package.json export.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveSdkDocumentation(context) {
|
|
40
|
+
const manifest = JSON.parse(readFileSync(path.join(context.root, 'package.json'), 'utf8'))
|
|
41
|
+
const dependencies = { ...manifest.devDependencies, ...manifest.dependencies }
|
|
42
|
+
const suffix = context.target === 'opencard' ? 'opencard' : 'taro'
|
|
43
|
+
const expected = `@open-dy/vibecoding-sdk-${suffix}`
|
|
44
|
+
const aliasPattern = new RegExp(`^npm:@[^/]+/vibecoding-sdk-${suffix}(?:@|$)`)
|
|
45
|
+
const dependency = Object.hasOwn(dependencies, expected) ? expected
|
|
46
|
+
: Object.keys(dependencies).find(key => aliasPattern.test(dependencies[key]))
|
|
47
|
+
?? (Object.hasOwn(dependencies, '@open-dy/vibecoding-sdk') ? '@open-dy/vibecoding-sdk'
|
|
48
|
+
: Object.keys(dependencies).find(key => /^npm:@[^/]+\/vibecoding-sdk(?:@|$)/.test(dependencies[key])))
|
|
49
|
+
if (!dependency) throw new CliError(`[SDK_NOT_DECLARED] ${expected} is not declared in ${context.root}/package.json`)
|
|
50
|
+
const root = installedPackage(context.root, dependency)
|
|
51
|
+
if (!root) return { status: 'not-installed', dependency, projectRoot: context.root }
|
|
52
|
+
const sdk = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
53
|
+
const exports = sdk.exports && typeof sdk.exports === 'object' && !Array.isArray(sdk.exports)
|
|
54
|
+
&& Object.keys(sdk.exports).some(key => key.startsWith('.')) ? sdk.exports : { '.': sdk.exports }
|
|
55
|
+
const docTargets = targets(exports['./reference/llms.txt'] ?? exports['./reference/*'])
|
|
56
|
+
.map(target => target.replaceAll('*', 'llms.txt'))
|
|
57
|
+
const index = [...docTargets, './dist/reference/llms.txt'].map(target => packageFile(root, target)).find(Boolean) ?? null
|
|
58
|
+
const types = Object.entries(exports).flatMap(([key, value]) => {
|
|
59
|
+
if (key.includes('*')) return []
|
|
60
|
+
const candidates = targets(value).filter(target => /\.d\.[cm]?ts$/.test(target))
|
|
61
|
+
// Root-level `types` is also used by older packages with a JS root export.
|
|
62
|
+
if (key === '.' && sdk.types) candidates.push(sdk.types.startsWith('./') ? sdk.types : `./${sdk.types}`)
|
|
63
|
+
const file = candidates.map(target => packageFile(root, target)).find(Boolean)
|
|
64
|
+
return file ? [{ importPath: key === '.' ? dependency : `${dependency}/${key.slice(2)}`, file }] : []
|
|
65
|
+
})
|
|
66
|
+
return { status: index ? 'ready' : 'docs-missing', dependency, name: sdk.name, version: sdk.version,
|
|
67
|
+
projectRoot: context.root, packageRoot: root, index, exports: sdk.exports ?? null,
|
|
68
|
+
imports: Object.keys(exports).filter(key => exports[key] != null || (key === '.' && sdk.exports === undefined))
|
|
69
|
+
.map(key => key === '.' ? dependency : `${dependency}/${key.slice(2)}`), types }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Wait only for this project's SDK package; never install or start services. */
|
|
73
|
+
export async function waitForSdkDocumentation(context, { timeoutSeconds = 90, sleepFn = sleep } = {}) {
|
|
74
|
+
const deadline = Date.now() + timeoutSeconds * 1000
|
|
75
|
+
while (true) {
|
|
76
|
+
const result = resolveSdkDocumentation(context)
|
|
77
|
+
if (result.status !== 'not-installed') return result
|
|
78
|
+
if (Date.now() >= deadline) {
|
|
79
|
+
throw new CliError(`[SDK_NOT_INSTALLED] ${result.dependency} was not available after ${timeoutSeconds}s in ${context.root}. ` +
|
|
80
|
+
'Check the current project dependency installation result; do not search other projects or rebuild the SDK for documentation.')
|
|
81
|
+
}
|
|
82
|
+
await sleepFn(Math.min(500, Math.max(0, deadline - Date.now())))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Print machine-readable SDK documentation paths for Agent lookup. */
|
|
87
|
+
export async function runDocs(context, options = {}) {
|
|
88
|
+
const result = await waitForSdkDocumentation(context, options)
|
|
89
|
+
console.log(JSON.stringify(result, null, 2))
|
|
90
|
+
return result
|
|
91
|
+
}
|
package/src/commands/pack.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import { spawn } from 'node:child_process'
|
|
|
16
16
|
import { CliError } from '../core/errors.mjs'
|
|
17
17
|
import { resolveProjectContext } from '../core/context.mjs'
|
|
18
18
|
import { buildOpenCard, buildPackInputs } from './build.mjs'
|
|
19
|
+
import { readCardRouting } from '../build/opencard-environment.mjs'
|
|
20
|
+
import { REQUIRED_SERVER_INCLUDES, OPTIONAL_SERVER_INCLUDES } from '../validators/server-artifact.mjs'
|
|
19
21
|
|
|
20
22
|
const DEFAULT_ARTIFACTS = '/tmp/microapp_artifacts'
|
|
21
23
|
|
|
@@ -71,13 +73,12 @@ async function zipDirectory(directory, output, { removeExisting = true } = {}) {
|
|
|
71
73
|
|
|
72
74
|
function createServerStage(server, stage) {
|
|
73
75
|
if (!isDirectory(server)) throw new CliError(`Server pack input is missing: ${server}`)
|
|
74
|
-
const
|
|
75
|
-
for (const name of required) {
|
|
76
|
+
for (const name of REQUIRED_SERVER_INCLUDES) {
|
|
76
77
|
if (!existsSync(path.join(server, name))) {
|
|
77
78
|
throw new CliError(`Server pack input is missing: ${path.join(server, name)}`)
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
|
-
for (const name of [
|
|
81
|
+
for (const name of [...REQUIRED_SERVER_INCLUDES, ...OPTIONAL_SERVER_INCLUDES].filter(name => name !== 'Dockerfile')) {
|
|
81
82
|
const source = path.join(server, name)
|
|
82
83
|
if (existsSync(source)) cpSync(source, path.join(stage, name), { recursive: true })
|
|
83
84
|
}
|
|
@@ -135,6 +136,8 @@ export async function packTaroArtifacts(context, artifacts) {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
export async function runPack(context, options = {}) {
|
|
139
|
+
// Validate before cleaning old artifacts, including projects with a legacy build:pack hook.
|
|
140
|
+
readCardRouting(context)
|
|
138
141
|
const artifacts = resolveArtifactsDirectory(context.root,
|
|
139
142
|
options.artifactsDir ?? process.env.ARTIFACTS_DIR ?? DEFAULT_ARTIFACTS)
|
|
140
143
|
if (context.target === 'opencard') {
|
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'
|
|
@@ -8,9 +8,11 @@ import { waitForPorts } from '../core/ports.mjs'
|
|
|
8
8
|
import { runCaptured, runTasks } from '../core/runner.mjs'
|
|
9
9
|
import { packageUses, validateDependencies } from '../validators/dependencies.mjs'
|
|
10
10
|
import { validateFeedCard } from '../validators/feed-card.mjs'
|
|
11
|
+
import { readCardRouting } from '../build/opencard-environment.mjs'
|
|
11
12
|
import { runBuild } from './build.mjs'
|
|
12
13
|
import { runDatabaseInit } from './db.mjs'
|
|
13
14
|
import { runLint } from './lint.mjs'
|
|
15
|
+
import { validateServerArtifactFileCount } from '../validators/server-artifact.mjs'
|
|
14
16
|
|
|
15
17
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
16
18
|
const devLauncher = fileURLToPath(new URL('../../bin/vibecoding', import.meta.url))
|
|
@@ -41,6 +43,11 @@ async function runStatic(context) {
|
|
|
41
43
|
)
|
|
42
44
|
}
|
|
43
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
|
+
}
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export function startDevDetached(context, spawnProcess = spawn) {
|
|
@@ -521,6 +528,23 @@ export function isIgnorableOpenCardConsoleFinding(value, options) {
|
|
|
521
528
|
/Failed to load resource: the server responded with a status of 404/i.test(text)
|
|
522
529
|
}
|
|
523
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
|
+
|
|
524
548
|
async function browserCheck(root, url, label) {
|
|
525
549
|
if (packageUses(root, '@open-dy/threejs')) {
|
|
526
550
|
console.log(`[vibecoding:verify] ${label}: skipping browser runtime check for @open-dy/threejs`)
|
|
@@ -528,7 +552,19 @@ async function browserCheck(root, url, label) {
|
|
|
528
552
|
}
|
|
529
553
|
parseHttpUrl(url, label)
|
|
530
554
|
const session = process.env.AGENT_BROWSER_SESSION || `vibecoding-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
531
|
-
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
|
+
}
|
|
532
568
|
try {
|
|
533
569
|
await call(['network', 'requests'], true)
|
|
534
570
|
await call(['open', url])
|
|
@@ -564,7 +600,9 @@ async function browserCheck(root, url, label) {
|
|
|
564
600
|
throw new CliError(`${label} runtime check found ${findings.length} issue(s):\n${findings.map((item) => `- ${flatten(item)}`).join('\n')}`)
|
|
565
601
|
}
|
|
566
602
|
} finally {
|
|
567
|
-
await call(['close'], true)
|
|
603
|
+
await call(['close'], true).catch((error) => {
|
|
604
|
+
console.log(`[vibecoding:verify] browser cleanup failed: ${error.message}`)
|
|
605
|
+
})
|
|
568
606
|
}
|
|
569
607
|
}
|
|
570
608
|
|
|
@@ -578,12 +616,13 @@ export async function runVerify(context, options = {}) {
|
|
|
578
616
|
if (options.staticOnly && options.portsOnly) {
|
|
579
617
|
throw new CliError('--static-only cannot be combined with --ports-only')
|
|
580
618
|
}
|
|
619
|
+
// Fail before waiting for dev/install or starting build/runtime checks, for both targets.
|
|
620
|
+
readCardRouting(context)
|
|
581
621
|
const full = Boolean(options.full || options.autoStart || options.staticOnly ||
|
|
582
622
|
(context.target === 'taro' && process.env.npm_lifecycle_event === 'verify'))
|
|
583
623
|
options = { timeoutSeconds: 90, ...options, full }
|
|
584
624
|
const frontendUrl = resolveFrontendUrl(context, options)
|
|
585
625
|
parseHttpUrl(frontendUrl, 'frontend')
|
|
586
|
-
if (full) await waitForDevInstall(context, options.timeoutSeconds)
|
|
587
626
|
if (full || options.staticOnly) await runStatic(context)
|
|
588
627
|
if (options.staticOnly) return
|
|
589
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,37 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { CliError } from '../core/errors.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @desc 加载工程明确声明的预览运行时;未声明时优先使用沙箱内置运行时。
|
|
9
|
+
* @params {string} projectRoot 当前工程根目录。
|
|
10
|
+
* @params {object} env 运行环境,可通过 AI_CODING_BUILTIN_PACKAGES_DIR 指定内置包目录。
|
|
11
|
+
* @return {Promise<object>} 已加载的 Preview Runner 模块;所选包缺失时抛出错误。
|
|
12
|
+
* @example await loadPreviewRunner(projectRoot, process.env)
|
|
13
|
+
*/
|
|
14
|
+
export async function loadPreviewRunner(projectRoot, env = process.env) {
|
|
15
|
+
let declaredRunner = false
|
|
16
|
+
try {
|
|
17
|
+
const manifest = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8'))
|
|
18
|
+
const specifier = manifest.devDependencies?.['@byted/microapp-preview-runner']
|
|
19
|
+
?? manifest.dependencies?.['@byted/microapp-preview-runner']
|
|
20
|
+
declaredRunner = typeof specifier === 'string' && specifier.trim().length > 0
|
|
21
|
+
} catch {}
|
|
22
|
+
const builtin = !declaredRunner && env.AI_CODING_BUILTIN_PACKAGES_DIR
|
|
23
|
+
const base = path.resolve(builtin || projectRoot)
|
|
24
|
+
const requireFromTarget = createRequire(path.join(base, 'package.json'))
|
|
25
|
+
let entry
|
|
26
|
+
try {
|
|
27
|
+
entry = requireFromTarget.resolve('@byted/microapp-preview-runner')
|
|
28
|
+
} catch {
|
|
29
|
+
throw new CliError(
|
|
30
|
+
`Cannot resolve @byted/microapp-preview-runner from ${base}. `
|
|
31
|
+
+ (declaredRunner ? 'Run pnpm install in the project.'
|
|
32
|
+
: 'Set AI_CODING_BUILTIN_PACKAGES_DIR or install it in the project.'),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
console.log(`[preview-runner] using ${builtin ? 'builtin' : 'project'} package: ${entry}`)
|
|
36
|
+
return import(pathToFileURL(entry).href)
|
|
37
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { startRoutingPreview } from './watch-card-routing.mjs'
|
|
2
2
|
import { spawn } from 'node:child_process'
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
|
|
4
|
-
import { createRequire } from 'node:module'
|
|
5
4
|
import path from 'node:path'
|
|
6
|
-
import {
|
|
5
|
+
import { loadPreviewRunner } from './preview-runner.mjs'
|
|
7
6
|
import { CliError } from '../core/errors.mjs'
|
|
8
7
|
import { attachLabeledOutput } from '../core/process-output.mjs'
|
|
9
8
|
import { resolveSdkTaroEntry } from '../core/runner.mjs'
|
|
@@ -12,30 +11,12 @@ import { withServiceLogLabel } from '../core/service-log-context.mjs'
|
|
|
12
11
|
const META_PATH = '/opt/gem/ai_coding_app/.meta/ai_coding_app_meta.json'
|
|
13
12
|
const READY_TIMEOUT_MS = 5 * 60 * 1000
|
|
14
13
|
|
|
14
|
+
export { loadPreviewRunner } from './preview-runner.mjs'
|
|
15
|
+
|
|
15
16
|
export function conversationId() {
|
|
16
17
|
try { return JSON.parse(readFileSync(META_PATH, 'utf8')).conversation_id ?? '' } catch { return '' }
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
export async function loadPreviewRunner(projectRoot, env = process.env) {
|
|
20
|
-
// Only PPE's explicit offline dependency overrides the platform runtime.
|
|
21
|
-
let offlineRunner = false
|
|
22
|
-
try {
|
|
23
|
-
const manifest = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8'))
|
|
24
|
-
offlineRunner = manifest.devDependencies?.['@byted/microapp-preview-runner']?.startsWith('file:patches/') === true
|
|
25
|
-
} catch {}
|
|
26
|
-
const base = offlineRunner ? projectRoot : env.AI_CODING_BUILTIN_PACKAGES_DIR || projectRoot
|
|
27
|
-
const requireFromTarget = createRequire(path.join(path.resolve(base), 'package.json'))
|
|
28
|
-
let entry
|
|
29
|
-
try {
|
|
30
|
-
entry = requireFromTarget.resolve('@byted/microapp-preview-runner')
|
|
31
|
-
} catch {
|
|
32
|
-
throw new CliError(
|
|
33
|
-
'Cannot resolve @byted/microapp-preview-runner. Set AI_CODING_BUILTIN_PACKAGES_DIR or install it in the project.',
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
return import(pathToFileURL(entry).href)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
20
|
export function waitForFile(file, label, { signal, timeoutMs = READY_TIMEOUT_MS, pollMs = 500 } = {}) {
|
|
40
21
|
return new Promise((resolve, reject) => {
|
|
41
22
|
const deadline = Date.now() + timeoutMs
|
|
@@ -59,9 +40,8 @@ export function waitForFile(file, label, { signal, timeoutMs = READY_TIMEOUT_MS,
|
|
|
59
40
|
}
|
|
60
41
|
|
|
61
42
|
export function resolveOpenCardPreviewUrl(root, env = process.env, id = conversationId()) {
|
|
62
|
-
const configured = env.OPEN_CARD_PREVIEW_URL?.trim()
|
|
63
|
-
if (configured) return configured
|
|
64
43
|
if (!existsSync(path.join(root, 'opencard/package.json'))) return undefined
|
|
44
|
+
// Match the card service's port; the runner consumes this single target URL.
|
|
65
45
|
const localCardPreview = `http://127.0.0.1:${resolvePreviewPort(env.OPENCARD_PORT, 10087)}`
|
|
66
46
|
return id
|
|
67
47
|
? `${localCardPreview}/api/apps/open_ai_coding_chat_server/aio_fe/${id}/card_preview`
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { lintWidgetShellFromSources } from '../adapters/interest-card/opencard/validate-widget-shell.mjs'
|
|
3
4
|
|
|
4
5
|
function listCssFiles(directory, files = []) {
|
|
5
6
|
if (!existsSync(directory)) return files
|
|
@@ -14,94 +15,41 @@ function listCssFiles(directory, files = []) {
|
|
|
14
15
|
return files
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
function rootRule(source) {
|
|
18
|
-
const match = /\.widget-root\s*\{([\s\S]*?)\}/m.exec(source)
|
|
19
|
-
return match?.[1] ?? ''
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function hasDeclaration(css, property, value) {
|
|
23
|
-
const expression = new RegExp(
|
|
24
|
-
`${property.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:\\s*${value}`,
|
|
25
|
-
'i',
|
|
26
|
-
)
|
|
27
|
-
return expression.test(css)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function formatFinding(file, line, column, message) {
|
|
31
|
-
return `${file}:${line}:${column} ${message}`
|
|
32
|
-
}
|
|
33
|
-
|
|
34
18
|
export function validateWidgetShell(context) {
|
|
35
19
|
const root = context.target === 'opencard' ? context.root : context.opencardRoot
|
|
36
20
|
if (!root) return { skipped: true, findings: [] }
|
|
37
21
|
|
|
38
22
|
const pageFile = path.join(root, 'src/pages/index/index.tsx')
|
|
39
|
-
|
|
40
|
-
const findings = []
|
|
23
|
+
let findings
|
|
41
24
|
if (!existsSync(pageFile)) {
|
|
42
|
-
findings
|
|
43
|
-
file: 'src/pages/index/index.tsx',
|
|
44
|
-
line: 1,
|
|
45
|
-
column: 1,
|
|
25
|
+
findings = [{
|
|
26
|
+
file: 'src/pages/index/index.tsx', line: 1, column: 1,
|
|
46
27
|
message: '缺少 OpenCard 页面入口。',
|
|
47
|
-
}
|
|
28
|
+
}]
|
|
48
29
|
} else {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
30
|
+
// Reuse the master checker: inspect returned roots and merged CSS, including variables.
|
|
31
|
+
const cssFiles = listCssFiles(path.join(root, 'src'))
|
|
32
|
+
.map((file) => ({
|
|
33
|
+
filename: path.relative(root, file).replace(/\\/g, '/'),
|
|
34
|
+
source: readFileSync(file, 'utf8'),
|
|
35
|
+
}))
|
|
36
|
+
.sort((a, b) => {
|
|
37
|
+
if (a.filename === 'src/app.css') return -1
|
|
38
|
+
if (b.filename === 'src/app.css') return 1
|
|
39
|
+
return a.filename.localeCompare(b.filename)
|
|
56
40
|
})
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const css = cssFiles.map((file) => readFileSync(file, 'utf8')).join('\n')
|
|
62
|
-
const shell = rootRule(css)
|
|
63
|
-
if (!shell) {
|
|
64
|
-
findings.push({
|
|
65
|
-
file: 'src/pages/index/index.css',
|
|
66
|
-
line: 1,
|
|
67
|
-
column: 1,
|
|
68
|
-
message: '缺少 .widget-root CSS。',
|
|
41
|
+
findings = lintWidgetShellFromSources({
|
|
42
|
+
root,
|
|
43
|
+
pageSource: readFileSync(pageFile, 'utf8'),
|
|
44
|
+
cssFiles,
|
|
69
45
|
})
|
|
70
|
-
} else {
|
|
71
|
-
const checks = [
|
|
72
|
-
[/box-sizing\s*:\s*border-box/i, '底板必须使用 box-sizing: border-box。'],
|
|
73
|
-
[/display\s*:\s*flex/i, '底板必须使用 Flex 布局。'],
|
|
74
|
-
[/height\s*:\s*100%/i, '底板必须使用 height: 100%,不能使用 100vh。'],
|
|
75
|
-
[/background(?:-color)?\s*:\s*(?!transparent|none)[^;]+/i, '底板必须有非透明背景。'],
|
|
76
|
-
[/border\s*:\s*1px\s+(?:solid|dashed|dotted)\s+[^;]+/i, '底板必须有 1px 描边。'],
|
|
77
|
-
[/border-radius\s*:\s*(?:12px|var\(--[A-Za-z0-9_-]+\))/i, '底板四角必须使用 12px 圆角。'],
|
|
78
|
-
]
|
|
79
|
-
for (const [expression, message] of checks) {
|
|
80
|
-
if (!expression.test(shell)) {
|
|
81
|
-
findings.push({
|
|
82
|
-
file: 'src/pages/index/index.css',
|
|
83
|
-
line: 1,
|
|
84
|
-
column: 1,
|
|
85
|
-
message,
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (hasDeclaration(shell, 'height', '100vh')) {
|
|
90
|
-
findings.push({
|
|
91
|
-
file: 'src/pages/index/index.css',
|
|
92
|
-
line: 1,
|
|
93
|
-
column: 1,
|
|
94
|
-
message: '固定 widget 禁止使用 height: 100vh。',
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
46
|
}
|
|
98
47
|
|
|
99
48
|
if (findings.length > 0) {
|
|
100
49
|
console.warn(`WARN: OpenCard widget 底板校验未通过(${findings.length}),不阻断构建`)
|
|
101
50
|
for (const finding of findings) {
|
|
102
|
-
console.warn(
|
|
51
|
+
console.warn(`${finding.file}:${finding.line}:${finding.column} ${finding.message}`)
|
|
103
52
|
}
|
|
104
53
|
}
|
|
105
|
-
|
|
106
54
|
return { skipped: false, findings }
|
|
107
55
|
}
|