@open-dy/vibecoding-cli 0.0.1-beta.2 → 0.0.1-beta.3
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/package.json +6 -4
- 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 +3 -0
- package/src/commands/verify.mjs +53 -16
- package/src/core/dev-process.mjs +53 -0
package/README.md
CHANGED
|
@@ -16,7 +16,11 @@ 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
|
+
`verify --full --auto-start` 在等待依赖安装日志前启动 dev;已有 dev 正在启动或运行时复用,一次校验最多启动一次。安装就绪只认最近一次启动的日志,安装失败直接报错;之后仍按静态检查、端口检查、API 与浏览器检查执行。`--static-only` 不启动 dev,也不等待 dev 日志。
|
|
22
|
+
|
|
23
|
+
`vibecoding docs --target taro`(OpenCard 使用 `--target opencard`,可指定 `--cwd <工程目录>`)只读定位实际安装的 SDK,输出 JSON:依赖键、版本、包位置、文档索引及公开类型路径。`ready` 时沿 `index` 查阅;`docs-missing` 表示已安装的旧包没有索引,按 `exports` / `types` 查公开入口。业务导入使用 `imports`,npm 别名以依赖键为前缀,不能换成包的 `name`。包尚不可用时最多等待 `--timeout` 秒(默认 90),超时返回 `SDK_NOT_INSTALLED`,应检查工程安装结果。命令不会安装、重建 SDK 或启动服务。
|
|
20
24
|
|
|
21
25
|
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
26
|
|
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.3",
|
|
4
4
|
"description": "Command orchestration, validation and packaging for Douyin vibecoding Taro and OpenCard projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,14 +34,16 @@
|
|
|
34
34
|
"pack:ppe": "node ./scripts/pack-ppe.mjs",
|
|
35
35
|
"test:release": "node ../scripts/run-release-contracts.mjs",
|
|
36
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"
|
|
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"
|
|
38
40
|
},
|
|
39
41
|
"dependencies": {
|
|
40
42
|
"ajv": "^8.18.0"
|
|
41
43
|
},
|
|
42
44
|
"peerDependencies": {
|
|
43
|
-
"@open-dy/vibecoding-sdk-taro": "0.0.1-beta.
|
|
44
|
-
"@open-dy/vibecoding-sdk-opencard": "0.0.1-beta.
|
|
45
|
+
"@open-dy/vibecoding-sdk-taro": "0.0.1-beta.3",
|
|
46
|
+
"@open-dy/vibecoding-sdk-opencard": "0.0.1-beta.3"
|
|
45
47
|
},
|
|
46
48
|
"peerDependenciesMeta": {
|
|
47
49
|
"@open-dy/vibecoding-sdk-taro": {
|
|
@@ -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,7 @@ 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'
|
|
19
20
|
|
|
20
21
|
const DEFAULT_ARTIFACTS = '/tmp/microapp_artifacts'
|
|
21
22
|
|
|
@@ -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
|
@@ -6,8 +6,10 @@ import { CliError } from '../core/errors.mjs'
|
|
|
6
6
|
import { readRuntimeEnvironment } from '../core/environment.mjs'
|
|
7
7
|
import { waitForPorts } from '../core/ports.mjs'
|
|
8
8
|
import { runCaptured, runTasks } from '../core/runner.mjs'
|
|
9
|
+
import { devProcessMatches, inspectDevProcess } from '../core/dev-process.mjs'
|
|
9
10
|
import { packageUses, validateDependencies } from '../validators/dependencies.mjs'
|
|
10
11
|
import { validateFeedCard } from '../validators/feed-card.mjs'
|
|
12
|
+
import { readCardRouting } from '../build/opencard-environment.mjs'
|
|
11
13
|
import { runBuild } from './build.mjs'
|
|
12
14
|
import { runDatabaseInit } from './db.mjs'
|
|
13
15
|
import { runLint } from './lint.mjs'
|
|
@@ -59,14 +61,32 @@ export function startDevDetached(context, spawnProcess = spawn) {
|
|
|
59
61
|
})
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
64
|
+
function resolveDevRuntimeRoot(context, env = process.env) {
|
|
65
|
+
// Match the supervisor: standalone OpenCard shares its host's runtime files.
|
|
66
|
+
const root = context.taroRoot ?? (context.target === 'opencard' ? path.dirname(context.root) : context.root)
|
|
67
|
+
return env.VIBECODING_RUNTIME_ROOT || (
|
|
65
68
|
process.platform === 'linux'
|
|
66
69
|
? '/workspace/tmp'
|
|
67
|
-
: path.join(
|
|
70
|
+
: path.join(root, '.tmp/microapp-dev')
|
|
68
71
|
)
|
|
69
|
-
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function resolveDevLogPath(context, env = process.env) {
|
|
75
|
+
return env.LOG_FILE || path.join(resolveDevRuntimeRoot(context, env), 'microapp-logs/dev.log')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readDevLog(context) {
|
|
79
|
+
try { return readFileSync(resolveDevLogPath(context), 'utf8') } catch { return '' }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hasRunningDev(context) {
|
|
83
|
+
return ['microapp-dev-script.pid', 'microapp-dev-run.pid'].some(name => {
|
|
84
|
+
let pid
|
|
85
|
+
try { pid = readFileSync(path.join(resolveDevRuntimeRoot(context), name), 'utf8').trim() } catch { return false }
|
|
86
|
+
if (!/^[1-9][0-9]*$/.test(pid) || !Number.isSafeInteger(Number(pid))) return false
|
|
87
|
+
try { process.kill(Number(pid), 0) } catch (error) { if (error.code !== 'EPERM') return false }
|
|
88
|
+
return devProcessMatches(context, inspectDevProcess(Number(pid)))
|
|
89
|
+
})
|
|
70
90
|
}
|
|
71
91
|
|
|
72
92
|
function printLastRunErrors(logFile, log) {
|
|
@@ -88,7 +108,15 @@ export async function waitForDevInstall(context, timeoutSeconds, options = {}) {
|
|
|
88
108
|
const sleepFn = options.sleepFn ?? sleep
|
|
89
109
|
const deadline = Date.now() + timeoutSeconds * 1000
|
|
90
110
|
while (Date.now() < deadline) {
|
|
91
|
-
|
|
111
|
+
// Exclude previous runs, including the old log before a new supervisor has
|
|
112
|
+
// appended its separator. A newly launched run supplies its starting offset.
|
|
113
|
+
const newLog = readLog().slice(options.logOffset ?? 0)
|
|
114
|
+
const sections = newLog.split('--- DEV_RUN_START ---')
|
|
115
|
+
const section = options.logOffset !== undefined && sections.length === 1 ? '' : sections.at(-1) || ''
|
|
116
|
+
const failure = section.split('\n').find(line =>
|
|
117
|
+
/(?:pnpm install failed|Dependency installation failed|CLI installation did not create)/i.test(line))
|
|
118
|
+
if (failure) throw new CliError(`Dev dependency installation failed: ${failure.trim()}. Full log: ${logFile}`)
|
|
119
|
+
if (section.includes('Dependencies installed successfully')) return
|
|
92
120
|
await sleepFn(options.pollMs ?? 1_000)
|
|
93
121
|
}
|
|
94
122
|
printLastRunErrors(logFile, readLog())
|
|
@@ -98,19 +126,25 @@ export async function waitForDevInstall(context, timeoutSeconds, options = {}) {
|
|
|
98
126
|
)
|
|
99
127
|
}
|
|
100
128
|
|
|
101
|
-
async function
|
|
129
|
+
async function prepareDev(context, options, frontendUrl) {
|
|
130
|
+
if (!options.autoStart || options.staticOnly) return {}
|
|
102
131
|
const ports = targetPorts(context, frontendUrl, options.full)
|
|
103
|
-
|
|
104
|
-
if (status.every(
|
|
105
|
-
|
|
106
|
-
throw new CliError(`Ports not listening: ${status.filter((entry) => !entry.listening).map((entry) => `${entry.label}:${entry.port}`).join(', ')}`)
|
|
107
|
-
}
|
|
132
|
+
const status = await waitForPorts(ports, 1)
|
|
133
|
+
if (status.every(entry => entry.listening) || hasRunningDev(context)) return {}
|
|
134
|
+
const logOffset = readDevLog(context).length
|
|
108
135
|
const pid = await startDevDetached(context)
|
|
109
|
-
console.log(`[vibecoding:verify] started dev (PID ${pid}); waiting for
|
|
110
|
-
|
|
111
|
-
|
|
136
|
+
console.log(`[vibecoding:verify] started dev (PID ${pid}); waiting for dependency installation`)
|
|
137
|
+
return { logOffset }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function verifyPorts(context, options, frontendUrl) {
|
|
141
|
+
const ports = targetPorts(context, frontendUrl, options.full)
|
|
142
|
+
const status = await waitForPorts(ports, options.autoStart ? options.timeoutSeconds : 1)
|
|
112
143
|
const missing = status.filter((entry) => !entry.listening)
|
|
113
144
|
if (missing.length > 0) {
|
|
145
|
+
if (!options.autoStart) {
|
|
146
|
+
throw new CliError(`Ports not listening: ${missing.map(entry => `${entry.label}:${entry.port}`).join(', ')}`)
|
|
147
|
+
}
|
|
114
148
|
printLastRunErrors(resolveDevLogPath(context))
|
|
115
149
|
throw new CliError(`Ports not ready after ${options.timeoutSeconds}s: ${missing.map((entry) => `${entry.label}:${entry.port}`).join(', ')}`)
|
|
116
150
|
}
|
|
@@ -578,12 +612,15 @@ export async function runVerify(context, options = {}) {
|
|
|
578
612
|
if (options.staticOnly && options.portsOnly) {
|
|
579
613
|
throw new CliError('--static-only cannot be combined with --ports-only')
|
|
580
614
|
}
|
|
615
|
+
// Fail before waiting for dev/install or starting build/runtime checks, for both targets.
|
|
616
|
+
readCardRouting(context)
|
|
581
617
|
const full = Boolean(options.full || options.autoStart || options.staticOnly ||
|
|
582
618
|
(context.target === 'taro' && process.env.npm_lifecycle_event === 'verify'))
|
|
583
619
|
options = { timeoutSeconds: 90, ...options, full }
|
|
584
620
|
const frontendUrl = resolveFrontendUrl(context, options)
|
|
585
621
|
parseHttpUrl(frontendUrl, 'frontend')
|
|
586
|
-
|
|
622
|
+
const installOptions = await prepareDev(context, options, frontendUrl)
|
|
623
|
+
if (full && !options.staticOnly) await waitForDevInstall(context, options.timeoutSeconds, installOptions)
|
|
587
624
|
if (full || options.staticOnly) await runStatic(context)
|
|
588
625
|
if (options.staticOnly) return
|
|
589
626
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { readFileSync, readlinkSync, realpathSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { resolveProjectContext } from './context.mjs'
|
|
5
|
+
|
|
6
|
+
/** Read only the recorded PID; never search processes or other projects. */
|
|
7
|
+
export function inspectDevProcess(pid) {
|
|
8
|
+
try {
|
|
9
|
+
const command = execFileSync('ps', ['-ww', '-p', String(pid), '-o', 'args='],
|
|
10
|
+
{ encoding: 'utf8', timeout: 1_000 }).trim()
|
|
11
|
+
const cwd = process.platform === 'linux' ? readlinkSync(`/proc/${pid}/cwd`)
|
|
12
|
+
: execFileSync('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'],
|
|
13
|
+
{ encoding: 'utf8', timeout: 1_000 }).split('\n').find(line => line.startsWith('n'))?.slice(1)
|
|
14
|
+
// Bootstrap may override its script's default root. Read just that field
|
|
15
|
+
// from Linux process metadata; never expose process environment contents.
|
|
16
|
+
let workspacePath
|
|
17
|
+
if (process.platform === 'linux') {
|
|
18
|
+
try { workspacePath = readFileSync(`/proc/${pid}/environ`, 'utf8')
|
|
19
|
+
.split('\0').find(entry => entry.startsWith('WORKSPACE_PATH='))?.slice('WORKSPACE_PATH='.length) } catch {}
|
|
20
|
+
}
|
|
21
|
+
return { command, cwd, workspacePath }
|
|
22
|
+
} catch { return null }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const flag = (command, name) => command.match(new RegExp(
|
|
26
|
+
`(?:^|\\s)--${name}(?:=|\\s+)(.*?)(?=\\s+--[\\w-]+(?:[=\\s]|$)|$)`,
|
|
27
|
+
))?.[1]?.trim().replace(/^(['"])(.*)\1$/, '$2')
|
|
28
|
+
const canonical = directory => {
|
|
29
|
+
try { return realpathSync(directory) } catch { return path.resolve(directory) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A live PID is reusable only when its dev entry, project and target match. */
|
|
33
|
+
export function devProcessMatches(context, state) {
|
|
34
|
+
if (!state?.cwd) return false
|
|
35
|
+
const { command, cwd } = state
|
|
36
|
+
const launcher = /(?:^|\s)(?:\S*\/)?vibecoding(?:-dev|\.mjs)?\s/.test(command)
|
|
37
|
+
&& /(?:^|\s)dev(?:\s|$)/.test(command)
|
|
38
|
+
const bootstrap = command.match(/^(?:\S*\/)?bash\s+(.*?scripts\/dev_run\.sh)(?:\s|$)/)
|
|
39
|
+
if (!launcher && !bootstrap) return false
|
|
40
|
+
const target = launcher ? flag(command, 'target') : 'taro'
|
|
41
|
+
if (!['taro', 'opencard'].includes(target)) return false
|
|
42
|
+
let root = path.resolve(cwd, launcher ? flag(command, 'cwd') || '.'
|
|
43
|
+
: state.workspacePath || path.join(path.dirname(bootstrap[1]), '..'))
|
|
44
|
+
if (launcher) {
|
|
45
|
+
// The public CLI can start from a project subdirectory without --cwd.
|
|
46
|
+
try { root = resolveProjectContext({ cwd: root, target }).root } catch {}
|
|
47
|
+
}
|
|
48
|
+
root = canonical(root)
|
|
49
|
+
const expected = canonical(context.root)
|
|
50
|
+
return root === expected && target === context.target
|
|
51
|
+
|| context.target === 'opencard' && target === 'taro'
|
|
52
|
+
&& canonical(path.join(root, 'opencard')) === expected
|
|
53
|
+
}
|