agentic-flow-kit 2.0.0 → 2.1.0
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/LICENSE +21 -0
- package/README.md +92 -23
- package/bin/flow-kit.mjs +3 -1
- package/locales/en/installer.json +87 -2
- package/locales/en/overrides/.codebuddy/skills/tfs-pr/SKILL.md +1 -1
- package/locales/en/overrides/workflow/README.md +2 -0
- package/locales/zh-CN/installer.json +87 -2
- package/package.json +2 -2
- package/schemas/install-manifest.schema.json +3 -1
- package/schemas/project-profile.schema.json +1 -1
- package/src/cli/args.mjs +3 -1
- package/src/cli/main.mjs +9 -2
- package/src/commands/doctor.mjs +50 -4
- package/src/commands/install.mjs +160 -13
- package/src/hosts/registry.mjs +154 -10
- package/src/i18n.mjs +3 -0
- package/src/install/apply.mjs +17 -3
- package/src/install/frontmatter.mjs +84 -0
- package/src/install/plan.mjs +174 -16
- package/src/models/known.mjs +13 -0
- package/src/models/probe.mjs +163 -0
- package/src/models/resolve.mjs +312 -0
- package/src/render/tokens.mjs +56 -9
- package/src/tui/ansi.mjs +55 -0
- package/src/tui/flows.mjs +306 -0
- package/src/tui/prompts.mjs +139 -0
- package/src/tui/screen.mjs +217 -0
- package/src/tui/widgets.mjs +111 -0
- package/src/tui/wizard.mjs +68 -0
- package/templates/engine/.agents/commands/new-task.md +2 -2
- package/templates/engine/.agents/commands/pipeline-run.md +5 -1
- package/templates/engine/.agents/commands/stages.md +2 -1
- package/templates/engine/.agents/config/agent-eval.json +17 -0
- package/templates/engine/.agents/config/mechanism-registry.md +2 -1
- package/templates/engine/.agents/config/memory-tiers.json +13 -0
- package/templates/engine/.agents/config/pipeline.json +36 -0
- package/templates/engine/.agents/config/steering.json +28 -0
- package/templates/engine/.agents/evals/README.md +49 -0
- package/templates/engine/.agents/evals/tasks.tsv +6 -0
- package/templates/engine/.agents/hooks/pre-write-check.cjs +27 -0
- package/templates/engine/.agents/notes/injected-memory.md +14 -0
- package/templates/engine/.agents/notes/steering-tiers.md +28 -0
- package/templates/engine/.agents/scripts/agent-eval.mjs +268 -0
- package/templates/engine/.agents/scripts/check-pipeline-state.mjs +85 -0
- package/templates/engine/.agents/scripts/claims.mjs +166 -0
- package/templates/engine/.agents/scripts/dream-run.mjs +78 -3
- package/templates/engine/.agents/scripts/fix-task.mjs +77 -0
- package/templates/engine/.agents/scripts/memory-tier.mjs +196 -0
- package/templates/engine/.agents/scripts/memory-wiki.mjs +296 -0
- package/templates/engine/.agents/scripts/outcome-metrics.mjs +195 -0
- package/templates/engine/.agents/scripts/path-lease.mjs +177 -0
- package/templates/engine/.agents/scripts/session-log.mjs +157 -0
- package/templates/engine/.agents/scripts/steering.mjs +75 -0
- package/templates/engine/.agents/scripts/verify.mjs +16 -5
- package/templates/engine/.agents/skills/arena/SKILL.md +4 -0
- package/templates/engine/.agents/skills/dream/SKILL.md +21 -2
- package/templates/engine/.agents/skills/eval/SKILL.md +16 -0
- package/templates/engine/.agents/skills/model-upgrade/SKILL.md +41 -0
- package/templates/engine/.agents/skills/session-resume/SKILL.md +7 -0
- package/templates/engine/.agents/skills/show-me-your-work/SKILL.md +14 -0
- package/templates/engine/.agents/skills/wiki/SKILL.md +4 -1
- package/templates/git/.gitattributes +1 -0
- package/templates/git/.githooks/package.json +3 -0
- package/templates/git/.github/workflows/ci.yml +32 -0
- package/templates/git/azure-pipelines.yml +46 -0
- package/templates/host-meta/SKILLS.md +3 -0
- package/templates/host-meta/claude/skill-pointer.md +6 -0
- package/templates/host-meta/cline/skill-index.md +6 -0
- package/templates/host-meta/codebuddy/skill-pointer.md +6 -0
- package/templates/host-meta/commandcode/skill-index.md +5 -0
- package/templates/host-meta/opencode/skill-pointer.md +6 -0
- package/templates/host-meta/qoder/skill-index.md +5 -0
- package/templates/host-meta/qwen/skill-index.md +5 -0
- package/templates/host-meta/trae/skill-index.md +6 -0
- package/templates/hosts/claude/settings.hardened.json +94 -0
- package/templates/hosts/claude/settings.json +1 -3
- package/templates/hosts/commandcode/settings.json +1 -3
- package/templates/hosts/qoder/settings.json +1 -3
- package/templates/hosts/qwen/settings.json +1 -3
- package/templates/profiles/go/AGENTS.md +52 -0
- package/templates/profiles/java/AGENTS.md +52 -0
- package/templates/profiles/rust/AGENTS.md +52 -0
- package/templates/profiles/typescript/AGENTS.md +52 -0
- package/templates/scaffold/workflow/INDEX.md +16 -0
- package/templates/scaffold/workflow/README.md +2 -0
- package/templates/scaffold/workflow/artifact-sources.md +25 -0
- package/templates/scaffold/workflow/non-engineer-intent.md +51 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { HOSTS, modelRoleForAgent } from '../hosts/registry.mjs';
|
|
5
|
+
import { getField } from '../install/frontmatter.mjs';
|
|
6
|
+
import { probeModels } from './probe.mjs';
|
|
7
|
+
import { knownFor } from './known.mjs';
|
|
8
|
+
|
|
9
|
+
export const ROLE_NAMES = ['code', 'judgment', 'review'];
|
|
10
|
+
export const LOCAL_REL = '.agents/config/model-roles.local.json';
|
|
11
|
+
const KIT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
12
|
+
|
|
13
|
+
// --model 支持两种形式:code=sonnet(全局)、claude:code=sonnet(宿主级)。
|
|
14
|
+
export function parseModelFlags(values = []) {
|
|
15
|
+
const out = { global: {}, hosts: {} };
|
|
16
|
+
for (const raw of values) {
|
|
17
|
+
const s = String(raw).trim();
|
|
18
|
+
const eq = s.indexOf('=');
|
|
19
|
+
if (eq <= 0) {
|
|
20
|
+
const err = new Error(`invalid --model: ${raw}`);
|
|
21
|
+
err.code = 'BAD_MODEL';
|
|
22
|
+
err.value = raw;
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
const left = s.slice(0, eq).trim();
|
|
26
|
+
const id = s.slice(eq + 1).trim();
|
|
27
|
+
const [host, role] = left.includes(':') ? left.split(':', 2) : [null, left];
|
|
28
|
+
if (!id || !ROLE_NAMES.includes(role)) {
|
|
29
|
+
const err = new Error(`invalid --model: ${raw}`);
|
|
30
|
+
err.code = 'BAD_MODEL';
|
|
31
|
+
err.value = raw;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
if (host) {
|
|
35
|
+
const key = host.trim().toLowerCase();
|
|
36
|
+
if (!HOSTS[key]) {
|
|
37
|
+
const err = new Error(`unknown host in --model: ${host}`);
|
|
38
|
+
err.code = 'BAD_MODEL';
|
|
39
|
+
err.value = raw;
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
out.hosts[key] = out.hosts[key] || {};
|
|
43
|
+
out.hosts[key][role] = id;
|
|
44
|
+
} else {
|
|
45
|
+
out.global[role] = id;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 升级时本地模型配置可能因 gitignore 丢失;从已安装宿主子代理回收 model 字段,
|
|
52
|
+
// 避免把用户已配置的模型静默改写回 inherit。只在未重新探测时作为兜底。
|
|
53
|
+
export function readInjectedModels({ target, hosts = [] } = {}) {
|
|
54
|
+
const out = {};
|
|
55
|
+
for (const host of hosts) {
|
|
56
|
+
const h = HOSTS[host];
|
|
57
|
+
if (!h?.agents?.modelField) continue;
|
|
58
|
+
const dir = path.join(target, h.dir, h.agents.dir);
|
|
59
|
+
let names;
|
|
60
|
+
try {
|
|
61
|
+
names = fs.readdirSync(dir);
|
|
62
|
+
} catch {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const values = {};
|
|
66
|
+
for (const name of names) {
|
|
67
|
+
if (!name.endsWith(h.agents.ext)) continue;
|
|
68
|
+
let text;
|
|
69
|
+
try {
|
|
70
|
+
text = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const value = getField(text, h.agents.modelField);
|
|
75
|
+
if (!value || value === h.agents.inheritValue) continue;
|
|
76
|
+
const role = modelRoleForAgent(name);
|
|
77
|
+
if (!values[role]) values[role] = value;
|
|
78
|
+
}
|
|
79
|
+
if (Object.keys(values).length) out[host] = values;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function loadRoleConfig(target) {
|
|
85
|
+
for (const file of [
|
|
86
|
+
path.join(target, '.agents/config/model-roles.json'),
|
|
87
|
+
path.join(KIT_ROOT, 'templates/engine/.agents/config/model-roles.json'),
|
|
88
|
+
]) {
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
91
|
+
} catch {
|
|
92
|
+
/* try next */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { roles: { review: { heterogeneous: true } } };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function readLocalModelRoles(target) {
|
|
99
|
+
const file = path.join(target, LOCAL_REL);
|
|
100
|
+
if (!fs.existsSync(file)) return { exists: false, file, roles: {}, hosts: {} };
|
|
101
|
+
try {
|
|
102
|
+
const json = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
103
|
+
return { exists: true, file, roles: json.roles || {}, hosts: json.hosts || {}, raw: json };
|
|
104
|
+
} catch {
|
|
105
|
+
return { exists: true, file, error: true, roles: {}, hosts: {} };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function roleValue(roles, role) {
|
|
110
|
+
const entry = roles?.[role];
|
|
111
|
+
if (typeof entry === 'string') return entry;
|
|
112
|
+
return entry?.model || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hostRoleValue(hosts, host, role) {
|
|
116
|
+
const entry = hosts?.[host];
|
|
117
|
+
if (!entry) return null;
|
|
118
|
+
return typeof entry[role] === 'string' ? entry[role] : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const ROLE_HINTS = {
|
|
122
|
+
code: [/haiku/i, /flash/i, /mini/i, /small/i, /coder/i, /sonnet/i, /qwen/i],
|
|
123
|
+
judgment: [/opus/i, /o3/i, /r1/i, /reason/i, /thinking/i, /pro/i, /max/i, /glm-5/i, /deepseek/i],
|
|
124
|
+
review: [/opus/i, /pro/i, /max/i, /r1/i, /thinking/i, /glm-5/i, /deepseek/i],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function scoreModel(id, role) {
|
|
128
|
+
const s = String(id);
|
|
129
|
+
return (ROLE_HINTS[role] || []).reduce((score, re) => score + (re.test(s) ? 1 : 0), 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function unique(list) {
|
|
133
|
+
return [...new Set(list.filter(Boolean))];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 按角色推荐:先探测值,再用已核实的内置别名兜底。
|
|
137
|
+
export function recommendRoles({ host, candidates = [], roleConfig = null } = {}) {
|
|
138
|
+
const detected = candidates.map((c) => (typeof c === 'string' ? c : c.id));
|
|
139
|
+
const out = {};
|
|
140
|
+
for (const role of ROLE_NAMES) {
|
|
141
|
+
const pool = unique([...detected, ...knownFor(host, role)]);
|
|
142
|
+
let best = null;
|
|
143
|
+
let bestScore = 0;
|
|
144
|
+
for (const id of pool) {
|
|
145
|
+
const score = scoreModel(id, role);
|
|
146
|
+
if (score > bestScore) {
|
|
147
|
+
best = id;
|
|
148
|
+
bestScore = score;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
out[role] = best || pool[0] || roleConfig?.roles?.[role]?.model || null;
|
|
152
|
+
}
|
|
153
|
+
if (out.review && out.code && out.review === out.code) {
|
|
154
|
+
out.review = unique([...detected, ...knownFor(host, 'review')]).find((id) => id !== out.code) || null;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function resolveModels({
|
|
160
|
+
hosts,
|
|
161
|
+
target = process.cwd(),
|
|
162
|
+
cli = { global: {}, hosts: {} },
|
|
163
|
+
tui = {},
|
|
164
|
+
previous = {},
|
|
165
|
+
mode = 'recommend',
|
|
166
|
+
probe = true,
|
|
167
|
+
allowCli = false,
|
|
168
|
+
home,
|
|
169
|
+
env,
|
|
170
|
+
exec,
|
|
171
|
+
roleConfig = null,
|
|
172
|
+
} = {}) {
|
|
173
|
+
const config = roleConfig || loadRoleConfig(target);
|
|
174
|
+
const local = readLocalModelRoles(target);
|
|
175
|
+
const warnings = [];
|
|
176
|
+
const perHost = {};
|
|
177
|
+
const candidates = {};
|
|
178
|
+
const sources = new Set();
|
|
179
|
+
|
|
180
|
+
for (const host of hosts) {
|
|
181
|
+
const h = HOSTS[host];
|
|
182
|
+
if (!h) continue;
|
|
183
|
+
|
|
184
|
+
const found = probe && mode !== 'inherit'
|
|
185
|
+
? probeModels({ host, target, home, env, allowCli, exec })
|
|
186
|
+
: [];
|
|
187
|
+
candidates[host] = found;
|
|
188
|
+
const recommended = recommendRoles({ host, candidates: found, roleConfig: config });
|
|
189
|
+
const values = {};
|
|
190
|
+
|
|
191
|
+
for (const role of ROLE_NAMES) {
|
|
192
|
+
const chain = [];
|
|
193
|
+
const cliHost = hostRoleValue(cli.hosts, host, role);
|
|
194
|
+
const cliGlobal = cli.global?.[role] || null;
|
|
195
|
+
const tuiValue = hostRoleValue(tui, host, role);
|
|
196
|
+
if (tuiValue === '__none__') {
|
|
197
|
+
values[role] = null;
|
|
198
|
+
sources.add('tui');
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (cliHost) chain.push([cliHost, 'cli']);
|
|
202
|
+
if (cliGlobal) chain.push([cliGlobal, 'cli']);
|
|
203
|
+
if (tuiValue) chain.push([tuiValue, 'tui']);
|
|
204
|
+
if (mode !== 'inherit') {
|
|
205
|
+
const localHost = hostRoleValue(local.hosts, host, role);
|
|
206
|
+
const localGlobal = roleValue(local.roles, role);
|
|
207
|
+
if (localHost) chain.push([localHost, 'local']);
|
|
208
|
+
if (localGlobal) chain.push([localGlobal, 'local']);
|
|
209
|
+
// 未重新探测时,保留已安装子代理的模型;显式探测/推荐优先。
|
|
210
|
+
const previousHost = !probe ? hostRoleValue(previous, host, role) : null;
|
|
211
|
+
if (previousHost) chain.push([previousHost, 'previous']);
|
|
212
|
+
if (probe && recommended[role]) chain.push([recommended[role], 'detected']);
|
|
213
|
+
}
|
|
214
|
+
if (chain.length) {
|
|
215
|
+
values[role] = chain[0][0];
|
|
216
|
+
sources.add(chain[0][1]);
|
|
217
|
+
} else {
|
|
218
|
+
values[role] = h.agents?.inheritValue || null;
|
|
219
|
+
if (values[role]) sources.add('inherit');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 独立复核默认要求异构;显式指定不覆盖,只告警。
|
|
224
|
+
// inherit / null 不是具体模型,不能拿来做异构比较(否则会把 inherit 改成推荐模型)。
|
|
225
|
+
const concrete = (v) => typeof v === 'string' && v !== 'inherit';
|
|
226
|
+
if (mode !== 'inherit'
|
|
227
|
+
&& concrete(values.review) && concrete(values.code) && values.review === values.code
|
|
228
|
+
&& config?.roles?.review?.heterogeneous !== false) {
|
|
229
|
+
// CLI / TUI / 本地配置都是用户显式选择:同模型只告警,不擅自改写。
|
|
230
|
+
const explicit = Boolean(
|
|
231
|
+
hostRoleValue(cli.hosts, host, 'review') || cli.global?.review
|
|
232
|
+
|| hostRoleValue(tui, host, 'review')
|
|
233
|
+
|| hostRoleValue(local.hosts, host, 'review') || roleValue(local.roles, 'review'),
|
|
234
|
+
);
|
|
235
|
+
if (!explicit) {
|
|
236
|
+
const alt = unique([...found.map((c) => c.id), ...knownFor(host, 'review')]).find((id) => id !== values.code);
|
|
237
|
+
if (alt) {
|
|
238
|
+
values.review = alt;
|
|
239
|
+
sources.add('detected');
|
|
240
|
+
} else if (h.agents?.inheritValue) {
|
|
241
|
+
values.review = h.agents.inheritValue;
|
|
242
|
+
warnings.push({ host, role: 'review', code: 'REVIEW_SAME_AS_CODE' });
|
|
243
|
+
} else {
|
|
244
|
+
values.review = null;
|
|
245
|
+
warnings.push({ host, role: 'review', code: 'REVIEW_SAME_AS_CODE' });
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
warnings.push({ host, role: 'review', code: 'REVIEW_SAME_AS_CODE' });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
perHost[host] = values;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const roles = {};
|
|
256
|
+
for (const role of ROLE_NAMES) {
|
|
257
|
+
// 角色摘要不依赖宿主顺序:取第一个真正解析出值的宿主。
|
|
258
|
+
const hostValue = hosts.map((host) => perHost[host]?.[role]).find(Boolean) || null;
|
|
259
|
+
const value = cli.global?.[role]
|
|
260
|
+
|| roleValue(local.roles, role)
|
|
261
|
+
|| hostValue
|
|
262
|
+
|| null;
|
|
263
|
+
roles[role] = { model: value };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
roles,
|
|
268
|
+
hosts: perHost,
|
|
269
|
+
source: [...sources].join('+') || 'inherit',
|
|
270
|
+
warnings,
|
|
271
|
+
candidates,
|
|
272
|
+
local,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function summarizeModels(models) {
|
|
277
|
+
if (!models?.hosts) return '-';
|
|
278
|
+
return Object.entries(models.hosts)
|
|
279
|
+
.map(([host, roles]) => `${host}: ${ROLE_NAMES.map((r) => `${r}=${roles[r] || 'inherit'}`).join(' ')}`)
|
|
280
|
+
.join('; ');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function writeLocalModelRoles(target, models, { overwrite = false } = {}) {
|
|
284
|
+
if (!models) return { written: false, reason: 'no-models' };
|
|
285
|
+
const file = path.join(target, LOCAL_REL);
|
|
286
|
+
if (fs.existsSync(file) && !overwrite) return { written: false, file, reason: 'exists' };
|
|
287
|
+
|
|
288
|
+
const roles = {};
|
|
289
|
+
for (const role of ROLE_NAMES) roles[role] = { model: models.roles?.[role]?.model || null };
|
|
290
|
+
if (!ROLE_NAMES.some((role) => roles[role].model)) {
|
|
291
|
+
return { written: false, file, reason: 'empty' };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let existing = {};
|
|
295
|
+
if (fs.existsSync(file)) {
|
|
296
|
+
try {
|
|
297
|
+
existing = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
298
|
+
} catch {
|
|
299
|
+
existing = {};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const data = {
|
|
304
|
+
version: 1,
|
|
305
|
+
...existing,
|
|
306
|
+
roles: { ...(existing.roles || {}), ...roles },
|
|
307
|
+
hosts: { ...(existing.hosts || {}), ...(models.hosts || {}) },
|
|
308
|
+
};
|
|
309
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
310
|
+
fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
311
|
+
return { written: true, file };
|
|
312
|
+
}
|
package/src/render/tokens.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { HOSTS } from '../hosts/registry.mjs';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
export function buildVars({ target, projectName, stack, language, hosts, kitVersion, installedAt, trackHosts = false }) {
|
|
5
|
-
const stackDefaults = {
|
|
3
|
+
const STACK_DEFAULTS = {
|
|
6
4
|
dotnet: {
|
|
7
5
|
description: 'ASP.NET Core 后端 + Vue 3 前端 + SQL Server。',
|
|
8
6
|
descriptionEn: 'ASP.NET Core backend + Vue 3 frontend + SQL Server.',
|
|
@@ -10,20 +8,66 @@ export function buildVars({ target, projectName, stack, language, hosts, kitVers
|
|
|
10
8
|
webPort: 5203,
|
|
11
9
|
},
|
|
12
10
|
node: {
|
|
13
|
-
description: 'Node.js
|
|
14
|
-
descriptionEn: 'A Node.js
|
|
11
|
+
description: 'Node.js 项目。',
|
|
12
|
+
descriptionEn: 'A Node.js project.',
|
|
13
|
+
apiPort: 3000,
|
|
14
|
+
webPort: 5173,
|
|
15
|
+
},
|
|
16
|
+
typescript: {
|
|
17
|
+
description: 'TypeScript 项目。',
|
|
18
|
+
descriptionEn: 'A TypeScript project.',
|
|
15
19
|
apiPort: 3000,
|
|
16
20
|
webPort: 5173,
|
|
17
21
|
},
|
|
22
|
+
rust: {
|
|
23
|
+
description: 'Rust 项目(Cargo)。',
|
|
24
|
+
descriptionEn: 'A Rust project (Cargo).',
|
|
25
|
+
apiPort: 8080,
|
|
26
|
+
webPort: 5173,
|
|
27
|
+
},
|
|
28
|
+
go: {
|
|
29
|
+
description: 'Go 项目(Go modules)。',
|
|
30
|
+
descriptionEn: 'A Go project (Go modules).',
|
|
31
|
+
apiPort: 8080,
|
|
32
|
+
webPort: 5173,
|
|
33
|
+
},
|
|
34
|
+
java: {
|
|
35
|
+
description: 'Java 项目(Maven 或 Gradle)。',
|
|
36
|
+
descriptionEn: 'A Java project (Maven or Gradle).',
|
|
37
|
+
apiPort: 8080,
|
|
38
|
+
webPort: 5173,
|
|
39
|
+
},
|
|
18
40
|
empty: {
|
|
19
41
|
description: '项目技术栈按实际填写。',
|
|
20
42
|
descriptionEn: 'Fill in the project stack description.',
|
|
21
43
|
apiPort: 3000,
|
|
22
44
|
webPort: 5173,
|
|
23
45
|
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function stackDescription(stack, language) {
|
|
49
|
+
const d = STACK_DEFAULTS[stack] || STACK_DEFAULTS.empty;
|
|
50
|
+
return language === 'en' ? d.descriptionEn : d.description;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 宿主权限白名单里随栈变化的命令。npm / node / git 是引擎运行时,留在各宿主模板的固定项里。
|
|
54
|
+
const STACK_SHELL = {
|
|
55
|
+
dotnet: ['dotnet build', 'dotnet run'],
|
|
56
|
+
rust: ['cargo build', 'cargo run', 'cargo test', 'cargo check'],
|
|
57
|
+
go: ['go build', 'go run', 'go test'],
|
|
58
|
+
java: ['mvn compile', 'mvn test', 'mvn -q test', 'gradle test', 'gradlew test', 'gradlew.bat test', './gradlew test'],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function stackAllowFragment(stack, tool, sep) {
|
|
62
|
+
const commands = STACK_SHELL[stack] || [];
|
|
63
|
+
if (!commands.length) return '';
|
|
64
|
+
return commands.map((cmd) => ` "${tool}(${cmd}${sep}*)",`).join('\n') + '\n';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 模板变量:只替换已知 token,未知 {{...}} 原样保留(避免误伤代码模板字符串)。
|
|
68
|
+
export function buildVars({ target, projectName, stack, language, hosts, kitVersion, installedAt, trackHosts = false }) {
|
|
69
|
+
const d = STACK_DEFAULTS[stack] || STACK_DEFAULTS.empty;
|
|
70
|
+
const description = stackDescription(stack, language);
|
|
27
71
|
const hostIgnores = trackHosts
|
|
28
72
|
? ''
|
|
29
73
|
: hosts
|
|
@@ -33,13 +77,16 @@ export function buildVars({ target, projectName, stack, language, hosts, kitVers
|
|
|
33
77
|
return {
|
|
34
78
|
projectName: projectName || pathBase(target),
|
|
35
79
|
stack,
|
|
36
|
-
stackDescription,
|
|
80
|
+
stackDescription: description,
|
|
37
81
|
language,
|
|
38
82
|
apiPort: String(d.apiPort),
|
|
39
83
|
webPort: String(d.webPort),
|
|
40
84
|
boardPort: '8933',
|
|
41
85
|
hosts: hosts.join(', '),
|
|
42
86
|
hostsJson: JSON.stringify(hosts),
|
|
87
|
+
stackAllowBashColon: stackAllowFragment(stack, 'Bash', ':'),
|
|
88
|
+
stackAllowBashSpace: stackAllowFragment(stack, 'Bash', ' '),
|
|
89
|
+
stackAllowShellSpace: stackAllowFragment(stack, 'Shell', ' '),
|
|
43
90
|
kitVersion,
|
|
44
91
|
installedAt,
|
|
45
92
|
nodeMinimum: '22',
|
package/src/tui/ansi.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// 零依赖 ANSI 渲染原语:转义序列、可视宽度、着色、进度条与圆角边框。
|
|
2
|
+
const ESC = '\x1b'; // 转义起始符(与 tui 其余文件的 \x1b 字面量写法统一)
|
|
3
|
+
// CSI 序列(ESC[ ... 字母)与 OSC 序列(ESC] ... BEL/ST)
|
|
4
|
+
const ANSI_RE = new RegExp(ESC + '\\[[0-9;]*[A-Za-z]|' + ESC + '\\][^\\x07]*(?:\\x07|' + ESC + '\\\\)', 'g');
|
|
5
|
+
|
|
6
|
+
// 移除 ANSI 转义序列
|
|
7
|
+
export function stripAnsi(str) {
|
|
8
|
+
return str.replace(ANSI_RE, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 东亚宽字符区间(CJK/全角),可视宽度按 2 计
|
|
12
|
+
const WIDE_RE = /[\u1100-\u115F\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6]/;
|
|
13
|
+
|
|
14
|
+
export function visualWidth(str) {
|
|
15
|
+
let width = 0;
|
|
16
|
+
for (const ch of stripAnsi(str)) width += WIDE_RE.test(ch) ? 2 : 1;
|
|
17
|
+
return width;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const paint = (code, reset) => (str) => ESC + code + str + ESC + reset;
|
|
21
|
+
|
|
22
|
+
export const cyan = paint('[36m', '[39m');
|
|
23
|
+
export const dim = paint('[2m', '[22m');
|
|
24
|
+
export const green = paint('[32m', '[39m');
|
|
25
|
+
export const red = paint('[31m', '[39m');
|
|
26
|
+
|
|
27
|
+
export const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
28
|
+
|
|
29
|
+
// 第 n 帧 spinner(按帧表长度自动回绕)
|
|
30
|
+
export const spinner = (n) => SPINNER_FRAMES[n % SPINNER_FRAMES.length];
|
|
31
|
+
|
|
32
|
+
// 单行进度:回车 + 清行 + spinner + 进度条(TUI 逐文件进度渲染用)。detail 是当前文件,随下一帧被清掉。
|
|
33
|
+
export function progressLine(done, total, detail = '') {
|
|
34
|
+
const tail = detail ? ` ${detail}` : '';
|
|
35
|
+
return `\r\x1b[2K${spinner(done)} ${progress(done, total)}${tail}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function progress(current, total, width = 24) {
|
|
39
|
+
// total=0 时 filled=0,避免除零
|
|
40
|
+
const filled = total > 0 ? Math.round((width * current) / total) : 0;
|
|
41
|
+
return '█'.repeat(filled) + '░'.repeat(width - filled) + ` ${current}/${total}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 圆角边框盒:对齐按 stripAnsi 后的可视宽度计算,正文含 CJK/色码不歪
|
|
45
|
+
export function box(title, bodyLines) {
|
|
46
|
+
const lines = bodyLines.map(String);
|
|
47
|
+
const titleW = visualWidth(title);
|
|
48
|
+
const inner = Math.max(titleW + 1, ...lines.map((l) => visualWidth(l)));
|
|
49
|
+
const out = [`╭─ ${title} ${'─'.repeat(inner - titleW - 1)}╮`];
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
out.push(`│ ${line}${' '.repeat(inner - visualWidth(line))} │`);
|
|
52
|
+
}
|
|
53
|
+
out.push(`╰${'─'.repeat(inner + 2)}╯`);
|
|
54
|
+
return out;
|
|
55
|
+
}
|