@mindbase/mindbase 1.0.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 +201 -0
- package/bin/gitlog.ts +3 -0
- package/bin/iconfont-editor.ts +3 -0
- package/bin/index.ts +29 -0
- package/bin/nodeclear.ts +3 -0
- package/bin/npmpublish.ts +3 -0
- package/bin/shared.ts +141 -0
- package/package.json +63 -0
- package/scripts/ensure-tsx.cjs +18 -0
- package/src/clear/app.ts +152 -0
- package/src/clear/config.ts +108 -0
- package/src/clear/file-cleaner.ts +137 -0
- package/src/clear/index.ts +28 -0
- package/src/clear/scanner.ts +154 -0
- package/src/gitlog/app.ts +272 -0
- package/src/gitlog/config.ts +91 -0
- package/src/gitlog/display.ts +178 -0
- package/src/gitlog/index.ts +5 -0
- package/src/gitlog/log-fetcher.ts +150 -0
- package/src/gitlog/pager.ts +178 -0
- package/src/gitlog/scanner.ts +60 -0
- package/src/iconfont/frontend/index.html +12 -0
- package/src/iconfont/frontend/src/App.vue +14 -0
- package/src/iconfont/frontend/src/main.ts +8 -0
- package/src/iconfont/frontend/src/views/IconEditor.vue +819 -0
- package/src/iconfont/frontend/vite.config.ts +15 -0
- package/src/iconfont/lib/css-generator.js +37 -0
- package/src/iconfont/lib/font-builder.js +82 -0
- package/src/iconfont/lib/glyph-extractor.js +69 -0
- package/src/iconfont/server/api.js +256 -0
- package/src/iconfont/server/index.js +64 -0
- package/src/index.ts +0 -0
- package/src/publish/app.ts +316 -0
- package/src/publish/builder.ts +120 -0
- package/src/publish/dependency.ts +144 -0
- package/src/publish/detector.ts +93 -0
- package/src/publish/index.ts +66 -0
- package/src/publish/npm-query.ts +244 -0
- package/src/publish/registry/adapters/npm.ts +128 -0
- package/src/publish/registry/registry-manager.ts +107 -0
- package/src/publish/scanner.ts +90 -0
- package/src/publish/types.ts +98 -0
- package/src/publish/version.ts +108 -0
- package/src/shared/config-manager.ts +57 -0
- package/src/shared/index.ts +1 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import edit from 'editor';
|
|
6
|
+
import { ConfigManager } from '../shared/index.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 获取配置文件路径
|
|
10
|
+
*/
|
|
11
|
+
function getConfigPath(): string {
|
|
12
|
+
const appData = process.env.APPDATA || join(homedir(), '.config');
|
|
13
|
+
return join(appData, 'node-tools', 'config.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 默认配置
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_CONFIG: CleanConfig = {
|
|
20
|
+
targets: {
|
|
21
|
+
directories: ['node_modules', 'pnpm'],
|
|
22
|
+
files: ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock']
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 配置类型
|
|
28
|
+
*/
|
|
29
|
+
export interface CleanConfig {
|
|
30
|
+
targets: {
|
|
31
|
+
directories: string[];
|
|
32
|
+
files: string[];
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 配置管理器实例
|
|
38
|
+
*/
|
|
39
|
+
const manager = new ConfigManager<CleanConfig>(getConfigPath(), DEFAULT_CONFIG);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 读取配置
|
|
43
|
+
*/
|
|
44
|
+
export function getConfig(): CleanConfig {
|
|
45
|
+
return manager.getConfig();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 写入配置
|
|
50
|
+
*/
|
|
51
|
+
export function setConfig(config: CleanConfig): void {
|
|
52
|
+
manager.setConfig(config);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 重置为默认配置
|
|
57
|
+
*/
|
|
58
|
+
export function resetConfig(): CleanConfig {
|
|
59
|
+
return manager.resetConfig();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 获取配置(用于显示)
|
|
64
|
+
*/
|
|
65
|
+
export function getConfigForDisplay(): string {
|
|
66
|
+
const config = getConfig();
|
|
67
|
+
return JSON.stringify(config, null, 2);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 编辑配置
|
|
72
|
+
*/
|
|
73
|
+
export function editConfig(callback?: (success: boolean) => void): void {
|
|
74
|
+
const configPath = getConfigPath();
|
|
75
|
+
|
|
76
|
+
// 确保配置文件存在
|
|
77
|
+
if (!existsSync(configPath)) {
|
|
78
|
+
setConfig(DEFAULT_CONFIG);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 优先使用 code(VS Code)
|
|
82
|
+
const editorCmd = process.env.EDITOR || (process.platform === 'win32' ? 'code' : 'vim');
|
|
83
|
+
|
|
84
|
+
if (editorCmd === 'code') {
|
|
85
|
+
// Windows/macOS/Linux 使用 VS Code
|
|
86
|
+
const spawnOptions = process.platform === 'win32'
|
|
87
|
+
? { shell: true, stdio: 'inherit' as const }
|
|
88
|
+
: { stdio: 'inherit' as const };
|
|
89
|
+
|
|
90
|
+
spawn('code', [configPath, '--wait'], spawnOptions)
|
|
91
|
+
.on('exit', (code) => {
|
|
92
|
+
callback?.(code === 0);
|
|
93
|
+
})
|
|
94
|
+
.on('error', () => {
|
|
95
|
+
// code 命令不可用,回退到 editor 包
|
|
96
|
+
edit(configPath, function (code, sig) {
|
|
97
|
+
callback?.(code === 0);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
// 使用环境变量指定的编辑器
|
|
102
|
+
edit(configPath, function (code, sig) {
|
|
103
|
+
callback?.(code === 0);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { getConfigPath };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fsExtra from "fs-extra";
|
|
2
|
+
const { remove } = fsExtra;
|
|
3
|
+
import { unlink } from "fs/promises";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 扫描结果项
|
|
7
|
+
*/
|
|
8
|
+
export interface ScanItem {
|
|
9
|
+
/** 完整路径 */
|
|
10
|
+
path: string;
|
|
11
|
+
/** 相对路径 */
|
|
12
|
+
relativePath: string;
|
|
13
|
+
/** 名称 */
|
|
14
|
+
name: string;
|
|
15
|
+
/** 类型 */
|
|
16
|
+
type: "dir" | "file";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 删除结果详情
|
|
21
|
+
*/
|
|
22
|
+
export interface CleanDetail extends ScanItem {
|
|
23
|
+
status: "success" | "failed";
|
|
24
|
+
error?: Error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 清理统计
|
|
29
|
+
*/
|
|
30
|
+
export interface CleanStats {
|
|
31
|
+
success: number;
|
|
32
|
+
failed: number;
|
|
33
|
+
details: CleanDetail[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 清理进度回调
|
|
38
|
+
*/
|
|
39
|
+
export interface CleanProgressCallback {
|
|
40
|
+
(progress: { current: number; total: number; success: number; failed: number }): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 删除目录
|
|
45
|
+
*/
|
|
46
|
+
export async function removeDir(path: string): Promise<void> {
|
|
47
|
+
await remove(path);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 删除文件
|
|
52
|
+
*/
|
|
53
|
+
export async function removeFile(path: string): Promise<void> {
|
|
54
|
+
await unlink(path);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 批量删除选中的项目(异步)
|
|
59
|
+
*/
|
|
60
|
+
export async function cleanAsync(items: ScanItem[]): Promise<CleanStats> {
|
|
61
|
+
const stats: CleanStats = {
|
|
62
|
+
success: 0,
|
|
63
|
+
failed: 0,
|
|
64
|
+
details: [],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const promises = items.map(async (item, index) => {
|
|
68
|
+
try {
|
|
69
|
+
if (item.type === "dir") {
|
|
70
|
+
await removeDir(item.path);
|
|
71
|
+
} else {
|
|
72
|
+
await removeFile(item.path);
|
|
73
|
+
}
|
|
74
|
+
return { ...item, status: "success" as const };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return { ...item, status: "failed" as const, error: error as Error };
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const results = await Promise.allSettled(promises);
|
|
81
|
+
|
|
82
|
+
results.forEach((result, index) => {
|
|
83
|
+
if (result.status === "fulfilled") {
|
|
84
|
+
const detail = result.value;
|
|
85
|
+
if (detail.status === "success") {
|
|
86
|
+
stats.success++;
|
|
87
|
+
} else {
|
|
88
|
+
stats.failed++;
|
|
89
|
+
}
|
|
90
|
+
stats.details.push(detail);
|
|
91
|
+
} else {
|
|
92
|
+
stats.failed++;
|
|
93
|
+
stats.details.push({
|
|
94
|
+
...items[index],
|
|
95
|
+
status: "failed",
|
|
96
|
+
error: new Error(String(result.reason)),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return stats;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 批量删除选中的项目(同步,兼容旧代码)
|
|
106
|
+
*/
|
|
107
|
+
export function clean(items: ScanItem[]): CleanStats {
|
|
108
|
+
const stats: CleanStats = {
|
|
109
|
+
success: 0,
|
|
110
|
+
failed: 0,
|
|
111
|
+
details: [],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
for (const item of items) {
|
|
115
|
+
let success = false;
|
|
116
|
+
try {
|
|
117
|
+
if (item.type === "dir") {
|
|
118
|
+
removeDir(item.path);
|
|
119
|
+
} else {
|
|
120
|
+
removeFile(item.path);
|
|
121
|
+
}
|
|
122
|
+
success = true;
|
|
123
|
+
} catch {
|
|
124
|
+
success = false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (success) {
|
|
128
|
+
stats.success++;
|
|
129
|
+
stats.details.push({ ...item, status: "success" });
|
|
130
|
+
} else {
|
|
131
|
+
stats.failed++;
|
|
132
|
+
stats.details.push({ ...item, status: "failed" });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return stats;
|
|
137
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// clear 领域导出
|
|
2
|
+
export {
|
|
3
|
+
scan,
|
|
4
|
+
formatSize,
|
|
5
|
+
type ScanItem,
|
|
6
|
+
type CleanConfig,
|
|
7
|
+
type ScanProgressCallback
|
|
8
|
+
} from './scanner.js';
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
getConfig,
|
|
12
|
+
setConfig,
|
|
13
|
+
resetConfig,
|
|
14
|
+
getConfigForDisplay,
|
|
15
|
+
editConfig,
|
|
16
|
+
getConfigPath,
|
|
17
|
+
DEFAULT_CONFIG
|
|
18
|
+
} from './config.js';
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
cleanAsync,
|
|
22
|
+
clean,
|
|
23
|
+
removeDir,
|
|
24
|
+
removeFile,
|
|
25
|
+
type CleanStats,
|
|
26
|
+
type CleanDetail,
|
|
27
|
+
type CleanProgressCallback
|
|
28
|
+
} from './file-cleaner.js';
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "fs";
|
|
2
|
+
import { join, relative } from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 扫描结果项
|
|
6
|
+
*/
|
|
7
|
+
export interface ScanItem {
|
|
8
|
+
/** 完整路径 */
|
|
9
|
+
path: string;
|
|
10
|
+
/** 相对路径 */
|
|
11
|
+
relativePath: string;
|
|
12
|
+
/** 名称 */
|
|
13
|
+
name: string;
|
|
14
|
+
/** 类型 */
|
|
15
|
+
type: "dir" | "file";
|
|
16
|
+
/** 大小(字节) */
|
|
17
|
+
size?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 清理配置
|
|
22
|
+
*/
|
|
23
|
+
export interface CleanConfig {
|
|
24
|
+
targets: {
|
|
25
|
+
directories: string[];
|
|
26
|
+
files: string[];
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 扫描进度回调
|
|
32
|
+
*/
|
|
33
|
+
export interface ScanProgressCallback {
|
|
34
|
+
(current: string, total: number): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 递归扫描目录,查找匹配的目标
|
|
39
|
+
*/
|
|
40
|
+
function scanDirectory(
|
|
41
|
+
basePath: string,
|
|
42
|
+
currentPath: string,
|
|
43
|
+
config: CleanConfig,
|
|
44
|
+
results: ScanItem[],
|
|
45
|
+
count = 0
|
|
46
|
+
): number {
|
|
47
|
+
const { targets } = config;
|
|
48
|
+
const targetDirs = targets.directories || [];
|
|
49
|
+
const targetFiles = targets.files || [];
|
|
50
|
+
|
|
51
|
+
let entries;
|
|
52
|
+
try {
|
|
53
|
+
entries = readdirSync(currentPath);
|
|
54
|
+
} catch {
|
|
55
|
+
// 无权限访问或目录不存在,跳过
|
|
56
|
+
return count;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 显示当前扫描的目录
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const fullPath = join(currentPath, entry);
|
|
62
|
+
let stat;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
stat = statSync(fullPath);
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (stat.isDirectory()) {
|
|
71
|
+
// 检查是否是目标目录
|
|
72
|
+
if (targetDirs.includes(entry)) {
|
|
73
|
+
const relativePath = relative(basePath, fullPath);
|
|
74
|
+
results.push({
|
|
75
|
+
path: fullPath,
|
|
76
|
+
relativePath,
|
|
77
|
+
name: entry,
|
|
78
|
+
type: "dir",
|
|
79
|
+
});
|
|
80
|
+
count++;
|
|
81
|
+
} else {
|
|
82
|
+
// 递归扫描子目录
|
|
83
|
+
count = scanDirectory(basePath, fullPath, config, results, count);
|
|
84
|
+
}
|
|
85
|
+
} else if (stat.isFile()) {
|
|
86
|
+
// 检查是否是目标文件
|
|
87
|
+
if (targetFiles.includes(entry)) {
|
|
88
|
+
const relativePath = relative(basePath, fullPath);
|
|
89
|
+
results.push({
|
|
90
|
+
path: fullPath,
|
|
91
|
+
relativePath,
|
|
92
|
+
name: entry,
|
|
93
|
+
type: "file",
|
|
94
|
+
});
|
|
95
|
+
count++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return count;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 获取目录大小(估算)
|
|
105
|
+
*/
|
|
106
|
+
function getDirectorySize(dirPath: string): number {
|
|
107
|
+
try {
|
|
108
|
+
let totalSize = 0;
|
|
109
|
+
const entries = readdirSync(dirPath);
|
|
110
|
+
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const fullPath = join(dirPath, entry);
|
|
113
|
+
try {
|
|
114
|
+
const stat = statSync(fullPath);
|
|
115
|
+
if (stat.isDirectory()) {
|
|
116
|
+
totalSize += getDirectorySize(fullPath);
|
|
117
|
+
} else if (stat.isFile()) {
|
|
118
|
+
totalSize += stat.size;
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// 跳过无法访问的文件
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return totalSize;
|
|
126
|
+
} catch {
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 扫描指定目录
|
|
133
|
+
*/
|
|
134
|
+
export function scan(scanPath: string, config: CleanConfig): ScanItem[] {
|
|
135
|
+
const results: ScanItem[] = [];
|
|
136
|
+
scanDirectory(scanPath, scanPath, config, results, 0);
|
|
137
|
+
return results;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 格式化文件大小
|
|
142
|
+
*/
|
|
143
|
+
export function formatSize(bytes: number): string {
|
|
144
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
145
|
+
let size = bytes;
|
|
146
|
+
let unitIndex = 0;
|
|
147
|
+
|
|
148
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
149
|
+
size /= 1024;
|
|
150
|
+
unitIndex++;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
154
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import * as clack from "@clack/prompts";
|
|
2
|
+
import { noteBox } from "@mindbase/cli-ui";
|
|
3
|
+
import { scanRepos, type GitRepo } from "./scanner.js";
|
|
4
|
+
import { fetchLogsFromRepos, type GitLogEntry, type LogOptions } from "./log-fetcher.js";
|
|
5
|
+
import { saveFilters, getSavedFilters, type LogFilters } from "./config.js";
|
|
6
|
+
import { formatLogEntries, formatRepoOverview, formatFilters, LINES_PER_ENTRY } from "./display.js";
|
|
7
|
+
import { runPager, type PagerAction } from "./pager.js";
|
|
8
|
+
|
|
9
|
+
/** 应用上下文 */
|
|
10
|
+
interface AppContext {
|
|
11
|
+
scanPath: string;
|
|
12
|
+
repos: GitRepo[];
|
|
13
|
+
filters: LogFilters;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 统一取消处理 */
|
|
17
|
+
function handleCancel<T>(result: T | symbol): T {
|
|
18
|
+
if (clack.isCancel(result)) {
|
|
19
|
+
clack.cancel("操作已取消");
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 将 LogFilters 转换为 LogOptions(跳过空值) */
|
|
26
|
+
function toLogOptions(filters: LogFilters): LogOptions {
|
|
27
|
+
return {
|
|
28
|
+
author: filters.author || undefined,
|
|
29
|
+
since: filters.since || undefined,
|
|
30
|
+
until: filters.until || undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ==================== 主入口 ====================
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 运行 gitlog 应用
|
|
38
|
+
*/
|
|
39
|
+
export async function runApp(scanPath: string): Promise<void> {
|
|
40
|
+
// 扫描仓库
|
|
41
|
+
const s = clack.spinner();
|
|
42
|
+
s.start(`扫描目录: ${scanPath}`);
|
|
43
|
+
const repos = scanRepos(scanPath);
|
|
44
|
+
s.stop(`找到 ${repos.length} 个 Git 仓库`);
|
|
45
|
+
|
|
46
|
+
if (repos.length === 0) {
|
|
47
|
+
clack.log.warn("未找到任何 Git 仓库");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 显示仓库列表
|
|
52
|
+
noteBox(formatRepoOverview(repos), "仓库概览");
|
|
53
|
+
|
|
54
|
+
// 加载保存的筛选条件(since 默认7天前)
|
|
55
|
+
const filters = getSavedFilters();
|
|
56
|
+
|
|
57
|
+
const ctx: AppContext = { scanPath, repos, filters };
|
|
58
|
+
await mainLoop(ctx);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ==================== 主循环 ====================
|
|
62
|
+
|
|
63
|
+
async function mainLoop(ctx: AppContext): Promise<void> {
|
|
64
|
+
while (true) {
|
|
65
|
+
const action = handleCancel(
|
|
66
|
+
await clack.select({
|
|
67
|
+
message: "选择操作",
|
|
68
|
+
options: [
|
|
69
|
+
{ value: "logs", label: "查看日志", hint: "选择仓库并查看提交日志" },
|
|
70
|
+
{ value: "repos", label: "查看仓库概览", hint: "显示仓库状态" },
|
|
71
|
+
{ value: "filters", label: "配置筛选条件", hint: "编辑作者、日期等筛选" },
|
|
72
|
+
{ value: "rescan", label: "重新扫描", hint: "重新扫描目录" },
|
|
73
|
+
{ value: "quit", label: "退出" },
|
|
74
|
+
],
|
|
75
|
+
})
|
|
76
|
+
) as string;
|
|
77
|
+
|
|
78
|
+
switch (action) {
|
|
79
|
+
case "logs":
|
|
80
|
+
await viewLogs(ctx);
|
|
81
|
+
break;
|
|
82
|
+
case "repos":
|
|
83
|
+
await viewRepos(ctx);
|
|
84
|
+
break;
|
|
85
|
+
case "filters":
|
|
86
|
+
await editFilters(ctx);
|
|
87
|
+
break;
|
|
88
|
+
case "rescan":
|
|
89
|
+
await rescan(ctx);
|
|
90
|
+
break;
|
|
91
|
+
case "quit":
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ==================== 查看日志 ====================
|
|
98
|
+
|
|
99
|
+
async function viewLogs(ctx: AppContext): Promise<void> {
|
|
100
|
+
const selected = handleCancel(
|
|
101
|
+
await clack.multiselect({
|
|
102
|
+
message: "选择要查看的仓库(a 全选/全反选)",
|
|
103
|
+
options: [
|
|
104
|
+
...ctx.repos.map((repo) => ({
|
|
105
|
+
value: repo.path,
|
|
106
|
+
label: `${repo.name} (${repo.currentBranch || "-"})`,
|
|
107
|
+
hint: repo.hasChanges ? "有更改" : "无更改",
|
|
108
|
+
})),
|
|
109
|
+
],
|
|
110
|
+
initialValues: ctx.filters.selectedRepos,
|
|
111
|
+
required: true,
|
|
112
|
+
})
|
|
113
|
+
) as string[];
|
|
114
|
+
|
|
115
|
+
// 处理全选
|
|
116
|
+
|
|
117
|
+
ctx.filters.selectedRepos = selected;
|
|
118
|
+
|
|
119
|
+
// 直接使用当前筛选条件获取日志
|
|
120
|
+
await fetchAndBrowse(selected, ctx);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 获取日志并进入分页浏览
|
|
125
|
+
*/
|
|
126
|
+
async function fetchAndBrowse(repos: string[], ctx: AppContext): Promise<void> {
|
|
127
|
+
const logOptions = toLogOptions(ctx.filters);
|
|
128
|
+
|
|
129
|
+
// 显示筛选条件摘要
|
|
130
|
+
const filterHint = [
|
|
131
|
+
ctx.filters.author && `作者: ${ctx.filters.author}`,
|
|
132
|
+
ctx.filters.since && `起始: ${ctx.filters.since}`,
|
|
133
|
+
ctx.filters.until && `结束: ${ctx.filters.until}`,
|
|
134
|
+
]
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.join(" | ");
|
|
137
|
+
if (filterHint) {
|
|
138
|
+
clack.log.info(`筛选条件: ${filterHint}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const s = clack.spinner();
|
|
142
|
+
s.start("正在获取日志...");
|
|
143
|
+
const logs = await fetchLogsFromRepos(repos, logOptions);
|
|
144
|
+
s.stop(`获取到 ${logs.length} 条日志`);
|
|
145
|
+
|
|
146
|
+
if (logs.length === 0) {
|
|
147
|
+
clack.log.warn("未找到匹配的日志");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 进入分页浏览
|
|
152
|
+
const lines = formatLogEntries(logs);
|
|
153
|
+
let action: PagerAction = await runPager(lines, { linesPerEntry: LINES_PER_ENTRY });
|
|
154
|
+
|
|
155
|
+
// 按 r 重新筛选
|
|
156
|
+
while (action === "refilter") {
|
|
157
|
+
const newFilters = await collectFilterInputs(ctx.filters);
|
|
158
|
+
ctx.filters = newFilters;
|
|
159
|
+
|
|
160
|
+
const newLogOptions = toLogOptions(newFilters);
|
|
161
|
+
s.start("正在获取日志...");
|
|
162
|
+
const newLogs = await fetchLogsFromRepos(repos, newLogOptions);
|
|
163
|
+
s.stop(`获取到 ${newLogs.length} 条日志`);
|
|
164
|
+
|
|
165
|
+
if (newLogs.length === 0) {
|
|
166
|
+
clack.log.warn("未找到匹配的日志");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const newLines = formatLogEntries(newLogs);
|
|
171
|
+
action = await runPager(newLines, { linesPerEntry: LINES_PER_ENTRY });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ==================== 仓库概览 ====================
|
|
176
|
+
|
|
177
|
+
async function viewRepos(ctx: AppContext): Promise<void> {
|
|
178
|
+
const overview = formatRepoOverview(ctx.repos);
|
|
179
|
+
noteBox(overview, "仓库概览");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ==================== 筛选条件 ====================
|
|
183
|
+
|
|
184
|
+
async function editFilters(ctx: AppContext): Promise<void> {
|
|
185
|
+
// 显示当前条件
|
|
186
|
+
noteBox(formatFilters(ctx.filters), "当前筛选条件");
|
|
187
|
+
|
|
188
|
+
// 收集新条件
|
|
189
|
+
const newFilters = await collectFilterInputs(ctx.filters);
|
|
190
|
+
|
|
191
|
+
// 保存
|
|
192
|
+
saveFilters(newFilters);
|
|
193
|
+
ctx.filters = newFilters;
|
|
194
|
+
clack.log.success("筛选条件已保存");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 交互式收集筛选条件
|
|
199
|
+
*/
|
|
200
|
+
async function collectFilterInputs(current?: LogFilters): Promise<LogFilters> {
|
|
201
|
+
const author = handleCancel(
|
|
202
|
+
await clack.text({
|
|
203
|
+
message: "作者(留空跳过)",
|
|
204
|
+
placeholder: "输入作者名或邮箱",
|
|
205
|
+
initialValue: current?.author || "",
|
|
206
|
+
})
|
|
207
|
+
) as string;
|
|
208
|
+
|
|
209
|
+
const since = handleCancel(
|
|
210
|
+
await clack.text({
|
|
211
|
+
message: "起始日期(留空跳过,格式 YYYY-MM-DD)",
|
|
212
|
+
placeholder: "如 2025-01-01",
|
|
213
|
+
initialValue: current?.since || "",
|
|
214
|
+
validate: (v) => {
|
|
215
|
+
if (v && !/^\d{4}-\d{2}-\d{2}$/.test(v)) return "日期格式应为 YYYY-MM-DD";
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
) as string;
|
|
219
|
+
|
|
220
|
+
const until = handleCancel(
|
|
221
|
+
await clack.text({
|
|
222
|
+
message: "结束日期(留空跳过,格式 YYYY-MM-DD)",
|
|
223
|
+
placeholder: "如 2025-12-31",
|
|
224
|
+
initialValue: current?.until || "",
|
|
225
|
+
validate: (v) => {
|
|
226
|
+
if (v && !/^\d{4}-\d{2}-\d{2}$/.test(v)) return "日期格式应为 YYYY-MM-DD";
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
) as string;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
author: author || "",
|
|
233
|
+
since: since || "",
|
|
234
|
+
until: until || "",
|
|
235
|
+
selectedRepos: current?.selectedRepos || [],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ==================== 重新扫描 ====================
|
|
240
|
+
|
|
241
|
+
async function rescan(ctx: AppContext): Promise<void> {
|
|
242
|
+
const s = clack.spinner();
|
|
243
|
+
s.start(`重新扫描: ${ctx.scanPath}`);
|
|
244
|
+
ctx.repos = scanRepos(ctx.scanPath);
|
|
245
|
+
s.stop(`找到 ${ctx.repos.length} 个 Git 仓库`);
|
|
246
|
+
|
|
247
|
+
if (ctx.repos.length > 0) {
|
|
248
|
+
noteBox(formatRepoOverview(ctx.repos), "仓库概览");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ==================== config 子命令 ====================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 处理 config 子命令
|
|
256
|
+
*/
|
|
257
|
+
export async function showConfig(options: { show?: boolean; reset?: boolean }): Promise<void> {
|
|
258
|
+
if (options.reset) {
|
|
259
|
+
const defaultFilters: LogFilters = {
|
|
260
|
+
author: "",
|
|
261
|
+
since: "",
|
|
262
|
+
until: "",
|
|
263
|
+
selectedRepos: [],
|
|
264
|
+
};
|
|
265
|
+
saveFilters(defaultFilters);
|
|
266
|
+
clack.log.success("筛选条件已重置");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const filters = getSavedFilters();
|
|
271
|
+
noteBox(formatFilters(filters), "当前筛选条件");
|
|
272
|
+
}
|