@open-dy/vibecoding-cli 0.0.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/assets/schemas/custom-card-schema.json +102 -0
- package/bin/vibecoding +89 -0
- package/bin/vibecoding-dev +308 -0
- package/bin/vibecoding-interest-card.mjs +35 -0
- package/bin/vibecoding.mjs +9 -0
- package/package.json +58 -0
- package/src/adapters/interest-card/opencard/commands.mjs +62 -0
- package/src/adapters/interest-card/opencard/dev-watch.mjs +203 -0
- package/src/adapters/interest-card/opencard/dev_run.sh +126 -0
- package/src/adapters/interest-card/opencard/eslint-plugin-opencard-taro.mjs +278 -0
- package/src/adapters/interest-card/opencard/lint-lynx-css.mjs +676 -0
- package/src/adapters/interest-card/opencard/lint.sh +131 -0
- package/src/adapters/interest-card/opencard/lynx-css-rules.mjs +508 -0
- package/src/adapters/interest-card/opencard/pack.sh +27 -0
- package/src/adapters/interest-card/opencard/validate-opencard-structure.mjs +65 -0
- package/src/adapters/interest-card/opencard/validate-widget-shell.mjs +902 -0
- package/src/adapters/interest-card/opencard/validate.mjs +11 -0
- package/src/adapters/interest-card/opencard/write-web-config.mjs +17 -0
- package/src/build/opencard-environment.mjs +49 -0
- package/src/build/turbo-config.mjs +140 -0
- package/src/cli.mjs +104 -0
- package/src/commands/build.mjs +138 -0
- package/src/commands/db.mjs +14 -0
- package/src/commands/dev.mjs +319 -0
- package/src/commands/lint.mjs +176 -0
- package/src/commands/pack.mjs +169 -0
- package/src/commands/turbo.mjs +36 -0
- package/src/commands/validate.mjs +26 -0
- package/src/commands/verify.mjs +602 -0
- package/src/core/args.mjs +101 -0
- package/src/core/context.mjs +101 -0
- package/src/core/environment.mjs +55 -0
- package/src/core/errors.mjs +14 -0
- package/src/core/ports.mjs +76 -0
- package/src/core/process-output.mjs +25 -0
- package/src/core/runner.mjs +90 -0
- package/src/core/service-log-context.mjs +27 -0
- package/src/core/web-config.mjs +13 -0
- package/src/lint/lint-lynx-css.mjs +244 -0
- package/src/lint/lynx-css-rules.mjs +138 -0
- package/src/preview/dev-watch.mjs +38 -0
- package/src/preview/start-preview.mjs +213 -0
- package/src/preview/watch-card-routing.mjs +89 -0
- package/src/scripts/pack.mjs +15 -0
- package/src/validators/dependencies.mjs +34 -0
- package/src/validators/empty-project.mjs +29 -0
- package/src/validators/feed-card.cjs +156 -0
- package/src/validators/feed-card.mjs +32 -0
- package/src/validators/opencard-structure.mjs +58 -0
- package/src/validators/routes.cjs +470 -0
- package/src/validators/routes.mjs +17 -0
- package/src/validators/sql.mjs +57 -0
- package/src/validators/widget-shell.mjs +107 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { UsageError } from './errors.mjs'
|
|
2
|
+
|
|
3
|
+
const BOOLEAN_FLAGS = new Set([
|
|
4
|
+
'auto-start',
|
|
5
|
+
'full',
|
|
6
|
+
'runtime-only',
|
|
7
|
+
'static-only',
|
|
8
|
+
'ports-only',
|
|
9
|
+
'skip-install',
|
|
10
|
+
'skip-root-install',
|
|
11
|
+
'incremental',
|
|
12
|
+
'help',
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
const VALUE_FLAGS = new Set(['target', 'cwd', 'timeout', 'frontend-url', 'scope'])
|
|
16
|
+
|
|
17
|
+
export function parseArgs(argv) {
|
|
18
|
+
const positionals = []
|
|
19
|
+
const flags = {}
|
|
20
|
+
const passthrough = []
|
|
21
|
+
let isPassthrough = false
|
|
22
|
+
|
|
23
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
24
|
+
const token = argv[index]
|
|
25
|
+
if (isPassthrough) {
|
|
26
|
+
passthrough.push(token)
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
if (token === '--') {
|
|
30
|
+
if (argv[0] !== 'turbo') {
|
|
31
|
+
throw new UsageError('The -- separator is only supported by vibecoding turbo')
|
|
32
|
+
}
|
|
33
|
+
isPassthrough = true
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (token === '-h') {
|
|
37
|
+
flags.help = true
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
if (!token.startsWith('--')) {
|
|
41
|
+
positionals.push(token)
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const raw = token.slice(2)
|
|
46
|
+
const separator = raw.indexOf('=')
|
|
47
|
+
const name = separator === -1 ? raw : raw.slice(0, separator)
|
|
48
|
+
const inlineValue = separator === -1 ? undefined : raw.slice(separator + 1)
|
|
49
|
+
|
|
50
|
+
if (BOOLEAN_FLAGS.has(name)) {
|
|
51
|
+
if (inlineValue !== undefined) {
|
|
52
|
+
throw new UsageError(`--${name} does not accept a value`)
|
|
53
|
+
}
|
|
54
|
+
flags[name] = true
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!VALUE_FLAGS.has(name)) {
|
|
59
|
+
throw new UsageError(`Unknown option: --${name}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const value = inlineValue ?? argv[index + 1]
|
|
63
|
+
if (!value || (inlineValue === undefined && value.startsWith('--'))) {
|
|
64
|
+
throw new UsageError(`Missing value for --${name}`)
|
|
65
|
+
}
|
|
66
|
+
flags[name] = value
|
|
67
|
+
if (inlineValue === undefined) index += 1
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { positionals, flags, passthrough }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function requireTarget(flags) {
|
|
74
|
+
const target = flags.target
|
|
75
|
+
if (!target) {
|
|
76
|
+
throw new UsageError(
|
|
77
|
+
'Missing required option: --target\nAvailable targets: taro, opencard',
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
if (target !== 'taro' && target !== 'opencard') {
|
|
81
|
+
throw new UsageError(
|
|
82
|
+
`Invalid target: ${target}\nAvailable targets: taro, opencard`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
return target
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function resolveBuildScope(command, target, flags) {
|
|
89
|
+
const scope = flags.scope
|
|
90
|
+
if (!scope) return undefined
|
|
91
|
+
if (command !== 'build') {
|
|
92
|
+
throw new UsageError('--scope is only supported by the build command')
|
|
93
|
+
}
|
|
94
|
+
if (target !== 'taro') {
|
|
95
|
+
throw new UsageError('--scope is only supported with --target taro')
|
|
96
|
+
}
|
|
97
|
+
if (scope !== 'tt') {
|
|
98
|
+
throw new UsageError(`Invalid build scope: ${scope}\nAvailable build scopes: tt`)
|
|
99
|
+
}
|
|
100
|
+
return scope
|
|
101
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { CliError } from './errors.mjs'
|
|
4
|
+
|
|
5
|
+
function hasAll(root, entries) {
|
|
6
|
+
return entries.every((entry) => existsSync(path.join(root, entry)))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isTaroRoot(root) {
|
|
10
|
+
return hasAll(root, ['package.json', 'src', 'config', 'server/package.json'])
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isOpenCardRoot(root) {
|
|
14
|
+
return hasAll(root, ['package.json', 'src', 'config', 'project.card.json'])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isPackageRoot(root) {
|
|
18
|
+
return hasAll(root, ['package.json'])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Infer the target for commands that do not need the target-specific options
|
|
23
|
+
* used by dev/build/lint. The nearest project marker wins, so an OpenCard
|
|
24
|
+
* directory nested inside a Taro project is still treated as OpenCard.
|
|
25
|
+
*/
|
|
26
|
+
export function inferProjectTarget({ cwd = process.cwd() } = {}) {
|
|
27
|
+
let current = path.resolve(cwd)
|
|
28
|
+
while (true) {
|
|
29
|
+
if (isOpenCardRoot(current)) return 'opencard'
|
|
30
|
+
if (isTaroRoot(current)) return 'taro'
|
|
31
|
+
const parent = path.dirname(current)
|
|
32
|
+
if (parent === current) return 'taro'
|
|
33
|
+
current = parent
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function findUp(start, predicate) {
|
|
38
|
+
let current = path.resolve(start)
|
|
39
|
+
while (true) {
|
|
40
|
+
if (predicate(current)) return current
|
|
41
|
+
const parent = path.dirname(current)
|
|
42
|
+
if (parent === current) return null
|
|
43
|
+
current = parent
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readPackage(root) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new CliError(`Cannot read ${path.join(root, 'package.json')}: ${error.message}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveProjectContext({ cwd = process.cwd(), target }) {
|
|
56
|
+
const requested = path.resolve(cwd)
|
|
57
|
+
let root
|
|
58
|
+
|
|
59
|
+
if (target === 'taro') {
|
|
60
|
+
root = findUp(requested, isTaroRoot)
|
|
61
|
+
} else {
|
|
62
|
+
root = findUp(requested, isOpenCardRoot)
|
|
63
|
+
if (!root) {
|
|
64
|
+
const taroRoot = findUp(requested, isTaroRoot)
|
|
65
|
+
const nested = taroRoot ? path.join(taroRoot, 'opencard') : null
|
|
66
|
+
if (nested && isOpenCardRoot(nested)) root = nested
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!root) {
|
|
71
|
+
const expected = target === 'taro'
|
|
72
|
+
? 'package.json, src/, config/ and server/package.json'
|
|
73
|
+
: 'package.json, src/, config/ and project.card.json'
|
|
74
|
+
throw new CliError(
|
|
75
|
+
`Cannot locate a ${target} project from ${requested}. Expected ${expected}.`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const packageJson = readPackage(root)
|
|
80
|
+
const taroRoot = target === 'taro' ? root : path.dirname(root)
|
|
81
|
+
const opencardRoot = target === 'taro'
|
|
82
|
+
? (isOpenCardRoot(path.join(root, 'opencard')) ? path.join(root, 'opencard') : null)
|
|
83
|
+
: root
|
|
84
|
+
const closedDomainServerRoot = target === 'taro'
|
|
85
|
+
? (isPackageRoot(path.join(root, 'closed-domain-server'))
|
|
86
|
+
? path.join(root, 'closed-domain-server')
|
|
87
|
+
: null)
|
|
88
|
+
: null
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
target,
|
|
92
|
+
root,
|
|
93
|
+
taroRoot,
|
|
94
|
+
opencardRoot,
|
|
95
|
+
closedDomainServerRoot,
|
|
96
|
+
serverRoot: target === 'taro' ? path.join(root, 'server') : null,
|
|
97
|
+
packageJson,
|
|
98
|
+
hasOpenCard: Boolean(opencardRoot && isOpenCardRoot(opencardRoot)),
|
|
99
|
+
hasClosedDomainServer: Boolean(closedDomainServerRoot),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { runCaptured } from './runner.mjs'
|
|
4
|
+
|
|
5
|
+
export function readEnvFile(root, name = '.env.dev') {
|
|
6
|
+
const file = path.join(root, name)
|
|
7
|
+
if (!existsSync(file)) return {}
|
|
8
|
+
const values = {}
|
|
9
|
+
for (const rawLine of readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
10
|
+
const line = rawLine.trim()
|
|
11
|
+
if (!line || line.startsWith('#')) continue
|
|
12
|
+
const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/)
|
|
13
|
+
if (!match) continue
|
|
14
|
+
let value = match[2].trim()
|
|
15
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
16
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
17
|
+
value = value.slice(1, -1)
|
|
18
|
+
}
|
|
19
|
+
values[match[1]] = value
|
|
20
|
+
}
|
|
21
|
+
return values
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseNullSeparatedEnvironment(output) {
|
|
25
|
+
const values = {}
|
|
26
|
+
for (const entry of output.split('\0')) {
|
|
27
|
+
const separator = entry.indexOf('=')
|
|
28
|
+
if (separator <= 0) continue
|
|
29
|
+
values[entry.slice(0, separator)] = entry.slice(separator + 1)
|
|
30
|
+
}
|
|
31
|
+
return values
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function readRuntimeEnvironment(root, options = {}) {
|
|
35
|
+
const baseEnv = options.baseEnv ?? process.env
|
|
36
|
+
const runCapturedFn = options.runCapturedFn ?? runCaptured
|
|
37
|
+
const result = await runCapturedFn('bash', [
|
|
38
|
+
'-c',
|
|
39
|
+
'set +u; if [ -f "$HOME/.bashrc" ]; then source "$HOME/.bashrc" >/dev/null 2>&1 || true; fi; env -0',
|
|
40
|
+
], {
|
|
41
|
+
allowFailure: true,
|
|
42
|
+
env: baseEnv,
|
|
43
|
+
})
|
|
44
|
+
const shellEnv = result.code === 0
|
|
45
|
+
? parseNullSeparatedEnvironment(result.stdout)
|
|
46
|
+
: {}
|
|
47
|
+
return {
|
|
48
|
+
...baseEnv,
|
|
49
|
+
...shellEnv,
|
|
50
|
+
// The dev command runs outside the cloud runtime. Do not inherit the
|
|
51
|
+
// sandbox image's production mode; explicit project configuration wins.
|
|
52
|
+
...(options.development ? { APP_ENV: 'DEV' } : {}),
|
|
53
|
+
...readEnvFile(root),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class CliError extends Error {
|
|
2
|
+
constructor(message, exitCode = 1) {
|
|
3
|
+
super(message)
|
|
4
|
+
this.name = 'CliError'
|
|
5
|
+
this.exitCode = exitCode
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class UsageError extends CliError {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message, 2)
|
|
12
|
+
this.name = 'UsageError'
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { runCaptured } from './runner.mjs'
|
|
2
|
+
|
|
3
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
4
|
+
const DEFAULT_TERMINATION_GRACE_MS = 3_000
|
|
5
|
+
|
|
6
|
+
function isProcessAlive(pid) {
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0)
|
|
9
|
+
return true
|
|
10
|
+
} catch {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function terminateProcesses(pids, options = {}) {
|
|
16
|
+
const graceMs = options.graceMs ?? DEFAULT_TERMINATION_GRACE_MS
|
|
17
|
+
const targets = [...new Set(pids.map(Number).filter(Number.isInteger))]
|
|
18
|
+
.filter((pid) => pid > 0 && isProcessAlive(pid))
|
|
19
|
+
|
|
20
|
+
for (const pid of targets) {
|
|
21
|
+
try { process.kill(pid, 'SIGTERM') } catch {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const deadline = Date.now() + graceMs
|
|
25
|
+
let survivors = targets.filter(isProcessAlive)
|
|
26
|
+
while (survivors.length > 0 && Date.now() < deadline) {
|
|
27
|
+
await sleep(Math.min(50, Math.max(1, deadline - Date.now())))
|
|
28
|
+
survivors = survivors.filter(isProcessAlive)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const pid of survivors) {
|
|
32
|
+
try { process.kill(pid, 'SIGKILL') } catch {}
|
|
33
|
+
}
|
|
34
|
+
return { terminated: targets.filter((pid) => !survivors.includes(pid)), forced: survivors }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function isPortListening(port) {
|
|
38
|
+
const result = await runCaptured('lsof', [`-tiTCP:${port}`, '-sTCP:LISTEN'], {
|
|
39
|
+
allowFailure: true,
|
|
40
|
+
})
|
|
41
|
+
return result.code === 0 && result.stdout.trim().length > 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function clearPort(port) {
|
|
45
|
+
const result = await runCaptured('lsof', [`-tiTCP:${port}`, '-sTCP:LISTEN'], {
|
|
46
|
+
allowFailure: true,
|
|
47
|
+
})
|
|
48
|
+
const pids = [...new Set(result.stdout.trim().split(/\s+/).filter(Boolean))]
|
|
49
|
+
if (pids.length === 0) {
|
|
50
|
+
console.log(`[vibecoding:dev] port ${port} is free`)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
console.log(`[vibecoding:dev] clearing port ${port} (PIDs: ${pids.join(', ')})`)
|
|
54
|
+
const { forced } = await terminateProcesses(pids)
|
|
55
|
+
if (forced.length > 0) {
|
|
56
|
+
console.warn(
|
|
57
|
+
`[vibecoding:dev] force-killed port ${port} processes after timeout (PIDs: ${forced.join(', ')})`,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function waitForPorts(ports, timeoutSeconds) {
|
|
63
|
+
const deadline = Date.now() + timeoutSeconds * 1000
|
|
64
|
+
while (Date.now() < deadline) {
|
|
65
|
+
const status = await Promise.all(ports.map(async (entry) => ({
|
|
66
|
+
...entry,
|
|
67
|
+
listening: await isPortListening(entry.port),
|
|
68
|
+
})))
|
|
69
|
+
if (status.every((entry) => entry.listening)) return status
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
71
|
+
}
|
|
72
|
+
return Promise.all(ports.map(async (entry) => ({
|
|
73
|
+
...entry,
|
|
74
|
+
listening: await isPortListening(entry.port),
|
|
75
|
+
})))
|
|
76
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline'
|
|
2
|
+
|
|
3
|
+
export function forwardLabeledStream(source, destination, label) {
|
|
4
|
+
const reader = createInterface({ input: source, crlfDelay: Infinity, terminal: false })
|
|
5
|
+
|
|
6
|
+
reader.on('line', (line) => {
|
|
7
|
+
if (!destination.write(`[${label}] ${line}\n`)) {
|
|
8
|
+
reader.pause()
|
|
9
|
+
destination.once('drain', () => reader.resume())
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
return reader
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function attachLabeledOutput(child, label, options = {}) {
|
|
17
|
+
const readers = []
|
|
18
|
+
const stdout = options.stdout ?? process.stdout
|
|
19
|
+
const stderr = options.stderr ?? process.stderr
|
|
20
|
+
|
|
21
|
+
if (child.stdout) readers.push(forwardLabeledStream(child.stdout, stdout, label))
|
|
22
|
+
if (child.stderr) readers.push(forwardLabeledStream(child.stderr, stderr, label))
|
|
23
|
+
|
|
24
|
+
return readers
|
|
25
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { CliError } from './errors.mjs'
|
|
5
|
+
|
|
6
|
+
export function resolveProjectBinary(root, name) {
|
|
7
|
+
const executable = process.platform === 'win32' ? `${name}.cmd` : name
|
|
8
|
+
return path.join(root, 'node_modules', '.bin', executable)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveSdkTaroEntry(root) {
|
|
12
|
+
return createRequire(path.join(root, 'package.json')).resolve('@open-dy/vibecoding-sdk-opencard/build/taro-toolchain')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function runProcess(command, args = [], options = {}) {
|
|
16
|
+
const label = options.label ?? path.basename(command)
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const child = spawn(command, args, {
|
|
19
|
+
cwd: options.cwd,
|
|
20
|
+
env: { ...process.env, ...options.env },
|
|
21
|
+
stdio: options.stdio ?? 'inherit',
|
|
22
|
+
detached: options.detached ?? false,
|
|
23
|
+
shell: options.shell ?? false,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
child.on('error', (error) => {
|
|
27
|
+
reject(new CliError(`${label} failed to start: ${error.message}`))
|
|
28
|
+
})
|
|
29
|
+
child.on('exit', (code, signal) => {
|
|
30
|
+
if (code === 0) {
|
|
31
|
+
resolve({ code: 0, signal, child })
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
reject(new CliError(
|
|
35
|
+
`${label} failed${signal ? ` with signal ${signal}` : ` with exit code ${code}`}`,
|
|
36
|
+
))
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function runCaptured(command, args = [], options = {}) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const child = spawn(command, args, {
|
|
44
|
+
cwd: options.cwd,
|
|
45
|
+
env: { ...process.env, ...options.env },
|
|
46
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
47
|
+
shell: false,
|
|
48
|
+
})
|
|
49
|
+
let stdout = ''
|
|
50
|
+
let stderr = ''
|
|
51
|
+
child.stdout.setEncoding('utf8')
|
|
52
|
+
child.stderr.setEncoding('utf8')
|
|
53
|
+
child.stdout.on('data', (chunk) => { stdout += chunk })
|
|
54
|
+
child.stderr.on('data', (chunk) => { stderr += chunk })
|
|
55
|
+
child.on('error', (error) => reject(new CliError(error.message)))
|
|
56
|
+
child.on('exit', (code) => {
|
|
57
|
+
if (code === 0 || options.allowFailure) {
|
|
58
|
+
resolve({ code: code ?? 1, stdout, stderr })
|
|
59
|
+
} else {
|
|
60
|
+
reject(new CliError(
|
|
61
|
+
`${options.label ?? path.basename(command)} failed with exit code ${code}\n${stderr || stdout}`,
|
|
62
|
+
))
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function runTasks(tasks, options = {}) {
|
|
69
|
+
const results = await Promise.allSettled(
|
|
70
|
+
tasks.map(async ({ name, run }) => {
|
|
71
|
+
console.log(`[vibecoding] starting ${name}`)
|
|
72
|
+
await run()
|
|
73
|
+
console.log(`[vibecoding] completed ${name}`)
|
|
74
|
+
return name
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const failures = results
|
|
79
|
+
.map((result, index) => ({ result, task: tasks[index] }))
|
|
80
|
+
.filter(({ result }) => result.status === 'rejected')
|
|
81
|
+
|
|
82
|
+
if (failures.length > 0) {
|
|
83
|
+
const details = failures.map(({ result, task }) =>
|
|
84
|
+
`- ${task.name}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`,
|
|
85
|
+
)
|
|
86
|
+
throw new CliError(
|
|
87
|
+
`${options.label ?? 'tasks'} failed (${failures.length}):\n${details.join('\n')}`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
|
|
3
|
+
const serviceLogContext = new AsyncLocalStorage()
|
|
4
|
+
const consoleMethods = ['debug', 'error', 'info', 'log', 'warn']
|
|
5
|
+
let consoleInstalled = false
|
|
6
|
+
|
|
7
|
+
function installContextualConsole() {
|
|
8
|
+
if (consoleInstalled) return
|
|
9
|
+
consoleInstalled = true
|
|
10
|
+
|
|
11
|
+
for (const method of consoleMethods) {
|
|
12
|
+
const original = console[method].bind(console)
|
|
13
|
+
console[method] = (...args) => {
|
|
14
|
+
const label = serviceLogContext.getStore()
|
|
15
|
+
if (!label || (typeof args[0] === 'string' && args[0].startsWith(`[${label}]`))) {
|
|
16
|
+
original(...args)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
original(`[${label}]`, ...args)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function withServiceLogLabel(label, run) {
|
|
25
|
+
installContextualConsole()
|
|
26
|
+
return serviceLogContext.run(label, run)
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
const WEB_CONFIG = {
|
|
5
|
+
hasAuditDouyinCloud: true,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function writeWebConfig(outputRoot) {
|
|
9
|
+
if (!outputRoot) throw new Error('writeWebConfig requires an output root')
|
|
10
|
+
const output = path.resolve(outputRoot, 'web.config.json')
|
|
11
|
+
writeFileSync(output, `${JSON.stringify(WEB_CONFIG, null, 2)}\n`, 'utf8')
|
|
12
|
+
return output
|
|
13
|
+
}
|