@shellus/way 0.6.9 → 0.6.10

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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/cli.js +37 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -242,6 +242,8 @@ projects:
242
242
 
243
243
  `before_backup` 失败会跳过该项目的 restic 备份并标记项目失败;`after_backup` 只在 restic 成功后执行,失败同样会标记项目失败。`--dry-run` 模式只打印钩子命令,不实际执行。钩子命令按 shell 命令执行,并会收到 `WAY_PROJECT`、`WAY_REMOTE`、`WAY_DIR`、`WAY_DRY_RUN` 环境变量。
244
244
 
245
+ Linux 上的 hook 达到 `timeout` 后,Way 会终止对应 shell 的完整进程组;进程未在宽限期内退出时会继续强制终止,避免数据库导出、压缩或同步子进程脱离 Way 后持续运行。hook 仍应保持幂等,并自行清理失败时生成的临时文件。
246
+
245
247
  **项目级 Uptime Kuma 通知示例**:
246
248
 
247
249
  ```yaml
@@ -460,4 +462,4 @@ way --remote=oss restic restore <snapshot-id> --target /tmp/restore
460
462
  - 变更规范和测试要求
461
463
  - 发布流程
462
464
 
463
- 内部开发参考:[CLAUDE.md](CLAUDE.md)
465
+ 项目维护规则:[AGENTS.md](AGENTS.md)
package/dist/cli.js CHANGED
@@ -324,6 +324,7 @@ function groupUptimeKumaResults(projectResults) {
324
324
  }
325
325
  return groupedResults;
326
326
  }
327
+ var HOOK_FORCE_KILL_DELAY_MS = 1e3;
327
328
  function normalizeHook(hook) {
328
329
  if (typeof hook === "string") return { run: hook };
329
330
  return hook;
@@ -346,6 +347,26 @@ function parseTimeout(timeout) {
346
347
  return value * 60 * 60 * 1e3;
347
348
  }
348
349
  }
350
+ function isMissingProcessError(error) {
351
+ return error instanceof Error && "code" in error && error.code === "ESRCH";
352
+ }
353
+ function signalProcessGroup(pid, signal) {
354
+ try {
355
+ process.kill(-pid, signal);
356
+ return true;
357
+ } catch (error) {
358
+ if (isMissingProcessError(error)) return false;
359
+ throw error;
360
+ }
361
+ }
362
+ function hookTimedOut(error) {
363
+ return typeof error === "object" && error !== null && "timedOut" in error && error.timedOut === true;
364
+ }
365
+ async function terminateProcessGroup(pid) {
366
+ if (!signalProcessGroup(pid, "SIGTERM")) return;
367
+ await new Promise((resolve) => setTimeout(resolve, HOOK_FORCE_KILL_DELAY_MS));
368
+ signalProcessGroup(pid, "SIGKILL");
369
+ }
349
370
  async function runProjectHooks(hooks, context) {
350
371
  if (!hooks?.length) return;
351
372
  for (const hook of hooks) {
@@ -356,10 +377,11 @@ async function runProjectHooks(hooks, context) {
356
377
  continue;
357
378
  }
358
379
  console.log(`Running ${context.label} hook for ${context.projectName}: ${normalized.run}`);
359
- await execaCommand(normalized.run, {
380
+ const subprocess = execaCommand(normalized.run, {
360
381
  shell: true,
361
382
  stdio: "inherit",
362
383
  timeout: parseTimeout(normalized.timeout),
384
+ detached: process.platform !== "win32",
363
385
  env: {
364
386
  WAY_PROJECT: context.projectName,
365
387
  WAY_REMOTE: context.remote,
@@ -367,6 +389,19 @@ async function runProjectHooks(hooks, context) {
367
389
  WAY_DRY_RUN: context.dryRun ? "1" : "0"
368
390
  }
369
391
  });
392
+ const processGroupPid = process.platform === "win32" ? void 0 : subprocess.pid;
393
+ const cleanupOnExit = processGroupPid === void 0 ? void 0 : () => signalProcessGroup(processGroupPid, "SIGTERM");
394
+ if (cleanupOnExit) process.once("exit", cleanupOnExit);
395
+ try {
396
+ await subprocess;
397
+ } catch (error) {
398
+ if (processGroupPid !== void 0 && hookTimedOut(error)) {
399
+ await terminateProcessGroup(processGroupPid);
400
+ }
401
+ throw error;
402
+ } finally {
403
+ if (cleanupOnExit) process.removeListener("exit", cleanupOnExit);
404
+ }
370
405
  }
371
406
  }
372
407
  async function notifyUptimeKuma(result, pushUrl) {
@@ -611,7 +646,7 @@ async function daemon(options) {
611
646
 
612
647
  // src/cli.ts
613
648
  var program = new Command();
614
- program.name("way").version("0.6.9").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", `
649
+ program.name("way").version("0.6.10").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", `
615
650
  \u793A\u4F8B:
616
651
  $ way backup \u6267\u884C\u6240\u6709\u9879\u76EE\u5907\u4EFD
617
652
  $ way backup data \u53EA\u5907\u4EFD data \u9879\u76EE
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shellus/way",
3
- "version": "0.6.9",
3
+ "version": "0.6.10",
4
4
  "description": "将备份作为持续运营的项目,而非一次性任务。基于 restic 的策略封装。",
5
5
  "type": "module",
6
6
  "bin": {