draftgo-cli 3.0.0 → 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.
- package/README.md +67 -17
- package/package.json +13 -8
- package/resources/skill/SKILL.md +118 -22
- package/resources/skill/core/architecture.md +4 -24
- package/resources/skill/core/modules.md +14 -4
- package/resources/skill/init/SKILL.md +3 -4
- package/resources/skill/practices/anti-patterns.md +14 -4
- package/resources/skill/practices/best-practices.md +25 -6
- package/resources/skill/practices/dev-declaration.md +23 -3
- package/resources/skill/pull/SKILL.md +9 -1
- package/resources/skill/push/SKILL.md +103 -68
- package/resources/skill/quickref/api-endpoints.md +63 -41
- package/resources/skill/quickref/api.json +5084 -4975
- package/resources/skill/quickref/app-api.md +4 -14
- package/resources/skill/rules/dev-workflow.md +154 -57
- package/resources/skill/rules/frontend.md +569 -21
- package/resources/skill/rules/parallel.md +10 -10
- package/resources/skill/scripts/__pycache__/draftgo_pull.cpython-312.pyc +0 -0
- package/resources/skill/scripts/__pycache__/draftgo_push.cpython-312.pyc +0 -0
- package/resources/skill/scripts/draftgo_delete.py +0 -2
- package/resources/skill/scripts/draftgo_init.py +15 -3
- package/resources/skill/scripts/draftgo_pull.py +154 -87
- package/resources/skill/scripts/draftgo_push.py +363 -174
- package/resources/skill/specs/custom-services.md +199 -0
- package/resources/skill/specs/data.md +195 -5
- package/resources/skill/specs/db-relations.md +227 -0
- package/resources/skill/specs/runtime.md +30 -0
- package/resources/skill/specs/security.md +3 -3
- package/resources/skill/specs/ui-protocol.md +79 -48
- package/resources/skill/story/SKILL.md +2 -7
- package/src/cli.js +9 -0
- package/src/commands/api.js +59 -0
- package/src/commands/autoPush.js +41 -0
- package/src/commands/check.js +27 -17
- package/src/commands/delete.js +6 -4
- package/src/commands/deploy.js +31 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/help.js +27 -9
- package/src/commands/init.js +17 -2
- package/src/commands/map.js +18 -7
- package/src/commands/new.js +20 -17
- package/src/commands/sync.js +10 -3
- package/src/commands/update.js +15 -56
- package/src/commands/upgrade.js +52 -0
- package/src/commands/verifyUi.js +199 -0
- package/src/index.js +12 -1
- package/src/localdev/compose.js +8 -1
- package/src/platforms.js +3 -3
- package/src/projectConfig.js +11 -1
- package/src/projectMap.js +274 -39
- package/src/skill.js +113 -29
- package/src/updateCheck.js +37 -5
- package/resources/skill/quickref/dg-components.md +0 -198
package/src/commands/map.js
CHANGED
|
@@ -40,13 +40,13 @@ function mapCommand(projectDir, flags = {}) {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
printList('自定义脚本', map.custom_scripts, (s) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
return `${
|
|
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();
|
package/src/commands/new.js
CHANGED
|
@@ -72,36 +72,39 @@ const TEMPLATES = {
|
|
|
72
72
|
indexEntry: {
|
|
73
73
|
name: slug,
|
|
74
74
|
slug,
|
|
75
|
-
mode: '
|
|
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}.
|
|
80
|
+
code_file: `.draftgo/custom_scripts/script_new_${slug}.go`,
|
|
81
81
|
},
|
|
82
|
-
file: `.draftgo/custom_scripts/script_new_${slug}.
|
|
83
|
-
content:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
101
|
+
slug,
|
|
99
102
|
status: 'published',
|
|
100
103
|
category_id: null,
|
|
101
|
-
content_file: `.draftgo/docs/articles/doc_new_${slug}.
|
|
104
|
+
content_file: `.draftgo/docs/articles/doc_new_${slug}.html`,
|
|
102
105
|
},
|
|
103
|
-
file: `.draftgo/docs/articles/doc_new_${slug}.
|
|
104
|
-
content:
|
|
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
|
|
package/src/commands/sync.js
CHANGED
|
@@ -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
|
|
44
|
-
args.push('--
|
|
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
|
-
|
|
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;
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
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
|
|
15
|
+
return;
|
|
54
16
|
}
|
|
55
|
-
if (cmpSemver(latest, current)
|
|
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
|
-
|
|
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);
|
package/src/localdev/compose.js
CHANGED
|
@@ -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
|
|
5
|
-
// scripts)
|
|
6
|
-
//
|
|
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
|
package/src/projectConfig.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|