clawt 2.18.0 → 2.19.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/README.md +10 -0
- package/dist/index.js +281 -28
- package/dist/postinstall.js +27 -0
- package/docs/spec.md +141 -0
- package/package.json +1 -1
- package/src/commands/projects.ts +324 -0
- package/src/constants/messages/index.ts +2 -0
- package/src/constants/messages/projects.ts +25 -0
- package/src/index.ts +2 -0
- package/src/types/command.ts +8 -0
- package/src/types/index.ts +2 -1
- package/src/types/project.ts +45 -0
- package/src/utils/formatter.ts +46 -0
- package/src/utils/fs.ts +32 -1
- package/src/utils/index.ts +2 -2
- package/tests/unit/utils/formatter.test.ts +91 -1
- package/tests/unit/utils/fs.test.ts +125 -2
package/README.md
CHANGED
|
@@ -214,6 +214,16 @@ clawt status --json # JSON 格式
|
|
|
214
214
|
clawt reset
|
|
215
215
|
```
|
|
216
216
|
|
|
217
|
+
### `clawt projects` — 跨项目 worktree 概览
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
clawt projects # 查看所有项目概览
|
|
221
|
+
clawt projects my-project # 查看指定项目的 worktree 详情
|
|
222
|
+
clawt projects --json # JSON 格式输出
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
展示所有项目的 worktree 数量、磁盘占用和最近活跃时间,或查看指定项目下每个 worktree 的详细信息。
|
|
226
|
+
|
|
217
227
|
### `clawt config` — 交互式查看和修改配置
|
|
218
228
|
|
|
219
229
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -373,6 +373,32 @@ var ALIAS_MESSAGES = {
|
|
|
373
373
|
ALIAS_LIST_TITLE: "\u5F53\u524D\u522B\u540D\u5217\u8868\uFF1A"
|
|
374
374
|
};
|
|
375
375
|
|
|
376
|
+
// src/constants/messages/projects.ts
|
|
377
|
+
var PROJECTS_MESSAGES = {
|
|
378
|
+
/** projects 命令全局概览标题 */
|
|
379
|
+
PROJECTS_OVERVIEW_TITLE: "\u9879\u76EE\u6982\u89C8",
|
|
380
|
+
/** projects 命令指定项目详情标题 */
|
|
381
|
+
PROJECTS_DETAIL_TITLE: (projectName) => `\u9879\u76EE\u8BE6\u60C5: ${projectName}`,
|
|
382
|
+
/** 无项目提示 */
|
|
383
|
+
PROJECTS_NO_PROJECTS: "(\u6682\u65E0\u9879\u76EE\uFF0Cworktrees \u76EE\u5F55\u4E3A\u7A7A)",
|
|
384
|
+
/** 项目不存在提示 */
|
|
385
|
+
PROJECTS_NOT_FOUND: (name) => `\u9879\u76EE ${name} \u4E0D\u5B58\u5728`,
|
|
386
|
+
/** worktree 数量标签 */
|
|
387
|
+
PROJECTS_WORKTREE_COUNT: (count) => `${count} \u4E2A worktree`,
|
|
388
|
+
/** 最近活跃时间标签 */
|
|
389
|
+
PROJECTS_LAST_ACTIVE: (relativeTime) => `\u6700\u8FD1\u6D3B\u8DC3: ${relativeTime}`,
|
|
390
|
+
/** 磁盘占用标签 */
|
|
391
|
+
PROJECTS_DISK_USAGE: (size) => `\u78C1\u76D8\u5360\u7528: ${size}`,
|
|
392
|
+
/** 总磁盘占用标签 */
|
|
393
|
+
PROJECTS_TOTAL_DISK_USAGE: (size) => `\u603B\u5360\u7528: ${size}`,
|
|
394
|
+
/** projects 详情无 worktree */
|
|
395
|
+
PROJECTS_DETAIL_NO_WORKTREES: "(\u8BE5\u9879\u76EE\u4E0B\u65E0 worktree)",
|
|
396
|
+
/** 路径标签 */
|
|
397
|
+
PROJECTS_PATH: (path2) => `\u8DEF\u5F84: ${path2}`,
|
|
398
|
+
/** 最后修改时间标签 */
|
|
399
|
+
PROJECTS_LAST_MODIFIED: (relativeTime) => `\u6700\u540E\u4FEE\u6539: ${relativeTime}`
|
|
400
|
+
};
|
|
401
|
+
|
|
376
402
|
// src/constants/messages/completion.ts
|
|
377
403
|
var COMPLETION_MESSAGES = {
|
|
378
404
|
/** completion 命令的主描述 */
|
|
@@ -422,6 +448,7 @@ var MESSAGES = {
|
|
|
422
448
|
...CONFIG_CMD_MESSAGES,
|
|
423
449
|
...STATUS_MESSAGES,
|
|
424
450
|
...ALIAS_MESSAGES,
|
|
451
|
+
...PROJECTS_MESSAGES,
|
|
425
452
|
...COMPLETION_MESSAGES
|
|
426
453
|
};
|
|
427
454
|
|
|
@@ -967,6 +994,32 @@ function formatRelativeTime(isoDateString) {
|
|
|
967
994
|
const years = Math.floor(diffDays / 365);
|
|
968
995
|
return `${years} \u5E74\u524D`;
|
|
969
996
|
}
|
|
997
|
+
function formatDiskSize(bytes) {
|
|
998
|
+
const KB = 1024;
|
|
999
|
+
const MB = KB * 1024;
|
|
1000
|
+
const GB = MB * 1024;
|
|
1001
|
+
if (bytes >= GB) {
|
|
1002
|
+
return `${(bytes / GB).toFixed(1)} GB`;
|
|
1003
|
+
}
|
|
1004
|
+
if (bytes >= MB) {
|
|
1005
|
+
return `${(bytes / MB).toFixed(1)} MB`;
|
|
1006
|
+
}
|
|
1007
|
+
if (bytes >= KB) {
|
|
1008
|
+
return `${(bytes / KB).toFixed(1)} KB`;
|
|
1009
|
+
}
|
|
1010
|
+
return `${bytes} B`;
|
|
1011
|
+
}
|
|
1012
|
+
function formatLocalISOString(date) {
|
|
1013
|
+
const tzOffsetMs = date.getTimezoneOffset() * 60 * 1e3;
|
|
1014
|
+
const localDate = new Date(date.getTime() - tzOffsetMs);
|
|
1015
|
+
const iso = localDate.toISOString().slice(0, -1);
|
|
1016
|
+
const totalMinutes = -date.getTimezoneOffset();
|
|
1017
|
+
const sign = totalMinutes >= 0 ? "+" : "-";
|
|
1018
|
+
const absMinutes = Math.abs(totalMinutes);
|
|
1019
|
+
const hours = String(Math.floor(absMinutes / 60)).padStart(2, "0");
|
|
1020
|
+
const minutes = String(absMinutes % 60).padStart(2, "0");
|
|
1021
|
+
return `${iso}${sign}${hours}:${minutes}`;
|
|
1022
|
+
}
|
|
970
1023
|
|
|
971
1024
|
// src/utils/branch.ts
|
|
972
1025
|
function sanitizeBranchName(branchName) {
|
|
@@ -1017,11 +1070,12 @@ function validateClaudeCodeInstalled() {
|
|
|
1017
1070
|
}
|
|
1018
1071
|
|
|
1019
1072
|
// src/utils/worktree.ts
|
|
1020
|
-
import { join as
|
|
1073
|
+
import { join as join3 } from "path";
|
|
1021
1074
|
import { existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
|
|
1022
1075
|
|
|
1023
1076
|
// src/utils/fs.ts
|
|
1024
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, rmdirSync } from "fs";
|
|
1077
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, rmdirSync, statSync } from "fs";
|
|
1078
|
+
import { join as join2 } from "path";
|
|
1025
1079
|
function ensureDir(dirPath) {
|
|
1026
1080
|
if (!existsSync2(dirPath)) {
|
|
1027
1081
|
mkdirSync2(dirPath, { recursive: true });
|
|
@@ -1038,11 +1092,30 @@ function removeEmptyDir(dirPath) {
|
|
|
1038
1092
|
}
|
|
1039
1093
|
return false;
|
|
1040
1094
|
}
|
|
1095
|
+
function calculateDirSize(dirPath) {
|
|
1096
|
+
let totalSize = 0;
|
|
1097
|
+
try {
|
|
1098
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
1099
|
+
for (const entry of entries) {
|
|
1100
|
+
const fullPath = join2(dirPath, entry.name);
|
|
1101
|
+
try {
|
|
1102
|
+
if (entry.isDirectory()) {
|
|
1103
|
+
totalSize += calculateDirSize(fullPath);
|
|
1104
|
+
} else if (entry.isFile()) {
|
|
1105
|
+
totalSize += statSync(fullPath).size;
|
|
1106
|
+
}
|
|
1107
|
+
} catch {
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
} catch {
|
|
1111
|
+
}
|
|
1112
|
+
return totalSize;
|
|
1113
|
+
}
|
|
1041
1114
|
|
|
1042
1115
|
// src/utils/worktree.ts
|
|
1043
1116
|
function getProjectWorktreeDir() {
|
|
1044
1117
|
const projectName = getProjectName();
|
|
1045
|
-
return
|
|
1118
|
+
return join3(WORKTREES_DIR, projectName);
|
|
1046
1119
|
}
|
|
1047
1120
|
function createWorktrees(branchName, count) {
|
|
1048
1121
|
const sanitized = sanitizeBranchName(branchName);
|
|
@@ -1052,7 +1125,7 @@ function createWorktrees(branchName, count) {
|
|
|
1052
1125
|
ensureDir(projectDir);
|
|
1053
1126
|
const results = [];
|
|
1054
1127
|
for (const name of branchNames) {
|
|
1055
|
-
const worktreePath =
|
|
1128
|
+
const worktreePath = join3(projectDir, name);
|
|
1056
1129
|
createWorktree(name, worktreePath);
|
|
1057
1130
|
results.push({ path: worktreePath, branch: name });
|
|
1058
1131
|
logger.info(`worktree \u521B\u5EFA\u5B8C\u6210: ${worktreePath} (\u5206\u652F: ${name})`);
|
|
@@ -1065,7 +1138,7 @@ function createWorktreesByBranches(branchNames) {
|
|
|
1065
1138
|
ensureDir(projectDir);
|
|
1066
1139
|
const results = [];
|
|
1067
1140
|
for (const name of branchNames) {
|
|
1068
|
-
const worktreePath =
|
|
1141
|
+
const worktreePath = join3(projectDir, name);
|
|
1069
1142
|
createWorktree(name, worktreePath);
|
|
1070
1143
|
results.push({ path: worktreePath, branch: name });
|
|
1071
1144
|
logger.info(`worktree \u521B\u5EFA\u5B8C\u6210: ${worktreePath} (\u5206\u652F: ${name})`);
|
|
@@ -1087,7 +1160,7 @@ function getProjectWorktrees() {
|
|
|
1087
1160
|
if (!entry.isDirectory()) {
|
|
1088
1161
|
continue;
|
|
1089
1162
|
}
|
|
1090
|
-
const fullPath =
|
|
1163
|
+
const fullPath = join3(projectDir, entry.name);
|
|
1091
1164
|
if (registeredPaths.has(fullPath)) {
|
|
1092
1165
|
worktrees.push({
|
|
1093
1166
|
path: fullPath,
|
|
@@ -1173,7 +1246,7 @@ import Enquirer from "enquirer";
|
|
|
1173
1246
|
// src/utils/claude.ts
|
|
1174
1247
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
1175
1248
|
import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
|
|
1176
|
-
import { join as
|
|
1249
|
+
import { join as join4 } from "path";
|
|
1177
1250
|
|
|
1178
1251
|
// src/utils/terminal.ts
|
|
1179
1252
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -1252,7 +1325,7 @@ function encodeClaudeProjectPath(absolutePath) {
|
|
|
1252
1325
|
}
|
|
1253
1326
|
function hasClaudeSessionHistory(worktreePath) {
|
|
1254
1327
|
const encodedName = encodeClaudeProjectPath(worktreePath);
|
|
1255
|
-
const projectDir =
|
|
1328
|
+
const projectDir = join4(CLAUDE_PROJECTS_DIR, encodedName);
|
|
1256
1329
|
if (!existsSync6(projectDir)) {
|
|
1257
1330
|
return false;
|
|
1258
1331
|
}
|
|
@@ -1309,13 +1382,13 @@ function launchInteractiveClaudeInNewTerminal(worktree, hasPreviousSession) {
|
|
|
1309
1382
|
}
|
|
1310
1383
|
|
|
1311
1384
|
// src/utils/validate-snapshot.ts
|
|
1312
|
-
import { join as
|
|
1313
|
-
import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync4, rmdirSync as rmdirSync2, statSync } from "fs";
|
|
1385
|
+
import { join as join5 } from "path";
|
|
1386
|
+
import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync4, rmdirSync as rmdirSync2, statSync as statSync2 } from "fs";
|
|
1314
1387
|
function getSnapshotPath(projectName, branchName) {
|
|
1315
|
-
return
|
|
1388
|
+
return join5(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
|
|
1316
1389
|
}
|
|
1317
1390
|
function getSnapshotHeadPath(projectName, branchName) {
|
|
1318
|
-
return
|
|
1391
|
+
return join5(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
|
|
1319
1392
|
}
|
|
1320
1393
|
function hasSnapshot(projectName, branchName) {
|
|
1321
1394
|
return existsSync7(getSnapshotPath(projectName, branchName));
|
|
@@ -1323,7 +1396,7 @@ function hasSnapshot(projectName, branchName) {
|
|
|
1323
1396
|
function getSnapshotModifiedTime(projectName, branchName) {
|
|
1324
1397
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
1325
1398
|
if (!existsSync7(snapshotPath)) return null;
|
|
1326
|
-
const stat =
|
|
1399
|
+
const stat = statSync2(snapshotPath);
|
|
1327
1400
|
return stat.mtime.toISOString();
|
|
1328
1401
|
}
|
|
1329
1402
|
function readSnapshot(projectName, branchName) {
|
|
@@ -1337,7 +1410,7 @@ function readSnapshot(projectName, branchName) {
|
|
|
1337
1410
|
function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
|
|
1338
1411
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
1339
1412
|
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
1340
|
-
const snapshotDir =
|
|
1413
|
+
const snapshotDir = join5(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
1341
1414
|
ensureDir(snapshotDir);
|
|
1342
1415
|
writeFileSync2(snapshotPath, treeHash, "utf-8");
|
|
1343
1416
|
writeFileSync2(headPath, headCommitHash, "utf-8");
|
|
@@ -1356,7 +1429,7 @@ function removeSnapshot(projectName, branchName) {
|
|
|
1356
1429
|
}
|
|
1357
1430
|
}
|
|
1358
1431
|
function getProjectSnapshotBranches(projectName) {
|
|
1359
|
-
const projectDir =
|
|
1432
|
+
const projectDir = join5(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
1360
1433
|
if (!existsSync7(projectDir)) {
|
|
1361
1434
|
return [];
|
|
1362
1435
|
}
|
|
@@ -1364,13 +1437,13 @@ function getProjectSnapshotBranches(projectName) {
|
|
|
1364
1437
|
return files.filter((f) => f.endsWith(".tree")).map((f) => f.replace(/\.tree$/, ""));
|
|
1365
1438
|
}
|
|
1366
1439
|
function removeProjectSnapshots(projectName) {
|
|
1367
|
-
const projectDir =
|
|
1440
|
+
const projectDir = join5(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
1368
1441
|
if (!existsSync7(projectDir)) {
|
|
1369
1442
|
return;
|
|
1370
1443
|
}
|
|
1371
1444
|
const files = readdirSync4(projectDir);
|
|
1372
1445
|
for (const file of files) {
|
|
1373
|
-
unlinkSync(
|
|
1446
|
+
unlinkSync(join5(projectDir, file));
|
|
1374
1447
|
}
|
|
1375
1448
|
try {
|
|
1376
1449
|
rmdirSync2(projectDir);
|
|
@@ -2169,7 +2242,7 @@ async function executeBatchTasks(worktrees, tasks, concurrency) {
|
|
|
2169
2242
|
|
|
2170
2243
|
// src/utils/dry-run.ts
|
|
2171
2244
|
import chalk4 from "chalk";
|
|
2172
|
-
import { join as
|
|
2245
|
+
import { join as join6 } from "path";
|
|
2173
2246
|
var DRY_RUN_TASK_DESC_MAX_LENGTH = 80;
|
|
2174
2247
|
function truncateTaskDesc(task) {
|
|
2175
2248
|
const oneLine = task.replace(/\n/g, " ").trim();
|
|
@@ -2197,7 +2270,7 @@ function printDryRunPreview(branchNames, tasks, concurrency) {
|
|
|
2197
2270
|
let hasConflict = false;
|
|
2198
2271
|
for (let i = 0; i < branchNames.length; i++) {
|
|
2199
2272
|
const branch = branchNames[i];
|
|
2200
|
-
const worktreePath =
|
|
2273
|
+
const worktreePath = join6(projectDir, branch);
|
|
2201
2274
|
const exists = checkBranchExists(branch);
|
|
2202
2275
|
if (exists) hasConflict = true;
|
|
2203
2276
|
const indexLabel = `[${i + 1}/${branchNames.length}]`;
|
|
@@ -3560,8 +3633,187 @@ function registerAliasCommand(program2) {
|
|
|
3560
3633
|
});
|
|
3561
3634
|
}
|
|
3562
3635
|
|
|
3636
|
+
// src/commands/projects.ts
|
|
3637
|
+
import { existsSync as existsSync9, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
3638
|
+
import { join as join7 } from "path";
|
|
3639
|
+
import chalk11 from "chalk";
|
|
3640
|
+
function registerProjectsCommand(program2) {
|
|
3641
|
+
program2.command("projects [name]").description("\u5C55\u793A\u6240\u6709\u9879\u76EE\u7684 worktree \u6982\u89C8\uFF0C\u6216\u67E5\u770B\u6307\u5B9A\u9879\u76EE\u7684 worktree \u8BE6\u60C5").option("--json", "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA").action((name, options) => {
|
|
3642
|
+
handleProjects({ name, json: options.json });
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
function handleProjects(options) {
|
|
3646
|
+
if (options.name) {
|
|
3647
|
+
handleProjectDetail(options.name, options.json);
|
|
3648
|
+
} else {
|
|
3649
|
+
handleProjectsOverview(options.json);
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
function handleProjectsOverview(json) {
|
|
3653
|
+
const result = collectProjectsOverview();
|
|
3654
|
+
logger.info(`projects \u547D\u4EE4\u6267\u884C\uFF0C\u5171 ${result.totalProjects} \u4E2A\u9879\u76EE`);
|
|
3655
|
+
if (json) {
|
|
3656
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
printProjectsOverviewAsText(result);
|
|
3660
|
+
}
|
|
3661
|
+
function handleProjectDetail(name, json) {
|
|
3662
|
+
const projectDir = join7(WORKTREES_DIR, name);
|
|
3663
|
+
if (!existsSync9(projectDir)) {
|
|
3664
|
+
printError(MESSAGES.PROJECTS_NOT_FOUND(name));
|
|
3665
|
+
process.exit(1);
|
|
3666
|
+
}
|
|
3667
|
+
const result = collectProjectDetail(name, projectDir);
|
|
3668
|
+
logger.info(`projects \u547D\u4EE4\u6267\u884C\uFF0C\u9879\u76EE: ${name}\uFF0C\u5171 ${result.worktrees.length} \u4E2A worktree`);
|
|
3669
|
+
if (json) {
|
|
3670
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3671
|
+
return;
|
|
3672
|
+
}
|
|
3673
|
+
printProjectDetailAsText(result);
|
|
3674
|
+
}
|
|
3675
|
+
function collectProjectsOverview() {
|
|
3676
|
+
if (!existsSync9(WORKTREES_DIR)) {
|
|
3677
|
+
return { projects: [], totalProjects: 0, totalDiskUsage: 0 };
|
|
3678
|
+
}
|
|
3679
|
+
const entries = readdirSync5(WORKTREES_DIR, { withFileTypes: true });
|
|
3680
|
+
const projects = [];
|
|
3681
|
+
for (const entry of entries) {
|
|
3682
|
+
if (!entry.isDirectory()) {
|
|
3683
|
+
continue;
|
|
3684
|
+
}
|
|
3685
|
+
const projectDir = join7(WORKTREES_DIR, entry.name);
|
|
3686
|
+
const overview = collectSingleProjectOverview(entry.name, projectDir);
|
|
3687
|
+
projects.push(overview);
|
|
3688
|
+
}
|
|
3689
|
+
sortByLastActiveTimeDesc(projects);
|
|
3690
|
+
const totalDiskUsage = projects.reduce((sum, p) => sum + p.diskUsage, 0);
|
|
3691
|
+
return {
|
|
3692
|
+
projects,
|
|
3693
|
+
totalProjects: projects.length,
|
|
3694
|
+
totalDiskUsage
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3697
|
+
function collectSingleProjectOverview(name, projectDir) {
|
|
3698
|
+
const subEntries = readdirSync5(projectDir, { withFileTypes: true });
|
|
3699
|
+
const worktreeDirs = subEntries.filter((e) => e.isDirectory());
|
|
3700
|
+
const worktreeCount = worktreeDirs.length;
|
|
3701
|
+
const diskUsage = calculateDirSize(projectDir);
|
|
3702
|
+
const lastActiveTime = resolveProjectLastActiveTime(projectDir, worktreeDirs.map((e) => join7(projectDir, e.name)));
|
|
3703
|
+
return {
|
|
3704
|
+
name,
|
|
3705
|
+
worktreeCount,
|
|
3706
|
+
lastActiveTime,
|
|
3707
|
+
diskUsage
|
|
3708
|
+
};
|
|
3709
|
+
}
|
|
3710
|
+
function collectProjectDetail(name, projectDir) {
|
|
3711
|
+
const subEntries = readdirSync5(projectDir, { withFileTypes: true });
|
|
3712
|
+
const worktrees = [];
|
|
3713
|
+
for (const entry of subEntries) {
|
|
3714
|
+
if (!entry.isDirectory()) {
|
|
3715
|
+
continue;
|
|
3716
|
+
}
|
|
3717
|
+
const wtPath = join7(projectDir, entry.name);
|
|
3718
|
+
const detail = collectSingleWorktreeDetail(entry.name, wtPath);
|
|
3719
|
+
worktrees.push(detail);
|
|
3720
|
+
}
|
|
3721
|
+
worktrees.sort((a, b) => new Date(b.lastModifiedTime).getTime() - new Date(a.lastModifiedTime).getTime());
|
|
3722
|
+
const totalDiskUsage = worktrees.reduce((sum, wt) => sum + wt.diskUsage, 0);
|
|
3723
|
+
return {
|
|
3724
|
+
name,
|
|
3725
|
+
projectDir,
|
|
3726
|
+
worktrees,
|
|
3727
|
+
totalDiskUsage
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
function collectSingleWorktreeDetail(branch, wtPath) {
|
|
3731
|
+
const stat = statSync3(wtPath);
|
|
3732
|
+
const diskUsage = calculateDirSize(wtPath);
|
|
3733
|
+
return {
|
|
3734
|
+
branch,
|
|
3735
|
+
path: wtPath,
|
|
3736
|
+
lastModifiedTime: formatLocalISOString(stat.mtime),
|
|
3737
|
+
diskUsage
|
|
3738
|
+
};
|
|
3739
|
+
}
|
|
3740
|
+
function resolveProjectLastActiveTime(projectDir, worktreePaths) {
|
|
3741
|
+
let latestTime = statSync3(projectDir).mtime;
|
|
3742
|
+
for (const wtPath of worktreePaths) {
|
|
3743
|
+
try {
|
|
3744
|
+
const wtStat = statSync3(wtPath);
|
|
3745
|
+
if (wtStat.mtime > latestTime) {
|
|
3746
|
+
latestTime = wtStat.mtime;
|
|
3747
|
+
}
|
|
3748
|
+
} catch {
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
return formatLocalISOString(latestTime);
|
|
3752
|
+
}
|
|
3753
|
+
function sortByLastActiveTimeDesc(projects) {
|
|
3754
|
+
projects.sort((a, b) => new Date(b.lastActiveTime).getTime() - new Date(a.lastActiveTime).getTime());
|
|
3755
|
+
}
|
|
3756
|
+
function printProjectsOverviewAsText(result) {
|
|
3757
|
+
printDoubleSeparator();
|
|
3758
|
+
printInfo(` ${chalk11.bold.cyan(MESSAGES.PROJECTS_OVERVIEW_TITLE)}`);
|
|
3759
|
+
printDoubleSeparator();
|
|
3760
|
+
printInfo("");
|
|
3761
|
+
if (result.projects.length === 0) {
|
|
3762
|
+
printInfo(` ${MESSAGES.PROJECTS_NO_PROJECTS}`);
|
|
3763
|
+
printInfo("");
|
|
3764
|
+
printDoubleSeparator();
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3767
|
+
for (const project of result.projects) {
|
|
3768
|
+
printProjectOverviewItem(project);
|
|
3769
|
+
}
|
|
3770
|
+
printSeparator();
|
|
3771
|
+
printInfo("");
|
|
3772
|
+
printInfo(` \u5171 ${chalk11.bold(String(result.totalProjects))} \u4E2A\u9879\u76EE ${chalk11.gray(MESSAGES.PROJECTS_TOTAL_DISK_USAGE(formatDiskSize(result.totalDiskUsage)))}`);
|
|
3773
|
+
printInfo("");
|
|
3774
|
+
printDoubleSeparator();
|
|
3775
|
+
}
|
|
3776
|
+
function printProjectOverviewItem(project) {
|
|
3777
|
+
const relativeTime = formatRelativeTime(project.lastActiveTime);
|
|
3778
|
+
const activeLabel = relativeTime ? MESSAGES.PROJECTS_LAST_ACTIVE(relativeTime) : "";
|
|
3779
|
+
const diskLabel = MESSAGES.PROJECTS_DISK_USAGE(formatDiskSize(project.diskUsage));
|
|
3780
|
+
printInfo(` ${chalk11.bold("\u25CF")} ${chalk11.bold(project.name)}`);
|
|
3781
|
+
printInfo(` ${MESSAGES.PROJECTS_WORKTREE_COUNT(project.worktreeCount)} ${chalk11.gray(activeLabel)} ${chalk11.gray(diskLabel)}`);
|
|
3782
|
+
printInfo("");
|
|
3783
|
+
}
|
|
3784
|
+
function printProjectDetailAsText(result) {
|
|
3785
|
+
printDoubleSeparator();
|
|
3786
|
+
printInfo(` ${chalk11.bold.cyan(MESSAGES.PROJECTS_DETAIL_TITLE(result.name))}`);
|
|
3787
|
+
printDoubleSeparator();
|
|
3788
|
+
printInfo("");
|
|
3789
|
+
printInfo(` ${chalk11.bold("\u25C6")} ${chalk11.bold(MESSAGES.PROJECTS_PATH(result.projectDir))}`);
|
|
3790
|
+
printInfo(` ${MESSAGES.PROJECTS_TOTAL_DISK_USAGE(formatDiskSize(result.totalDiskUsage))}`);
|
|
3791
|
+
printInfo("");
|
|
3792
|
+
printSeparator();
|
|
3793
|
+
printInfo("");
|
|
3794
|
+
if (result.worktrees.length === 0) {
|
|
3795
|
+
printInfo(` ${MESSAGES.PROJECTS_DETAIL_NO_WORKTREES}`);
|
|
3796
|
+
printInfo("");
|
|
3797
|
+
printDoubleSeparator();
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
for (const wt of result.worktrees) {
|
|
3801
|
+
printWorktreeDetailItem(wt);
|
|
3802
|
+
}
|
|
3803
|
+
printDoubleSeparator();
|
|
3804
|
+
}
|
|
3805
|
+
function printWorktreeDetailItem(wt) {
|
|
3806
|
+
const relativeTime = formatRelativeTime(wt.lastModifiedTime);
|
|
3807
|
+
const modifiedLabel = relativeTime ? MESSAGES.PROJECTS_LAST_MODIFIED(relativeTime) : "";
|
|
3808
|
+
const diskLabel = MESSAGES.PROJECTS_DISK_USAGE(formatDiskSize(wt.diskUsage));
|
|
3809
|
+
printInfo(` ${chalk11.bold("\u25CF")} ${chalk11.bold(wt.branch)}`);
|
|
3810
|
+
printInfo(` ${wt.path}`);
|
|
3811
|
+
printInfo(` ${chalk11.gray(modifiedLabel)} ${chalk11.gray(diskLabel)}`);
|
|
3812
|
+
printInfo("");
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3563
3815
|
// src/commands/completion.ts
|
|
3564
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as
|
|
3816
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync11 } from "fs";
|
|
3565
3817
|
import { resolve as resolve2 } from "path";
|
|
3566
3818
|
import { homedir as homedir2 } from "os";
|
|
3567
3819
|
|
|
@@ -3612,25 +3864,25 @@ compdef _clawt_completion clawt
|
|
|
3612
3864
|
}
|
|
3613
3865
|
|
|
3614
3866
|
// src/utils/completion-engine.ts
|
|
3615
|
-
import { existsSync as
|
|
3616
|
-
import { join as
|
|
3867
|
+
import { existsSync as existsSync10, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
3868
|
+
import { join as join8, dirname, basename as basename2 } from "path";
|
|
3617
3869
|
function completeFilePath(partial) {
|
|
3618
3870
|
const cwd = process.cwd();
|
|
3619
3871
|
const hasDir = partial.includes("/");
|
|
3620
|
-
const searchDir = hasDir ?
|
|
3872
|
+
const searchDir = hasDir ? join8(cwd, dirname(partial)) : cwd;
|
|
3621
3873
|
const prefix = hasDir ? basename2(partial) : partial;
|
|
3622
|
-
if (!
|
|
3874
|
+
if (!existsSync10(searchDir)) {
|
|
3623
3875
|
return [];
|
|
3624
3876
|
}
|
|
3625
|
-
const entries =
|
|
3877
|
+
const entries = readdirSync6(searchDir);
|
|
3626
3878
|
const results = [];
|
|
3627
3879
|
const dirPrefix = hasDir ? dirname(partial) + "/" : "";
|
|
3628
3880
|
for (const entry of entries) {
|
|
3629
3881
|
if (!entry.startsWith(prefix)) continue;
|
|
3630
3882
|
if (entry.startsWith(".")) continue;
|
|
3631
|
-
const fullPath =
|
|
3883
|
+
const fullPath = join8(searchDir, entry);
|
|
3632
3884
|
try {
|
|
3633
|
-
const stat =
|
|
3885
|
+
const stat = statSync4(fullPath);
|
|
3634
3886
|
if (stat.isDirectory()) {
|
|
3635
3887
|
results.push(dirPrefix + entry + "/");
|
|
3636
3888
|
} else if (stat.isFile()) {
|
|
@@ -3717,7 +3969,7 @@ function generateCompletions(program2, args) {
|
|
|
3717
3969
|
|
|
3718
3970
|
// src/commands/completion.ts
|
|
3719
3971
|
function appendToFile(filePath, content) {
|
|
3720
|
-
if (
|
|
3972
|
+
if (existsSync11(filePath)) {
|
|
3721
3973
|
const current = readFileSync5(filePath, "utf-8");
|
|
3722
3974
|
if (current.includes("clawt completion")) {
|
|
3723
3975
|
printInfo(MESSAGES.COMPLETION_INSTALL_EXISTS + ": " + filePath);
|
|
@@ -3795,6 +4047,7 @@ registerSyncCommand(program);
|
|
|
3795
4047
|
registerResetCommand(program);
|
|
3796
4048
|
registerStatusCommand(program);
|
|
3797
4049
|
registerAliasCommand(program);
|
|
4050
|
+
registerProjectsCommand(program);
|
|
3798
4051
|
registerCompletionCommand(program);
|
|
3799
4052
|
var config = loadConfig();
|
|
3800
4053
|
applyAliases(program, config.aliases);
|
package/dist/postinstall.js
CHANGED
|
@@ -364,6 +364,32 @@ var ALIAS_MESSAGES = {
|
|
|
364
364
|
ALIAS_LIST_TITLE: "\u5F53\u524D\u522B\u540D\u5217\u8868\uFF1A"
|
|
365
365
|
};
|
|
366
366
|
|
|
367
|
+
// src/constants/messages/projects.ts
|
|
368
|
+
var PROJECTS_MESSAGES = {
|
|
369
|
+
/** projects 命令全局概览标题 */
|
|
370
|
+
PROJECTS_OVERVIEW_TITLE: "\u9879\u76EE\u6982\u89C8",
|
|
371
|
+
/** projects 命令指定项目详情标题 */
|
|
372
|
+
PROJECTS_DETAIL_TITLE: (projectName) => `\u9879\u76EE\u8BE6\u60C5: ${projectName}`,
|
|
373
|
+
/** 无项目提示 */
|
|
374
|
+
PROJECTS_NO_PROJECTS: "(\u6682\u65E0\u9879\u76EE\uFF0Cworktrees \u76EE\u5F55\u4E3A\u7A7A)",
|
|
375
|
+
/** 项目不存在提示 */
|
|
376
|
+
PROJECTS_NOT_FOUND: (name) => `\u9879\u76EE ${name} \u4E0D\u5B58\u5728`,
|
|
377
|
+
/** worktree 数量标签 */
|
|
378
|
+
PROJECTS_WORKTREE_COUNT: (count) => `${count} \u4E2A worktree`,
|
|
379
|
+
/** 最近活跃时间标签 */
|
|
380
|
+
PROJECTS_LAST_ACTIVE: (relativeTime) => `\u6700\u8FD1\u6D3B\u8DC3: ${relativeTime}`,
|
|
381
|
+
/** 磁盘占用标签 */
|
|
382
|
+
PROJECTS_DISK_USAGE: (size) => `\u78C1\u76D8\u5360\u7528: ${size}`,
|
|
383
|
+
/** 总磁盘占用标签 */
|
|
384
|
+
PROJECTS_TOTAL_DISK_USAGE: (size) => `\u603B\u5360\u7528: ${size}`,
|
|
385
|
+
/** projects 详情无 worktree */
|
|
386
|
+
PROJECTS_DETAIL_NO_WORKTREES: "(\u8BE5\u9879\u76EE\u4E0B\u65E0 worktree)",
|
|
387
|
+
/** 路径标签 */
|
|
388
|
+
PROJECTS_PATH: (path) => `\u8DEF\u5F84: ${path}`,
|
|
389
|
+
/** 最后修改时间标签 */
|
|
390
|
+
PROJECTS_LAST_MODIFIED: (relativeTime) => `\u6700\u540E\u4FEE\u6539: ${relativeTime}`
|
|
391
|
+
};
|
|
392
|
+
|
|
367
393
|
// src/constants/messages/completion.ts
|
|
368
394
|
var COMPLETION_MESSAGES = {
|
|
369
395
|
/** completion 命令的主描述 */
|
|
@@ -400,6 +426,7 @@ var MESSAGES = {
|
|
|
400
426
|
...CONFIG_CMD_MESSAGES,
|
|
401
427
|
...STATUS_MESSAGES,
|
|
402
428
|
...ALIAS_MESSAGES,
|
|
429
|
+
...PROJECTS_MESSAGES,
|
|
403
430
|
...COMPLETION_MESSAGES
|
|
404
431
|
};
|
|
405
432
|
|