draftgo-cli 3.0.1 → 3.0.29

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 (53) hide show
  1. package/README.md +67 -17
  2. package/package.json +13 -8
  3. package/resources/skill/SKILL.md +118 -22
  4. package/resources/skill/core/architecture.md +4 -24
  5. package/resources/skill/core/modules.md +14 -4
  6. package/resources/skill/init/SKILL.md +3 -4
  7. package/resources/skill/practices/anti-patterns.md +14 -4
  8. package/resources/skill/practices/best-practices.md +25 -6
  9. package/resources/skill/practices/dev-declaration.md +23 -3
  10. package/resources/skill/pull/SKILL.md +9 -1
  11. package/resources/skill/push/SKILL.md +103 -68
  12. package/resources/skill/quickref/api-endpoints.md +63 -41
  13. package/resources/skill/quickref/api.json +5084 -4975
  14. package/resources/skill/quickref/app-api.md +4 -14
  15. package/resources/skill/rules/dev-workflow.md +154 -57
  16. package/resources/skill/rules/frontend.md +569 -21
  17. package/resources/skill/rules/parallel.md +10 -10
  18. package/resources/skill/scripts/__pycache__/draftgo_pull.cpython-312.pyc +0 -0
  19. package/resources/skill/scripts/__pycache__/draftgo_push.cpython-312.pyc +0 -0
  20. package/resources/skill/scripts/draftgo_delete.py +0 -2
  21. package/resources/skill/scripts/draftgo_init.py +15 -3
  22. package/resources/skill/scripts/draftgo_pull.py +154 -87
  23. package/resources/skill/scripts/draftgo_push.py +363 -174
  24. package/resources/skill/specs/custom-services.md +199 -0
  25. package/resources/skill/specs/data.md +195 -5
  26. package/resources/skill/specs/db-relations.md +227 -0
  27. package/resources/skill/specs/runtime.md +30 -0
  28. package/resources/skill/specs/security.md +3 -3
  29. package/resources/skill/specs/ui-protocol.md +79 -48
  30. package/resources/skill/story/SKILL.md +2 -7
  31. package/src/cli.js +9 -0
  32. package/src/commands/api.js +59 -0
  33. package/src/commands/autoPush.js +41 -0
  34. package/src/commands/check.js +27 -17
  35. package/src/commands/delete.js +6 -4
  36. package/src/commands/deploy.js +31 -0
  37. package/src/commands/doctor.js +1 -1
  38. package/src/commands/help.js +27 -9
  39. package/src/commands/init.js +17 -2
  40. package/src/commands/map.js +18 -7
  41. package/src/commands/new.js +20 -17
  42. package/src/commands/sync.js +10 -3
  43. package/src/commands/update.js +15 -56
  44. package/src/commands/upgrade.js +52 -0
  45. package/src/commands/verifyUi.js +199 -0
  46. package/src/index.js +12 -1
  47. package/src/localdev/compose.js +8 -1
  48. package/src/platforms.js +3 -3
  49. package/src/projectConfig.js +11 -1
  50. package/src/projectMap.js +274 -39
  51. package/src/skill.js +113 -29
  52. package/src/updateCheck.js +37 -5
  53. package/resources/skill/quickref/dg-components.md +0 -198
@@ -40,13 +40,13 @@ function mapCommand(projectDir, flags = {}) {
40
40
  });
41
41
 
42
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('外部 API', map.external_apis, (a) => {
47
- const route = `${a.method || 'GET'} ${a.path || '/'}`;
48
- const tags = a.tags && a.tags.length ? ` #${a.tags.join(',#')}` : '';
49
- return `${a.id || 'new'} ${a.code || '(无 code)'} ${route} ${a.name || ''}${a.base_url ? ` ${a.base_url}` : ''}${tags}`;
43
+ const bindings = [
44
+ ...(s.routes || []),
45
+ ...(s.events || []).map((event) => `EVENT ${event}`),
46
+ ...(s.schedules || []).map((cron) => `CRON ${cron}`),
47
+ ];
48
+ const summary = bindings.length ? ` [${bindings.join(' | ')}]` : '';
49
+ return `${s.slug || '(无 slug)'} ${s.mode || 'mode?'} ${s.name || ''}${summary}${s.code_file ? ` ${s.code_file}` : ''}`;
50
50
  });
51
51
 
52
52
  printList('文档', map.docs, (d) => {
@@ -82,6 +82,17 @@ function mapCommand(projectDir, flags = {}) {
82
82
  });
83
83
  printList('角色', map.roles, (r) => `${r.code || r.id || 'new'} ${r.name || ''}`);
84
84
 
85
+ console.log('');
86
+ log.info('页面分区:');
87
+ const areaRows = [
88
+ ['home', map.pageGroups.home.length],
89
+ ['admin', map.pageGroups.admin.length],
90
+ ['business', map.pageGroups.business.length],
91
+ ['system', map.pageGroups.system.length],
92
+ ['unknown', map.pageGroups.unknown.length],
93
+ ];
94
+ log.dim(' ' + areaRows.map(([k, v]) => `${k} ${v}`).join(' '));
95
+
85
96
  console.log('');
86
97
  log.info('入口引用:');
87
98
  const routes = Object.keys(map.routeRefs).sort();
@@ -72,36 +72,39 @@ const TEMPLATES = {
72
72
  indexEntry: {
73
73
  name: slug,
74
74
  slug,
75
- mode: 'route',
75
+ mode: 'mixed',
76
+ language: 'go',
76
77
  status: 'active',
77
- triggers: { path: `/${slug}`, methods: ['GET', 'POST'] },
78
78
  config: {},
79
79
  permission: { default: 'admin' },
80
- code_file: `.draftgo/custom_scripts/script_new_${slug}.py`,
80
+ code_file: `.draftgo/custom_scripts/script_new_${slug}.go`,
81
81
  },
82
- file: `.draftgo/custom_scripts/script_new_${slug}.py`,
83
- content: `# ${slug}
84
- # 路由脚本:/api/x/${slug}
85
-
86
- def handle(request, context):
87
- """
88
- request: { method, path, query, body, headers, user }
89
- context: { db, config, logger }
90
- """
91
- return {"message": "ok"}
82
+ file: `.draftgo/custom_scripts/script_new_${slug}.go`,
83
+ content: `package main
84
+
85
+ import "draftgo/sdk"
86
+
87
+ // GET /api/x/${slug}/
88
+ func Register(app *sdk.App) {
89
+ app.Route("GET", "/", get${slug.replace(/[^A-Za-z0-9_]/g, '_')})
90
+ }
91
+
92
+ func get${slug.replace(/[^A-Za-z0-9_]/g, '_')}(ctx *sdk.Context) (any, error) {
93
+ return ctx.Respond(map[string]any{"message": "ok"}, 200, nil), nil
94
+ }
92
95
  `,
93
96
  }),
94
97
 
95
98
  doc: (slug) => ({
96
99
  indexEntry: {
97
100
  title: slug,
98
- key: slug,
101
+ slug,
99
102
  status: 'published',
100
103
  category_id: null,
101
- content_file: `.draftgo/docs/articles/doc_new_${slug}.md`,
104
+ content_file: `.draftgo/docs/articles/doc_new_${slug}.html`,
102
105
  },
103
- file: `.draftgo/docs/articles/doc_new_${slug}.md`,
104
- content: `# ${slug}\n\n文档内容\n`,
106
+ file: `.draftgo/docs/articles/doc_new_${slug}.html`,
107
+ content: `<!DOCTYPE html>\n<html>\n<head><meta charset="UTF-8"></head>\n<body>\n<div class="p-6">\n <h1 class="text-2xl font-bold">${slug}</h1>\n <p>文档内容</p>\n</div>\n</body>\n</html>`,
105
108
  }),
106
109
  };
107
110
 
@@ -40,15 +40,22 @@ function sync(projectDir, command, positional, flags = {}) {
40
40
  }
41
41
 
42
42
  const args = positional.length ? positional : ['--all'];
43
- if (command === 'push' && flags.force) {
44
- args.push('--force');
43
+ if (command === 'push' && flags['dry-run']) {
44
+ args.push('--dry-run');
45
+ }
46
+ if (command === 'push' && flags['probe-routes']) {
47
+ args.push('--probe-routes');
45
48
  }
46
49
  const r = spawnSync(py.bin, [script, ...args], {
47
50
  cwd: projectDir,
48
51
  stdio: 'inherit',
49
52
  shell: false,
50
53
  });
51
- return r.status || 0;
54
+ if (r.error) {
55
+ log.err(`${command} 启动失败:${r.error.message}`);
56
+ return 1;
57
+ }
58
+ return Number.isInteger(r.status) ? r.status : 1;
52
59
  }
53
60
 
54
61
  module.exports = sync;
@@ -1,71 +1,25 @@
1
1
  'use strict';
2
2
 
3
- const { spawnSync } = require('child_process');
4
3
  const log = require('../logger');
5
4
  const { all, byName, resolveTargets } = require('../installers');
6
5
  const { ensureRuntime, writeInstalledVersion, readInstalledVersion, getPackageVersion } = require('../skill');
7
6
  const { fetchLatestVersion, cmpSemver } = require('../updateCheck');
8
7
  const { detectTargets } = require('../detect');
9
8
 
10
- // Env flag we set before re-exec to avoid infinite upgrade loops.
11
- const REENTRY_FLAG = 'DRAFTGO_UPDATE_REENTERED';
12
-
13
- function runNpmInstallLatest() {
14
- const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
15
- log.step('执行:npm install -g draftgo-cli@latest');
16
- const r = spawnSync(npm, ['install', '-g', 'draftgo-cli@latest'], { stdio: 'inherit' });
17
- if (r.error) { log.err(`升级失败:${r.error.message}`); return false; }
18
- if (r.status !== 0) {
19
- log.err(`npm install 返回码 ${r.status}`);
20
- log.dim(' 常见原因:权限不足(macOS/Linux 用 sudo,或用 nvm 管理 Node)');
21
- return false;
22
- }
23
- return true;
24
- }
25
-
26
- function reExecSameCommand(projectDir) {
27
- // Re-run `draftgo update` from the freshly installed global CLI, so the user
28
- // gets the newest skill content written into .draftgo/skill in the same call.
29
- const cmd = process.platform === 'win32' ? 'draftgo.cmd' : 'draftgo';
30
- const args = ['update', '--project', projectDir, '--skip-update-check'];
31
- log.step('以新版 CLI 重新执行:draftgo update');
32
- const r = spawnSync(cmd, args, {
33
- stdio: 'inherit',
34
- env: { ...process.env, [REENTRY_FLAG]: '1' },
35
- });
36
- if (r.error) {
37
- log.err(`重新执行失败:${r.error.message}`);
38
- log.dim(' 请手动在项目目录再跑一次:draftgo update');
39
- return 1;
40
- }
41
- return r.status || 0;
42
- }
43
-
44
- async function maybeAutoUpgrade(projectDir, flags) {
45
- if (flags['skip-update-check']) return { upgraded: false };
46
- if (process.env[REENTRY_FLAG] === '1') return { upgraded: false };
47
- if (process.env.DRAFTGO_NO_UPDATE_CHECK === '1') return { upgraded: false };
48
-
9
+ async function reportAvailableUpgrade(flags) {
10
+ if (flags['skip-update-check'] || process.env.DRAFTGO_NO_UPDATE_CHECK === '1') return;
49
11
  const current = getPackageVersion();
50
12
  const latest = await fetchLatestVersion();
51
13
  if (!latest) {
52
14
  log.dim(' (查询最新版失败,按当前 CLI 继续)');
53
- return { upgraded: false };
15
+ return;
54
16
  }
55
- if (cmpSemver(latest, current) <= 0) {
17
+ if (cmpSemver(latest, current) > 0) {
18
+ log.warn(`draftgo-cli 有新版:${current} → ${latest}`);
19
+ log.dim(' 运行 `draftgo upgrade` 可显式升级;本次继续使用当前版本刷新 skill。');
20
+ } else {
56
21
  log.dim(` CLI 已是最新(${current})`);
57
- return { upgraded: false };
58
22
  }
59
-
60
- log.warn(`draftgo-cli 有新版:${current} → ${latest},自动升级中……`);
61
- const ok = runNpmInstallLatest();
62
- if (!ok) {
63
- log.warn('自动升级失败,回退到当前 CLI 继续执行。');
64
- return { upgraded: false };
65
- }
66
- log.ok(`CLI 已升级到 ${latest}。`);
67
- const code = reExecSameCommand(projectDir);
68
- return { upgraded: true, exitCode: code };
69
23
  }
70
24
 
71
25
  async function update(projectDir, positional, flags) {
@@ -74,9 +28,7 @@ async function update(projectDir, positional, flags) {
74
28
  const prev = readInstalledVersion(projectDir);
75
29
  log.info(`当前已装 skill:${prev || '未安装'},CLI 版本:${getPackageVersion()}`);
76
30
 
77
- // Auto-upgrade-and-rerun path.
78
- const { upgraded, exitCode } = await maybeAutoUpgrade(projectDir, flags);
79
- if (upgraded) return exitCode;
31
+ await reportAvailableUpgrade(flags);
80
32
 
81
33
  // Determine which entry files to refresh:
82
34
  // - no positional → refresh installed targets plus project-level tool signals
@@ -111,14 +63,20 @@ async function update(projectDir, positional, flags) {
111
63
 
112
64
  ensureRuntime(projectDir);
113
65
  log.step('用 CLI 内置版本覆盖各 AI 工具 skill 目录');
66
+ const failures = [];
114
67
  for (const t of resolved) {
115
68
  try {
116
69
  const r = t.install(projectDir, { force: true });
117
70
  log.ok(`${t.displayName.padEnd(16)} → ${r.path}`);
118
71
  } catch (e) {
119
72
  log.err(`${t.displayName} 更新失败:${e.message}`);
73
+ failures.push(t.displayName);
120
74
  }
121
75
  }
76
+ if (failures.length) {
77
+ log.err(`更新未完成,失败目标:${failures.join(', ')}`);
78
+ return 1;
79
+ }
122
80
  writeInstalledVersion(projectDir);
123
81
 
124
82
  log.title('完成');
@@ -126,3 +84,4 @@ async function update(projectDir, positional, flags) {
126
84
  }
127
85
 
128
86
  module.exports = update;
87
+ module.exports.reportAvailableUpgrade = reportAvailableUpgrade;
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('child_process');
4
+ const log = require('../logger');
5
+ const { getPackageVersion } = require('../skill');
6
+ const { fetchLatestVersion, cmpSemver } = require('../updateCheck');
7
+
8
+ const REENTRY_FLAG = 'DRAFTGO_UPGRADE_REENTERED';
9
+
10
+ async function upgrade(projectDir) {
11
+ if (process.env[REENTRY_FLAG] === '1') {
12
+ log.err('检测到重复升级重入,已停止以避免循环。');
13
+ return 1;
14
+ }
15
+ const current = getPackageVersion();
16
+ const latest = await fetchLatestVersion(5000, { force: true });
17
+ if (!latest) {
18
+ log.err('无法查询 npm 最新版本,未执行升级。');
19
+ return 1;
20
+ }
21
+
22
+ if (cmpSemver(latest, current) <= 0) {
23
+ log.ok(`CLI 已是最新:${current}`);
24
+ return require('./update')(projectDir, [], { 'skip-update-check': true });
25
+ }
26
+
27
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
28
+ log.step(`显式升级 draftgo-cli:${current} → ${latest}`);
29
+ const install = spawnSync(npm, ['install', '-g', `draftgo-cli@${latest}`], { stdio: 'inherit', shell: false });
30
+ if (install.error) {
31
+ log.err(`升级启动失败:${install.error.message}`);
32
+ return 1;
33
+ }
34
+ if (install.status !== 0) {
35
+ log.err(`npm install 返回码 ${install.status}`);
36
+ return Number.isInteger(install.status) ? install.status : 1;
37
+ }
38
+
39
+ const cmd = process.platform === 'win32' ? 'draftgo.cmd' : 'draftgo';
40
+ const rerun = spawnSync(cmd, ['update', '--project', projectDir, '--skip-update-check'], {
41
+ stdio: 'inherit',
42
+ shell: false,
43
+ env: { ...process.env, [REENTRY_FLAG]: '1' },
44
+ });
45
+ if (rerun.error) {
46
+ log.err(`新版 CLI 重跑失败:${rerun.error.message}`);
47
+ return 1;
48
+ }
49
+ return Number.isInteger(rerun.status) ? rerun.status : 1;
50
+ }
51
+
52
+ module.exports = upgrade;
@@ -0,0 +1,199 @@
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
+ const UI_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less', '.html', '.htm', '.jsx', '.tsx', '.vue', '.svelte']);
9
+
10
+ function isUiFile(file) {
11
+ const normalized = String(file || '').replace(/\\/g, '/').toLowerCase();
12
+ const ext = path.extname(normalized);
13
+ if (UI_EXTENSIONS.has(ext)) return true;
14
+ if (!['.js', '.ts'].includes(ext)) return false;
15
+ return /(^|\/)(frontend|web|ui|components|pages|views|client)(\/|$)/.test(normalized)
16
+ || /(^|\/)(app|main|client)\.(js|ts)$/.test(normalized)
17
+ || normalized.startsWith('.draftgo/pages/')
18
+ || normalized.startsWith('.draftgo/navigations/');
19
+ }
20
+
21
+ function gitChangedFiles(projectDir) {
22
+ const r = spawnSync('git', ['status', '--porcelain=v1', '--untracked-files=all'], {
23
+ cwd: projectDir,
24
+ encoding: 'utf8',
25
+ shell: false,
26
+ });
27
+ if (r.error || r.status !== 0) return null;
28
+ return String(r.stdout || '').split(/\r?\n/).filter(Boolean).map((line) => {
29
+ const raw = line.slice(3).trim();
30
+ const renamed = raw.includes(' -> ') ? raw.split(' -> ').pop() : raw;
31
+ return renamed.replace(/^"|"$/g, '');
32
+ });
33
+ }
34
+
35
+ function decideMobileCheck(projectDir, mode) {
36
+ if (mode === 'always') return { run: true, reason: 'mobile-check=always' };
37
+ if (mode === 'never') return { run: false, reason: 'mobile-check=never' };
38
+ const changed = gitChangedFiles(projectDir);
39
+ if (changed === null || changed.length === 0) {
40
+ return { run: true, reason: '无法从 Git diff 确认影响范围,执行单视口 smoke check' };
41
+ }
42
+ const uiFiles = changed.filter(isUiFile);
43
+ if (!uiFiles.length) return { run: false, reason: '本次变更未涉及页面、样式或前端组件' };
44
+ return { run: true, reason: `检测到 ${uiFiles.length} 个 UI 文件变更` };
45
+ }
46
+
47
+ function numberFlag(value, fallback, min, max) {
48
+ const n = Number(value);
49
+ return Number.isFinite(n) ? Math.max(min, Math.min(max, Math.round(n))) : fallback;
50
+ }
51
+
52
+ function executableCandidates() {
53
+ if (process.platform === 'win32') {
54
+ const roots = [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], process.env.LOCALAPPDATA].filter(Boolean);
55
+ const rels = [
56
+ ['Microsoft', 'Edge', 'Application', 'msedge.exe'],
57
+ ['Google', 'Chrome', 'Application', 'chrome.exe'],
58
+ ['Chromium', 'Application', 'chrome.exe'],
59
+ ];
60
+ return roots.flatMap((root) => rels.map((parts) => path.join(root, ...parts)));
61
+ }
62
+ if (process.platform === 'darwin') {
63
+ return [
64
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
65
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
66
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
67
+ ];
68
+ }
69
+ return ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/microsoft-edge', '/usr/bin/chromium', '/usr/bin/chromium-browser'];
70
+ }
71
+
72
+ async function launchBrowser(chromium, requested) {
73
+ const attempts = [];
74
+ if (requested && requested !== 'chromium') attempts.push({ channel: requested });
75
+ for (const executablePath of executableCandidates().filter((candidate) => fs.existsSync(candidate))) {
76
+ attempts.push({ executablePath });
77
+ }
78
+ if (!requested || requested === 'chromium') attempts.push({});
79
+ for (const channel of ['msedge', 'chrome']) {
80
+ if (!attempts.some((a) => a.channel === channel)) attempts.push({ channel });
81
+ }
82
+
83
+ let lastError = null;
84
+ for (const options of attempts) {
85
+ try {
86
+ return await chromium.launch({ headless: true, ...options });
87
+ } catch (err) {
88
+ lastError = err;
89
+ }
90
+ }
91
+ throw new Error(`未找到可用的 Chromium/Chrome/Edge。${lastError ? ` ${lastError.message.split('\n')[0]}` : ''}`);
92
+ }
93
+
94
+ async function verifyUi(projectDir, positional, flags = {}) {
95
+ const url = String(flags.url || positional[0] || '').trim();
96
+ if (!/^https?:\/\//i.test(url)) {
97
+ log.err('用法:draftgo verify-ui <http://localhost:port/path>');
98
+ return 1;
99
+ }
100
+
101
+ const mode = String(flags['mobile-check'] || 'auto').toLowerCase();
102
+ if (!['auto', 'always', 'never'].includes(mode)) {
103
+ log.err('--mobile-check 只支持 auto、always、never。');
104
+ return 1;
105
+ }
106
+ const decision = decideMobileCheck(projectDir, mode);
107
+ if (!decision.run) {
108
+ log.ok(`跳过 UI smoke check:${decision.reason}`);
109
+ return 0;
110
+ }
111
+
112
+ let chromium;
113
+ try {
114
+ ({ chromium } = require('playwright-core'));
115
+ } catch {
116
+ log.err('缺少 playwright-core,请重新安装或升级 draftgo-cli。');
117
+ return 1;
118
+ }
119
+
120
+ const width = numberFlag(flags.width, 390, 240, 3840);
121
+ const height = numberFlag(flags.height, 844, 320, 2160);
122
+ const waitMs = numberFlag(flags['wait-ms'], 500, 0, 10000);
123
+ const screenshotMode = String(flags.screenshot || 'on-failure').toLowerCase();
124
+ if (!['on-failure', 'always', 'never'].includes(screenshotMode)) {
125
+ log.err('--screenshot 只支持 on-failure、always、never。');
126
+ return 1;
127
+ }
128
+
129
+ let browser;
130
+ try {
131
+ browser = await launchBrowser(chromium, flags.browser && String(flags.browser));
132
+ const page = await browser.newPage({ viewport: { width, height } });
133
+ const consoleErrors = [];
134
+ const pageErrors = [];
135
+ page.on('console', (msg) => { if (msg.type() === 'error') consoleErrors.push(msg.text()); });
136
+ page.on('pageerror', (err) => pageErrors.push(err.message));
137
+
138
+ const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
139
+ if (waitMs) await page.waitForTimeout(waitMs);
140
+ const state = await page.evaluate(() => {
141
+ const body = document.body;
142
+ const root = document.documentElement;
143
+ return {
144
+ title: document.title,
145
+ bodyTextLength: body ? (body.innerText || '').trim().length : 0,
146
+ bodyHeight: body ? body.getBoundingClientRect().height : 0,
147
+ hasVisibleMedia: body ? Array.from(body.querySelectorAll('img,svg,canvas,video,iframe,input,button')).some((element) => {
148
+ const rect = element.getBoundingClientRect();
149
+ return rect.width > 1 && rect.height > 1;
150
+ }) : false,
151
+ horizontalOverflow: root.scrollWidth > window.innerWidth + 1,
152
+ scrollWidth: root.scrollWidth,
153
+ viewportWidth: window.innerWidth,
154
+ };
155
+ });
156
+
157
+ const issues = [];
158
+ if (response && response.status() >= 400) issues.push(`页面返回 HTTP ${response.status()}`);
159
+ if (!state.bodyHeight || (!state.bodyTextLength && !state.hasVisibleMedia)) issues.push('页面疑似空白');
160
+ if (state.horizontalOverflow) issues.push(`页面横向溢出:scrollWidth=${state.scrollWidth}, viewport=${state.viewportWidth}`);
161
+ if (consoleErrors.length) issues.push(`console error ${consoleErrors.length} 条`);
162
+ if (pageErrors.length) issues.push(`page error ${pageErrors.length} 条`);
163
+ if (flags.selector) {
164
+ const visible = await page.locator(String(flags.selector)).first().isVisible().catch(() => false);
165
+ if (!visible) issues.push(`关键元素不可见:${flags.selector}`);
166
+ }
167
+
168
+ const shouldScreenshot = screenshotMode === 'always' || (screenshotMode === 'on-failure' && issues.length > 0);
169
+ let screenshotPath = null;
170
+ if (shouldScreenshot) {
171
+ const dir = path.join(projectDir, '.draftgo', 'artifacts');
172
+ fs.mkdirSync(dir, { recursive: true });
173
+ screenshotPath = path.join(dir, `ui-check-${process.pid}-${Date.now()}.png`);
174
+ await page.screenshot({ path: screenshotPath, fullPage: true });
175
+ }
176
+
177
+ if (issues.length) {
178
+ issues.forEach((issue) => log.err(issue));
179
+ if (consoleErrors.length) consoleErrors.slice(0, 5).forEach((msg) => log.dim(` console: ${msg}`));
180
+ if (pageErrors.length) pageErrors.slice(0, 5).forEach((msg) => log.dim(` page: ${msg}`));
181
+ if (screenshotPath) log.info(`失败截图:${screenshotPath}`);
182
+ return 1;
183
+ }
184
+
185
+ log.ok(`UI smoke check 通过:${width}x${height},${decision.reason}`);
186
+ if (screenshotPath) log.info(`截图:${screenshotPath}`);
187
+ return 0;
188
+ } catch (err) {
189
+ log.err(`UI smoke check 失败:${err.message}`);
190
+ return 1;
191
+ } finally {
192
+ if (browser) await browser.close().catch(() => {});
193
+ }
194
+ }
195
+
196
+ module.exports = verifyUi;
197
+ module.exports.decideMobileCheck = decideMobileCheck;
198
+ module.exports.gitChangedFiles = gitChangedFiles;
199
+ module.exports.isUiFile = isUiFile;
package/src/index.js CHANGED
@@ -26,7 +26,6 @@ async function run(argv) {
26
26
  case 'init':
27
27
  return await require('./commands/init')(projectDir, positional, flags);
28
28
  case 'update':
29
- case 'upgrade':
30
29
  return await require('./commands/update')(projectDir, positional, flags);
31
30
  case 'uninstall':
32
31
  case 'remove':
@@ -35,10 +34,17 @@ async function run(argv) {
35
34
  return require('./commands/status')(projectDir);
36
35
  case 'doctor':
37
36
  return await require('./commands/doctor')(projectDir, flags);
37
+ case 'upgrade':
38
+ return await require('./commands/upgrade')(projectDir, positional, flags);
38
39
  case 'map':
39
40
  return require('./commands/map')(projectDir, flags);
40
41
  case 'check':
41
42
  return require('./commands/check')(projectDir, flags);
43
+ case 'verify-ui':
44
+ case 'verifyui':
45
+ return await require('./commands/verifyUi')(projectDir, positional, flags);
46
+ case 'api':
47
+ return require('./commands/api')(positional, flags);
42
48
  case 'dev':
43
49
  case 'build':
44
50
  return require('./commands/projectScript')(projectDir, command, flags);
@@ -48,6 +54,11 @@ async function run(argv) {
48
54
  case 'del':
49
55
  case 'rm':
50
56
  return await require('./commands/delete')(projectDir, positional, flags);
57
+ case 'deploy':
58
+ return require('./commands/deploy')(projectDir, positional, flags);
59
+ case 'auto-push':
60
+ case 'autopush':
61
+ return require('./commands/autoPush')(projectDir, positional, flags);
51
62
  case 'pull':
52
63
  case 'push':
53
64
  return require('./commands/sync')(projectDir, command, positional, flags);
@@ -71,6 +71,11 @@ function sanitizeProjectName(raw, fallback = 'draftgo') {
71
71
  return v || fallback;
72
72
  }
73
73
 
74
+ function defaultRedisKeyPrefix(projectName, mysql) {
75
+ const dbName = mysql && mysql.database ? sanitizeProjectName(mysql.database, '') : '';
76
+ return dbName || projectName;
77
+ }
78
+
74
79
  // `opts`:
75
80
  // projectName: docker compose project name (also drives container_name)
76
81
  // appPort: host port for the app (default 3000)
@@ -85,6 +90,7 @@ function generate(projectDir, opts) {
85
90
  const appPort = Number(opts.appPort || 3000);
86
91
  env.APP_PORT = String(appPort);
87
92
  env.SECRET_KEY = env.SECRET_KEY || randomHex(32);
93
+ env.REDIS_KEY_PREFIX = env.REDIS_KEY_PREFIX || defaultRedisKeyPrefix(projectName, opts.mysql);
88
94
 
89
95
  const services = {};
90
96
  const dependsOn = [];
@@ -194,6 +200,7 @@ function generate(projectDir, opts) {
194
200
  ` REDIS_PORT: "${redisPort}"`,
195
201
  ' REDIS_DB: "0"',
196
202
  ` REDIS_PASSWORD: "${redisPass}"`,
203
+ ' REDIS_KEY_PREFIX: ${REDIS_KEY_PREFIX}',
197
204
  ' LOG_DIR: /app/backend/logs',
198
205
  ' LOG_FILE_NAME: app.log',
199
206
  ' LOG_LEVEL: INFO',
@@ -252,6 +259,6 @@ function generate(projectDir, opts) {
252
259
 
253
260
  module.exports = {
254
261
  stackDir, composePath, envPath,
255
- generate, sanitizeProjectName,
262
+ generate, sanitizeProjectName, defaultRedisKeyPrefix,
256
263
  randomAlnum, randomHex,
257
264
  };
package/src/platforms.js CHANGED
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  // Single source of truth describing every supported AI-tool target.
4
- // Each platform receives the full skill body (SKILL.md + sub-skills + rules +
5
- // scripts) inside its own directory, so each tool can read everything natively
6
- // without any indirection.
4
+ // Each platform receives the complete instruction body (SKILL.md + sub-skills
5
+ // + rules + scripts). Only the large OpenAPI snapshot is shared once under
6
+ // .draftgo/skill-shared.
7
7
  //
8
8
  // mainFile : the markdown file the AI tool reads first
9
9
  // assetDir : where sub-skills (init/sync/bug), rules/, scripts/ are placed
@@ -12,7 +12,17 @@ const { platforms } = require('./platforms');
12
12
 
13
13
  function writeProjectConfig(projectDir, server, token) {
14
14
  const cfg = path.join(projectDir, '.draftgo', 'config.json');
15
- writeText(cfg, JSON.stringify({ server, token }, null, 2) + '\n');
15
+ let existing = {};
16
+ if (exists(cfg)) {
17
+ try { existing = JSON.parse(require('fs').readFileSync(cfg, 'utf8')); } catch { /* reset malformed config */ }
18
+ }
19
+ const config = {
20
+ ...existing,
21
+ server,
22
+ token,
23
+ auto_push: existing.auto_push === true,
24
+ };
25
+ writeText(cfg, JSON.stringify(config, null, 2) + '\n');
16
26
  appendGitignoreLine(projectDir, '.draftgo/config.json');
17
27
  return cfg;
18
28
  }