@code4bug/jarvis-agent 1.1.7 → 1.2.1
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/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +416 -64
- package/dist/components/MarkdownText.d.ts +4 -0
- package/dist/components/MarkdownText.js +10 -3
- package/dist/components/MessageItem.js +4 -1
- package/dist/components/StatusBar.js +9 -6
- package/dist/components/WelcomeHeader.js +4 -2
- package/dist/components/setup/SetupConfirmStep.d.ts +8 -0
- package/dist/components/setup/SetupConfirmStep.js +12 -0
- package/dist/components/setup/SetupDoneStep.d.ts +7 -0
- package/dist/components/setup/SetupDoneStep.js +5 -0
- package/dist/components/setup/SetupFormStep.d.ts +11 -0
- package/dist/components/setup/SetupFormStep.js +44 -0
- package/dist/components/setup/SetupHeader.d.ts +9 -0
- package/dist/components/setup/SetupHeader.js +25 -0
- package/dist/components/setup/SetupProviderStep.d.ts +6 -0
- package/dist/components/setup/SetupProviderStep.js +20 -0
- package/dist/components/setup/SetupWelcomeStep.d.ts +5 -0
- package/dist/components/setup/SetupWelcomeStep.js +5 -0
- package/dist/config/bootstrap.d.ts +38 -0
- package/dist/config/bootstrap.js +155 -0
- package/dist/config/constants.d.ts +7 -6
- package/dist/config/constants.js +29 -16
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/loader.js +4 -0
- package/dist/core/hint.js +3 -3
- package/dist/core/query.js +3 -2
- package/dist/hooks/useDoubleCtrlCExit.js +32 -15
- package/dist/index.js +2 -2
- package/dist/screens/AppBootstrap.d.ts +1 -0
- package/dist/screens/AppBootstrap.js +14 -0
- package/dist/screens/repl.js +6 -2
- package/dist/screens/setup/SetupWizard.d.ts +7 -0
- package/dist/screens/setup/SetupWizard.js +198 -0
- package/dist/screens/slashCommands.d.ts +1 -1
- package/dist/screens/slashCommands.js +2 -2
- package/dist/services/api/llm.js +2 -2
- package/dist/tools/createSkill.js +59 -1
- package/dist/tools/readFile.js +28 -3
- package/package.json +1 -1
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { execSync } from 'child_process';
|
|
10
|
-
import {
|
|
10
|
+
import { APP_VERSION, getAppName } from '../config/constants.js';
|
|
11
|
+
import { LLMServiceImpl, getDefaultConfig } from '../services/api/llm.js';
|
|
12
|
+
import { allTools } from '../tools/index.js';
|
|
13
|
+
import { loadAllAgents } from '../agents/index.js';
|
|
11
14
|
// ===== 辅助函数 =====
|
|
12
15
|
/** 安全执行命令 */
|
|
13
16
|
function safeExec(cmd) {
|
|
@@ -123,7 +126,393 @@ function countSourceFiles(dir) {
|
|
|
123
126
|
walk(dir);
|
|
124
127
|
return { total, byExt };
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
/** 读取文本文件,超长时截断 */
|
|
130
|
+
function readTextFile(filePath, maxChars = 6000) {
|
|
131
|
+
try {
|
|
132
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
133
|
+
if (content.length <= maxChars)
|
|
134
|
+
return content;
|
|
135
|
+
return `${content.slice(0, maxChars)}\n\n[...已截断,共 ${content.length} 字符]`;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return '';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function shouldIgnoreEntry(name) {
|
|
142
|
+
return name.startsWith('.')
|
|
143
|
+
|| name === 'node_modules'
|
|
144
|
+
|| name === 'dist'
|
|
145
|
+
|| name === 'build'
|
|
146
|
+
|| name === 'target'
|
|
147
|
+
|| name === '__pycache__'
|
|
148
|
+
|| name === '.git';
|
|
149
|
+
}
|
|
150
|
+
function normalizePath(relativePath) {
|
|
151
|
+
return relativePath.split(path.sep).join('/');
|
|
152
|
+
}
|
|
153
|
+
function isTextLikeFile(fileName) {
|
|
154
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
155
|
+
return [
|
|
156
|
+
'.md', '.txt', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.conf',
|
|
157
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
158
|
+
'.py', '.java', '.go', '.rs', '.kt', '.kts',
|
|
159
|
+
'.c', '.cc', '.cpp', '.h', '.hpp',
|
|
160
|
+
'.sh', '.bash', '.zsh', '.sql', '.xml', '.gradle', '.properties',
|
|
161
|
+
].includes(ext) || fileName === 'Dockerfile' || fileName === 'Makefile';
|
|
162
|
+
}
|
|
163
|
+
function collectRootContextFiles(cwd) {
|
|
164
|
+
const preferredPatterns = [
|
|
165
|
+
/^README(\..+)?$/i,
|
|
166
|
+
/^package\.json$/i,
|
|
167
|
+
/^pnpm-lock\.ya?ml$/i,
|
|
168
|
+
/^package-lock\.json$/i,
|
|
169
|
+
/^yarn\.lock$/i,
|
|
170
|
+
/^tsconfig.*\.json$/i,
|
|
171
|
+
/^vite\.config\./i,
|
|
172
|
+
/^webpack\.config\./i,
|
|
173
|
+
/^next\.config\./i,
|
|
174
|
+
/^nuxt\.config\./i,
|
|
175
|
+
/^pom\.xml$/i,
|
|
176
|
+
/^build\.gradle(\.kts)?$/i,
|
|
177
|
+
/^settings\.gradle(\.kts)?$/i,
|
|
178
|
+
/^pyproject\.toml$/i,
|
|
179
|
+
/^requirements.*\.txt$/i,
|
|
180
|
+
/^go\.mod$/i,
|
|
181
|
+
/^Cargo\.toml$/i,
|
|
182
|
+
/^composer\.json$/i,
|
|
183
|
+
/^Gemfile$/i,
|
|
184
|
+
/^Dockerfile$/i,
|
|
185
|
+
/^docker-compose\.ya?ml$/i,
|
|
186
|
+
/^Makefile$/i,
|
|
187
|
+
/^AGENT.*\.md$/i,
|
|
188
|
+
/^SKILL.*\.md$/i,
|
|
189
|
+
];
|
|
190
|
+
try {
|
|
191
|
+
const entries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
192
|
+
return entries
|
|
193
|
+
.filter((entry) => entry.isFile() && !shouldIgnoreEntry(entry.name))
|
|
194
|
+
.map((entry) => entry.name)
|
|
195
|
+
.filter((name) => preferredPatterns.some((pattern) => pattern.test(name)))
|
|
196
|
+
.sort((a, b) => a.localeCompare(b))
|
|
197
|
+
.slice(0, 12);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function chooseRepresentativeFiles(dirPath, relativeDir, maxFiles = 3) {
|
|
204
|
+
const preferredNames = [
|
|
205
|
+
'index', 'main', 'app', 'cli', 'server', 'client', 'api',
|
|
206
|
+
'router', 'routes', 'controller', 'service', 'model',
|
|
207
|
+
'commands', 'tools', 'query', 'engine',
|
|
208
|
+
];
|
|
209
|
+
try {
|
|
210
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
211
|
+
.filter((entry) => entry.isFile() && !shouldIgnoreEntry(entry.name) && isTextLikeFile(entry.name))
|
|
212
|
+
.sort((a, b) => {
|
|
213
|
+
const aBase = path.parse(a.name).name.toLowerCase();
|
|
214
|
+
const bBase = path.parse(b.name).name.toLowerCase();
|
|
215
|
+
const aScore = preferredNames.findIndex((name) => name === aBase);
|
|
216
|
+
const bScore = preferredNames.findIndex((name) => name === bBase);
|
|
217
|
+
const normalizedAScore = aScore === -1 ? preferredNames.length : aScore;
|
|
218
|
+
const normalizedBScore = bScore === -1 ? preferredNames.length : bScore;
|
|
219
|
+
if (normalizedAScore !== normalizedBScore)
|
|
220
|
+
return normalizedAScore - normalizedBScore;
|
|
221
|
+
return a.name.localeCompare(b.name);
|
|
222
|
+
});
|
|
223
|
+
return entries
|
|
224
|
+
.slice(0, maxFiles)
|
|
225
|
+
.map((entry) => normalizePath(path.join(relativeDir, entry.name)));
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function collectSourceContextFiles(cwd) {
|
|
232
|
+
const candidateDirs = ['src', 'app', 'lib', 'cmd', 'internal', 'pkg', 'server', 'client', 'backend', 'frontend'];
|
|
233
|
+
const result = [];
|
|
234
|
+
for (const dirName of candidateDirs) {
|
|
235
|
+
const dirPath = path.join(cwd, dirName);
|
|
236
|
+
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory())
|
|
237
|
+
continue;
|
|
238
|
+
result.push(...chooseRepresentativeFiles(dirPath, dirName, 3));
|
|
239
|
+
try {
|
|
240
|
+
const subDirs = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
241
|
+
.filter((entry) => entry.isDirectory() && !shouldIgnoreEntry(entry.name))
|
|
242
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
243
|
+
.slice(0, 4);
|
|
244
|
+
for (const subDir of subDirs) {
|
|
245
|
+
result.push(...chooseRepresentativeFiles(path.join(dirPath, subDir.name), path.join(dirName, subDir.name), 2));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// ignore
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return Array.from(new Set(result)).slice(0, 18);
|
|
253
|
+
}
|
|
254
|
+
/** 采样项目关键文件,提供给大模型做总结 */
|
|
255
|
+
function collectProjectContext(cwd) {
|
|
256
|
+
const candidates = [
|
|
257
|
+
...collectRootContextFiles(cwd),
|
|
258
|
+
...collectSourceContextFiles(cwd),
|
|
259
|
+
];
|
|
260
|
+
const parts = [];
|
|
261
|
+
for (const relativePath of candidates) {
|
|
262
|
+
const fullPath = path.join(cwd, relativePath);
|
|
263
|
+
if (!fs.existsSync(fullPath))
|
|
264
|
+
continue;
|
|
265
|
+
const content = readTextFile(fullPath);
|
|
266
|
+
if (!content)
|
|
267
|
+
continue;
|
|
268
|
+
parts.push(`## 文件: ${normalizePath(relativePath)}\n\n\`\`\`\n${content}\n\`\`\``);
|
|
269
|
+
}
|
|
270
|
+
return parts.join('\n\n');
|
|
271
|
+
}
|
|
272
|
+
function stripMarkdownCodeFence(text) {
|
|
273
|
+
const trimmed = text.trim();
|
|
274
|
+
const match = trimmed.match(/^```(?:md|markdown)?\s*([\s\S]*?)\s*```$/i);
|
|
275
|
+
return match ? match[1].trim() : trimmed;
|
|
276
|
+
}
|
|
277
|
+
function stripJsonCodeFence(text) {
|
|
278
|
+
const trimmed = text.trim();
|
|
279
|
+
const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
280
|
+
return match ? match[1].trim() : trimmed;
|
|
281
|
+
}
|
|
282
|
+
function listTopLevelDirectories(cwd) {
|
|
283
|
+
try {
|
|
284
|
+
return fs.readdirSync(cwd, { withFileTypes: true })
|
|
285
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' && entry.name !== 'dist')
|
|
286
|
+
.map((entry) => entry.name)
|
|
287
|
+
.sort((a, b) => a.localeCompare(b));
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function buildRealCommandSuggestions(cwd, pkg) {
|
|
294
|
+
const commands = [];
|
|
295
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) {
|
|
296
|
+
commands.push('pnpm install');
|
|
297
|
+
}
|
|
298
|
+
else if (fs.existsSync(path.join(cwd, 'package-lock.json'))) {
|
|
299
|
+
commands.push('npm install');
|
|
300
|
+
}
|
|
301
|
+
if (pkg?.scripts?.dev)
|
|
302
|
+
commands.push('npm run dev');
|
|
303
|
+
if (pkg?.scripts?.start)
|
|
304
|
+
commands.push('npm run start');
|
|
305
|
+
if (pkg?.scripts?.build)
|
|
306
|
+
commands.push('npm run build');
|
|
307
|
+
if (pkg?.scripts?.test)
|
|
308
|
+
commands.push('npm test');
|
|
309
|
+
return commands;
|
|
310
|
+
}
|
|
311
|
+
function renderJarvisMd(input) {
|
|
312
|
+
const { projectName, packageJson: pkg, projectTypes, gitInfo, dirTree, devCommands, summary } = input;
|
|
313
|
+
const md = [];
|
|
314
|
+
md.push(`# ${pkg?.name || projectName}`);
|
|
315
|
+
md.push('');
|
|
316
|
+
md.push('## 项目概览');
|
|
317
|
+
md.push('');
|
|
318
|
+
md.push(summary.overview || '待补充');
|
|
319
|
+
md.push('');
|
|
320
|
+
md.push('## 技术栈');
|
|
321
|
+
md.push('');
|
|
322
|
+
md.push(`- 项目类型:${projectTypes.join(', ')}`);
|
|
323
|
+
md.push(`- 运行时:${pkg?.type || '未发现'}`);
|
|
324
|
+
md.push(`- 版本:${pkg?.version || '未发现'}`);
|
|
325
|
+
md.push(`- Git 分支:${gitInfo?.branch || '未发现'}`);
|
|
326
|
+
md.push('');
|
|
327
|
+
md.push('## 目录与模块');
|
|
328
|
+
md.push('');
|
|
329
|
+
for (const line of summary.architecture) {
|
|
330
|
+
md.push(`- ${line}`);
|
|
331
|
+
}
|
|
332
|
+
md.push('');
|
|
333
|
+
md.push('```');
|
|
334
|
+
md.push(`${projectName}/`);
|
|
335
|
+
md.push(...dirTree);
|
|
336
|
+
md.push('```');
|
|
337
|
+
md.push('');
|
|
338
|
+
md.push('## 当前能力');
|
|
339
|
+
md.push('');
|
|
340
|
+
for (const line of summary.capabilities) {
|
|
341
|
+
md.push(`- ${line}`);
|
|
342
|
+
}
|
|
343
|
+
md.push('');
|
|
344
|
+
md.push('## 开发与构建');
|
|
345
|
+
md.push('');
|
|
346
|
+
if (devCommands.length > 0) {
|
|
347
|
+
md.push('```bash');
|
|
348
|
+
md.push(...devCommands);
|
|
349
|
+
md.push('```');
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
md.push('待补充');
|
|
353
|
+
}
|
|
354
|
+
md.push('');
|
|
355
|
+
md.push('## 协作约定');
|
|
356
|
+
md.push('');
|
|
357
|
+
for (const line of summary.collaborationNotes) {
|
|
358
|
+
md.push(`- ${line}`);
|
|
359
|
+
}
|
|
360
|
+
md.push('');
|
|
361
|
+
md.push(`> 由 ${getAppName()} /init 自动生成`);
|
|
362
|
+
md.push('');
|
|
363
|
+
return md.join('\n');
|
|
364
|
+
}
|
|
365
|
+
async function generateJarvisMdWithLLM(input) {
|
|
366
|
+
const service = new LLMServiceImpl({
|
|
367
|
+
...getDefaultConfig(),
|
|
368
|
+
});
|
|
369
|
+
const projectContext = collectProjectContext(input.cwd);
|
|
370
|
+
const topLevelDirectories = listTopLevelDirectories(input.cwd);
|
|
371
|
+
const devCommands = buildRealCommandSuggestions(input.cwd, input.packageJson);
|
|
372
|
+
const toolNames = allTools.map((tool) => tool.name);
|
|
373
|
+
const agentNames = Array.from(loadAllAgents().values()).map((agent) => agent.meta.name);
|
|
374
|
+
const extStats = Object.entries(input.fileStats.byExt)
|
|
375
|
+
.sort((a, b) => b[1] - a[1])
|
|
376
|
+
.map(([ext, count]) => `${ext}: ${count}`)
|
|
377
|
+
.join(', ');
|
|
378
|
+
const prompt = [
|
|
379
|
+
'你是项目初始化助手。请基于提供的真实项目信息,为 JARVIS.md 先生成结构化摘要。',
|
|
380
|
+
'',
|
|
381
|
+
'要求:',
|
|
382
|
+
'1. 只能依据给定信息总结,不要编造不存在的模块、流程、命令、目录或能力',
|
|
383
|
+
'2. 全文使用中文',
|
|
384
|
+
'3. 必须只输出合法 JSON,不要附加解释,不要输出 Markdown',
|
|
385
|
+
'4. 内容要有总结性,但必须可被事实支撑',
|
|
386
|
+
'5. 如果信息不足,明确写“待补充”或“未发现”,不要猜测',
|
|
387
|
+
'6. 禁止使用 emoji、营销文案、夸张措辞',
|
|
388
|
+
'',
|
|
389
|
+
'JSON 结构如下:',
|
|
390
|
+
'{',
|
|
391
|
+
' "overview": "1 段中文概述,80-160 字",',
|
|
392
|
+
' "architecture": ["3 到 6 条,每条一句,描述目录或模块职责"],',
|
|
393
|
+
' "capabilities": ["4 到 8 条,每条一句,描述当前已实现能力"],',
|
|
394
|
+
' "collaborationNotes": ["3 到 6 条,每条一句,描述开发协作约定或注意事项"]',
|
|
395
|
+
'}',
|
|
396
|
+
'',
|
|
397
|
+
'以下是项目事实:',
|
|
398
|
+
`- 项目名称: ${input.packageJson?.name || input.projectName}`,
|
|
399
|
+
`- 项目目录名: ${input.projectName}`,
|
|
400
|
+
`- 项目版本: ${input.packageJson?.version || '未发现'}`,
|
|
401
|
+
`- 项目描述: ${input.packageJson?.description || '未发现'}`,
|
|
402
|
+
`- 项目类型: ${input.projectTypes.join(', ')}`,
|
|
403
|
+
`- Git 分支: ${input.gitInfo?.branch || '未发现'}`,
|
|
404
|
+
`- Git 远程: ${input.gitInfo?.remote || '未发现'}`,
|
|
405
|
+
`- 最近提交: ${input.gitInfo?.lastCommit || '未发现'}`,
|
|
406
|
+
`- 源文件总数: ${input.fileStats.total}`,
|
|
407
|
+
`- 扩展名统计: ${extStats || '未发现'}`,
|
|
408
|
+
`- 顶层目录: ${topLevelDirectories.join(', ') || '未发现'}`,
|
|
409
|
+
`- 内置工具: ${toolNames.join(', ') || '未发现'}`,
|
|
410
|
+
`- 内置智能体: ${agentNames.join(', ') || '未发现'}`,
|
|
411
|
+
`- 可确认开发命令: ${devCommands.join(' | ') || '未发现'}`,
|
|
412
|
+
'',
|
|
413
|
+
'目录树(浅层):',
|
|
414
|
+
`${input.projectName}/`,
|
|
415
|
+
...input.dirTree,
|
|
416
|
+
'',
|
|
417
|
+
'关键文件内容:',
|
|
418
|
+
projectContext || '(未读取到关键文件)',
|
|
419
|
+
].join('\n');
|
|
420
|
+
let result = '';
|
|
421
|
+
const transcript = [
|
|
422
|
+
{ role: 'user', content: prompt },
|
|
423
|
+
];
|
|
424
|
+
await new Promise((resolve, reject) => {
|
|
425
|
+
service.streamMessage(transcript, [], {
|
|
426
|
+
onText: (text) => { result += text; },
|
|
427
|
+
onToolUse: () => { },
|
|
428
|
+
onComplete: () => resolve(),
|
|
429
|
+
onError: (error) => reject(error),
|
|
430
|
+
}, undefined, { includeUserProfile: false }).catch(reject);
|
|
431
|
+
});
|
|
432
|
+
const cleaned = stripJsonCodeFence(stripMarkdownCodeFence(result));
|
|
433
|
+
if (!cleaned) {
|
|
434
|
+
throw new Error('大模型未返回有效内容');
|
|
435
|
+
}
|
|
436
|
+
let summary;
|
|
437
|
+
try {
|
|
438
|
+
const parsed = JSON.parse(cleaned);
|
|
439
|
+
summary = {
|
|
440
|
+
overview: String(parsed.overview || '').trim() || '待补充',
|
|
441
|
+
architecture: Array.isArray(parsed.architecture) ? parsed.architecture.map((item) => String(item).trim()).filter(Boolean) : [],
|
|
442
|
+
capabilities: Array.isArray(parsed.capabilities) ? parsed.capabilities.map((item) => String(item).trim()).filter(Boolean) : [],
|
|
443
|
+
collaborationNotes: Array.isArray(parsed.collaborationNotes) ? parsed.collaborationNotes.map((item) => String(item).trim()).filter(Boolean) : [],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
throw new Error(`解析大模型总结失败: ${error.message}`);
|
|
448
|
+
}
|
|
449
|
+
if (summary.architecture.length === 0)
|
|
450
|
+
summary.architecture = ['待补充'];
|
|
451
|
+
if (summary.capabilities.length === 0)
|
|
452
|
+
summary.capabilities = ['待补充'];
|
|
453
|
+
if (summary.collaborationNotes.length === 0)
|
|
454
|
+
summary.collaborationNotes = ['待补充'];
|
|
455
|
+
return renderJarvisMd({
|
|
456
|
+
projectName: input.projectName,
|
|
457
|
+
packageJson: input.packageJson,
|
|
458
|
+
projectTypes: input.projectTypes,
|
|
459
|
+
gitInfo: input.gitInfo,
|
|
460
|
+
dirTree: input.dirTree,
|
|
461
|
+
devCommands,
|
|
462
|
+
summary,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
function generateBasicJarvisMd(input) {
|
|
466
|
+
const { cwd, projectName, packageJson: pkg, projectTypes, gitInfo, dirTree } = input;
|
|
467
|
+
const md = [];
|
|
468
|
+
md.push(`# ${pkg?.name || projectName}`);
|
|
469
|
+
md.push('');
|
|
470
|
+
if (pkg?.description) {
|
|
471
|
+
md.push(pkg.description);
|
|
472
|
+
md.push('');
|
|
473
|
+
}
|
|
474
|
+
md.push('## 项目概览');
|
|
475
|
+
md.push('');
|
|
476
|
+
md.push(`- 项目类型:${projectTypes.join(', ')}`);
|
|
477
|
+
md.push(`- 当前目录:\`${cwd}\``);
|
|
478
|
+
md.push(`- 版本:${pkg?.version || '未发现'}`);
|
|
479
|
+
md.push(`- Git 分支:${gitInfo?.branch || '未发现'}`);
|
|
480
|
+
md.push('');
|
|
481
|
+
md.push('## 目录结构');
|
|
482
|
+
md.push('');
|
|
483
|
+
md.push('```');
|
|
484
|
+
md.push(`${projectName}/`);
|
|
485
|
+
md.push(...dirTree);
|
|
486
|
+
md.push('```');
|
|
487
|
+
md.push('');
|
|
488
|
+
if (pkg?.scripts) {
|
|
489
|
+
md.push('## 开发命令');
|
|
490
|
+
md.push('');
|
|
491
|
+
md.push('```bash');
|
|
492
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) {
|
|
493
|
+
md.push('pnpm install');
|
|
494
|
+
}
|
|
495
|
+
else if (fs.existsSync(path.join(cwd, 'package-lock.json'))) {
|
|
496
|
+
md.push('npm install');
|
|
497
|
+
}
|
|
498
|
+
if (pkg.scripts.dev)
|
|
499
|
+
md.push('npm run dev');
|
|
500
|
+
if (pkg.scripts.build)
|
|
501
|
+
md.push('npm run build');
|
|
502
|
+
if (pkg.scripts.test)
|
|
503
|
+
md.push('npm test');
|
|
504
|
+
md.push('```');
|
|
505
|
+
md.push('');
|
|
506
|
+
}
|
|
507
|
+
md.push('## 协作约定');
|
|
508
|
+
md.push('');
|
|
509
|
+
md.push('- 本文件为自动生成结果;若需更准确的业务背景,请补充 README 或项目文档。');
|
|
510
|
+
md.push('');
|
|
511
|
+
md.push(`> 由 ${getAppName()} /init 自动生成`);
|
|
512
|
+
md.push('');
|
|
513
|
+
return md.join('\n');
|
|
514
|
+
}
|
|
515
|
+
export async function executeInit() {
|
|
127
516
|
const cwd = process.cwd();
|
|
128
517
|
const projectName = path.basename(cwd);
|
|
129
518
|
const pkg = readPackageJson();
|
|
@@ -133,7 +522,7 @@ export function executeInit() {
|
|
|
133
522
|
const dirTree = scanDirectoryTree(cwd);
|
|
134
523
|
// ===== 构建终端显示文本 =====
|
|
135
524
|
const display = [];
|
|
136
|
-
display.push(`${
|
|
525
|
+
display.push(`${getAppName()} ${APP_VERSION} - 项目初始化`);
|
|
137
526
|
display.push('');
|
|
138
527
|
// 项目基本信息
|
|
139
528
|
display.push('[ 项目信息 ]');
|
|
@@ -207,73 +596,36 @@ export function executeInit() {
|
|
|
207
596
|
display.push('');
|
|
208
597
|
}
|
|
209
598
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
md.push(`|------|-----|`);
|
|
224
|
-
md.push(`| 名称 | ${pkg?.name || projectName} |`);
|
|
225
|
-
if (pkg?.version)
|
|
226
|
-
md.push(`| 版本 | ${pkg.version} |`);
|
|
227
|
-
md.push(`| 类型 | ${projectTypes.join(', ')} |`);
|
|
228
|
-
if (gitInfo?.branch)
|
|
229
|
-
md.push(`| Git 分支 | ${gitInfo.branch} |`);
|
|
230
|
-
if (gitInfo?.remote)
|
|
231
|
-
md.push(`| Git 远程 | ${gitInfo.remote} |`);
|
|
232
|
-
md.push('');
|
|
233
|
-
md.push('## 目录结构');
|
|
234
|
-
md.push('');
|
|
235
|
-
md.push('```');
|
|
236
|
-
md.push(`${projectName}/`);
|
|
237
|
-
for (const line of dirTree) {
|
|
238
|
-
md.push(line);
|
|
599
|
+
let jarvisMdContent = '';
|
|
600
|
+
let generationMode = '基础扫描';
|
|
601
|
+
try {
|
|
602
|
+
jarvisMdContent = await generateJarvisMdWithLLM({
|
|
603
|
+
projectName,
|
|
604
|
+
cwd,
|
|
605
|
+
packageJson: pkg,
|
|
606
|
+
projectTypes,
|
|
607
|
+
gitInfo,
|
|
608
|
+
fileStats,
|
|
609
|
+
dirTree,
|
|
610
|
+
});
|
|
611
|
+
generationMode = '大模型总结';
|
|
239
612
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
md.push('npm install');
|
|
250
|
-
md.push('');
|
|
251
|
-
}
|
|
252
|
-
if (pkg.scripts.dev) {
|
|
253
|
-
md.push('# 开发模式');
|
|
254
|
-
md.push(`npm run dev`);
|
|
255
|
-
}
|
|
256
|
-
else if (pkg.scripts.start) {
|
|
257
|
-
md.push('# 启动');
|
|
258
|
-
md.push(`npm run start`);
|
|
259
|
-
}
|
|
260
|
-
if (pkg.scripts.build) {
|
|
261
|
-
md.push('');
|
|
262
|
-
md.push('# 构建');
|
|
263
|
-
md.push('npm run build');
|
|
264
|
-
}
|
|
265
|
-
md.push('```');
|
|
266
|
-
md.push('');
|
|
613
|
+
catch {
|
|
614
|
+
jarvisMdContent = generateBasicJarvisMd({
|
|
615
|
+
cwd,
|
|
616
|
+
projectName,
|
|
617
|
+
packageJson: pkg,
|
|
618
|
+
projectTypes,
|
|
619
|
+
gitInfo,
|
|
620
|
+
dirTree,
|
|
621
|
+
});
|
|
267
622
|
}
|
|
268
|
-
md.push('---');
|
|
269
|
-
md.push('');
|
|
270
|
-
md.push(`> 由 ${APP_NAME} /init 自动生成`);
|
|
271
|
-
md.push('');
|
|
272
|
-
const jarvisMdContent = md.join('\n');
|
|
273
623
|
const jarvisMdPath = path.join(cwd, 'JARVIS.md');
|
|
274
624
|
const isNew = !fs.existsSync(jarvisMdPath);
|
|
275
625
|
// 写入文件
|
|
276
626
|
fs.writeFileSync(jarvisMdPath, jarvisMdContent, 'utf-8');
|
|
627
|
+
display.push(`[ 输出 ]`);
|
|
628
|
+
display.push(` 生成方式: ${generationMode}`);
|
|
277
629
|
display.push(isNew ? '已生成 JARVIS.md' : '已更新 JARVIS.md');
|
|
278
630
|
return {
|
|
279
631
|
displayText: display.join('\n'),
|
|
@@ -3,6 +3,10 @@ import React from 'react';
|
|
|
3
3
|
* Markdown 终端渲染组件
|
|
4
4
|
* 支持表格(动态列宽+自动换行)、代码块(深色背景+边框)、加粗、列表等
|
|
5
5
|
* 对流式不完整文本做容错补全
|
|
6
|
+
*
|
|
7
|
+
* 渲染策略:将 ANSI 字符串按 \n 拆行,每行用独立 <Text> 渲染。
|
|
8
|
+
* 直接将含 \n 的 ANSI 字符串塞入单个 <Text> 会导致 ink 布局引擎
|
|
9
|
+
* 与 ANSI 转义序列冲突,出现光标错位和输出错乱。
|
|
6
10
|
*/
|
|
7
11
|
declare function MarkdownText({ text, color }: {
|
|
8
12
|
text: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React, { useMemo } from 'react';
|
|
3
|
-
import { Text } from 'ink';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
4
|
import { Marked } from 'marked';
|
|
5
5
|
// @ts-ignore — marked-terminal 无内置类型声明
|
|
6
6
|
import { markedTerminal } from 'marked-terminal';
|
|
@@ -158,9 +158,16 @@ function renderMarkdown(text) {
|
|
|
158
158
|
* Markdown 终端渲染组件
|
|
159
159
|
* 支持表格(动态列宽+自动换行)、代码块(深色背景+边框)、加粗、列表等
|
|
160
160
|
* 对流式不完整文本做容错补全
|
|
161
|
+
*
|
|
162
|
+
* 渲染策略:将 ANSI 字符串按 \n 拆行,每行用独立 <Text> 渲染。
|
|
163
|
+
* 直接将含 \n 的 ANSI 字符串塞入单个 <Text> 会导致 ink 布局引擎
|
|
164
|
+
* 与 ANSI 转义序列冲突,出现光标错位和输出错乱。
|
|
161
165
|
*/
|
|
162
166
|
function MarkdownText({ text, color }) {
|
|
163
|
-
const
|
|
164
|
-
|
|
167
|
+
const lines = useMemo(() => {
|
|
168
|
+
const rendered = renderMarkdown(text);
|
|
169
|
+
return rendered.split('\n');
|
|
170
|
+
}, [text]);
|
|
171
|
+
return (_jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { wrap: "wrap", color: color, children: line }, i))) }));
|
|
165
172
|
}
|
|
166
173
|
export default React.memo(MarkdownText);
|
|
@@ -79,8 +79,11 @@ function MessageItem({ msg, showDetails = false }) {
|
|
|
79
79
|
// 有 subAgentId 时:标题行 + 内容分两行,避免内容粘连
|
|
80
80
|
_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsxs(Text, { color: "blue", dimColor: true, children: [msg.subAgentId, " \u203A "] })] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(MarkdownText, { text: msg.content }) })] })) : (_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsx(Box, { flexDirection: "column", flexShrink: 1, children: _jsx(MarkdownText, { text: msg.content }) })] })), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
|
|
81
81
|
}
|
|
82
|
+
if (msg.type === 'system' && msg.content) {
|
|
83
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsx(Box, { flexDirection: "column", flexShrink: 1, children: _jsx(MarkdownText, { text: msg.content, color: "gray" }) })] }), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
|
|
84
|
+
}
|
|
82
85
|
if (msg.status !== 'pending' && msg.content) {
|
|
83
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
86
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsx(Box, { flexDirection: "column", flexShrink: 1, children: _jsx(MarkdownText, { text: msg.content, color: "gray" }) })] }), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
|
|
84
87
|
}
|
|
85
88
|
return null;
|
|
86
89
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import {
|
|
4
|
+
import { PROJECT_NAME, getContextTokenLimit, getModelName, isThinkingModeToggleEnabled } from '../config/constants.js';
|
|
5
5
|
/** 生成 token 用量进度条 */
|
|
6
6
|
function tokenProgressBar(used, limit, barWidth) {
|
|
7
7
|
const ratio = Math.min(used / limit, 1);
|
|
@@ -12,16 +12,19 @@ function tokenProgressBar(used, limit, barWidth) {
|
|
|
12
12
|
return { bar, color };
|
|
13
13
|
}
|
|
14
14
|
function StatusBar({ width, totalTokens, activeAgents = 0 }) {
|
|
15
|
-
const
|
|
15
|
+
const modelName = getModelName();
|
|
16
|
+
const contextTokenLimit = getContextTokenLimit();
|
|
17
|
+
const thinkingModeToggleEnabled = isThinkingModeToggleEnabled();
|
|
18
|
+
const left = ` ${modelName} │ ${PROJECT_NAME}`;
|
|
16
19
|
// 右侧:智能体数量(有后台 Agent 时显示)+ token 进度条 + 思考模式切换(可选)
|
|
17
20
|
const agentPart = activeAgents > 0 ? `⬡ ${activeAgents} agent${activeAgents > 1 ? 's' : ''} │ ` : '';
|
|
18
|
-
const tokenLabel = `${totalTokens}/${
|
|
21
|
+
const tokenLabel = `${totalTokens}/${contextTokenLimit}`;
|
|
19
22
|
const barWidth = 10;
|
|
20
|
-
const { bar, color } = tokenProgressBar(totalTokens,
|
|
21
|
-
const effortPart =
|
|
23
|
+
const { bar, color } = tokenProgressBar(totalTokens, contextTokenLimit, barWidth);
|
|
24
|
+
const effortPart = thinkingModeToggleEnabled ? ' │ ● medium · /effort' : '';
|
|
22
25
|
// 右侧完整文本长度(用于计算间距)
|
|
23
26
|
const rightLen = agentPart.length + tokenLabel.length + 1 + barWidth + effortPart.length + 1;
|
|
24
27
|
const gap = Math.max(width - left.length - rightLen, 1);
|
|
25
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: left }), _jsx(Text, { children: ' '.repeat(gap) }), activeAgents > 0 && _jsx(Text, { color: "cyan", children: agentPart }), _jsxs(Text, { color: "gray", children: [tokenLabel, " "] }), _jsx(Text, { color: color, children: bar }),
|
|
28
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: left }), _jsx(Text, { children: ' '.repeat(gap) }), activeAgents > 0 && _jsx(Text, { color: "cyan", children: agentPart }), _jsxs(Text, { color: "gray", children: [tokenLabel, " "] }), _jsx(Text, { color: color, children: bar }), thinkingModeToggleEnabled && _jsx(Text, { color: "gray", children: effortPart }), _jsx(Text, { children: " " })] }));
|
|
26
29
|
}
|
|
27
30
|
export default React.memo(StatusBar);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import {
|
|
4
|
+
import { APP_VERSION, getAppName, getModelName } from '../config/constants.js';
|
|
5
5
|
function truncatePath(p, max) {
|
|
6
6
|
if (p.length <= max)
|
|
7
7
|
return p;
|
|
@@ -20,6 +20,8 @@ const LOGO_COLORS = ['cyan', 'cyan', 'blueBright', 'blueBright', 'magenta', 'mag
|
|
|
20
20
|
function WelcomeHeader({ width }) {
|
|
21
21
|
const maxPath = Math.max(width - 10, 20);
|
|
22
22
|
const showLogo = width >= 52;
|
|
23
|
-
|
|
23
|
+
const appName = getAppName();
|
|
24
|
+
const modelName = getModelName();
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, width: width, children: [showLogo && (_jsx(Box, { flexDirection: "column", children: LOGO_LINES.map((line, i) => (_jsx(Text, { color: LOGO_COLORS[i], children: line }, i))) })), _jsxs(Box, { marginTop: 0, children: [_jsx(Text, { color: "gray" }), _jsx(Text, { color: "white", bold: true, children: "Your AI-Powered Dev Companion" }), _jsx(Text, { color: "gray" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: '─'.repeat(Math.min(width - 4, 48)) }) }), _jsxs(Box, { marginTop: 0, children: [_jsx(Text, { color: "gray", children: "model " }), _jsx(Text, { color: "cyan", children: modelName }), _jsxs(Text, { color: "gray", children: [" ", appName, " "] }), _jsx(Text, { color: "magenta", children: APP_VERSION })] }), _jsx(Box, { children: _jsx(Text, { color: "gray", children: truncatePath(process.cwd(), maxPath) }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: '─'.repeat(Math.min(width - 4, 48)) }) }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "init" }), _jsx(Text, { color: "gray", children: " \u521D\u59CB\u5316 " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "help" }), _jsx(Text, { color: "gray", children: " \u5E2E\u52A9 " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "new" }), _jsx(Text, { color: "gray", children: " \u65B0\u4F1A\u8BDD " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "agent" }), _jsx(Text, { color: "gray", children: " \u5207\u6362" })] }), _jsx(Text, { children: ' ' })] }));
|
|
24
26
|
}
|
|
25
27
|
export default React.memo(WelcomeHeader);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SetupFormData } from '../../config/bootstrap.js';
|
|
2
|
+
interface SetupConfirmStepProps {
|
|
3
|
+
form: SetupFormData;
|
|
4
|
+
preview: string;
|
|
5
|
+
isSubmitting: boolean;
|
|
6
|
+
}
|
|
7
|
+
export default function SetupConfirmStep({ form, preview, isSubmitting }: SetupConfirmStepProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
function maskApiKey(apiKey) {
|
|
4
|
+
if (!apiKey)
|
|
5
|
+
return '未填写';
|
|
6
|
+
if (apiKey.length <= 8)
|
|
7
|
+
return '*'.repeat(apiKey.length);
|
|
8
|
+
return `${apiKey.slice(0, 4)}${'*'.repeat(Math.max(apiKey.length - 8, 4))}${apiKey.slice(-4)}`;
|
|
9
|
+
}
|
|
10
|
+
export default function SetupConfirmStep({ form, preview, isSubmitting }) {
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "white", bold: true, children: "\u786E\u8BA4\u5199\u5165\u914D\u7F6E" }), _jsx(Text, { color: "gray", children: "\u786E\u8BA4\u65E0\u8BEF\u540E\u6309 Enter \u5199\u5165\u3002Esc \u53EF\u8FD4\u56DE\u4FEE\u6539\u3002" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["\u914D\u7F6E\u540D\u79F0\uFF1A", form.profileName] }), _jsxs(Text, { children: ["API \u5730\u5740\uFF1A", form.apiUrl] }), _jsxs(Text, { children: ["\u6A21\u578B ID\uFF1A", form.model] }), _jsxs(Text, { children: ["API Key\uFF1A", maskApiKey(form.apiKey)] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "JSON \u9884\u89C8" }), preview.split('\n').map((line, index) => (_jsx(Text, { color: "gray", children: line }, `${index}-${line}`)))] }), isSubmitting ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "cyan", children: "\u6B63\u5728\u5199\u5165\u914D\u7F6E..." }) })) : null] }));
|
|
12
|
+
}
|