@shellus/way 0.3.1 → 0.4.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 CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  推荐在所有地方使用策略备份:开发环境、个人电脑、生产服务器。
6
6
 
7
+ ## 版本说明
8
+
9
+ - **v0.4.0+**: TypeScript 版本,使用 systemd,凭证直接写在 YAML
10
+ - **v0.3.x**: TypeScript 版本,使用 crontab,支持 .env
11
+ - **v0.2.x**: Bash 版本
12
+
7
13
  ## 设计原理
8
14
 
9
15
  ### 1. 备份目的
@@ -40,6 +46,11 @@
40
46
 
41
47
  假设本机被入侵,攻击者不应能通过本机凭证定位或删除备份数据。
42
48
 
49
+ **v0.4.0 安全增强**:
50
+ - 使用 systemd 替代 crontab(避免在 crontab 留下痕迹)
51
+ - 移除 .env 文件支持(避免被 `find .env` 发现)
52
+ - 凭证直接写在 `~/.way/repositories.yaml`(权限 600)
53
+
43
54
  ### 9. 多层冗余原则
44
55
 
45
56
  单一备份链路存在单点故障风险。应在多个维度建立冗余:
@@ -55,52 +66,95 @@
55
66
  ## 依赖
56
67
 
57
68
  - [restic](https://restic.net/) - 备份引擎
58
- - Node.js >= 18(npm 安装方式)或 yq(脚本安装方式)
69
+ - Node.js >= 18
59
70
 
60
- npm 安装方式会自动处理依赖,脚本安装方式的 install.sh 会自动安装 restic 和 yq。
71
+ ## 安装
72
+
73
+ ```bash
74
+ npm install -g @shellus/way
75
+ ```
61
76
 
62
77
  ## 快速开始
63
78
 
64
- ### 方式 1: npm 安装(推荐)
79
+ ### 1. 初始化配置
65
80
 
66
81
  ```bash
67
- npm install -g @shellus/way
68
-
69
- # 初始化配置
70
82
  mkdir -p ~/.way
71
83
  cp $(npm root -g)/@shellus/way/repositories.yaml.example ~/.way/repositories.yaml
72
84
  cp $(npm root -g)/@shellus/way/rules.yaml.example ~/.way/rules.yaml
73
85
  ```
74
86
 
75
- ### 方式 2: 脚本安装
87
+ 编辑 `~/.way/repositories.yaml` 填入实际凭证,设置权限:
88
+
89
+ ```bash
90
+ chmod 600 ~/.way/repositories.yaml
91
+ ```
92
+
93
+ ### 2. 初始化 restic 仓库
94
+
95
+ ```bash
96
+ # 本地仓库
97
+ way init
98
+
99
+ # S3 仓库(指定其他仓库)
100
+ way --remote=s3 init
101
+ ```
102
+
103
+ ### 3. 配置定时备份
76
104
 
77
105
  ```bash
78
- # 安装(way + restic + yq)
79
- curl -fsSL https://github.com/shellus/way/releases/latest/download/install.sh | bash
106
+ way systemd install
107
+ way systemd status
80
108
  ```
81
109
 
82
- ### 使用
110
+ ### 4. 手动执行备份
83
111
 
84
112
  ```bash
113
+ way run # 备份所有项目
114
+ way run data # 只备份 data 项目
115
+ way snapshots # 查看快照
116
+ ```
117
+
118
+ ## 命令参考
85
119
 
86
- # way 自己的命令(不与 restic 冲突)
120
+ ```bash
121
+ # 备份命令
87
122
  way run # 执行备份(读取 rules.yaml 的项目和排除规则)
88
123
  way run data # 只备份 data 项目
89
124
  way gc # 按 retention 策略清理旧快照
90
- way cron install # 安装定时任务
91
- way cron show # 显示将要安装的 crontab
125
+
126
+ # systemd 定时任务
127
+ way systemd install # 安装 systemd 定时任务
128
+ way systemd show # 显示 systemd 配置
129
+ way systemd status # 查看定时任务状态
130
+ way systemd uninstall # 卸载定时任务
92
131
 
93
132
  # 透传 restic(way 只设置环境变量)
94
133
  way snapshots # → restic snapshots
95
134
  way restore abc123 # → restic restore abc123
96
135
  way check # → restic check
97
136
  way stats # → restic stats
98
- way prune # → restic prune(直接透传)
99
137
 
100
138
  # 指定 repository(默认用 repositories.yaml 中的 default)
101
139
  way --remote=oss snapshots
102
140
  ```
103
141
 
142
+ ## 生命周期
143
+
144
+ ```mermaid
145
+ graph LR
146
+ A[way run] --> B[读取配置]
147
+ B --> C[遍历项目]
148
+ C --> D[执行 restic backup]
149
+ D --> E[推送 Uptime Kuma]
150
+
151
+ F[systemd timer] --> G[定时触发]
152
+ G --> A
153
+
154
+ H[way gc] --> I[restic forget]
155
+ I --> J[restic prune]
156
+ ```
157
+
104
158
  ---
105
159
 
106
160
  ## 配置说明
@@ -123,7 +177,7 @@ WAY_DIR=/path/to/config way snapshots
123
177
  备份规则配置,参考 [`rules.yaml`](rules.yaml):
124
178
 
125
179
  - **projects**: 备份项目、路径、专属排除规则
126
- - **schedule**: 备份时间、清理任务的 cron 表达式
180
+ - **schedule**: 备份时间(systemd timer OnCalendar 格式)
127
181
  - **retention**: 快照保留策略
128
182
  - **global_excludes**: 全局排除规则
129
183
 
@@ -142,43 +196,45 @@ restic 使用 Go 的 filepath.Match 语法:
142
196
 
143
197
  备份目的地配置,参考 [`repositories.yaml`](repositories.yaml)。
144
198
 
145
- ### .env
199
+ **v0.4.0 变更**:凭证直接写明文,不再支持 `${VAR}` 环境变量语法。
146
200
 
147
- 敏感凭证通过环境变量注入,在 `.env` 中定义:
148
-
149
- ```bash
150
- RESTIC_PASSWORD=your-restic-password
151
- AWS_ACCESS_KEY_ID=your-access-key
152
- AWS_SECRET_ACCESS_KEY=your-secret-key
201
+ ```yaml
202
+ repositories:
203
+ local:
204
+ type: local
205
+ path: /backup/repo
206
+ credentials:
207
+ password: your-password # 直接明文
153
208
  ```
154
209
 
155
- `repositories.yaml` 中使用 `${VAR}` 语法引用:
210
+ 建议设置文件权限:`chmod 600 ~/.way/repositories.yaml`
156
211
 
157
- ```yaml
158
- credentials:
159
- password: ${RESTIC_PASSWORD}
160
- access_key_id: ${AWS_ACCESS_KEY_ID}
161
- ```
212
+ ## 配置备份
162
213
 
163
- ## 凭证离线备份
214
+ `~/.way/` 目录包含所有配置和凭证,建议整体备份到安全位置:
164
215
 
165
- `repositories.yaml` 和 `.env` 包含 restic 仓库密码和云存储凭证。这两个文件被 `.gitignore` 排除,不会进入版本控制,也不会被 way 自身备份。
216
+ ```bash
217
+ # 打包配置目录
218
+ tar czf way-config-$(date +%Y%m%d).tar.gz -C ~ .way
219
+
220
+ # 加密备份(推荐)
221
+ gpg -c way-config-$(date +%Y%m%d).tar.gz
222
+ ```
166
223
 
167
- **如果本机不可访问且遗忘了这些凭证,所有备份数据将永久无法恢复。**
224
+ **如果本机不可访问且遗忘了配置和凭证,所有备份数据将永久无法恢复。**
168
225
 
169
- 必须在本机之外单独保存一份:
226
+ 必须在本机之外保存 `~/.way/` 备份:
170
227
 
171
228
  - 密码管理器(推荐)
172
- - 加密笔记
173
- - 离线存储(U 盘、纸质打印)
229
+ - 加密云存储
230
+ - 离线存储(U 盘、纸质打印关键凭证)
174
231
 
175
- 需要保存的信息:
232
+ 关键信息:
176
233
 
177
- | 项目 | 来源 |
234
+ | 项目 | 位置 |
178
235
  |------|------|
179
- | restic 仓库密码 | `.env` 中的 `RESTIC_PASSWORD` |
180
- | S3 访问凭证 | `.env` 中的 `AWS_ACCESS_KEY_ID`、`AWS_SECRET_ACCESS_KEY` |
181
- | 仓库地址 | `repositories.yaml` 中的 `url` |
236
+ | 备份规则 | `~/.way/rules.yaml` |
237
+ | 仓库配置和凭证 | `~/.way/repositories.yaml` |
182
238
 
183
239
  ## 数据恢复
184
240
 
@@ -222,4 +278,10 @@ way --remote=oss restore <snapshot-id> --target /tmp/restore
222
278
 
223
279
  ## 开发
224
280
 
225
- 参见 [CLAUDE.md](CLAUDE.md)
281
+ 参见 [CONTRIBUTING.md](CONTRIBUTING.md) 了解:
282
+ - 项目结构和技术栈
283
+ - 开发环境搭建
284
+ - 变更规范和测试要求
285
+ - 发布流程
286
+
287
+ 内部开发参考:[CLAUDE.md](CLAUDE.md)
package/dist/cli.js CHANGED
@@ -7,43 +7,14 @@ import { Command } from "commander";
7
7
  import fs from "fs";
8
8
  import path from "path";
9
9
  import yaml from "js-yaml";
10
- import dotenv from "dotenv";
11
-
12
- // src/core/env.ts
13
- function expandEnv(value) {
14
- if (!value) return "";
15
- const match = value.match(/^\$\{(.+)\}$/);
16
- if (match) {
17
- return process.env[match[1]] || "";
18
- }
19
- return value;
20
- }
21
-
22
- // src/core/config.ts
23
10
  function loadConfig(wayDir, remoteName) {
24
- const envPath = path.join(wayDir, ".env");
25
- if (fs.existsSync(envPath)) {
26
- dotenv.config({ path: envPath });
27
- }
28
11
  const repoFile = path.join(wayDir, "repositories.yaml");
29
12
  const repoConfig = yaml.load(fs.readFileSync(repoFile, "utf8"));
30
13
  const repoName = remoteName === "default" ? repoConfig.default : remoteName;
31
14
  const repository = repoConfig.repositories[repoName];
32
15
  if (!repository) throw new Error(`Repository not found: ${repoName}`);
33
- if (repository.credentials.password) {
34
- repository.credentials.password = expandEnv(repository.credentials.password);
35
- }
36
- if (repository.credentials.access_key_id) {
37
- repository.credentials.access_key_id = expandEnv(repository.credentials.access_key_id);
38
- }
39
- if (repository.credentials.secret_access_key) {
40
- repository.credentials.secret_access_key = expandEnv(repository.credentials.secret_access_key);
41
- }
42
16
  const rulesFile = path.join(wayDir, "rules.yaml");
43
17
  const rules = yaml.load(fs.readFileSync(rulesFile, "utf8"));
44
- if (rules.uptime_kuma?.push_url) {
45
- rules.uptime_kuma.push_url = expandEnv(rules.uptime_kuma.push_url);
46
- }
47
18
  return { repository, rules };
48
19
  }
49
20
 
@@ -82,7 +53,16 @@ function buildS3Options(repo) {
82
53
  return options;
83
54
  }
84
55
  async function execRestic(args, env, s3Options = []) {
85
- await execa("restic", [...s3Options, ...args], { env: { ...process.env, ...env }, stdio: "inherit" });
56
+ try {
57
+ await execa("restic", [...s3Options, ...args], { env: { ...process.env, ...env }, stdio: "inherit" });
58
+ } catch (error) {
59
+ if (error.code === "ENOENT") {
60
+ console.error("Error: restic not found. Please install restic first.");
61
+ console.error("Visit: https://restic.net/");
62
+ process.exit(1);
63
+ }
64
+ throw error;
65
+ }
86
66
  }
87
67
 
88
68
  // src/commands/run.ts
@@ -161,68 +141,112 @@ async function gc(options) {
161
141
  await execRestic(args, env, s3Options);
162
142
  }
163
143
 
164
- // src/commands/cron.ts
144
+ // src/commands/systemd.ts
165
145
  import { execSync } from "child_process";
166
146
  import fs2 from "fs";
167
- async function cron(options) {
147
+ import path2 from "path";
148
+ function cronToSystemd(cron) {
149
+ const parts = cron.trim().split(/\s+/);
150
+ if (parts.length !== 5) return "daily";
151
+ const [minute, hour, day, month, weekday] = parts;
152
+ if (day === "*" && month === "*" && weekday === "*") {
153
+ return `*-*-* ${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:00`;
154
+ }
155
+ return "daily";
156
+ }
157
+ async function systemd(options) {
168
158
  const wayDir = process.env.WAY_DIR || `${process.env.HOME}/.way`;
169
159
  const config = loadConfig(wayDir, options.remote);
170
- const markerStart = "# === way backup schedule ===";
171
- const markerEnd = "# === way backup schedule end ===";
172
- const generateCron = () => {
173
- const lines = [markerStart];
174
- const backupSchedules = config.rules.schedule?.backup || [];
175
- for (const schedule of backupSchedules) {
176
- lines.push(`${schedule} /usr/local/bin/way run`);
177
- }
178
- if (config.rules.schedule?.prune) {
179
- lines.push(`${config.rules.schedule.prune} /usr/local/bin/way gc`);
180
- }
181
- if (config.rules.schedule?.check) {
182
- lines.push(`${config.rules.schedule.check} /usr/local/bin/way check`);
183
- }
184
- lines.push(markerEnd);
185
- return lines.join("\n");
186
- };
160
+ const serviceContent = `[Unit]
161
+ Description=Way Backup Service
162
+ After=network.target
163
+
164
+ [Service]
165
+ Type=oneshot
166
+ ExecStart=/usr/local/bin/way run
167
+ Environment="WAY_DIR=${wayDir}"
168
+ `;
169
+ const timerContent = `[Unit]
170
+ Description=Way Backup Timer
171
+
172
+ [Timer]
173
+ OnCalendar=${cronToSystemd(config.rules.schedule?.backup?.[0] || "daily")}
174
+ Persistent=true
175
+
176
+ [Install]
177
+ WantedBy=timers.target
178
+ `;
187
179
  if (options.action === "show") {
188
- console.log(generateCron());
180
+ console.log("=== way-backup.service ===");
181
+ console.log(serviceContent);
182
+ console.log("\n=== way-backup.timer ===");
183
+ console.log(timerContent);
189
184
  return;
190
185
  }
186
+ const systemdDir = `${process.env.HOME}/.config/systemd/user`;
187
+ const servicePath = path2.join(systemdDir, "way-backup.service");
188
+ const timerPath = path2.join(systemdDir, "way-backup.timer");
191
189
  if (options.action === "install") {
192
- if (!fs2.existsSync("/usr/local/bin/way")) {
193
- throw new Error("Error: /usr/local/bin/way not found");
190
+ fs2.mkdirSync(systemdDir, { recursive: true });
191
+ fs2.writeFileSync(servicePath, serviceContent);
192
+ fs2.writeFileSync(timerPath, timerContent);
193
+ execSync("systemctl --user daemon-reload");
194
+ execSync("systemctl --user enable way-backup.timer");
195
+ execSync("systemctl --user start way-backup.timer");
196
+ console.log("Systemd timer installed and started");
197
+ execSync("systemctl --user status way-backup.timer", { stdio: "inherit" });
198
+ }
199
+ if (options.action === "uninstall") {
200
+ try {
201
+ execSync("systemctl --user stop way-backup.timer", { stdio: "ignore" });
202
+ } catch {
194
203
  }
195
- let existing = "";
196
204
  try {
197
- existing = execSync("crontab -l 2>/dev/null", { encoding: "utf8" });
205
+ execSync("systemctl --user disable way-backup.timer", { stdio: "ignore" });
198
206
  } catch {
199
207
  }
200
- const filtered = existing.split("\n").filter((line) => {
201
- return !line.includes(markerStart) && !line.includes(markerEnd);
202
- }).join("\n");
203
- const newCrontab = filtered + "\n" + generateCron();
204
- execSync("crontab -", { input: newCrontab });
205
- console.log("Crontab installed successfully");
206
- execSync('crontab -l | grep -A100 "' + markerStart + '" | head -20', { stdio: "inherit", shell: "/bin/bash" });
208
+ if (fs2.existsSync(servicePath)) fs2.unlinkSync(servicePath);
209
+ if (fs2.existsSync(timerPath)) fs2.unlinkSync(timerPath);
210
+ execSync("systemctl --user daemon-reload");
211
+ console.log("Systemd timer uninstalled");
212
+ }
213
+ if (options.action === "status") {
214
+ execSync("systemctl --user status way-backup.timer", { stdio: "inherit" });
207
215
  }
208
216
  }
209
217
 
210
218
  // src/cli.ts
211
219
  var program = new Command();
212
- program.name("way").version("0.3.1").description("\u7B56\u7565\u5907\u4EFD\u5DE5\u5177").option("--remote <name>", "\u6307\u5B9A\u4ED3\u5E93", "default");
213
- program.command("run [project]").description("\u6267\u884C\u5907\u4EFD").action(async (project, options, command) => {
220
+ program.name("way").version("0.4.0").description("\u7B56\u7565\u5907\u4EFD\u5DE5\u5177 - \u57FA\u4E8E restic \u7684\u7B56\u7565\u5C01\u88C5").option("--remote <name>", "\u6307\u5B9A\u4ED3\u5E93", "default").addHelpText("after", `
221
+ \u793A\u4F8B:
222
+ $ way run \u6267\u884C\u6240\u6709\u9879\u76EE\u5907\u4EFD
223
+ $ way run data \u53EA\u5907\u4EFD data \u9879\u76EE
224
+ $ way run --dry-run \u6A21\u62DF\u5907\u4EFD\uFF08\u4E0D\u5B9E\u9645\u5199\u5165\uFF09
225
+ $ way gc \u6E05\u7406\u65E7\u5FEB\u7167
226
+ $ way systemd install \u5B89\u88C5\u5B9A\u65F6\u4EFB\u52A1
227
+ $ way snapshots \u67E5\u770B\u5FEB\u7167\u5217\u8868
228
+ $ way --remote=s3 snapshots \u4F7F\u7528 s3 \u4ED3\u5E93
229
+
230
+ \u6587\u6863: https://github.com/shellus/way
231
+ `);
232
+ program.command("run [project]").description("\u6267\u884C\u5907\u4EFD").allowUnknownOption().allowExcessArguments().action(async (project, options, command) => {
214
233
  const remote = command.parent.opts().remote;
215
- const extraArgs = command.args.slice(1);
216
- await run({ remote, project, extraArgs });
234
+ const allArgs = command.parent.args.slice(1);
235
+ if (project?.startsWith("--")) {
236
+ await run({ remote, project: void 0, extraArgs: allArgs });
237
+ } else {
238
+ const extraArgs = project ? allArgs.slice(1) : allArgs;
239
+ await run({ remote, project, extraArgs });
240
+ }
217
241
  });
218
242
  program.command("gc").description("\u6E05\u7406\u65E7\u5FEB\u7167").allowUnknownOption().allowExcessArguments().action(async function() {
219
243
  const remote = this.parent.opts().remote;
220
244
  const extraArgs = this.parent.args.slice(1);
221
245
  await gc({ remote, extraArgs });
222
246
  });
223
- program.command("cron <action>").description("\u7BA1\u7406\u5B9A\u65F6\u4EFB\u52A1 (show|install)").action(async (action, options, command) => {
247
+ program.command("systemd <action>").description("\u7BA1\u7406 systemd \u5B9A\u65F6\u4EFB\u52A1 (show|install|uninstall|status)").action(async (action, options, command) => {
224
248
  const remote = command.parent.opts().remote;
225
- await cron({ remote, action });
249
+ await systemd({ remote, action });
226
250
  });
227
251
  program.command("env").description("\u663E\u793A\u73AF\u5883\u53D8\u91CF").action(() => {
228
252
  const env = Object.entries(process.env).sort(([a], [b]) => a.localeCompare(b));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shellus/way",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "将备份作为持续运营的项目,而非一次性任务。基于 restic 的策略封装。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,12 @@
22
22
  "type": "git",
23
23
  "url": "git+https://github.com/shellus/way.git"
24
24
  },
25
- "keywords": ["backup", "restic", "snapshot", "cron"],
25
+ "keywords": [
26
+ "backup",
27
+ "restic",
28
+ "snapshot",
29
+ "systemd"
30
+ ],
26
31
  "author": "shellus",
27
32
  "license": "MIT",
28
33
  "bugs": {
@@ -37,7 +42,6 @@
37
42
  },
38
43
  "dependencies": {
39
44
  "commander": "^14.0.3",
40
- "dotenv": "^17.3.1",
41
45
  "execa": "^9.6.1",
42
46
  "js-yaml": "^4.1.1"
43
47
  },
@@ -1,5 +1,5 @@
1
1
  # 备份目的地配置 (restic repositories)
2
- # 凭证支持直接填写或引用环境变量: ${ENV_VAR}
2
+ # 凭证直接填写明文(不再支持环境变量)
3
3
  # 复制为 repositories.yaml 并填入实际值
4
4
 
5
5
  default: local
@@ -9,7 +9,7 @@ repositories:
9
9
  type: local
10
10
  path: /tmp/way-backup
11
11
  credentials:
12
- password: ${RESTIC_PASSWORD}
12
+ password: test123
13
13
 
14
14
  s3:
15
15
  type: s3
@@ -18,13 +18,13 @@ repositories:
18
18
  options:
19
19
  bucket_lookup: dns
20
20
  credentials:
21
- password: ${RESTIC_PASSWORD}
22
- access_key_id: ${AWS_ACCESS_KEY_ID}
23
- secret_access_key: ${AWS_SECRET_ACCESS_KEY}
21
+ password: your-restic-password
22
+ access_key_id: your-access-key
23
+ secret_access_key: your-secret-key
24
24
 
25
25
  sftp:
26
26
  type: sftp
27
27
  host: backup.example.com
28
28
  path: /backup/myhost
29
29
  credentials:
30
- password: ${RESTIC_PASSWORD}
30
+ password: your-restic-password
@@ -4,14 +4,13 @@
4
4
 
5
5
  # Uptime Kuma 通知(备份完成后推送状态)
6
6
  uptime_kuma:
7
- push_url: "${UPTIME_KUMA_PUSH_URL}" # 支持环境变量
7
+ push_url: "https://uptime.example.com/api/push/xxxxx"
8
8
 
9
- # 备份时间 (cron格式,留空则不执行)
9
+ # 备份时间(cron 格式,会自动转换为 systemd OnCalendar)
10
10
  schedule:
11
11
  backup:
12
- - "0 12 * * *" # 每天中午
13
- - "0 18 * * *" # 每天傍晚
14
- prune: "0 4 * * 0" # 每周日凌晨4点清理旧快照
12
+ - "0 12 * * *" # 每天中午 12:00
13
+ prune: "0 4 * * 0" # 每周日凌晨 4:00 清理
15
14
  check: "" # 完整性检查,留空不执行
16
15
 
17
16
  # 快照保留策略
package/README.local.md DELETED
@@ -1,44 +0,0 @@
1
- # 本机环境配置
2
-
3
- ## 存储后端
4
-
5
- 腾讯云 COS(成都区域):
6
- - Endpoint: `cos.ap-chengdu.myqcloud.com`
7
- - Bucket: `xa-backup-1251087920`
8
- - 需要 `s3.bucket-lookup=dns` 选项
9
-
10
- ## 备份项目
11
-
12
- | 项目 | 路径 | 说明 |
13
- |------|------|------|
14
- | data | /data | Docker 服务、项目源码、开发工具配置 |
15
- | home | /root/.ssh, /root/.claude 等 | SSH 密钥、Claude 配置、Shell 配置 |
16
- | system | /etc/fstab | 系统挂载配置 |
17
-
18
- ## 排除规则备忘
19
-
20
- playwright-mcp 浏览器缓存目录名是 `browser-profile`(不是 `chrome-profile`)。
21
-
22
- ## 定时任务
23
-
24
- ```
25
- 1 12 * * * way run # 12:01 午休后
26
- 30 15 * * * way run # 15:30 下午
27
- 30 18 * * * way run # 18:30 下班前
28
- 0 4 * * 0 way gc # 每周日凌晨清理
29
- ```
30
-
31
- 安装:`way cron install`
32
-
33
- ## 验证命令
34
-
35
- ```bash
36
- # 查看快照
37
- way snapshots
38
-
39
- # 检查排除规则
40
- way ls <snapshot-id> | grep -E 'node_modules|Cache'
41
-
42
- # 测试备份
43
- way run --dry-run
44
- ```