@shellus/way 0.3.1 → 0.4.1

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,113 @@ 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 wayPath = execSync("which way", { encoding: "utf-8" }).trim();
161
+ const serviceContent = `[Unit]
162
+ Description=Way Backup Service
163
+ After=network.target
164
+
165
+ [Service]
166
+ Type=oneshot
167
+ ExecStart=${wayPath} run
168
+ Environment="WAY_DIR=${wayDir}"
169
+ `;
170
+ const timerContent = `[Unit]
171
+ Description=Way Backup Timer
172
+
173
+ [Timer]
174
+ OnCalendar=${cronToSystemd(config.rules.schedule?.backup?.[0] || "daily")}
175
+ Persistent=true
176
+
177
+ [Install]
178
+ WantedBy=timers.target
179
+ `;
187
180
  if (options.action === "show") {
188
- console.log(generateCron());
181
+ console.log("=== way-backup.service ===");
182
+ console.log(serviceContent);
183
+ console.log("\n=== way-backup.timer ===");
184
+ console.log(timerContent);
189
185
  return;
190
186
  }
187
+ const systemdDir = `${process.env.HOME}/.config/systemd/user`;
188
+ const servicePath = path2.join(systemdDir, "way-backup.service");
189
+ const timerPath = path2.join(systemdDir, "way-backup.timer");
191
190
  if (options.action === "install") {
192
- if (!fs2.existsSync("/usr/local/bin/way")) {
193
- throw new Error("Error: /usr/local/bin/way not found");
191
+ fs2.mkdirSync(systemdDir, { recursive: true });
192
+ fs2.writeFileSync(servicePath, serviceContent);
193
+ fs2.writeFileSync(timerPath, timerContent);
194
+ execSync("systemctl --user daemon-reload");
195
+ execSync("systemctl --user enable way-backup.timer");
196
+ execSync("systemctl --user start way-backup.timer");
197
+ console.log("Systemd timer installed and started");
198
+ execSync("systemctl --user status way-backup.timer", { stdio: "inherit" });
199
+ }
200
+ if (options.action === "uninstall") {
201
+ try {
202
+ execSync("systemctl --user stop way-backup.timer", { stdio: "ignore" });
203
+ } catch {
194
204
  }
195
- let existing = "";
196
205
  try {
197
- existing = execSync("crontab -l 2>/dev/null", { encoding: "utf8" });
206
+ execSync("systemctl --user disable way-backup.timer", { stdio: "ignore" });
198
207
  } catch {
199
208
  }
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" });
209
+ if (fs2.existsSync(servicePath)) fs2.unlinkSync(servicePath);
210
+ if (fs2.existsSync(timerPath)) fs2.unlinkSync(timerPath);
211
+ execSync("systemctl --user daemon-reload");
212
+ console.log("Systemd timer uninstalled");
213
+ }
214
+ if (options.action === "status") {
215
+ execSync("systemctl --user status way-backup.timer", { stdio: "inherit" });
207
216
  }
208
217
  }
209
218
 
210
219
  // src/cli.ts
211
220
  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) => {
221
+ 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", `
222
+ \u793A\u4F8B:
223
+ $ way run \u6267\u884C\u6240\u6709\u9879\u76EE\u5907\u4EFD
224
+ $ way run data \u53EA\u5907\u4EFD data \u9879\u76EE
225
+ $ way run --dry-run \u6A21\u62DF\u5907\u4EFD\uFF08\u4E0D\u5B9E\u9645\u5199\u5165\uFF09
226
+ $ way gc \u6E05\u7406\u65E7\u5FEB\u7167
227
+ $ way systemd install \u5B89\u88C5\u5B9A\u65F6\u4EFB\u52A1
228
+ $ way snapshots \u67E5\u770B\u5FEB\u7167\u5217\u8868
229
+ $ way --remote=s3 snapshots \u4F7F\u7528 s3 \u4ED3\u5E93
230
+
231
+ \u6587\u6863: https://github.com/shellus/way
232
+ `);
233
+ program.command("run [project]").description("\u6267\u884C\u5907\u4EFD").allowUnknownOption().allowExcessArguments().action(async (project, options, command) => {
214
234
  const remote = command.parent.opts().remote;
215
- const extraArgs = command.args.slice(1);
216
- await run({ remote, project, extraArgs });
235
+ const allArgs = command.parent.args.slice(1);
236
+ if (project?.startsWith("--")) {
237
+ await run({ remote, project: void 0, extraArgs: allArgs });
238
+ } else {
239
+ const extraArgs = project ? allArgs.slice(1) : allArgs;
240
+ await run({ remote, project, extraArgs });
241
+ }
217
242
  });
218
243
  program.command("gc").description("\u6E05\u7406\u65E7\u5FEB\u7167").allowUnknownOption().allowExcessArguments().action(async function() {
219
244
  const remote = this.parent.opts().remote;
220
245
  const extraArgs = this.parent.args.slice(1);
221
246
  await gc({ remote, extraArgs });
222
247
  });
223
- program.command("cron <action>").description("\u7BA1\u7406\u5B9A\u65F6\u4EFB\u52A1 (show|install)").action(async (action, options, command) => {
248
+ program.command("systemd <action>").description("\u7BA1\u7406 systemd \u5B9A\u65F6\u4EFB\u52A1 (show|install|uninstall|status)").action(async (action, options, command) => {
224
249
  const remote = command.parent.opts().remote;
225
- await cron({ remote, action });
250
+ await systemd({ remote, action });
226
251
  });
227
252
  program.command("env").description("\u663E\u793A\u73AF\u5883\u53D8\u91CF").action(() => {
228
253
  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.1",
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
- ```