@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.
Files changed (54) hide show
  1. package/README.md +47 -0
  2. package/assets/schemas/custom-card-schema.json +102 -0
  3. package/bin/vibecoding +89 -0
  4. package/bin/vibecoding-dev +308 -0
  5. package/bin/vibecoding-interest-card.mjs +35 -0
  6. package/bin/vibecoding.mjs +9 -0
  7. package/package.json +58 -0
  8. package/src/adapters/interest-card/opencard/commands.mjs +62 -0
  9. package/src/adapters/interest-card/opencard/dev-watch.mjs +203 -0
  10. package/src/adapters/interest-card/opencard/dev_run.sh +126 -0
  11. package/src/adapters/interest-card/opencard/eslint-plugin-opencard-taro.mjs +278 -0
  12. package/src/adapters/interest-card/opencard/lint-lynx-css.mjs +676 -0
  13. package/src/adapters/interest-card/opencard/lint.sh +131 -0
  14. package/src/adapters/interest-card/opencard/lynx-css-rules.mjs +508 -0
  15. package/src/adapters/interest-card/opencard/pack.sh +27 -0
  16. package/src/adapters/interest-card/opencard/validate-opencard-structure.mjs +65 -0
  17. package/src/adapters/interest-card/opencard/validate-widget-shell.mjs +902 -0
  18. package/src/adapters/interest-card/opencard/validate.mjs +11 -0
  19. package/src/adapters/interest-card/opencard/write-web-config.mjs +17 -0
  20. package/src/build/opencard-environment.mjs +49 -0
  21. package/src/build/turbo-config.mjs +140 -0
  22. package/src/cli.mjs +104 -0
  23. package/src/commands/build.mjs +138 -0
  24. package/src/commands/db.mjs +14 -0
  25. package/src/commands/dev.mjs +319 -0
  26. package/src/commands/lint.mjs +176 -0
  27. package/src/commands/pack.mjs +169 -0
  28. package/src/commands/turbo.mjs +36 -0
  29. package/src/commands/validate.mjs +26 -0
  30. package/src/commands/verify.mjs +602 -0
  31. package/src/core/args.mjs +101 -0
  32. package/src/core/context.mjs +101 -0
  33. package/src/core/environment.mjs +55 -0
  34. package/src/core/errors.mjs +14 -0
  35. package/src/core/ports.mjs +76 -0
  36. package/src/core/process-output.mjs +25 -0
  37. package/src/core/runner.mjs +90 -0
  38. package/src/core/service-log-context.mjs +27 -0
  39. package/src/core/web-config.mjs +13 -0
  40. package/src/lint/lint-lynx-css.mjs +244 -0
  41. package/src/lint/lynx-css-rules.mjs +138 -0
  42. package/src/preview/dev-watch.mjs +38 -0
  43. package/src/preview/start-preview.mjs +213 -0
  44. package/src/preview/watch-card-routing.mjs +89 -0
  45. package/src/scripts/pack.mjs +15 -0
  46. package/src/validators/dependencies.mjs +34 -0
  47. package/src/validators/empty-project.mjs +29 -0
  48. package/src/validators/feed-card.cjs +156 -0
  49. package/src/validators/feed-card.mjs +32 -0
  50. package/src/validators/opencard-structure.mjs +58 -0
  51. package/src/validators/routes.cjs +470 -0
  52. package/src/validators/routes.mjs +17 -0
  53. package/src/validators/sql.mjs +57 -0
  54. 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
+ }