ccus-cli 0.2.27 → 0.2.28
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 +3 -1
- package/dist/cli.js +15 -1
- package/dist/lib/sync.js +9 -1
- package/dist/lib/time.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -260,6 +260,7 @@ ccus sync config --suffix laptop # 给目标文件名加机器
|
|
|
260
260
|
ccus sync config --no-suffix # 移除已配置的机器后缀
|
|
261
261
|
ccus sync config # 不带参数:打印当前配置
|
|
262
262
|
ccus sync # 用已存配置立即同步一次
|
|
263
|
+
ccus sync --week 2026-09-28 # 重新同步该日期所在整周,仅本次生效
|
|
263
264
|
ccus sync install # 注册系统调度器:每周五 18:00 自动同步(Windows 直接创建计划任务)
|
|
264
265
|
ccus sync uninstall # 卸载系统调度器
|
|
265
266
|
ccus sync status # 查看目标目录、周期、上次同步时间、是否到期
|
|
@@ -271,7 +272,8 @@ ccus sync status # 查看目标目录、周期
|
|
|
271
272
|
- **周一归档上一周**:周一同步时会额外把刚结束的上一整周(`last-week`)导出并归档到对应的上一周子目录(周一是第一个能拿到完整上一周数据的日子);同一天内多次同步用 `sync-state.lastArchivedWeek` 去重,不重复归档。
|
|
272
273
|
- **复制语义**:本地 `data-dir/exports` 仍保留一份,目标目录再放一份,本地照旧可 `aggregate` / `dashboard`。导出产物与 `ccus export` 完全一致,目标目录可直接喂给 `ccus aggregate`。
|
|
273
274
|
- **机器后缀**:`ccus sync config --suffix laptop` 会在**目标目录副本**的文件名扩展名前加 `-laptop`(如 `…_to_2026-06-07-laptop.json.gz`),本地原文件名不变。同一个人多台电脑同步到同一目标目录时,靠后缀让各机器的文件互不覆盖、都保留下来,正好供 `ccus aggregate` 按 personKey 合并去重。用 `ccus sync config --no-suffix` 移除后缀。
|
|
274
|
-
-
|
|
275
|
+
- **补同步指定周**:`ccus sync --week YYYY-MM-DD` 接受周内任一天,按本地时区和已配置的统计日开始时间重新导出周一至周日的数据,再复制到已有目标目录。保留机器后缀,覆盖该周同名文件;不修改配置或自动同步状态,不额外归档上一周。不能与 `--target` / `--interval` / `--range` / `--suffix` / `--no-suffix` 混用。
|
|
276
|
+
- **配置与同步分离**:`ccus sync config` 只读写配置(`--target` / `--interval` / `--range` 写进数据目录下的 `sync-config.json`,可手编;不带参数则打印当前配置);`ccus sync` 按已存配置执行一次同步,直接传入配置参数时仍会先保存再同步。普通同步的上次时间记录在 `sync-state.json`。
|
|
275
277
|
- **周期**:默认 `3h`(每 3 小时最多同步一次的滚动周期);也支持 `daily`(按**自然日**判断——同一天内不重复同步,跨到下一天才再同步)与 `<N>h` / `<N>m` 的滚动周期。
|
|
276
278
|
|
|
277
279
|
### 两种触发方式
|
package/dist/cli.js
CHANGED
|
@@ -72,7 +72,7 @@ const version_1 = require("./lib/version");
|
|
|
72
72
|
/** CLI 帮助信息保持简洁,方便直接挂到 README 或终端里查看。 */
|
|
73
73
|
function printHelp() {
|
|
74
74
|
process.stdout.write("统计日配置:ccus config [--day-start 07:00] [--data-dir PATH]\n统计命令可用 --day-start HH:mm 临时覆盖,默认 07:00(本地时区)。\n\n");
|
|
75
|
-
process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH] (默认装 Claude statusLine;--codex 改写 Codex 的 ~/.codex/config.toml notify,配 --uninstall 移除)\n ccus statusline emit [--data-dir PATH] [--input FILE] [--no-store]\n ccus dashboard build [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard open [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard serve [--range today|this-week|last-week|5h] [--port 0] [--host 127.0.0.1] [--open] [--data-dir PATH]\n ccus export [RANGE] [--out FILE] [--data-dir PATH] (RANGE: this-week|tw, last-week|lw, today, 5h; e.g. ccus export lw)\n ccus sessions [RANGE] [--out FILE] [--data-dir PATH] (把 Claude 与 Codex 的活跃 session 打包成 zip,默认本周)\n ccus sessions repair [codex|claude] [--min-gap 3h] [--dry-run] (按末条记录时间修复 session 修改时间,默认仅跨天)\n ccus aggregate --input-dir DIR [--out-dir DIR]\n ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]\n ccus sync [--data-dir PATH]\n ccus sync config [--target DIR] [--interval 3h|daily|<N>h|<N>m] [--range this-week] [--suffix NAME | --no-suffix] [--data-dir PATH]\n ccus sync install [--print] [--data-dir PATH] (注册每周五 18:00 的系统调度器)\n ccus sync uninstall [--print] (卸载系统调度器)\n ccus sync status [--data-dir PATH]\n ccus api config [--enable|--disable] [--provider zhipu|custom] [--token-env NAME] [--token VAL] [--url URL] [--project P] [--organization O] [--ttl 5m] [--extractor-file FILE] [--data-dir PATH]\n ccus api test [--data-dir PATH] (立即拉取第三方额度并打印,验证配置是否生效)\n ccus api status [--data-dir PATH]\n ccus open [--data-dir PATH] [--print]\n ccus update [--data-dir PATH]\n ccus --version\n\nGlobal flags:\n --verbose | --debug | -v 输出详细调试日志到 stderr(等价于设置 CCUS_DEBUG=1),方便排查问题\n`);
|
|
75
|
+
process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH] (默认装 Claude statusLine;--codex 改写 Codex 的 ~/.codex/config.toml notify,配 --uninstall 移除)\n ccus statusline emit [--data-dir PATH] [--input FILE] [--no-store]\n ccus dashboard build [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard open [--range today|this-week|last-week|5h] [--out FILE] [--data-dir PATH]\n ccus dashboard serve [--range today|this-week|last-week|5h] [--port 0] [--host 127.0.0.1] [--open] [--data-dir PATH]\n ccus export [RANGE] [--out FILE] [--data-dir PATH] (RANGE: this-week|tw, last-week|lw, today, 5h; e.g. ccus export lw)\n ccus sessions [RANGE] [--out FILE] [--data-dir PATH] (把 Claude 与 Codex 的活跃 session 打包成 zip,默认本周)\n ccus sessions repair [codex|claude] [--min-gap 3h] [--dry-run] (按末条记录时间修复 session 修改时间,默认仅跨天)\n ccus aggregate --input-dir DIR [--out-dir DIR]\n ccus aggregate serve --input-dir DIR [--port 0] [--host 127.0.0.1]\n ccus sync [--week YYYY-MM-DD] [--data-dir PATH]\n ccus sync config [--target DIR] [--interval 3h|daily|<N>h|<N>m] [--range this-week] [--suffix NAME | --no-suffix] [--data-dir PATH]\n ccus sync install [--print] [--data-dir PATH] (注册每周五 18:00 的系统调度器)\n ccus sync uninstall [--print] (卸载系统调度器)\n ccus sync status [--data-dir PATH]\n ccus api config [--enable|--disable] [--provider zhipu|custom] [--token-env NAME] [--token VAL] [--url URL] [--project P] [--organization O] [--ttl 5m] [--extractor-file FILE] [--data-dir PATH]\n ccus api test [--data-dir PATH] (立即拉取第三方额度并打印,验证配置是否生效)\n ccus api status [--data-dir PATH]\n ccus open [--data-dir PATH] [--print]\n ccus update [--data-dir PATH]\n ccus --version\n\nGlobal flags:\n --verbose | --debug | -v 输出详细调试日志到 stderr(等价于设置 CCUS_DEBUG=1),方便排查问题\n`);
|
|
76
76
|
}
|
|
77
77
|
/** 一个轻量的参数解析器,当前命令面不复杂,没必要引入额外依赖。 */
|
|
78
78
|
function parseOptions(args) {
|
|
@@ -905,6 +905,18 @@ async function handleBackgroundCheckUpdate(options) {
|
|
|
905
905
|
async function handleSync(options) {
|
|
906
906
|
const startedAt = process.hrtime.bigint();
|
|
907
907
|
const dataDir = getDataDir(options);
|
|
908
|
+
if (options.week !== undefined) {
|
|
909
|
+
const week = getStringOption(options, "week");
|
|
910
|
+
if (!week)
|
|
911
|
+
throw new Error("--week 缺少日期,用法:ccus sync --week YYYY-MM-DD。");
|
|
912
|
+
if (["target", "interval", "range", "suffix", "no-suffix"].some(key => options[key] !== undefined)) {
|
|
913
|
+
throw new Error("--week 不能与同步配置参数同时使用;请先用 ccus sync config 配置目标目录和后缀。");
|
|
914
|
+
}
|
|
915
|
+
const result = await (0, sync_1.performSync)(dataDir, runExport, new Date(), week);
|
|
916
|
+
process.stdout.write(`已重新同步到 ${result.destPath}\n`);
|
|
917
|
+
process.stdout.write(`耗时:${(0, sync_1.formatSyncElapsed)(Number(process.hrtime.bigint() - startedAt) / 1_000_000)}\n`);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
908
920
|
const target = getStringOption(options, "target");
|
|
909
921
|
const interval = getStringOption(options, "interval");
|
|
910
922
|
const range = getStringOption(options, "range");
|
|
@@ -959,6 +971,8 @@ async function handleSyncStatus(options) {
|
|
|
959
971
|
* 不带任何参数时仅打印当前配置,方便确认。
|
|
960
972
|
*/
|
|
961
973
|
async function handleSyncConfig(options) {
|
|
974
|
+
if (options.week !== undefined)
|
|
975
|
+
throw new Error("--week 只用于本次同步,请使用 ccus sync --week YYYY-MM-DD。");
|
|
962
976
|
const dataDir = getDataDir(options);
|
|
963
977
|
const target = getStringOption(options, "target");
|
|
964
978
|
const interval = getStringOption(options, "interval");
|
package/dist/lib/sync.js
CHANGED
|
@@ -189,11 +189,19 @@ async function exportAndCopy(dataDir, runExport, targetDir, range, suffix) {
|
|
|
189
189
|
* 用 `sync-state.lastArchivedWeek` 去重,避免周一当天每次同步都重复归档。
|
|
190
190
|
* 失败时写 error 状态后向上抛,由调用方决定展示或静默。
|
|
191
191
|
*/
|
|
192
|
-
async function performSync(dataDir, runExport, now = new Date()) {
|
|
192
|
+
async function performSync(dataDir, runExport, now = new Date(), week) {
|
|
193
193
|
const config = readSyncConfig(dataDir);
|
|
194
194
|
if (!config.targetDir) {
|
|
195
195
|
throw new Error("未配置同步目标目录。请先运行 `ccus sync config --target DIR`。");
|
|
196
196
|
}
|
|
197
|
+
// 补同步独立执行,不推迟自动同步,也不触发周一归档。
|
|
198
|
+
if (week !== undefined) {
|
|
199
|
+
const dayStartMinutes = (0, time_1.parseDayStart)((await (0, config_1.readConfig)(dataDir)).dayStart);
|
|
200
|
+
const range = `week:${week}`;
|
|
201
|
+
(0, time_1.resolveRange)(range, now, dayStartMinutes);
|
|
202
|
+
const primary = await exportAndCopy(dataDir, runExport, config.targetDir, range, config.suffix);
|
|
203
|
+
return { ...primary, archivedLastWeekDest: null };
|
|
204
|
+
}
|
|
197
205
|
const state = readSyncStateSync(dataDir);
|
|
198
206
|
let lastArchivedWeek = state?.lastArchivedWeek;
|
|
199
207
|
try {
|
package/dist/lib/time.js
CHANGED
|
@@ -78,6 +78,23 @@ const RANGE_ALIASES = {
|
|
|
78
78
|
function resolveRange(range, now = new Date(), dayStartMinutes = exports.DEFAULT_DAY_START_MINUTES) {
|
|
79
79
|
const raw = (range ?? "5h").trim().toLowerCase();
|
|
80
80
|
const normalized = RANGE_ALIASES[raw] ?? raw;
|
|
81
|
+
if (normalized.startsWith("week:")) {
|
|
82
|
+
const dateKey = normalized.slice(5);
|
|
83
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateKey);
|
|
84
|
+
if (!match)
|
|
85
|
+
throw new Error("指定周的日期必须是 YYYY-MM-DD,例如 2026-09-28。");
|
|
86
|
+
const date = new Date(0);
|
|
87
|
+
date.setFullYear(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
88
|
+
date.setHours(0, dayStartMinutes, 0, 0);
|
|
89
|
+
if (localDateKey(date) !== dateKey)
|
|
90
|
+
throw new Error(`无效的周日期:${dateKey}。`);
|
|
91
|
+
return {
|
|
92
|
+
label: normalized,
|
|
93
|
+
start: startOfLocalWeek(date, dayStartMinutes),
|
|
94
|
+
end: endOfLocalWeek(date, dayStartMinutes),
|
|
95
|
+
dayStartMinutes,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
81
98
|
if (normalized === "today") {
|
|
82
99
|
return { label: "today", start: startOfReportingDay(now, dayStartMinutes), end: now, dayStartMinutes };
|
|
83
100
|
}
|