ccus-cli 0.1.11 → 0.1.12
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 -0
- package/dist/cli.js +11 -4
- package/dist/lib/sync.js +45 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -187,6 +187,8 @@ ccus aggregate serve --input-dir ./team-exports
|
|
|
187
187
|
```bash
|
|
188
188
|
ccus sync config --target ./team-exports # 配置同步目标目录(默认周期 3h)
|
|
189
189
|
ccus sync config --interval daily # 改周期,可写 3h(默认)/ daily / 6h / 30m
|
|
190
|
+
ccus sync config --suffix laptop # 给目标文件名加机器后缀,区分多台电脑
|
|
191
|
+
ccus sync config --no-suffix # 移除已配置的机器后缀
|
|
190
192
|
ccus sync config # 不带参数:打印当前配置
|
|
191
193
|
ccus sync # 用已存配置立即同步一次
|
|
192
194
|
ccus sync install # 注册系统调度器:每周五 18:00 自动同步(Windows 直接创建计划任务)
|
|
@@ -199,6 +201,7 @@ ccus sync status # 查看目标目录、周期
|
|
|
199
201
|
- **目标目录按周建子目录**:子目录名形如 `2026_06_01_2026_06_07`(该周周一~周日,全下划线),不存在时自动创建。
|
|
200
202
|
- **周一归档上一周**:周一同步时会额外把刚结束的上一整周(`last-week`)导出并归档到对应的上一周子目录(周一是第一个能拿到完整上一周数据的日子);同一天内多次同步用 `sync-state.lastArchivedWeek` 去重,不重复归档。
|
|
201
203
|
- **复制语义**:本地 `data-dir/exports` 仍保留一份,目标目录再放一份,本地照旧可 `aggregate` / `dashboard`。导出产物与 `ccus export` 完全一致,目标目录可直接喂给 `ccus aggregate`。
|
|
204
|
+
- **机器后缀**:`ccus sync config --suffix laptop` 会在**目标目录副本**的文件名扩展名前加 `-laptop`(如 `…_to_2026-06-07-laptop.json.gz`),本地原文件名不变。同一个人多台电脑同步到同一目标目录时,靠后缀让各机器的文件互不覆盖、都保留下来,正好供 `ccus aggregate` 按 personKey 合并去重。用 `ccus sync config --no-suffix` 移除后缀。
|
|
202
205
|
- **配置与同步分离**:`ccus sync config` 只读写配置(`--target` / `--interval` / `--range` 写进数据目录下的 `sync-config.json`,可手编;不带参数则打印当前配置);`ccus sync` 只用已存配置执行一次同步。上次同步时间记录在 `sync-state.json`。
|
|
203
206
|
- **周期**:默认 `3h`(每 3 小时最多同步一次的滚动周期);也支持 `daily`(按**自然日**判断——同一天内不重复同步,跨到下一天才再同步)与 `<N>h` / `<N>m` 的滚动周期。
|
|
204
207
|
|
package/dist/cli.js
CHANGED
|
@@ -61,7 +61,7 @@ const update_check_1 = require("./lib/update-check");
|
|
|
61
61
|
const version_1 = require("./lib/version");
|
|
62
62
|
/** CLI 帮助信息保持简洁,方便直接挂到 README 或终端里查看。 */
|
|
63
63
|
function printHelp() {
|
|
64
|
-
process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH]\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 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] [--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 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`);
|
|
64
|
+
process.stdout.write(`ccus\n\nCommands:\n ccus install [--settings PATH] [--command CMD] [--data-dir PATH]\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 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 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`);
|
|
65
65
|
}
|
|
66
66
|
/** 一个轻量的参数解析器,当前命令面不复杂,没必要引入额外依赖。 */
|
|
67
67
|
function parseOptions(args) {
|
|
@@ -642,12 +642,14 @@ async function handleSync(options) {
|
|
|
642
642
|
const target = getStringOption(options, "target");
|
|
643
643
|
const interval = getStringOption(options, "interval");
|
|
644
644
|
const range = getStringOption(options, "range");
|
|
645
|
-
|
|
645
|
+
const suffix = getStringOption(options, "suffix");
|
|
646
|
+
if (target !== undefined || interval !== undefined || range !== undefined || suffix !== undefined) {
|
|
646
647
|
const current = (0, sync_1.readSyncConfig)(dataDir);
|
|
647
648
|
const next = {
|
|
648
649
|
targetDir: target !== undefined ? node_path_1.default.resolve(target) : current.targetDir,
|
|
649
650
|
intervalLabel: interval ?? current.intervalLabel,
|
|
650
651
|
range: range ?? current.range,
|
|
652
|
+
suffix: suffix !== undefined ? (0, sync_1.sanitizeSuffix)(suffix) : current.suffix,
|
|
651
653
|
};
|
|
652
654
|
await (0, sync_1.writeSyncConfig)(dataDir, next);
|
|
653
655
|
(0, debug_1.debugLog)("sync", "config updated", next);
|
|
@@ -674,6 +676,7 @@ async function handleSyncStatus(options) {
|
|
|
674
676
|
`目标目录: ${config.targetDir ?? "(未配置)"}`,
|
|
675
677
|
`同步周期: ${config.intervalLabel}`,
|
|
676
678
|
`导出范围: ${config.range}`,
|
|
679
|
+
`文件后缀: ${config.suffix ?? "(无)"}`,
|
|
677
680
|
`上次同步: ${state?.lastSyncedAt ?? "(从未)"}${state?.lastResult ? ` [${state.lastResult}]` : ""}`,
|
|
678
681
|
state?.lastArchivedWeek ? `已归档上一周: ${state.lastArchivedWeek}` : null,
|
|
679
682
|
state?.lastError ? `上次错误: ${state.lastError}` : null,
|
|
@@ -692,19 +695,23 @@ async function handleSyncConfig(options) {
|
|
|
692
695
|
const target = getStringOption(options, "target");
|
|
693
696
|
const interval = getStringOption(options, "interval");
|
|
694
697
|
const range = getStringOption(options, "range");
|
|
698
|
+
const suffix = getStringOption(options, "suffix");
|
|
699
|
+
const clearSuffix = getBooleanOption(options, "no-suffix");
|
|
695
700
|
const current = (0, sync_1.readSyncConfig)(dataDir);
|
|
696
|
-
const changed = target !== undefined || interval !== undefined || range !== undefined;
|
|
701
|
+
const changed = target !== undefined || interval !== undefined || range !== undefined || suffix !== undefined || clearSuffix;
|
|
697
702
|
const next = {
|
|
698
703
|
targetDir: target !== undefined ? node_path_1.default.resolve(target) : current.targetDir,
|
|
699
704
|
intervalLabel: interval ?? current.intervalLabel,
|
|
700
705
|
range: range ?? current.range,
|
|
706
|
+
// --no-suffix 优先清除;否则有 --suffix 就更新,没有就保持原值。
|
|
707
|
+
suffix: clearSuffix ? null : suffix !== undefined ? (0, sync_1.sanitizeSuffix)(suffix) : current.suffix,
|
|
701
708
|
};
|
|
702
709
|
if (changed) {
|
|
703
710
|
await (0, sync_1.writeSyncConfig)(dataDir, next);
|
|
704
711
|
(0, debug_1.debugLog)("sync", "config updated", next);
|
|
705
712
|
}
|
|
706
713
|
const header = changed ? "同步配置已更新:" : "当前同步配置:";
|
|
707
|
-
process.stdout.write(`${header}\n 目标目录: ${next.targetDir ?? "(未配置)"}\n 同步周期: ${next.intervalLabel}\n 导出范围: ${next.range}\n`);
|
|
714
|
+
process.stdout.write(`${header}\n 目标目录: ${next.targetDir ?? "(未配置)"}\n 同步周期: ${next.intervalLabel}\n 导出范围: ${next.range}\n 文件后缀: ${next.suffix ?? "(无)"}\n`);
|
|
708
715
|
}
|
|
709
716
|
/**
|
|
710
717
|
* `ccus sync install`:安装一个系统调度器,每周五 18:00 跑一次 `ccus sync`。
|
package/dist/lib/sync.js
CHANGED
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.parseSyncInterval = parseSyncInterval;
|
|
7
|
+
exports.sanitizeSuffix = sanitizeSuffix;
|
|
8
|
+
exports.applyFileSuffix = applyFileSuffix;
|
|
7
9
|
exports.readSyncConfig = readSyncConfig;
|
|
8
10
|
exports.writeSyncConfig = writeSyncConfig;
|
|
9
11
|
exports.readSyncStateSync = readSyncStateSync;
|
|
@@ -40,6 +42,41 @@ function parseSyncInterval(label) {
|
|
|
40
42
|
// 无法识别的标签退回每天语义,保持「默认每天」的承诺。
|
|
41
43
|
return { kind: "daily" };
|
|
42
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* 清洗用户传入的文件名后缀:去首尾分隔、把非法字符折叠成连字符。
|
|
47
|
+
*
|
|
48
|
+
* 只保留字母数字与 `._-`,避免生成非法/危险的文件名;清洗后为空则返回 null。
|
|
49
|
+
*/
|
|
50
|
+
function sanitizeSuffix(raw) {
|
|
51
|
+
if (raw === undefined) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const cleaned = raw
|
|
55
|
+
.trim()
|
|
56
|
+
.replaceAll(/[^a-zA-Z0-9._-]+/g, "-")
|
|
57
|
+
.replaceAll(/-+/g, "-")
|
|
58
|
+
.replaceAll(/^[-_.]+|[-_.]+$/g, "");
|
|
59
|
+
return cleaned === "" ? null : cleaned;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 把后缀插到文件名的扩展名之前(`.json.gz` / `.json` 视为整体扩展名)。
|
|
63
|
+
*
|
|
64
|
+
* 例如 `alice_export_2026-06-01_to_2026-06-07.json.gz` + `laptop`
|
|
65
|
+
* → `alice_export_2026-06-01_to_2026-06-07-laptop.json.gz`。
|
|
66
|
+
*/
|
|
67
|
+
function applyFileSuffix(fileName, suffix) {
|
|
68
|
+
if (!suffix) {
|
|
69
|
+
return fileName;
|
|
70
|
+
}
|
|
71
|
+
const tag = `-${suffix}`;
|
|
72
|
+
for (const ext of [".json.gz", ".json"]) {
|
|
73
|
+
if (fileName.endsWith(ext)) {
|
|
74
|
+
return `${fileName.slice(0, fileName.length - ext.length)}${tag}${ext}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const ext = node_path_1.default.extname(fileName);
|
|
78
|
+
return `${fileName.slice(0, fileName.length - ext.length)}${tag}${ext}`;
|
|
79
|
+
}
|
|
43
80
|
/** 把同一本地日映射成稳定日键,用于 daily 周期的「是否跨天」判断。 */
|
|
44
81
|
function localDayKey(date) {
|
|
45
82
|
const year = date.getFullYear();
|
|
@@ -53,7 +90,7 @@ function localDayKey(date) {
|
|
|
53
90
|
* statusline 兜底路径需要同步、极快、绝不抛错,所以这里用同步 IO。
|
|
54
91
|
*/
|
|
55
92
|
function readSyncConfig(dataDir) {
|
|
56
|
-
const fallback = { targetDir: null, intervalLabel: DEFAULT_INTERVAL_LABEL, range: DEFAULT_RANGE };
|
|
93
|
+
const fallback = { targetDir: null, intervalLabel: DEFAULT_INTERVAL_LABEL, range: DEFAULT_RANGE, suffix: null };
|
|
57
94
|
try {
|
|
58
95
|
const raw = node_fs_1.default.readFileSync((0, paths_1.getSyncConfigPath)(dataDir), "utf8");
|
|
59
96
|
const parsed = JSON.parse(raw);
|
|
@@ -61,6 +98,7 @@ function readSyncConfig(dataDir) {
|
|
|
61
98
|
targetDir: typeof parsed.targetDir === "string" && parsed.targetDir.trim() !== "" ? parsed.targetDir : null,
|
|
62
99
|
intervalLabel: typeof parsed.intervalLabel === "string" && parsed.intervalLabel.trim() !== "" ? parsed.intervalLabel : DEFAULT_INTERVAL_LABEL,
|
|
63
100
|
range: typeof parsed.range === "string" && parsed.range.trim() !== "" ? parsed.range : DEFAULT_RANGE,
|
|
101
|
+
suffix: typeof parsed.suffix === "string" && parsed.suffix.trim() !== "" ? parsed.suffix : null,
|
|
64
102
|
};
|
|
65
103
|
}
|
|
66
104
|
catch {
|
|
@@ -126,14 +164,15 @@ function isSyncDue(config, state, now = new Date()) {
|
|
|
126
164
|
*
|
|
127
165
|
* 复制语义:本地 exports 仍保留一份,目标目录再放一份。周目录名按导出窗口的周一~周日推导。
|
|
128
166
|
*/
|
|
129
|
-
async function exportAndCopy(dataDir, runExport, targetDir, range) {
|
|
167
|
+
async function exportAndCopy(dataDir, runExport, targetDir, range, suffix) {
|
|
130
168
|
const { outputPath, window } = await runExport({ "data-dir": dataDir, range });
|
|
131
169
|
const weekDir = (0, time_1.formatWeekDirName)(window.start, window.end);
|
|
132
170
|
const targetWeekDir = node_path_1.default.join(targetDir, weekDir);
|
|
133
171
|
await promises_1.default.mkdir(targetWeekDir, { recursive: true });
|
|
134
|
-
|
|
172
|
+
// 后缀只加在目标目录的副本上,本地 exports 原文件名保持不变。
|
|
173
|
+
const destPath = node_path_1.default.join(targetWeekDir, applyFileSuffix(node_path_1.default.basename(outputPath), suffix));
|
|
135
174
|
await promises_1.default.copyFile(outputPath, destPath);
|
|
136
|
-
(0, debug_1.debugLog)("sync", "copied bundle", { range, outputPath, destPath, weekDir });
|
|
175
|
+
(0, debug_1.debugLog)("sync", "copied bundle", { range, outputPath, destPath, weekDir, suffix });
|
|
137
176
|
return { outputPath, destPath, weekDir, targetWeekDir };
|
|
138
177
|
}
|
|
139
178
|
/**
|
|
@@ -152,14 +191,14 @@ async function performSync(dataDir, runExport, now = new Date()) {
|
|
|
152
191
|
const state = readSyncStateSync(dataDir);
|
|
153
192
|
let lastArchivedWeek = state?.lastArchivedWeek;
|
|
154
193
|
try {
|
|
155
|
-
const primary = await exportAndCopy(dataDir, runExport, config.targetDir, config.range);
|
|
194
|
+
const primary = await exportAndCopy(dataDir, runExport, config.targetDir, config.range, config.suffix);
|
|
156
195
|
// 周一(getDay() === 1)是上一周结束后第一个能拿到完整数据的日子:顺带归档 last-week。
|
|
157
196
|
let archivedLastWeekDest = null;
|
|
158
197
|
if (now.getDay() === 1) {
|
|
159
198
|
const lastWeek = (0, time_1.resolveRange)("last-week", now);
|
|
160
199
|
const lastWeekDir = (0, time_1.formatWeekDirName)(lastWeek.start, lastWeek.end);
|
|
161
200
|
if (lastArchivedWeek !== lastWeekDir) {
|
|
162
|
-
const archived = await exportAndCopy(dataDir, runExport, config.targetDir, "last-week");
|
|
201
|
+
const archived = await exportAndCopy(dataDir, runExport, config.targetDir, "last-week", config.suffix);
|
|
163
202
|
lastArchivedWeek = archived.weekDir;
|
|
164
203
|
archivedLastWeekDest = archived.destPath;
|
|
165
204
|
(0, debug_1.debugLog)("sync", "archived last week", { weekDir: archived.weekDir, destPath: archived.destPath });
|