draftgo-cli 2.0.3 → 2.0.5

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.
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { analyzeProject } = require('../projectMap');
5
+
6
+ function check(projectDir, flags = {}) {
7
+ const result = analyzeProject(projectDir);
8
+
9
+ if (flags.output === 'json') {
10
+ console.log(JSON.stringify(result, null, 2));
11
+ return result.errors.length || (flags.strict && result.warnings.length) ? 1 : 0;
12
+ }
13
+
14
+ log.title('draftgo check');
15
+ log.info(`项目目录:${projectDir}`);
16
+
17
+ console.log('');
18
+ log.info(`资源概览:pages=${result.map.pages.length}, nav=${result.map.navigations.length}, db=${result.map.db_meta.length}, scripts=${result.map.custom_scripts.length}, aihub=${result.map.aihub.length}`);
19
+
20
+ if (result.errors.length) {
21
+ console.log('');
22
+ console.log(`${log.c.red('x')} 错误:`);
23
+ result.errors.forEach((msg) => console.log(` - ${msg}`));
24
+ }
25
+
26
+ if (result.warnings.length) {
27
+ console.log('');
28
+ console.log(`${log.c.yellow('!')} 提醒:`);
29
+ result.warnings.forEach((msg) => console.log(` - ${msg}`));
30
+ }
31
+
32
+ if (!result.errors.length && !result.warnings.length) {
33
+ console.log('');
34
+ log.ok('未发现明显闭环问题。');
35
+ }
36
+
37
+ if (result.errors.length) {
38
+ console.log('');
39
+ console.log(`${log.c.red('x')} 检查未通过:请先修复错误。`);
40
+ return 1;
41
+ }
42
+ if (flags.strict && result.warnings.length) {
43
+ console.log('');
44
+ console.log(`${log.c.red('x')} strict 模式:存在提醒项,检查未通过。`);
45
+ return 1;
46
+ }
47
+
48
+ console.log('');
49
+ log.ok(result.warnings.length ? '检查完成:存在提醒项,建议开发代理处理后再 push。' : '检查通过。');
50
+ return 0;
51
+ }
52
+
53
+ module.exports = check;
@@ -5,7 +5,11 @@ const { getPackageVersion } = require('../skill');
5
5
 
6
6
  function help() {
7
7
  const targets = all.map((i) => ` ${i.name.padEnd(12)} ${i.displayName}`).join('\n');
8
- console.log(`draftgo v${getPackageVersion()} — manage the DraftGo skill across AI coding agents
8
+ console.log(`draftgo v${getPackageVersion()} — DraftGo Next workbench CLI for AI coding agents
9
+
10
+ DraftGo Next frontend baseline: React + Vite + shadcn/ui + Tailwind.
11
+ Database pages use dg-* as the shadcn HTML runtime protocol.
12
+ The CLI is the workbench layer for local runtime, resource sync, checks, and push/pull flows.
9
13
 
10
14
  Usage:
11
15
  draftgo init [<target>...] Install skill. No target = auto-detect.
@@ -19,6 +23,19 @@ Usage:
19
23
  draftgo status Show installed targets and skill version.
20
24
  draftgo doctor Diagnose environment (python, targets,
21
25
  CLI freshness).
26
+ draftgo map Print a local DraftGo project resource map
27
+ for fast AI orientation.
28
+ draftgo check Check local resource closure: routes,
29
+ entry binding, files, obvious mock risks.
30
+ draftgo dev Run this project's npm dev script.
31
+ draftgo build Run this project's npm build script.
32
+ draftgo pull [type] [id...] Pull DraftGo resources via the bundled
33
+ sync script. Defaults to --all.
34
+ draftgo push <type> [id...] Push DraftGo resources via the bundled
35
+ sync script.
36
+ draftgo local up|down|logs|status
37
+ Manage .draftgo/docker/docker-compose.yaml
38
+ generated by draftgo local-dev.
22
39
  draftgo list-targets List supported AI tools.
23
40
  draftgo connect Bind this project to an existing DraftGo
24
41
  server. Prompts for BaseURL + access
@@ -31,6 +48,11 @@ Usage:
31
48
  draftgo -v | --version Print CLI version.
32
49
  draftgo -h | --help Show this help.
33
50
 
51
+ v3 Workbench:
52
+ draftgo local up|down|logs Local stack lifecycle commands.
53
+ draftgo dev|build|check Project workflow gates.
54
+ draftgo pull|push First-class resource sync wrappers.
55
+
34
56
  Flags:
35
57
  --project <dir> Operate on <dir> instead of the current directory.
36
58
  --force Overwrite existing skill body during install/update.
@@ -42,6 +64,8 @@ Flags:
42
64
  --no-setup (init) Don't offer either flow after installing.
43
65
  --server <url> (connect) Provide the DraftGo BaseURL non-interactively.
44
66
  --token <sat> (connect) Provide the access token non-interactively.
67
+ --output <json> (map/check) Print machine-readable JSON.
68
+ --strict (check) Treat warnings as failures.
45
69
 
46
70
  Environment:
47
71
  DRAFTGO_NO_UPDATE_CHECK=1 Disable the automatic CLI-freshness check.
@@ -55,6 +79,8 @@ Examples:
55
79
  draftgo init claudecode kiro # install for both
56
80
  draftgo init all # install for every supported target
57
81
  draftgo update # upgrade CLI if needed, then refresh skill
82
+ draftgo map # inspect pages/nav/db/scripts before development
83
+ draftgo check --strict # fail on closure warnings before push
58
84
  draftgo uninstall all --purge # full removal incl. runtime data
59
85
  `);
60
86
  }
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+ const log = require('../logger');
6
+ const { exists } = require('../fsx');
7
+ const { detectDocker } = require('../localdev/detect');
8
+
9
+ function composeFile(projectDir) {
10
+ return path.join(projectDir, '.draftgo', 'docker', 'docker-compose.yaml');
11
+ }
12
+
13
+ function runCompose(projectDir, args, stdio = 'inherit') {
14
+ const docker = detectDocker();
15
+ if (!docker.ok) {
16
+ log.err(docker.reason === 'compose-missing'
17
+ ? '检测到 Docker,但缺少 compose 插件。'
18
+ : '未检测到可用 Docker。');
19
+ return 1;
20
+ }
21
+
22
+ const file = composeFile(projectDir);
23
+ if (!exists(file)) {
24
+ log.err('未找到 .draftgo/docker/docker-compose.yaml。');
25
+ log.dim(' 请先运行 `draftgo local-dev` 生成本地 DraftGo stack。');
26
+ return 1;
27
+ }
28
+
29
+ const r = spawnSync(docker.composeCmd, [...docker.composeArgs, '-f', file, ...args], {
30
+ cwd: path.dirname(file),
31
+ stdio,
32
+ shell: false,
33
+ });
34
+ return r.status || 0;
35
+ }
36
+
37
+ function local(projectDir, positional) {
38
+ const action = positional[0] || 'status';
39
+
40
+ switch (action) {
41
+ case 'up':
42
+ return runCompose(projectDir, ['up', '-d']);
43
+ case 'down':
44
+ return runCompose(projectDir, ['down']);
45
+ case 'logs':
46
+ return runCompose(projectDir, ['logs', ...(positional.slice(1).length ? positional.slice(1) : ['-f', 'app'])]);
47
+ case 'status':
48
+ case 'ps':
49
+ return runCompose(projectDir, ['ps']);
50
+ default:
51
+ log.err(`未知 local 子命令:${action}`);
52
+ log.dim(' 可用:draftgo local up | down | logs | status');
53
+ return 1;
54
+ }
55
+ }
56
+
57
+ module.exports = local;
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const log = require('../logger');
4
+ const { buildProjectMap } = require('../projectMap');
5
+
6
+ function printList(title, rows, render) {
7
+ console.log('');
8
+ log.info(`${title}:${rows.length}`);
9
+ if (rows.length === 0) {
10
+ log.dim(' (无)');
11
+ return;
12
+ }
13
+ for (const row of rows) log.dim(` • ${render(row)}`);
14
+ }
15
+
16
+ function mapCommand(projectDir, flags = {}) {
17
+ const map = buildProjectMap(projectDir);
18
+
19
+ if (flags.output === 'json') {
20
+ console.log(JSON.stringify(map, null, 2));
21
+ return 0;
22
+ }
23
+
24
+ log.title('draftgo map');
25
+ log.info(`项目目录:${projectDir}`);
26
+
27
+ printList('页面', map.pages, (p) => {
28
+ const id = p.id == null ? 'new' : p.id;
29
+ return `${String(id).padEnd(4)} ${p.route || '(无 route)'} ${p.title || '未命名'}${p.html_file ? ` ${p.html_file}` : ''}`;
30
+ });
31
+
32
+ printList('导航', map.navigations, (n) => {
33
+ const id = n.id == null ? 'new' : n.id;
34
+ return `${String(id).padEnd(4)} ${n.code || 'default'} ${n.name || '未命名'}${n.html_file ? ` ${n.html_file}` : ''}`;
35
+ });
36
+
37
+ printList('动态 DB', map.db_meta, (m) => {
38
+ const fields = m.fields.length ? ` (${m.fields.slice(0, 8).join(', ')}${m.fields.length > 8 ? ', ...' : ''})` : '';
39
+ return `${m.type || '(无 type)'} ${m.label || ''}${fields}`;
40
+ });
41
+
42
+ printList('自定义脚本', map.custom_scripts, (s) => {
43
+ return `${s.slug || '(无 slug)'} ${s.mode || 'mode?'} ${s.name || ''}${s.code_file ? ` ${s.code_file}` : ''}`;
44
+ });
45
+
46
+ printList('AIHub', map.aihub, (a) => `${a.id || 'new'} ${a.type || ''} ${a.name || ''}`);
47
+ printList('角色', map.roles, (r) => `${r.code || r.id || 'new'} ${r.name || ''}`);
48
+
49
+ console.log('');
50
+ log.info('入口引用:');
51
+ const routes = Object.keys(map.routeRefs).sort();
52
+ if (routes.length === 0) log.dim(' (未发现 data-page-route / href 引用)');
53
+ else routes.forEach((route) => log.dim(` • ${route} ← ${map.routeRefs[route].join(', ')}`));
54
+
55
+ return 0;
56
+ }
57
+
58
+ module.exports = mapCommand;
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const log = require('../logger');
7
+
8
+ function projectScript(projectDir, scriptName) {
9
+ const pkgPath = path.join(projectDir, 'package.json');
10
+ if (!fs.existsSync(pkgPath)) {
11
+ log.err(`当前项目没有 package.json,无法运行 draftgo ${scriptName}。`);
12
+ return 1;
13
+ }
14
+
15
+ let pkg;
16
+ try {
17
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
18
+ } catch (err) {
19
+ log.err(`package.json 解析失败:${err.message}`);
20
+ return 1;
21
+ }
22
+
23
+ if (!pkg.scripts || !pkg.scripts[scriptName]) {
24
+ log.err(`package.json 中没有 scripts.${scriptName}。`);
25
+ return 1;
26
+ }
27
+
28
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
29
+ const r = spawnSync(npmCmd, ['run', scriptName], {
30
+ cwd: projectDir,
31
+ stdio: 'inherit',
32
+ shell: false,
33
+ });
34
+ return r.status || 0;
35
+ }
36
+
37
+ module.exports = projectScript;
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+ const log = require('../logger');
6
+ const { exists } = require('../fsx');
7
+ const { findPython } = require('../python');
8
+ const { platforms } = require('../platforms');
9
+
10
+ function findSyncScript(projectDir, command) {
11
+ const scriptName = command === 'pull' ? 'draftgo_pull.py' : 'draftgo_push.py';
12
+
13
+ for (const platform of platforms) {
14
+ const candidate = path.join(projectDir, platform.assetDir, 'scripts', scriptName);
15
+ if (exists(candidate)) return candidate;
16
+ }
17
+
18
+ const bundled = path.resolve(__dirname, '..', '..', 'resources', 'skill', 'scripts', scriptName);
19
+ return exists(bundled) ? bundled : null;
20
+ }
21
+
22
+ function sync(projectDir, command, positional) {
23
+ const py = findPython();
24
+ if (!py) {
25
+ log.err('未检测到 Python,无法运行 DraftGo 同步脚本。');
26
+ return 1;
27
+ }
28
+
29
+ const script = findSyncScript(projectDir, command);
30
+ if (!script) {
31
+ log.err(`未找到 ${command} 同步脚本。`);
32
+ log.dim(' 请先运行 `draftgo init` 安装 DraftGo skill,或重新安装 CLI。');
33
+ return 1;
34
+ }
35
+
36
+ if (!exists(path.join(projectDir, '.draftgo', 'config.json'))) {
37
+ log.err('未找到 .draftgo/config.json。');
38
+ log.dim(' 请先运行 `draftgo connect` 或 `/draftgo init`。');
39
+ return 1;
40
+ }
41
+
42
+ const args = positional.length ? positional : ['--all'];
43
+ const r = spawnSync(py.bin, [script, ...args], {
44
+ cwd: projectDir,
45
+ stdio: 'inherit',
46
+ shell: false,
47
+ });
48
+ return r.status || 0;
49
+ }
50
+
51
+ module.exports = sync;
package/src/index.js CHANGED
@@ -35,6 +35,18 @@ async function run(argv) {
35
35
  return require('./commands/status')(projectDir);
36
36
  case 'doctor':
37
37
  return await require('./commands/doctor')(projectDir, flags);
38
+ case 'map':
39
+ return require('./commands/map')(projectDir, flags);
40
+ case 'check':
41
+ return require('./commands/check')(projectDir, flags);
42
+ case 'dev':
43
+ case 'build':
44
+ return require('./commands/projectScript')(projectDir, command, flags);
45
+ case 'pull':
46
+ case 'push':
47
+ return require('./commands/sync')(projectDir, command, positional, flags);
48
+ case 'local':
49
+ return require('./commands/local')(projectDir, positional, flags);
38
50
  case 'list-targets':
39
51
  case 'targets':
40
52
  return require('./commands/listTargets')();
@@ -0,0 +1,228 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function exists(p) {
7
+ try { fs.accessSync(p); return true; } catch { return false; }
8
+ }
9
+
10
+ function readTextSafe(p) {
11
+ try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
12
+ }
13
+
14
+ function readJsonSafe(projectDir, rel) {
15
+ const abs = path.join(projectDir, rel);
16
+ try {
17
+ const raw = fs.readFileSync(abs, 'utf8');
18
+ const parsed = JSON.parse(raw);
19
+ return { ok: true, path: rel, items: Array.isArray(parsed) ? parsed : [], raw: parsed };
20
+ } catch (err) {
21
+ return { ok: false, path: rel, items: [], error: err.message };
22
+ }
23
+ }
24
+
25
+ function normalizeRoute(route) {
26
+ if (!route || typeof route !== 'string') return '';
27
+ const clean = route.trim();
28
+ if (!clean) return '';
29
+ if (/^https?:\/\//i.test(clean) || clean.startsWith('#') || clean.startsWith('mailto:')) return '';
30
+ return clean.startsWith('/') ? clean : `/${clean}`;
31
+ }
32
+
33
+ function relToAbs(projectDir, rel) {
34
+ if (!rel || typeof rel !== 'string') return null;
35
+ const clean = rel.replace(/\\/g, '/');
36
+ return path.isAbsolute(clean) ? clean : path.join(projectDir, clean);
37
+ }
38
+
39
+ function findGeneratedFile(projectDir, dirRel, prefix, exts) {
40
+ const dir = path.join(projectDir, dirRel);
41
+ if (!exists(dir)) return null;
42
+ const files = fs.readdirSync(dir).filter((f) => {
43
+ if (!f.startsWith(prefix)) return false;
44
+ return exts.some((ext) => f.endsWith(ext));
45
+ });
46
+ return files.length ? path.join(dirRel, files[0]).replace(/\\/g, '/') : null;
47
+ }
48
+
49
+ function itemFile(projectDir, section, item) {
50
+ if (!item || typeof item !== 'object') return null;
51
+ const direct = item.html_file || item.code_file || item.file;
52
+ if (direct && exists(relToAbs(projectDir, direct))) return direct.replace(/\\/g, '/');
53
+ const id = item.id == null ? '' : String(item.id);
54
+ if (!id) return null;
55
+ if (section === 'pages') return findGeneratedFile(projectDir, '.draftgo/pages', `page_${id}_`, ['.html']);
56
+ if (section === 'navigations') return findGeneratedFile(projectDir, '.draftgo/navigations', `nav_${id}_`, ['.html']);
57
+ return null;
58
+ }
59
+
60
+ function extractRoutes(html) {
61
+ const routes = new Set();
62
+ if (!html) return routes;
63
+ const patterns = [
64
+ /\bdata-page-route\s*=\s*["']([^"']+)["']/gi,
65
+ /\bhref\s*=\s*["']([^"']+)["']/gi,
66
+ ];
67
+ for (const re of patterns) {
68
+ let m;
69
+ while ((m = re.exec(html))) {
70
+ const route = normalizeRoute(m[1]);
71
+ if (route) routes.add(route);
72
+ }
73
+ }
74
+ return routes;
75
+ }
76
+
77
+ function pageKey(page) {
78
+ if (page && page.id != null) return `page:${page.id}`;
79
+ return `page:${normalizeRoute(page && page.route) || page && page.title || 'new'}`;
80
+ }
81
+
82
+ function addRouteRefs(refs, route, source) {
83
+ const clean = normalizeRoute(route);
84
+ if (!clean) return;
85
+ if (!refs.has(clean)) refs.set(clean, new Set());
86
+ refs.get(clean).add(source);
87
+ }
88
+
89
+ function summarizePage(projectDir, item) {
90
+ const file = itemFile(projectDir, 'pages', item);
91
+ return {
92
+ id: item.id,
93
+ title: item.title || '',
94
+ route: normalizeRoute(item.route),
95
+ permission: item.permission || null,
96
+ tag: item.tag || '',
97
+ html_file: file,
98
+ };
99
+ }
100
+
101
+ function summarizeNav(projectDir, item) {
102
+ const file = itemFile(projectDir, 'navigations', item);
103
+ return {
104
+ id: item.id,
105
+ code: item.code || '',
106
+ name: item.name || '',
107
+ status: item.status,
108
+ html_file: file,
109
+ };
110
+ }
111
+
112
+ function buildProjectMap(projectDir) {
113
+ const indexes = {
114
+ pages: readJsonSafe(projectDir, '.draftgo/pages/index.json'),
115
+ navigations: readJsonSafe(projectDir, '.draftgo/navigations/index.json'),
116
+ db_meta: readJsonSafe(projectDir, '.draftgo/db_meta/index.json'),
117
+ custom_scripts: readJsonSafe(projectDir, '.draftgo/custom_scripts/index.json'),
118
+ aihub: readJsonSafe(projectDir, '.draftgo/aihub/index.json'),
119
+ external_apis: readJsonSafe(projectDir, '.draftgo/external_apis/index.json'),
120
+ roles: readJsonSafe(projectDir, '.draftgo/roles/index.json'),
121
+ users: readJsonSafe(projectDir, '.draftgo/users/index.json'),
122
+ };
123
+
124
+ const pages = indexes.pages.items.map((p) => summarizePage(projectDir, p));
125
+ const navigations = indexes.navigations.items.map((n) => summarizeNav(projectDir, n));
126
+ const routeRefs = new Map();
127
+
128
+ for (const nav of navigations) {
129
+ const html = nav.html_file ? readTextSafe(relToAbs(projectDir, nav.html_file)) : '';
130
+ for (const route of extractRoutes(html)) addRouteRefs(routeRefs, route, `nav:${nav.id || nav.code || nav.name}`);
131
+ }
132
+
133
+ for (const page of pages) {
134
+ const html = page.html_file ? readTextSafe(relToAbs(projectDir, page.html_file)) : '';
135
+ for (const route of extractRoutes(html)) addRouteRefs(routeRefs, route, pageKey(page));
136
+ }
137
+
138
+ return {
139
+ projectDir,
140
+ indexes: Object.fromEntries(Object.entries(indexes).map(([k, v]) => [k, { ok: v.ok, path: v.path, count: v.items.length }])),
141
+ pages,
142
+ navigations,
143
+ db_meta: indexes.db_meta.items.map((m) => ({
144
+ id: m.id,
145
+ type: m.type || '',
146
+ label: m.label || '',
147
+ fields: m.schema && m.schema.properties ? Object.keys(m.schema.properties) : [],
148
+ })),
149
+ custom_scripts: indexes.custom_scripts.items.map((s) => ({
150
+ id: s.id,
151
+ name: s.name || '',
152
+ slug: s.slug || '',
153
+ mode: s.mode || '',
154
+ status: s.status,
155
+ code_file: itemFile(projectDir, 'custom_scripts', s),
156
+ })),
157
+ aihub: indexes.aihub.items.map((a) => ({ id: a.id, name: a.name || '', type: a.type || '', status: a.status })),
158
+ external_apis: indexes.external_apis.items.map((a) => ({ id: a.id, name: a.name || '', status: a.status })),
159
+ roles: indexes.roles.items.map((r) => ({ id: r.id, code: r.code || '', name: r.name || '', status: r.status })),
160
+ users_count: indexes.users.items.length,
161
+ routeRefs: Object.fromEntries([...routeRefs.entries()].map(([route, sources]) => [route, [...sources]])),
162
+ };
163
+ }
164
+
165
+ function isProbablySystemPage(page) {
166
+ const tag = String(page.tag || '');
167
+ const title = String(page.title || '');
168
+ const route = normalizeRoute(page.route);
169
+ return tag.includes('系统') || title.includes('系统') || ['/login', '/setup'].includes(route);
170
+ }
171
+
172
+ function analyzeProject(projectDir) {
173
+ const map = buildProjectMap(projectDir);
174
+ const errors = [];
175
+ const warnings = [];
176
+ const routeSeen = new Map();
177
+
178
+ if (!exists(path.join(projectDir, '.draftgo'))) {
179
+ errors.push('未找到 .draftgo/,请先运行 draftgo init 或 draftgo connect。');
180
+ }
181
+
182
+ for (const page of map.pages) {
183
+ const label = `${page.title || '未命名页面'}${page.id != null ? `#${page.id}` : ''}`;
184
+ if (!page.route) errors.push(`${label} 缺少 route。`);
185
+ if (!page.title) warnings.push(`${label} 缺少 title。`);
186
+ if (!page.html_file) errors.push(`${label} 找不到 html_file 或 page_${page.id}_*.html。`);
187
+
188
+ if (page.route) {
189
+ if (routeSeen.has(page.route)) {
190
+ errors.push(`route 重复:${page.route}(${routeSeen.get(page.route)} 与 ${label})。`);
191
+ } else {
192
+ routeSeen.set(page.route, label);
193
+ }
194
+
195
+ if (page.route !== '/' && !isProbablySystemPage(page)) {
196
+ const refs = map.routeRefs[page.route] || [];
197
+ const own = pageKey(page);
198
+ const externalRefs = refs.filter((s) => s !== own);
199
+ if (externalRefs.length === 0) {
200
+ warnings.push(`${label} (${page.route}) 未在导航、首页或其他页面入口中发现绑定引用。`);
201
+ }
202
+ }
203
+ }
204
+
205
+ if (page.html_file) {
206
+ const html = readTextSafe(relToAbs(projectDir, page.html_file));
207
+ if (/\b(mockData|demoData|fakeData|sampleData|staticData)\b/i.test(html)) {
208
+ warnings.push(`${label} 疑似包含 mock/demo/fake/staticData,确认是否为用户明确要求的静态/demo。`);
209
+ }
210
+ if (/\b(onclick|addEventListener)\b[\s\S]{0,120}\b(toast|alert)\b/i.test(html) && !/\b(App\.(post|put|delete|get)|fetch\s*\()/i.test(html)) {
211
+ warnings.push(`${label} 疑似只有反馈提示、缺少真实数据读写或 API 调用。`);
212
+ }
213
+ }
214
+ }
215
+
216
+ if (map.pages.length === 0 && exists(path.join(projectDir, '.draftgo'))) {
217
+ warnings.push('未发现 pages/index.json 页面缓存;开发前建议先拉取页面。');
218
+ }
219
+
220
+ return { map, errors, warnings };
221
+ }
222
+
223
+ module.exports = {
224
+ buildProjectMap,
225
+ analyzeProject,
226
+ normalizeRoute,
227
+ extractRoutes,
228
+ };