draftgo-cli 3.0.33 → 3.0.38

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 (64) hide show
  1. package/README.md +220 -269
  2. package/package.json +6 -2
  3. package/resources/skill/SKILL.md +114 -55
  4. package/resources/skill/init/SKILL.md +29 -15
  5. package/resources/skill/manifest.json +13 -5
  6. package/resources/skill/push/SKILL.md +41 -29
  7. package/resources/skill/references/aihub.md +8 -5
  8. package/resources/skill/references/api-endpoints.md +5 -3
  9. package/resources/skill/references/architecture.md +1 -1
  10. package/resources/skill/references/checkout.md +116 -0
  11. package/resources/skill/references/custom-services.md +9 -10
  12. package/resources/skill/references/data.md +4 -2
  13. package/resources/skill/references/frontend.md +99 -23
  14. package/resources/skill/references/mcp.md +101 -0
  15. package/resources/skill/references/modules.md +8 -8
  16. package/resources/skill/references/parallel.md +6 -3
  17. package/resources/skill/references/runtime.md +7 -10
  18. package/resources/skill/scripts/README.md +8 -0
  19. package/resources/skill/story/SKILL.md +8 -8
  20. package/src/cli.js +5 -0
  21. package/src/commandRegistry.js +7 -1
  22. package/src/commands/api.js +24 -187
  23. package/src/commands/autoPush.js +48 -17
  24. package/src/commands/check.js +17 -47
  25. package/src/commands/checkout.js +18 -0
  26. package/src/commands/commit.js +21 -0
  27. package/src/commands/conflict.js +30 -0
  28. package/src/commands/conflicts.js +16 -0
  29. package/src/commands/connect.js +60 -48
  30. package/src/commands/delete.js +79 -64
  31. package/src/commands/deploy.js +18 -10
  32. package/src/commands/diff.js +23 -0
  33. package/src/commands/help.js +99 -75
  34. package/src/commands/init.js +4 -10
  35. package/src/commands/local.js +23 -6
  36. package/src/commands/map.js +89 -89
  37. package/src/commands/mcp.js +126 -0
  38. package/src/commands/sync.js +28 -43
  39. package/src/commands/verifyUi.js +3 -2
  40. package/src/localdev/index.js +37 -7
  41. package/src/localdev/mysqlClient.js +1 -1
  42. package/src/mcp/client.js +275 -0
  43. package/src/mcp/hosts.js +520 -0
  44. package/src/mcp/protocol.js +173 -0
  45. package/src/mcp/stdio.js +300 -0
  46. package/src/mcp/tools.js +37 -0
  47. package/src/platforms.js +3 -4
  48. package/src/projectConfig.js +91 -49
  49. package/src/projectMap.js +123 -460
  50. package/src/skill.js +6 -28
  51. package/src/worktree/backend.js +250 -0
  52. package/src/worktree/errors.js +28 -0
  53. package/src/worktree/index.js +461 -0
  54. package/src/worktree/manifest.js +75 -0
  55. package/src/worktree/streams.js +200 -0
  56. package/src/worktree/types.js +103 -0
  57. package/src/worktree/validate.js +37 -0
  58. package/resources/skill/pull/SKILL.md +0 -33
  59. package/resources/skill/references/api.json +0 -20248
  60. package/resources/skill/scripts/draftgo_delete.py +0 -149
  61. package/resources/skill/scripts/draftgo_init.py +0 -80
  62. package/resources/skill/scripts/draftgo_pull.py +0 -427
  63. package/resources/skill/scripts/draftgo_push.py +0 -1022
  64. package/src/python.js +0 -27
package/src/projectMap.js CHANGED
@@ -1,86 +1,30 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('crypto');
3
4
  const fs = require('fs');
4
5
  const path = require('path');
5
6
  const parse5 = require('parse5');
6
-
7
- function exists(p) {
8
- try { fs.accessSync(p); return true; } catch { return false; }
9
- }
10
-
11
- function readTextSafe(p) {
12
- try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
13
- }
14
-
15
- function readJsonSafe(projectDir, rel) {
16
- const abs = path.join(projectDir, rel);
17
- try {
18
- const raw = fs.readFileSync(abs, 'utf8');
19
- const parsed = JSON.parse(raw);
20
- return { ok: true, path: rel, items: Array.isArray(parsed) ? parsed : [], raw: parsed };
21
- } catch (err) {
22
- return { ok: false, path: rel, items: [], error: err.message };
23
- }
24
- }
7
+ const { loadManifest, absolutePath } = require('./worktree/manifest');
8
+ const { mediaType } = require('./worktree/types');
25
9
 
26
10
  function normalizeRoute(route) {
27
11
  if (!route || typeof route !== 'string') return '';
28
12
  const clean = route.trim();
29
- if (!clean) return '';
30
- if (/^https?:\/\//i.test(clean) || clean.startsWith('#') || clean.startsWith('mailto:')) return '';
13
+ if (!clean || /^https?:\/\//i.test(clean) || clean.startsWith('#') || clean.startsWith('mailto:')) return '';
31
14
  return clean.startsWith('/') ? clean : `/${clean}`;
32
15
  }
33
16
 
34
- function relToAbs(projectDir, rel) {
35
- if (!rel || typeof rel !== 'string') return null;
36
- const clean = rel.replace(/\\/g, '/');
37
- return path.isAbsolute(clean) ? clean : path.join(projectDir, clean);
38
- }
39
-
40
- function findGeneratedFile(projectDir, dirRel, prefix, exts) {
41
- const dir = path.join(projectDir, dirRel);
42
- if (!exists(dir)) return null;
43
- const files = fs.readdirSync(dir).filter((f) => {
44
- if (!f.startsWith(prefix)) return false;
45
- return exts.some((ext) => f.endsWith(ext));
46
- });
47
- return files.length ? path.join(dirRel, files[0]).replace(/\\/g, '/') : null;
48
- }
49
-
50
- function itemFile(projectDir, section, item) {
51
- if (!item || typeof item !== 'object') return null;
52
- const direct = item.html_file || item.code_file || item.file;
53
- if (direct && exists(relToAbs(projectDir, direct))) return direct.replace(/\\/g, '/');
54
- const id = item.id == null ? '' : String(item.id);
55
- if (!id) return null;
56
- if (section === 'pages') return findGeneratedFile(projectDir, '.draftgo/pages', `page_${id}_`, ['.html']);
57
- if (section === 'navigations') return findGeneratedFile(projectDir, '.draftgo/navigations', `nav_${id}_`, ['.html']);
58
- return null;
59
- }
60
-
61
17
  function extractRoutes(html) {
62
18
  const routes = new Set();
63
19
  if (!html) return routes;
64
20
  let document;
65
- try {
66
- document = parse5.parse(html);
67
- } catch {
68
- return routes;
69
- }
21
+ try { document = parse5.parse(html); } catch { return routes; }
70
22
  const visit = (node) => {
71
- for (const attr of node.attrs || []) {
72
- if (attr.name === 'href' || attr.name === 'data-page-route') {
73
- const route = normalizeRoute(attr.value);
23
+ for (const attribute of node.attrs || []) {
24
+ if (attribute.name === 'href' || attribute.name === 'data-page-route') {
25
+ const route = normalizeRoute(attribute.value);
74
26
  if (route) routes.add(route);
75
27
  }
76
- if (attr.name.startsWith('on')) {
77
- const re = /\b(?:App\.)?(?:navigate|go|openPage)\s*\(\s*["']([^"']+)["']/g;
78
- let match;
79
- while ((match = re.exec(attr.value))) {
80
- const route = normalizeRoute(match[1]);
81
- if (route) routes.add(route);
82
- }
83
- }
84
28
  }
85
29
  for (const child of node.childNodes || []) visit(child);
86
30
  };
@@ -88,392 +32,93 @@ function extractRoutes(html) {
88
32
  return routes;
89
33
  }
90
34
 
91
- function extractScriptBindings(code) {
92
- const bindings = { routes: [], events: [], schedules: [] };
93
- if (!code) return bindings;
94
- let match;
95
- const routeRe = /@route\(\s*["']([A-Za-z]+)\s+([^"']+)["']\s*\)/g;
96
- while ((match = routeRe.exec(code))) bindings.routes.push(`${match[1].toUpperCase()} ${match[2]}`);
97
- const eventRe = /@on\(\s*["']([^"']+)["']\s*\)/g;
98
- while ((match = eventRe.exec(code))) bindings.events.push(match[1]);
99
- const scheduledRe = /@scheduled\(\s*["']([^"']+)["']\s*\)/g;
100
- while ((match = scheduledRe.exec(code))) bindings.schedules.push(match[1]);
101
- const goRouteRe = /\.Route\(\s*["']([A-Za-z]+)["']\s*,\s*["']([^"']+)["']/g;
102
- while ((match = goRouteRe.exec(code))) bindings.routes.push(`${match[1].toUpperCase()} ${match[2]}`);
103
- const goEventRe = /\.On\(\s*["']([^"']+)["']/g;
104
- while ((match = goEventRe.exec(code))) bindings.events.push(match[1]);
105
- const goScheduleRe = /\.Schedule\(\s*["']([^"']+)["']/g;
106
- while ((match = goScheduleRe.exec(code))) bindings.schedules.push(match[1]);
107
- return bindings;
35
+ function extractScriptBindings() {
36
+ return { routes: [], events: [], schedules: [] };
108
37
  }
109
38
 
110
- function pageKey(page) {
111
- if (page && page.id != null) return `page:${page.id}`;
112
- return `page:${normalizeRoute(page && page.route) || page && page.title || 'new'}`;
39
+ function htmlParseErrors(html) {
40
+ const errors = [];
41
+ try {
42
+ parse5.parse(html, { onParseError(error) {
43
+ if (error.code !== 'missing-doctype') errors.push({
44
+ code: error.code || 'html-parse-error', line: error.startLine || null, column: error.startCol || null,
45
+ });
46
+ } });
47
+ } catch (error) {
48
+ errors.push({ code: error.message || 'html-parse-error', line: null, column: null });
49
+ }
50
+ return errors;
113
51
  }
114
52
 
115
- function addRouteRefs(refs, route, source) {
116
- const clean = normalizeRoute(route);
117
- if (!clean) return;
118
- if (!refs.has(clean)) refs.set(clean, new Set());
119
- refs.get(clean).add(source);
53
+ function sha256File(file) {
54
+ const hash = crypto.createHash('sha256');
55
+ hash.update(fs.readFileSync(file));
56
+ return hash.digest('hex');
120
57
  }
121
58
 
122
- function classifyPageArea(item) {
123
- const route = normalizeRoute(item && item.route);
124
- const title = String((item && item.title) || '');
125
- const tag = String((item && item.tag) || '');
126
- const text = `${route} ${title} ${tag}`;
127
-
128
- if (!route) return 'unknown';
129
- if (route === '/' || /(^|[\s/])(home|index|首页|主页)([\s/]|$)/i.test(text)) return 'home';
130
- if (/^\/(login|setup|install|register)(\/|$)/i.test(route) || /(系统|内置|平台配置|基础配置)/.test(text)) return 'system';
131
- if (/^\/admin(\/|$)/i.test(route) || /(管理|后台|运营|审核|权限|控制台|配置)/.test(text)) return 'admin';
132
- if (/(业务|内容|案例|产品|新闻|订单|客户|会员|课程|活动|资料|下载|预约|表单|详情|列表|列表页|工作台|中心|门户|商城|服务)/.test(text)) return 'business';
133
- return 'unknown';
59
+ function decodeTextFile(file, contentType) {
60
+ const match = String(contentType || '').match(/;\s*charset\s*=\s*[']?([^;'\s]+)/i);
61
+ const charset = match ? match[1] : 'utf-8';
62
+ return new TextDecoder(charset, { fatal: true }).decode(fs.readFileSync(file));
134
63
  }
135
64
 
136
- function normalizeHtmlSkeleton(html) {
137
- if (!html) return '';
138
- let out = html;
139
- out = out.replace(/<!--([\s\S]*?)-->/g, '');
140
- out = out.replace(/<script\b[\s\S]*?<\/script>/gi, '<script></script>');
141
- out = out.replace(/<style\b[\s\S]*?<\/style>/gi, '<style></style>');
142
- out = out.replace(/\b(data-page-route|href|id|for|aria-label|aria-labelledby|aria-describedby|title|value|name|placeholder)\s*=\s*("[^"]*"|'[^']*')/gi, '$1=""');
143
- out = out.replace(/\b(class|style)\s*=\s*("[^"]*"|'[^']*')/gi, '$1=""');
144
- out = out.replace(/>[^<]*</g, '><');
145
- out = out.replace(/\s+/g, ' ');
146
- return out.trim();
65
+ function classifyPageArea(entry) {
66
+ const route = normalizeRoute(entry.route);
67
+ if (route === '/') return 'home';
68
+ if (/^\/(login|setup|install|register)(\/|$)/i.test(route)) return 'system';
69
+ if (/^\/admin(\/|$)/i.test(route)) return 'admin';
70
+ return route ? 'business' : 'unknown';
147
71
  }
148
72
 
149
- function collectPageGroups(pages) {
73
+ function collectGroups(pages) {
150
74
  const groups = { home: [], admin: [], business: [], system: [], unknown: [] };
151
- for (const page of pages) {
152
- const key = groups[page.area] ? page.area : 'unknown';
153
- groups[key].push(page);
154
- }
75
+ for (const page of pages) groups[page.area || 'unknown'].push(page);
155
76
  return groups;
156
77
  }
157
78
 
158
- function summarizePage(projectDir, item) {
159
- const file = itemFile(projectDir, 'pages', item);
160
- return {
161
- id: item.id,
162
- title: item.title || '',
163
- route: normalizeRoute(item.route),
164
- permission: item.permission || null,
165
- tag: item.tag || '',
166
- html_file: file,
167
- area: classifyPageArea(item),
168
- };
169
- }
170
-
171
- function summarizeNav(projectDir, item) {
172
- const file = itemFile(projectDir, 'navigations', item);
173
- return {
174
- id: item.id,
175
- code: item.code || '',
176
- name: item.name || '',
177
- status: item.status,
178
- html_file: file,
179
- };
180
- }
181
-
182
- function summarizeAIHub(item) {
183
- const data = item && typeof item.data === 'object' && item.data ? item.data : {};
184
- const spec = data.spec && typeof data.spec === 'object' ? data.spec : {};
185
- const base = {
186
- id: item.id,
187
- name: item.name || '',
188
- type: item.type || '',
189
- status: item.status,
190
- };
191
-
192
- if (item.type === 'agent') {
193
- const tools = spec.tools && typeof spec.tools === 'object' ? spec.tools : {};
194
- const outputFormat = spec.output_format && typeof spec.output_format === 'object' ? spec.output_format : {};
195
- const modelSelection = spec.model_selection && typeof spec.model_selection === 'object' ? spec.model_selection : {};
196
- return {
197
- ...base,
198
- mode: spec.mode || 'chat',
199
- model_id: spec.model_id || null,
200
- model: spec.model || data.model || '',
201
- fallback_models: Array.isArray(spec.fallback_models) ? spec.fallback_models : [],
202
- output_format: {
203
- mode: outputFormat.mode || 'native',
204
- json_strategy: outputFormat.json && outputFormat.json.strategy || null,
205
- has_schema: Boolean(outputFormat.json && outputFormat.json.schema),
206
- },
207
- model_selection: {
208
- user_selectable: Boolean(modelSelection.user_selectable),
209
- on_invalid: modelSelection.on_invalid || 'ignore',
210
- },
211
- tools: {
212
- enabled: Boolean(tools.enabled && Array.isArray(tools.sources) && tools.sources.length),
213
- sources: Array.isArray(tools.sources)
214
- ? tools.sources.map((s) => ({
215
- type: s.type || '',
216
- id: s.id,
217
- name: s.name || '',
218
- tool_filter: Array.isArray(s.tool_filter) ? s.tool_filter : [],
219
- }))
220
- : [],
221
- },
222
- skills_count: Array.isArray(spec.skills) ? spec.skills.length : 0,
223
- };
224
- }
225
-
226
- if (item.type === 'model') {
227
- return {
228
- ...base,
229
- provider: data.provider || 'openai-compatible',
230
- models_count: Array.isArray(data.models) ? data.models.length : 0,
231
- supports_response_format: data.supports_response_format,
232
- supports_json_schema: data.supports_json_schema,
233
- image_generation_probe: data.image_generation_probe
234
- ? {
235
- ok: Boolean(data.image_generation_probe.ok),
236
- model: data.image_generation_probe.model || '',
237
- tested_at: data.image_generation_probe.tested_at || '',
238
- }
239
- : null,
240
- };
241
- }
242
-
243
- if (item.type === 'mcp') {
244
- return {
245
- ...base,
246
- transport: data.transport || '',
247
- tools_count: Array.isArray(data.tools) ? data.tools.length : 0,
248
- enabled: data.enabled !== false,
249
- };
250
- }
251
-
252
- if (item.type === 'skill') {
253
- return {
254
- ...base,
255
- enabled: data.enabled !== false,
256
- content_length: typeof data.content === 'string' ? data.content.length : 0,
257
- };
258
- }
259
-
260
- if (item.type === 'prompt') {
261
- return {
262
- ...base,
263
- tags: Array.isArray(item.tags) ? item.tags : [],
264
- content_length: typeof data.content === 'string' ? data.content.length : 0,
265
- };
266
- }
267
-
268
- return base;
269
- }
270
-
271
- function summarizeDoc(item) {
272
- return {
273
- id: item.id,
274
- title: item.title || '',
275
- slug: item.slug || '',
276
- category_id: item.category_id == null ? null : item.category_id,
277
- status: item.status,
278
- content_file: item.content_file || null,
279
- };
280
- }
281
-
282
- function summarizeDocCategory(item) {
283
- return {
284
- id: item.id,
285
- name: item.name || '',
286
- slug: item.slug || '',
287
- parent_id: item.parent_id == null ? null : item.parent_id,
288
- sort_order: item.sort_order == null ? 0 : item.sort_order,
289
- status: item.status,
290
- };
291
- }
292
-
293
- function summarizeSystemConfig(item) {
294
- return {
295
- config_key: item.config_key || '',
296
- category: item.category || '',
297
- value_type: item.value_type || '',
298
- is_sensitive: Boolean(item.is_sensitive),
299
- status: item.status,
300
- };
301
- }
302
-
303
79
  function buildProjectMap(projectDir) {
304
- const indexes = {
305
- pages: readJsonSafe(projectDir, '.draftgo/pages/index.json'),
306
- navigations: readJsonSafe(projectDir, '.draftgo/navigations/index.json'),
307
- db_meta: readJsonSafe(projectDir, '.draftgo/db_meta/index.json'),
308
- custom_scripts: readJsonSafe(projectDir, '.draftgo/custom_scripts/index.json'),
309
- aihub: readJsonSafe(projectDir, '.draftgo/aihub/index.json'),
310
- docs: readJsonSafe(projectDir, '.draftgo/docs/articles/index.json'),
311
- doc_categories: readJsonSafe(projectDir, '.draftgo/doc_categories/index.json'),
312
- system_config: readJsonSafe(projectDir, '.draftgo/system_config/index.json'),
313
- roles: readJsonSafe(projectDir, '.draftgo/roles/index.json'),
314
- users: readJsonSafe(projectDir, '.draftgo/users/index.json'),
315
- };
80
+ const manifest = loadManifest(projectDir);
81
+ const checkouts = Object.values(manifest.entries).map((entry) => ({ ...entry }));
82
+ const pages = checkouts.filter((entry) => entry.resource_type === 'pages').map((entry) => ({
83
+ ...entry, id: entry.resource_id, html_file: entry.local_path, area: classifyPageArea(entry),
84
+ }));
85
+ const navigations = checkouts.filter((entry) => entry.resource_type === 'navigations').map((entry) => ({
86
+ ...entry, id: entry.resource_id, html_file: entry.local_path,
87
+ }));
88
+ const docs = checkouts.filter((entry) => entry.resource_type === 'docs').map((entry) => ({
89
+ ...entry, id: entry.resource_id, content_file: entry.local_path,
90
+ }));
316
91
 
317
- const pages = indexes.pages.items.map((p) => summarizePage(projectDir, p));
318
- const navigations = indexes.navigations.items.map((n) => summarizeNav(projectDir, n));
319
92
  const routeRefs = new Map();
320
- const pageGroups = collectPageGroups(pages);
321
-
322
- for (const nav of navigations) {
323
- const html = nav.html_file ? readTextSafe(relToAbs(projectDir, nav.html_file)) : '';
324
- for (const route of extractRoutes(html)) addRouteRefs(routeRefs, route, `nav:${nav.id || nav.code || nav.name}`);
325
- }
326
-
327
- for (const page of pages) {
328
- const html = page.html_file ? readTextSafe(relToAbs(projectDir, page.html_file)) : '';
329
- for (const route of extractRoutes(html)) addRouteRefs(routeRefs, route, pageKey(page));
93
+ for (const entry of [...pages, ...navigations]) {
94
+ const file = absolutePath(projectDir, entry.local_path);
95
+ if (!fs.existsSync(file)) continue;
96
+ let source;
97
+ try { source = decodeTextFile(file, entry.content_type); } catch { continue; }
98
+ for (const route of extractRoutes(source)) {
99
+ if (!routeRefs.has(route)) routeRefs.set(route, new Set());
100
+ routeRefs.get(route).add(`${entry.resource_type}:${entry.resource_id}`);
101
+ }
330
102
  }
331
-
332
103
  return {
333
104
  projectDir,
334
- indexes: Object.fromEntries(Object.entries(indexes).map(([k, v]) => [k, { ok: v.ok, path: v.path, count: v.items.length }])),
105
+ manifest_path: '.draftgo/worktree/manifest.json',
106
+ checkouts,
335
107
  pages,
336
108
  navigations,
337
- db_meta: indexes.db_meta.items.map((m) => ({
338
- id: m.id,
339
- type: m.type || '',
340
- label: m.label || '',
341
- fields: m.schema && m.schema.properties ? Object.keys(m.schema.properties) : [],
342
- })),
343
- custom_scripts: indexes.custom_scripts.items.map((s) => {
344
- const codeFile = itemFile(projectDir, 'custom_scripts', s);
345
- const bindings = extractScriptBindings(codeFile ? readTextSafe(relToAbs(projectDir, codeFile)) : '');
346
- return {
347
- id: s.id,
348
- name: s.name || '',
349
- slug: s.slug || '',
350
- mode: s.mode || '',
351
- status: s.status,
352
- code_file: codeFile,
353
- routes: bindings.routes,
354
- events: bindings.events,
355
- schedules: bindings.schedules,
356
- };
357
- }),
358
- aihub: indexes.aihub.items.map(summarizeAIHub),
359
- docs: indexes.docs.items.map(summarizeDoc),
360
- doc_categories: indexes.doc_categories.items.map(summarizeDocCategory),
361
- system_config: indexes.system_config.items.map(summarizeSystemConfig),
362
- roles: indexes.roles.items.map((r) => ({ id: r.id, code: r.code || '', name: r.name || '', status: r.status })),
363
- users_count: indexes.users.items.length,
364
- pageGroups,
365
- routeRefs: Object.fromEntries([...routeRefs.entries()].map(([route, sources]) => [route, [...sources]])),
109
+ docs,
110
+ pageGroups: collectGroups(pages),
111
+ routeRefs: Object.fromEntries([...routeRefs].map(([route, sources]) => [route, [...sources]])),
112
+ remote_only: ['db_meta', 'aihub', 'system_config', 'roles', 'users', 'doc_categories', 'custom_services'],
366
113
  };
367
114
  }
368
115
 
369
- function htmlParseErrors(html) {
370
- const errors = [];
371
- const ignored = new Set(['missing-doctype']);
372
- try {
373
- parse5.parse(html, {
374
- onParseError(error) {
375
- if (ignored.has(error.code)) return;
376
- errors.push({
377
- code: error.code || 'html-parse-error',
378
- line: error.startLine || null,
379
- column: error.startCol || null,
380
- });
381
- },
382
- });
383
- } catch (error) {
384
- errors.push({ code: error.message || 'html-parse-error', line: null, column: null });
385
- }
386
- return errors;
387
- }
388
-
389
- function extractStyleText(html) {
390
- const parts = [];
391
- if (!html) return '';
392
-
393
- const styleAttr = /\bstyle\s*=\s*(["'])([\s\S]*?)\1/gi;
394
- let m;
395
- while ((m = styleAttr.exec(html))) parts.push(m[2]);
396
-
397
- const styleBlock = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
398
- while ((m = styleBlock.exec(html))) parts.push(m[1]);
399
-
400
- return parts.join('\n');
401
- }
402
-
403
- function maskDgVarFunctions(css) {
404
- let out = '';
405
- for (let i = 0; i < css.length; i += 1) {
406
- const rest = css.slice(i).toLowerCase();
407
- const fnStart = rest.startsWith('var(') ? 'var(' : rest.startsWith('color-mix(') ? 'color-mix(' : null;
408
- if (!fnStart) {
409
- out += css[i];
410
- continue;
411
- }
412
-
413
- let j = i + fnStart.length;
414
- let depth = 1;
415
- while (j < css.length && depth > 0) {
416
- if (css[j] === '(') depth += 1;
417
- else if (css[j] === ')') depth -= 1;
418
- j += 1;
419
- }
420
-
421
- const fn = css.slice(i, j);
422
- if (/^var\(\s*--dg-/i.test(fn) || /^color-mix\([\s\S]*var\(\s*--dg-/i.test(fn)) {
423
- out += ' '.repeat(fn.length);
424
- }
425
- else out += fn;
426
- i = j - 1;
427
- }
428
- return out;
429
- }
430
-
431
- function hasHardcodedColor(css) {
432
- const masked = maskDgVarFunctions(css);
433
- return /#[0-9a-f]{3,8}\b/i.test(masked)
434
- || /\b(?:rgb|rgba|hsl|hsla|color-mix)\s*\(/i.test(masked);
435
- }
436
-
437
- function hasDarkThemeCoverage(html) {
438
- return /\[data-theme\s*=\s*["']dark["']\]/i.test(html)
439
- || /\[data-theme\s*~=\s*["']dark["']\]/i.test(html)
440
- || /\bdata-theme\s*=\s*["']dark["']/i.test(html);
441
- }
442
-
443
- function reportColorThemeRisk(label, html, addWarning) {
444
- const css = extractStyleText(html);
445
- if (!css || !hasHardcodedColor(css)) return;
446
- if (hasDarkThemeCoverage(html)) return;
447
- addWarning('DG-COLOR-001', 'low', `${label} 发现硬编码配色且未看到深色主题覆盖;优先使用系统 var(--dg-*) token,自主配色需兼容浅色与深色。`);
448
- }
449
-
450
- function collectOrphanFiles(projectDir, relDir, indexRel, fileField, prefix, exts) {
451
- const dir = path.join(projectDir, relDir);
452
- if (!exists(dir)) return [];
453
- const index = readJsonSafe(projectDir, indexRel);
454
- const indexed = new Set(index.items.map((it) => String(it[fileField] || '').replace(/\\/g, '/')));
455
- return fs.readdirSync(dir)
456
- .filter((name) => name.startsWith(`${prefix}_`) && exts.includes(path.extname(name)))
457
- .map((name) => path.join(relDir, name).replace(/\\/g, '/'))
458
- .filter((rel) => !indexed.has(rel));
459
- }
460
-
461
- function reportPaginationHack(label, html, addWarning) {
462
- if (/\bApp\.get\s*\(\s*['"]db\/[^'"]+['"]\s*,\s*\{[\s\S]{0,240}\bpage_size\s*:\s*9999\b/i.test(html)) {
463
- addWarning('DG-DATA-001', 'high', `${label} 使用 page_size: 9999 拉取 DB 数据;DraftGo 规范是不传 page/page_size 即返回全量。`);
464
- }
465
- }
466
-
467
- function needsAdminCompanion(page) {
468
- const text = `${page.route || ''} ${page.title || ''} ${page.tag || ''}`;
469
- return /(案例|产品|新闻|订单|客户|会员|课程|活动|资料|下载|预约|表单|内容|招聘|发布|审核|上下架|商城)/.test(text);
470
- }
471
-
472
- function isProbablySystemPage(page) {
473
- const tag = String(page.tag || '');
474
- const title = String(page.title || '');
475
- const route = normalizeRoute(page.route);
476
- return tag.includes('系统') || title.includes('系统') || ['/login', '/setup'].includes(route);
116
+ function worktreeFiles(projectDir, directory) {
117
+ const root = path.join(projectDir, '.draftgo', 'worktree', directory);
118
+ if (!fs.existsSync(root)) return [];
119
+ return fs.readdirSync(root, { withFileTypes: true })
120
+ .filter((entry) => entry.isFile())
121
+ .map((entry) => path.join(root, entry.name));
477
122
  }
478
123
 
479
124
  function analyzeProject(projectDir) {
@@ -485,49 +130,66 @@ function analyzeProject(projectDir) {
485
130
  warnings.push(message);
486
131
  warningDetails.push({ code, confidence, message });
487
132
  };
488
- for (const page of map.pages) {
489
- const label = `${page.title || '未命名页面'}${page.id != null ? `#${page.id}` : ''}`;
490
- if (page.route && page.route !== '/' && !isProbablySystemPage(page)) {
491
- const refs = map.routeRefs[page.route] || [];
492
- const own = pageKey(page);
493
- const externalRefs = refs.filter((source) => source !== own);
494
- if (externalRefs.length === 0) {
495
- addWarning('DG-ROUTE-001', 'medium', `${label} (${page.route}) 未在导航、首页或其他页面入口中发现绑定引用。`);
496
- }
133
+ const registered = new Set();
134
+
135
+ for (const entry of map.checkouts) {
136
+ let local;
137
+ let base;
138
+ try {
139
+ local = absolutePath(projectDir, entry.local_path);
140
+ base = absolutePath(projectDir, entry.base_path);
141
+ } catch (error) {
142
+ errors.push(`${entry.resource_type} ${entry.resource_id}: ${error.message}`);
143
+ continue;
497
144
  }
498
- if (!page.html_file) continue;
499
- const html = readTextSafe(relToAbs(projectDir, page.html_file));
500
- const parseErrors = htmlParseErrors(html);
501
- for (const parseError of parseErrors.slice(0, 5)) {
502
- const at = parseError.line ? `(行 ${parseError.line}${parseError.column ? `:${parseError.column}` : ''})` : '';
503
- addWarning('DG-HTML-001', 'high', `${label} HTML 解析提醒:${parseError.code}${at}。`);
145
+ registered.add(path.resolve(local).toLowerCase());
146
+ if (!fs.existsSync(local)) {
147
+ errors.push(`${entry.resource_type} ${entry.resource_id}: local content is missing (${entry.local_path}).`);
148
+ }
149
+ if (!fs.existsSync(base)) {
150
+ errors.push(`${entry.resource_type} ${entry.resource_id}: checkout base is missing (${entry.base_path}).`);
151
+ } else if (sha256File(base) !== entry.base_hash) {
152
+ errors.push(`${entry.resource_type} ${entry.resource_id}: checkout base hash does not match the manifest.`);
504
153
  }
505
- }
506
154
 
507
- for (const nav of map.navigations) {
508
- if (!nav.html_file) continue;
509
- const navLabel = `导航${nav.name || nav.code || nav.id || ''}`;
510
- const html = readTextSafe(relToAbs(projectDir, nav.html_file));
511
- const parseErrors = htmlParseErrors(html);
512
- for (const parseError of parseErrors.slice(0, 5)) {
513
- const at = parseError.line ? `(行 ${parseError.line}${parseError.column ? `:${parseError.column}` : ''})` : '';
514
- addWarning('DG-HTML-001', 'high', `${navLabel} HTML 解析提醒:${parseError.code}${at}。`);
155
+ const type = mediaType(entry.content_type);
156
+ let expectedExtension = entry.file_extension;
157
+ if (type === 'text/html' || type === 'application/xhtml+xml') expectedExtension = '.html';
158
+ else if (type === 'text/markdown' || type === 'text/x-markdown') expectedExtension = '.md';
159
+ else if (type === 'text/plain') expectedExtension = '.txt';
160
+ if (path.extname(entry.local_path).toLowerCase() !== String(expectedExtension).toLowerCase()) {
161
+ errors.push(`${entry.resource_type} ${entry.resource_id}: local extension does not preserve ${entry.content_type}.`);
162
+ }
163
+ if (fs.existsSync(local) && (type === 'text/html' || type === 'application/xhtml+xml')) {
164
+ let source;
165
+ try { source = decodeTextFile(local, entry.content_type); }
166
+ catch (error) {
167
+ errors.push(`${entry.resource_type} ${entry.resource_id}: ${error.message}`);
168
+ continue;
169
+ }
170
+ for (const issue of htmlParseErrors(source).slice(0, 20)) {
171
+ const position = issue.line ? ` at ${issue.line}:${issue.column || 1}` : '';
172
+ addWarning('DG-HTML-001', 'high', `${entry.resource_type} ${entry.resource_id}: ${issue.code}${position}.`);
173
+ }
515
174
  }
516
175
  }
517
176
 
518
- const orphanGroups = [
519
- ['页面', '.draftgo/pages', '.draftgo/pages/index.json', 'html_file', 'page', ['.html']],
520
- ['导航', '.draftgo/navigations', '.draftgo/navigations/index.json', 'html_file', 'nav', ['.html']],
521
- ['文档', '.draftgo/docs/articles', '.draftgo/docs/articles/index.json', 'content_file', 'article', ['.html', '.md']],
522
- ['自定义脚本', '.draftgo/custom_scripts', '.draftgo/custom_scripts/index.json', 'code_file', 'script', ['.py', '.js', '.ts', '.sh', '.go', '.txt']],
523
- ];
524
- for (const [label, relDir, indexRel, fileField, prefix, exts] of orphanGroups) {
525
- const orphans = collectOrphanFiles(projectDir, relDir, indexRel, fileField, prefix, exts);
526
- if (orphans.length) {
527
- addWarning('DG-ORPHAN-001', 'high', `${label}目录存在 ${orphans.length} 个未登记到 index.json 的文件:${orphans.slice(0, 5).join('')}。push 不会上传这些文件。`);
177
+ for (const directory of ['pages', 'navigations', 'docs']) {
178
+ for (const file of worktreeFiles(projectDir, directory)) {
179
+ if (!registered.has(path.resolve(file).toLowerCase())) {
180
+ const relative = path.relative(projectDir, file).replace(/\\/g, '/');
181
+ addWarning('DG-WORKTREE-ORPHAN', 'high', `Unregistered worktree file: ${relative}.`);
182
+ }
183
+ }
184
+ }
185
+ for (const page of map.pages) {
186
+ if (!page.route || page.route === '/' || page.area === 'system') continue;
187
+ const sources = map.routeRefs[normalizeRoute(page.route)] || [];
188
+ if (!sources.some((source) => source.startsWith('navigations:'))) {
189
+ addWarning('DG-ROUTE-001', 'medium',
190
+ `pages ${page.resource_id}: route ${page.route} is not referenced by a checked-out navigation.`);
528
191
  }
529
192
  }
530
-
531
193
  return { map, errors, warnings, warningDetails };
532
194
  }
533
195
 
@@ -537,4 +199,5 @@ module.exports = {
537
199
  normalizeRoute,
538
200
  extractRoutes,
539
201
  extractScriptBindings,
202
+ htmlParseErrors,
540
203
  };