@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,89 @@
1
+ import { watchFile, unwatchFile, readFileSync } from 'node:fs'
2
+ import { resolveCardRoutingPath, openCardBuildEnvironment } from '../build/opencard-environment.mjs'
3
+
4
+ // Polling follows atomic file replacements and also works when config/ is absent
5
+ // at startup. Debouncing avoids restarting while an editor is still saving.
6
+ export async function startRoutingPreview({ root, start, onError = console.error, signal, pollMs = 250, debounceMs = 150 }) {
7
+ const context = { root }
8
+ const file = resolveCardRoutingPath(context)
9
+ let mode = openCardBuildEnvironment(context).IS_CARD_CLOSED_DOMAIN
10
+ const controller = new AbortController()
11
+ const cancelStart = () => controller.abort()
12
+ signal?.addEventListener('abort', cancelStart, { once: true })
13
+ if (signal?.aborted) controller.abort()
14
+ async function launch(value) {
15
+ const runtime = await start({ IS_CARD_CLOSED_DOMAIN: value }, controller.signal)
16
+ let closing
17
+ return { ...runtime, close: () => (closing ??= Promise.resolve().then(() => runtime.close())) }
18
+ }
19
+ let current
20
+ try {
21
+ current = await launch(mode)
22
+ } catch (error) {
23
+ signal?.removeEventListener('abort', cancelStart)
24
+ throw error
25
+ }
26
+ let stopped = false
27
+ let timer
28
+ let queue = Promise.resolve()
29
+ let lastError
30
+
31
+ function readMode() {
32
+ // During watching, deletion/incomplete saves must not become an open default.
33
+ const routing = JSON.parse(readFileSync(file, 'utf8'))
34
+ const domain = routing?.cardData?.serviceDomain
35
+ if (!['closed', 'open', 'none'].includes(domain)) throw new Error(`${file}: cardData.serviceDomain must be closed, open, or none`)
36
+ return String(domain === 'closed')
37
+ }
38
+ async function refresh() {
39
+ if (stopped) return
40
+ let next
41
+ try {
42
+ next = readMode()
43
+ lastError = undefined
44
+ } catch (error) {
45
+ if (lastError !== error.message) console.error(`[opencard] Routing unchanged: ${error.message}`)
46
+ lastError = error.message
47
+ return
48
+ }
49
+ if (mode === next) return
50
+ console.log(`[opencard] Domain changed; restarting preview (closed=${next})`)
51
+ await current.close()
52
+ if (stopped) return
53
+ current = await launch(next)
54
+ mode = next
55
+ if (stopped) await current.close()
56
+ }
57
+ function changed() {
58
+ clearTimeout(timer)
59
+ timer = setTimeout(() => {
60
+ queue = queue.then(refresh).catch(error => {
61
+ if (!stopped) { onError(error); void close().catch(onError) }
62
+ })
63
+ }, debounceMs)
64
+ }
65
+ watchFile(file, { interval: pollMs }, changed)
66
+ // Catch a save that happened while the first compiler was starting.
67
+ changed()
68
+ let closing
69
+ function close() {
70
+ if (closing) return closing
71
+ stopped = true
72
+ controller.abort()
73
+ clearTimeout(timer)
74
+ unwatchFile(file, changed)
75
+ signal?.removeEventListener('abort', abort)
76
+ signal?.removeEventListener('abort', cancelStart)
77
+ closing = (async () => { await current.close(); await queue })()
78
+ return closing
79
+ }
80
+ function abort() { void close().catch(onError) }
81
+ signal?.addEventListener('abort', abort, { once: true })
82
+ if (signal?.aborted) await close()
83
+ return {
84
+ get child() { return current.child },
85
+ get runtime() { return current.runtime },
86
+ get port() { return current.port },
87
+ close,
88
+ }
89
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { runPack } from '../commands/pack.mjs'
3
+
4
+ // Compatibility entry for scripts/pack.sh: do not source .env.dev or .bashrc,
5
+ // let runPack resolve ARTIFACTS_DIR with the standard default fallback.
6
+ const root = process.argv[2]
7
+ if (!root) {
8
+ console.error('Missing project root')
9
+ process.exitCode = 2
10
+ } else {
11
+ runPack({ root, target: 'taro' }).catch(error => {
12
+ console.error(error.message)
13
+ process.exitCode = error.exitCode ?? 1
14
+ })
15
+ }
@@ -0,0 +1,34 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { CliError } from '../core/errors.mjs'
4
+
5
+ const FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']
6
+ const FORBIDDEN = ['three', '@types/three']
7
+
8
+ export function packageUses(root, name) {
9
+ try {
10
+ const manifest = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'))
11
+ return FIELDS.some((field) => Boolean(manifest[field]?.[name]))
12
+ } catch { return false }
13
+ }
14
+
15
+ export function validateDependencies(context) {
16
+ const roots = context.target === 'opencard'
17
+ ? [context.root]
18
+ : [context.root, ...(context.hasOpenCard ? [context.opencardRoot] : [])]
19
+ const issues = []
20
+ for (const root of roots) {
21
+ if (!existsSync(path.join(root, 'package.json'))) continue
22
+ const manifest = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'))
23
+ for (const field of FIELDS) {
24
+ for (const name of FORBIDDEN) {
25
+ if (manifest[field]?.[name]) issues.push(`${path.join(root, 'package.json')}: ${field}.${name}`)
26
+ }
27
+ }
28
+ }
29
+ if (issues.length > 0) {
30
+ throw new CliError(
31
+ `Unsupported web Three.js dependencies found:\n${issues.map((item) => `- ${item}`).join('\n')}\nUse @open-dy/threejs instead.`,
32
+ )
33
+ }
34
+ }
@@ -0,0 +1,29 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { CliError } from '../core/errors.mjs'
4
+
5
+ // Keep the generated-project guard aligned with both source templates. The
6
+ // interest-card template used a different empty-page copy before this check
7
+ // moved into the shared CLI, so one generic signature pair is not enough.
8
+ const EMPTY_TEMPLATE_SIGNATURE_SETS = [
9
+ ['这是一个空白的小程序示意', '你可以继续描述对小程序的需求~'],
10
+ ['这是一个空白的兴趣卡落地页示意', '你可以继续描述对兴趣卡落地页的需求~'],
11
+ ]
12
+
13
+ export function validateNonEmptyTaro(context) {
14
+ if (context.target !== 'taro') return
15
+ const pages = path.join(context.root, 'src/pages')
16
+ const index = path.join(pages, 'index/index.tsx')
17
+ if (!existsSync(index)) return
18
+ if (existsSync(pages)) {
19
+ const hasOtherPage = readdirSync(pages).some((name) =>
20
+ name !== 'index' && statSync(path.join(pages, name)).isDirectory(),
21
+ )
22
+ if (hasOtherPage) return
23
+ }
24
+ const content = readFileSync(index, 'utf8')
25
+ if (EMPTY_TEMPLATE_SIGNATURE_SETS.some((signatures) =>
26
+ signatures.every((signature) => content.includes(signature)))) {
27
+ throw new CliError('The Taro project still contains the empty template page')
28
+ }
29
+ }
@@ -0,0 +1,156 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const Ajv = require("ajv");
4
+
5
+ function failFast(message) {
6
+ throw new Error(`\n[Validation Failed] ${message}`);
7
+ }
8
+
9
+ function reportAndExit(errors) {
10
+ failFast(
11
+ `Found ${errors.length} issue(s). Please fix ALL of them in a single revision before re-running validation.\n\n` +
12
+ errors.map((error, index) => `(${index + 1}) ${error}\n`).join("\n"),
13
+ );
14
+ }
15
+
16
+ function readCardDataServiceDomain(projectRoot) {
17
+ const routingPath = path.join(projectRoot, "config", "card-routing.json");
18
+ if (!fs.existsSync(routingPath)) {
19
+ failFast(
20
+ `Missing ${routingPath}. Cannot choose a Feed card Schema until cardData.serviceDomain declares open or closed.`,
21
+ );
22
+ }
23
+
24
+ let routing;
25
+ try {
26
+ routing = JSON.parse(fs.readFileSync(routingPath, "utf8"));
27
+ } catch (error) {
28
+ failFast(`Invalid JSON in ${routingPath}: ${error.message}`);
29
+ }
30
+
31
+ const serviceDomain = routing?.cardData?.serviceDomain;
32
+ if (serviceDomain !== "open" && serviceDomain !== "closed") {
33
+ failFast(
34
+ `${routingPath} must declare cardData.serviceDomain as "open" or "closed" before validating feed-card.json; got ${JSON.stringify(serviceDomain)}.`,
35
+ );
36
+ }
37
+ return serviceDomain;
38
+ }
39
+
40
+ function resolveFeedCardSchema(projectRoot) {
41
+ // card-routing.json is the sole source of truth for the selected service
42
+ // domain. Both domains share the custom Feed card data contract; keep
43
+ // project-local constraints when present; newly generated projects can rely
44
+ // on the schema shipped with the CLI.
45
+ readCardDataServiceDomain(projectRoot);
46
+ const schemaName = "custom-card-schema.json";
47
+ const schemaPath = [
48
+ path.join(projectRoot, "scripts", "schemas", schemaName),
49
+ path.join(projectRoot, "feed", schemaName),
50
+ ].find((candidate) => fs.existsSync(candidate)) ??
51
+ path.resolve(__dirname, "../../assets/schemas", schemaName);
52
+ if (!fs.existsSync(schemaPath)) {
53
+ failFast(`Schema file missing on disk: ${schemaPath}`);
54
+ }
55
+ return schemaPath;
56
+ }
57
+
58
+ function validateFeedCard(context, options = {}) {
59
+ if (context.target !== "taro") {
60
+ throw new Error("Feed card validation requires --target taro");
61
+ }
62
+ const projectRoot = context.root;
63
+ const appId = process.env.APP_ID || "";
64
+ const cardAppId = process.env.OPEN_CARD_ID || "";
65
+
66
+ const jsonPath = path.join(projectRoot, "feed/feed-card.json");
67
+ const opencardDir = path.join(projectRoot, "opencard");
68
+ const hasOpencard = fs.existsSync(opencardDir);
69
+ const hasFeedCard = fs.existsSync(jsonPath);
70
+
71
+ if (hasOpencard && !hasFeedCard) {
72
+ failFast(
73
+ `Missing ${jsonPath} while opencard/ exists.\n Fix: Read config/card-routing.json first, then generate feed/feed-card.json with the matching Feed card Schema.`,
74
+ );
75
+ }
76
+
77
+ if (!hasFeedCard) {
78
+ return { skipped: true };
79
+ }
80
+
81
+ let data;
82
+ try {
83
+ const fileContent = fs.readFileSync(jsonPath, "utf8");
84
+ data = JSON.parse(fileContent);
85
+ } catch (e) {
86
+ failFast(
87
+ `Invalid JSON syntax in ${jsonPath}: ${e.message}\n Fix: Check trailing commas, unmatched brackets, unescaped quotes, and re-emit syntactically valid JSON.`,
88
+ );
89
+ }
90
+
91
+ const templateType = data?.TemplateType;
92
+ const templateStyleType = data?.TemplateStyleType;
93
+
94
+ if (templateType !== 5 || templateStyleType !== 6) {
95
+ failFast(
96
+ `Only custom feed card is supported: expected TemplateType=5 and TemplateStyleType=6, got TemplateType=${JSON.stringify(templateType)}, TemplateStyleType=${JSON.stringify(templateStyleType)}.\n Fix: Regenerate feed-card.json using the Schema selected from the project's real service-domain configuration.`,
97
+ );
98
+ }
99
+
100
+ if (!hasOpencard) {
101
+ failFast(
102
+ `feed-card.json declares a custom Feed card (TemplateStyleType=6) but opencard/ is missing.\n Fix: Install the opencard sub-app (see install-opencard.sh) before shipping the custom card.`,
103
+ );
104
+ }
105
+
106
+ const schemaPath = resolveFeedCardSchema(projectRoot);
107
+
108
+ const errors = [];
109
+
110
+ if (appId && data.AppID !== appId) {
111
+ errors.push(
112
+ `AppID mismatch: expected "${appId}", got "${data.AppID}".\n Fix: Update AppID to "${appId}" and regenerate PageURL so its query parameters reflect the new AppID.`,
113
+ );
114
+ }
115
+
116
+ if (cardAppId && data.CardAppID !== cardAppId) {
117
+ errors.push(
118
+ `CardAppID mismatch: expected "${cardAppId}", got "${data.CardAppID}".\n Fix: Update CardAppID to "${cardAppId}" (custom feed card requires this exact value).`,
119
+ );
120
+ }
121
+
122
+ try {
123
+ const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
124
+ const ajv = new Ajv({ allErrors: true, unicodeRegExp: true });
125
+ const validate = ajv.compile(schema);
126
+ if (!validate(data)) {
127
+ errors.push(
128
+ `JSON Schema validation failed (schema: ${path.basename(schemaPath)}). Raw ajv errors:\n` +
129
+ JSON.stringify(validate.errors, null, 2) +
130
+ "\n Fix: For each error above, locate the offending field via the instancePath, and adjust value/type/length/enum to satisfy the rule. All listed errors must be fixed together.",
131
+ );
132
+ }
133
+ } catch (error) {
134
+ errors.push(
135
+ `JSON Schema validation failed (schema: ${path.basename(schemaPath)}). ${error.message}`,
136
+ );
137
+ }
138
+
139
+ if (errors.length > 0) {
140
+ reportAndExit(errors);
141
+ }
142
+
143
+ if (options.touchTimestamp !== false) {
144
+ data.__timestamp = Date.now();
145
+ try {
146
+ fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf8");
147
+ } catch {
148
+ // Match the template validator: timestamp updates are best-effort.
149
+ }
150
+ }
151
+
152
+ console.log("All validations passed!");
153
+ return { skipped: false };
154
+ }
155
+
156
+ module.exports = { resolveFeedCardSchema, validateFeedCard };
@@ -0,0 +1,32 @@
1
+ // Share one implementation with the synchronous legacy template entry point.
2
+ import validator from './feed-card.cjs'
3
+ import { CliError } from '../core/errors.mjs'
4
+
5
+ function runValidation(callback) {
6
+ try {
7
+ return callback()
8
+ } catch (error) {
9
+ throw new CliError(error.message)
10
+ }
11
+ }
12
+
13
+ /**
14
+ * @desc 定位工程实际使用的 Feed 卡片 schema 文件。
15
+ * @params {string} projectRoot 工程根目录。
16
+ * @return {string} schema 文件的绝对路径。
17
+ * @example resolveFeedCardSchema(process.cwd())
18
+ */
19
+ export function resolveFeedCardSchema(projectRoot) {
20
+ return runValidation(() => validator.resolveFeedCardSchema(projectRoot))
21
+ }
22
+
23
+ /**
24
+ * @desc 校验 Feed 卡片结构、字段和可发布性约束。
25
+ * @params {object} context CLI 工程上下文。
26
+ * @params {object} [options] 校验选项。
27
+ * @return {unknown} 校验器返回的结果;发现错误时抛出 CliError。
28
+ * @example validateFeedCard(context, { full: true })
29
+ */
30
+ export function validateFeedCard(context, options = {}) {
31
+ return runValidation(() => validator.validateFeedCard(context, options))
32
+ }
@@ -0,0 +1,58 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { CliError } from '../core/errors.mjs'
4
+
5
+ const FORBIDDEN = ['app.ts', 'app.tsx', 'app.js', 'app.jsx']
6
+ const TIMING = /__lynx_timing_flag\s*=\s*(["'])__lynx_timing_actual_fmp\1/g
7
+
8
+ function walk(directory, files = []) {
9
+ if (!existsSync(directory)) return files
10
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
11
+ const target = path.join(directory, entry.name)
12
+ if (entry.isDirectory()) walk(target, files)
13
+ else if (/\.[jt]sx?$/.test(entry.name)) files.push(target)
14
+ }
15
+ return files
16
+ }
17
+
18
+ export function validateOpenCardStructure(context) {
19
+ const root = context.target === 'opencard' ? context.root : context.opencardRoot
20
+ if (!root) return { skipped: true }
21
+ const src = path.join(root, 'src')
22
+ const forbidden = FORBIDDEN.filter((name) => existsSync(path.join(src, name)))
23
+ if (forbidden.length > 0) {
24
+ throw new CliError(`OpenCard does not support App entry files: ${forbidden.join(', ')}`)
25
+ }
26
+
27
+ let timingCount = 0
28
+ for (const file of walk(src)) {
29
+ const content = readFileSync(file, 'utf8')
30
+ if (/\.[jt]sx$/.test(file)) timingCount += content.match(TIMING)?.length ?? 0
31
+ }
32
+ if (timingCount !== 1) {
33
+ throw new CliError(
34
+ `OpenCard src must contain exactly one Actual FMP timing flag; found ${timingCount}`,
35
+ )
36
+ }
37
+
38
+ let pageSource = ''
39
+ for (const file of walk(path.join(src, 'pages'))) {
40
+ pageSource += `\n${readFileSync(file, 'utf8')}`
41
+ }
42
+ const navigationCount = pageSource.match(/\bnavigateToMiniProgram\s*\(/g)?.length ?? 0
43
+ if (navigationCount > 0) {
44
+ const preloadCount = pageSource.match(/\.preloadMiniApp\s*\(\s*\{\s*appList\b/g)?.length ?? 0
45
+ if (
46
+ preloadCount !== 1 ||
47
+ !/\buseDidShow\s*\(/.test(pageSource) ||
48
+ !/\buseReady\s*\(/.test(pageSource)
49
+ ) {
50
+ throw new CliError(
51
+ `OpenCard found ${navigationCount} navigateToMiniProgram call(s); ` +
52
+ 'requires exactly one preloadMiniApp guarded by useReady and first useDidShow; ' +
53
+ `found ${preloadCount} preload call(s)`,
54
+ )
55
+ }
56
+ }
57
+ return { skipped: false }
58
+ }