@yorz/cli 0.6.1 → 0.6.4

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 (65) hide show
  1. package/dist/cli/index.js +268 -108
  2. package/dist/gui/assets/{abnfDiagram-VRR7QNED-CpQSXKD3.js → abnfDiagram-VRR7QNED-Dysui3FU.js} +1 -1
  3. package/dist/gui/assets/{arc-BFSj3oux.js → arc-Dva0_Fbz.js} +1 -1
  4. package/dist/gui/assets/{architectureDiagram-ZJ3FMSHR-DUUbTneR.js → architectureDiagram-ZJ3FMSHR-CSusN0wM.js} +1 -1
  5. package/dist/gui/assets/{blockDiagram-677ZJIJ3-b_cB7_jE.js → blockDiagram-677ZJIJ3-Boy63uL4.js} +1 -1
  6. package/dist/gui/assets/{c4Diagram-LMCZKHZV-BW2WDVS6.js → c4Diagram-LMCZKHZV-DREOCesJ.js} +1 -1
  7. package/dist/gui/assets/channel-BAEHbRlk.js +1 -0
  8. package/dist/gui/assets/{chunk-2Q5K7J3B-DBPdFkwf.js → chunk-2Q5K7J3B-CQGkW5Le.js} +1 -1
  9. package/dist/gui/assets/{chunk-32BRIVSS-DIbss3Mf.js → chunk-32BRIVSS-ByKgaOB0.js} +1 -1
  10. package/dist/gui/assets/{chunk-5VM5RSS4-iGPM2FiL.js → chunk-5VM5RSS4-ouTiIpFg.js} +1 -1
  11. package/dist/gui/assets/{chunk-EX3LRPZG-CFO6icXh.js → chunk-EX3LRPZG-BBKKhIQU.js} +1 -1
  12. package/dist/gui/assets/{chunk-JWPE2WC7-BoSzxOe1.js → chunk-JWPE2WC7-i8qqnqZU.js} +1 -1
  13. package/dist/gui/assets/{chunk-MOJQB5TN-DenA9g0A.js → chunk-MOJQB5TN-BmLvCJEW.js} +1 -1
  14. package/dist/gui/assets/{chunk-RYQCIY6F-6OVqI5Jh.js → chunk-RYQCIY6F-DgIPwUtq.js} +1 -1
  15. package/dist/gui/assets/{chunk-V7JOEXUC-CwTJoQCL.js → chunk-V7JOEXUC-CHFlZVs7.js} +1 -1
  16. package/dist/gui/assets/{chunk-VR4S4FIN-D1Hv1RW-.js → chunk-VR4S4FIN-CF4gb-1V.js} +1 -1
  17. package/dist/gui/assets/{chunk-XXDRQBXY-5jwTXqEl.js → chunk-XXDRQBXY-6Ajr-TJy.js} +1 -1
  18. package/dist/gui/assets/classDiagram-OUVF2IWQ-2TSTdFv6.js +1 -0
  19. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-2TSTdFv6.js +1 -0
  20. package/dist/gui/assets/{cose-bilkent-JH36ORCC-Cc8VWY9x.js → cose-bilkent-JH36ORCC-Bq1NoHo9.js} +1 -1
  21. package/dist/gui/assets/{cynefin-VYW2F7L2-BXFPytZ_.js → cynefin-VYW2F7L2-BLZ43yIC.js} +1 -1
  22. package/dist/gui/assets/{cynefinDiagram-TSTJHNR4-WZDfGA2i.js → cynefinDiagram-TSTJHNR4-DOG8J1W-.js} +1 -1
  23. package/dist/gui/assets/{dagre-VKFMJZFB-Dip6iJOF.js → dagre-VKFMJZFB-6s-zIAym.js} +1 -1
  24. package/dist/gui/assets/{diagram-FQU43EPY-CFLYoHwF.js → diagram-FQU43EPY-MdHGL1Ep.js} +1 -1
  25. package/dist/gui/assets/{diagram-G47NLZAW-BswWoT3h.js → diagram-G47NLZAW-vNqROz2G.js} +1 -1
  26. package/dist/gui/assets/{diagram-NH7WQ7WH-BizhF8Pc.js → diagram-NH7WQ7WH-mBHJAt54.js} +1 -1
  27. package/dist/gui/assets/{diagram-OA4YK3LP-BckN9mPI.js → diagram-OA4YK3LP-Duzu45NT.js} +1 -1
  28. package/dist/gui/assets/{diagram-WEI45ONY-ht6wkF4x.js → diagram-WEI45ONY-B9KIE7jm.js} +1 -1
  29. package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-DyVIzHp5.js → ebnfDiagram-CCIWWBDH-D-VxYOb1.js} +1 -1
  30. package/dist/gui/assets/{erDiagram-Q63AITRT-DRFWvAAm.js → erDiagram-Q63AITRT-bGX1YWnp.js} +1 -1
  31. package/dist/gui/assets/{flowDiagram-23GEKE2U-CiBwda7G.js → flowDiagram-23GEKE2U-BFbLxzKU.js} +1 -1
  32. package/dist/gui/assets/{ganttDiagram-NO4QXBWP-vRQNKo6Y.js → ganttDiagram-NO4QXBWP-CFlioSP9.js} +1 -1
  33. package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-BYCaD1Fo.js → gitGraphDiagram-IHSO6WYX-9m210Kgm.js} +1 -1
  34. package/dist/gui/assets/index-DDGLj_Fo.js +36 -0
  35. package/dist/gui/assets/{infoDiagram-FWYZ7A6U-1Y32iWPH.js → infoDiagram-FWYZ7A6U-D35ODxoC.js} +1 -1
  36. package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-BQDQ7prq.js → ishikawaDiagram-FXEZZL3T-B-mZqJl4.js} +1 -1
  37. package/dist/gui/assets/{journeyDiagram-5HDEW3XC-DSWI24Bt.js → journeyDiagram-5HDEW3XC-CmFLCu-y.js} +1 -1
  38. package/dist/gui/assets/{kanban-definition-HUTT4EX6-fiE0W_uC.js → kanban-definition-HUTT4EX6-C-risdsz.js} +1 -1
  39. package/dist/gui/assets/{linear-DqWevqMh.js → linear-D5TY5eoS.js} +1 -1
  40. package/dist/gui/assets/{mermaid.core-bsNbTKFB.js → mermaid.core-wGjSg1h-.js} +4 -4
  41. package/dist/gui/assets/{mindmap-definition-LN4V7U3C-DnUeh13j.js → mindmap-definition-LN4V7U3C-BSdCxt4H.js} +1 -1
  42. package/dist/gui/assets/{pegDiagram-2B236MQR-DE_8gOxy.js → pegDiagram-2B236MQR-DrexK9bW.js} +1 -1
  43. package/dist/gui/assets/{pieDiagram-ENE6RG2P-Wf_jl50X.js → pieDiagram-ENE6RG2P-ay6UKmgt.js} +1 -1
  44. package/dist/gui/assets/{quadrantDiagram-ABIIQ3AL-Dt-Yn4ZI.js → quadrantDiagram-ABIIQ3AL-DgoOnLZO.js} +1 -1
  45. package/dist/gui/assets/{railroadDiagram-RFXS5EU6-0I6kqmlH.js → railroadDiagram-RFXS5EU6-Dsg3x2_p.js} +1 -1
  46. package/dist/gui/assets/{requirementDiagram-TGXJPOKE-Dozq39w_.js → requirementDiagram-TGXJPOKE-BtbCKvwF.js} +1 -1
  47. package/dist/gui/assets/{sankeyDiagram-HTMAVEWB-Cs7yO0KJ.js → sankeyDiagram-HTMAVEWB-TabZvNT3.js} +1 -1
  48. package/dist/gui/assets/{sequenceDiagram-DBY2YBRQ-EQLpVCpV.js → sequenceDiagram-DBY2YBRQ-C53WsGFo.js} +1 -1
  49. package/dist/gui/assets/{sizeCapture-X5ZJPWSS-BXUb6X9N.js → sizeCapture-X5ZJPWSS-D_JRhNnb.js} +1 -1
  50. package/dist/gui/assets/{stateDiagram-2N3HPSRC-CYHgCnOc.js → stateDiagram-2N3HPSRC-D-_V4W7N.js} +1 -1
  51. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-COKZr5A4.js +1 -0
  52. package/dist/gui/assets/{swimlanes-5IMT3BWC-BgsUQUHx.js → swimlanes-5IMT3BWC-DgyOlILO.js} +2 -2
  53. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-BaU_Y0XV.js +8 -0
  54. package/dist/gui/assets/{timeline-definition-FHXFAJF6-CQJufImP.js → timeline-definition-FHXFAJF6-7M8Rj4NA.js} +1 -1
  55. package/dist/gui/assets/{vennDiagram-L72KCM5P-L-2iYmD5.js → vennDiagram-L72KCM5P-DVyooCr2.js} +1 -1
  56. package/dist/gui/assets/{wardleyDiagram-EHGQE667-BamRRrE7.js → wardleyDiagram-EHGQE667-CYry07bZ.js} +1 -1
  57. package/dist/gui/assets/{xychartDiagram-FW5EYKEG-itKwurP4.js → xychartDiagram-FW5EYKEG-Dvx8ojch.js} +1 -1
  58. package/dist/gui/index.html +1 -1
  59. package/package.json +1 -1
  60. package/dist/gui/assets/channel-ChMv_4IC.js +0 -1
  61. package/dist/gui/assets/classDiagram-OUVF2IWQ-YMNyCLvX.js +0 -1
  62. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-YMNyCLvX.js +0 -1
  63. package/dist/gui/assets/index-DzV2HQw-.js +0 -36
  64. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-CqWHqtRs.js +0 -1
  65. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-CoZVjlW6.js +0 -8
package/dist/cli/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { homedir, platform, networkInterfaces } from "node:os";
2
+ import { homedir, platform } from "node:os";
3
3
  import { Command, Option } from "commander";
4
4
  import { stat, mkdir, readFile, writeFile, rename, rm, access, readdir, appendFile, copyFile } from "node:fs/promises";
5
5
  import { join, isAbsolute, resolve, basename as basename$1, dirname, extname, sep, relative, normalize } from "node:path";
6
6
  import { createHash, randomUUID, randomBytes } from "node:crypto";
7
7
  import { existsSync, openSync, closeSync, statSync, readSync, mkdirSync, watch, createReadStream } from "node:fs";
8
- import { spawn, execFile } from "node:child_process";
8
+ import { spawn, execFile, execFileSync } from "node:child_process";
9
9
  import { promisify } from "node:util";
10
10
  import { serve } from "@hono/node-server";
11
+ import { isIP } from "node:net";
11
12
  import { Hono } from "hono";
12
13
  import { streamSSE } from "hono/streaming";
13
14
  import { fileURLToPath } from "node:url";
@@ -22,7 +23,7 @@ import { createOpencode } from "@opencode-ai/sdk";
22
23
  import { createInterface as createInterface$1 } from "node:readline/promises";
23
24
  import MarkdownIt from "markdown-it";
24
25
  const name = "@yorz/cli";
25
- const version = "0.6.1";
26
+ const version = "0.6.4";
26
27
  const type = "module";
27
28
  const description = "";
28
29
  const bin = { "yorz": "./dist/cli/index.js" };
@@ -861,12 +862,13 @@ function sanitize(text) {
861
862
  return cleaned.replace(/^-+|-+$/g, "").slice(0, 64);
862
863
  }
863
864
  function uniquify(name2, existing) {
864
- if (!existing.includes(name2)) return name2;
865
+ const occupied = new Set(existing.map((item) => item.toLocaleLowerCase("en-US")));
866
+ if (!occupied.has(name2.toLocaleLowerCase("en-US"))) return name2;
865
867
  const ext = extname(name2);
866
868
  const base = stripExt(name2);
867
869
  for (let i = 1; i < 1e3; i++) {
868
870
  const candidate = `${base}-${i}${ext}`;
869
- if (!existing.includes(candidate)) return candidate;
871
+ if (!occupied.has(candidate.toLocaleLowerCase("en-US"))) return candidate;
870
872
  }
871
873
  return `${base}-${randomUUID().slice(0, 4)}${ext}`;
872
874
  }
@@ -1712,6 +1714,22 @@ function createSessionsRoutes(resolveProject) {
1712
1714
  return app;
1713
1715
  }
1714
1716
  const execFileP$1 = promisify(execFile);
1717
+ function withHiddenWindowsConsole(options, platform2 = process.platform) {
1718
+ if (platform2 !== "win32") return options;
1719
+ return { ...options, windowsHide: true };
1720
+ }
1721
+ async function execFileWithoutWindow(file, args, options) {
1722
+ const result = await execFileP$1(file, args, withHiddenWindowsConsole(options));
1723
+ return { stdout: result.stdout, stderr: result.stderr };
1724
+ }
1725
+ function spawnWithoutWindow(command, args, options) {
1726
+ return spawn(command, args, withHiddenWindowsConsole(options));
1727
+ }
1728
+ function resolveBrowserOpenInvocation(platform2, url) {
1729
+ if (platform2 === "win32") return { command: "explorer.exe", args: [url] };
1730
+ if (platform2 === "darwin") return { command: "open", args: [url] };
1731
+ return { command: "xdg-open", args: [url] };
1732
+ }
1715
1733
  class GitError extends Error {
1716
1734
  code;
1717
1735
  stderr;
@@ -1766,7 +1784,7 @@ function assertSafeRelativePath(cwd, p) {
1766
1784
  }
1767
1785
  async function runGit(cwd, args) {
1768
1786
  try {
1769
- const result = await execFileP$1("git", args, {
1787
+ const result = await execFileWithoutWindow("git", args, {
1770
1788
  cwd,
1771
1789
  encoding: "utf8",
1772
1790
  maxBuffer: 16 * 1024 * 1024
@@ -1785,7 +1803,7 @@ async function runGitChecked(cwd, args) {
1785
1803
  }
1786
1804
  async function runGitRaw(cwd, args) {
1787
1805
  try {
1788
- const result = await execFileP$1("git", args, {
1806
+ const result = await execFileWithoutWindow("git", args, {
1789
1807
  cwd,
1790
1808
  encoding: "utf8",
1791
1809
  maxBuffer: 16 * 1024 * 1024
@@ -3404,6 +3422,8 @@ class CommandManager {
3404
3422
  */
3405
3423
  pendingEnds = /* @__PURE__ */ new Map();
3406
3424
  persisted = /* @__PURE__ */ new Set();
3425
+ /** 记录由 API 主动停止的运行,防止 Windows exit 事件把 killed 覆盖成 exited。 */
3426
+ intentionalStops = /* @__PURE__ */ new Set();
3407
3427
  initPromise = null;
3408
3428
  /** Idempotent startup pass: reap leftovers, drop expired records. */
3409
3429
  init() {
@@ -3494,7 +3514,9 @@ class CommandManager {
3494
3514
  shell: true,
3495
3515
  cwd: this.projectPath,
3496
3516
  env: process.env,
3497
- detached: true,
3517
+ // POSIX 依赖独立进程组发送负 PID 信号;Windows 使用 taskkill /T,且 detached
3518
+ // 会导致继承的日志文件描述符失效,所以 Windows 必须保持 attached。
3519
+ detached: process.platform !== "win32",
3498
3520
  // No pipes: the child writes straight into the log fd. Keeps a chatty
3499
3521
  // dev server from buffering through this process, and lets the REST
3500
3522
  // first paint and the SSE tail read from one single source.
@@ -3570,6 +3592,7 @@ class CommandManager {
3570
3592
  this.runSubscribers.delete(runId);
3571
3593
  this.persisted.delete(runId);
3572
3594
  this.pendingEnds.delete(runId);
3595
+ this.intentionalStops.delete(runId);
3573
3596
  this.notifyRuns();
3574
3597
  return true;
3575
3598
  }
@@ -3628,23 +3651,12 @@ class CommandManager {
3628
3651
  for (const [runId, child] of this.children) {
3629
3652
  const pid = child.pid;
3630
3653
  if (!pid) continue;
3631
- try {
3632
- process.kill(-pid, "SIGKILL");
3633
- } catch {
3634
- try {
3635
- process.kill(pid, "SIGKILL");
3636
- } catch {
3637
- }
3638
- }
3654
+ killKnownProcessTreeSync(pid);
3639
3655
  this.children.delete(runId);
3640
3656
  }
3641
3657
  for (const t of this.tailers.values()) t.stop();
3642
3658
  }
3643
- /**
3644
- * Startup pass. Any record still marked `running` belongs to a previous
3645
- * service process: kill it if it somehow survived (service SIGKILLed), then
3646
- * mark it `killed`. Also drops records that finished long ago.
3647
- */
3659
+ /** 将上次运行遗留的记录标记为结束,并清理超过保留期的记录。 */
3648
3660
  async reap() {
3649
3661
  const runs = await this.store.list().catch(() => []);
3650
3662
  if (runs.length === 0) return;
@@ -3654,8 +3666,10 @@ class CommandManager {
3654
3666
  for (const run of runs) {
3655
3667
  if (run.status === "running") {
3656
3668
  if (run.pid > 0 && isAlive(run.pid)) {
3657
- killGroup(run.pid, "SIGKILL");
3658
- log$3().warn("reaped orphan command process", { runId: run.runId, pid: run.pid });
3669
+ log$3().warn("ignored unverified persisted command pid", {
3670
+ runId: run.runId,
3671
+ pid: run.pid
3672
+ });
3659
3673
  }
3660
3674
  kept.push({ ...run, status: "killed", endedAt: run.endedAt ?? now, exitCode: null });
3661
3675
  continue;
@@ -3671,22 +3685,37 @@ class CommandManager {
3671
3685
  }
3672
3686
  }
3673
3687
  async terminate(run) {
3688
+ this.intentionalStops.add(run.runId);
3674
3689
  if (run.pid > 0) {
3675
- killGroup(run.pid, "SIGTERM");
3676
- const exited = await waitForExit(run.pid, STOP_GRACE_MS);
3677
- if (!exited) {
3678
- log$3().warn("command did not exit on SIGTERM, escalating", {
3679
- runId: run.runId,
3680
- pid: run.pid
3681
- });
3682
- killGroup(run.pid, "SIGKILL");
3683
- await waitForExit(run.pid, STOP_GRACE_MS);
3690
+ const child = this.children.get(run.runId);
3691
+ if (process.platform === "win32") {
3692
+ if (child?.pid === run.pid) {
3693
+ await killWindowsProcessTree(run.pid);
3694
+ const exited = await waitForExit(run.pid, STOP_GRACE_MS);
3695
+ if (!exited) throw new Error(`Windows command process tree did not exit: ${run.pid}`);
3696
+ } else {
3697
+ if (isAlive(run.pid)) {
3698
+ throw new Error(`refused to kill unverified Windows command pid: ${run.pid}`);
3699
+ }
3700
+ }
3701
+ } else {
3702
+ killPosixProcessGroup(run.pid, "SIGTERM");
3703
+ const exited = await waitForExit(run.pid, STOP_GRACE_MS);
3704
+ if (!exited) {
3705
+ log$3().warn("command did not exit on SIGTERM, escalating", {
3706
+ runId: run.runId,
3707
+ pid: run.pid
3708
+ });
3709
+ killPosixProcessGroup(run.pid, "SIGKILL");
3710
+ await waitForExit(run.pid, STOP_GRACE_MS);
3711
+ }
3684
3712
  }
3685
3713
  }
3686
3714
  this.children.delete(run.runId);
3687
3715
  await this.markRunEnded(run.runId, "killed", null, "SIGTERM");
3688
3716
  }
3689
3717
  async handleExit(runId, status, exitCode, signal) {
3718
+ if (this.intentionalStops.has(runId)) return;
3690
3719
  if (!this.persisted.has(runId)) {
3691
3720
  this.pendingEnds.set(runId, { status, exitCode, signal: signal ?? null });
3692
3721
  return;
@@ -3796,7 +3825,7 @@ function isAlive(pid) {
3796
3825
  return false;
3797
3826
  }
3798
3827
  }
3799
- function killGroup(pid, signal) {
3828
+ function killPosixProcessGroup(pid, signal) {
3800
3829
  try {
3801
3830
  process.kill(-pid, signal);
3802
3831
  } catch {
@@ -3806,6 +3835,27 @@ function killGroup(pid, signal) {
3806
3835
  }
3807
3836
  }
3808
3837
  }
3838
+ async function killWindowsProcessTree(pid) {
3839
+ await new Promise((resolve2, reject) => {
3840
+ execFile("taskkill.exe", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }, (error) => {
3841
+ if (!error || !isAlive(pid)) resolve2();
3842
+ else reject(error);
3843
+ });
3844
+ });
3845
+ }
3846
+ function killKnownProcessTreeSync(pid) {
3847
+ if (process.platform === "win32") {
3848
+ try {
3849
+ execFileSync("taskkill.exe", ["/PID", String(pid), "/T", "/F"], {
3850
+ stdio: "ignore",
3851
+ windowsHide: true
3852
+ });
3853
+ } catch {
3854
+ }
3855
+ return;
3856
+ }
3857
+ killPosixProcessGroup(pid, "SIGKILL");
3858
+ }
3809
3859
  async function waitForExit(pid, timeoutMs) {
3810
3860
  const until = Date.now() + timeoutMs;
3811
3861
  while (Date.now() < until) {
@@ -4289,11 +4339,9 @@ class WorktreeManager {
4289
4339
  const head = await runGitChecked(mainPath, ["rev-parse", "HEAD"]);
4290
4340
  const mergeCommit = head.stdout.trim();
4291
4341
  await this.migrateSessions(wtPath, mainPath);
4292
- await runGitRaw(mainPath, ["worktree", "remove", wtPath]);
4293
- if (existsSync(wtPath)) {
4294
- await runGitRaw(mainPath, ["worktree", "remove", "--force", wtPath]);
4295
- }
4296
- await runGitRaw(mainPath, ["branch", "-d", branch]);
4342
+ await this.registry.release(entry.id);
4343
+ await removeMergedWorktree(mainPath, wtPath);
4344
+ await deleteWorktreeBranch(mainPath, branch, "-d");
4297
4345
  await this.registry.remove(entry.id);
4298
4346
  if (mainEntry) {
4299
4347
  await this.registry.reload(mainEntry.id);
@@ -4320,15 +4368,21 @@ class WorktreeManager {
4320
4368
  const { mainPath, branch } = entry.worktree;
4321
4369
  const wtPath = entry.path;
4322
4370
  if (existsSync(wtPath)) {
4323
- const status = await runGitChecked(wtPath, ["status", "--porcelain"]);
4324
- if (status.stdout.trim().length > 0) {
4325
- throw new GitError("dirty", "存在未提交 git 的变更");
4371
+ if (existsSync(join(wtPath, ".git"))) {
4372
+ const status = await runGitChecked(wtPath, ["status", "--porcelain"]);
4373
+ if (status.stdout.trim().length > 0) {
4374
+ throw new GitError("dirty", "存在未提交 git 的变更");
4375
+ }
4326
4376
  }
4327
- await trash(wtPath);
4328
4377
  }
4378
+ await this.registry.release(worktreeProjectId);
4379
+ await trashWorktreePath(wtPath);
4329
4380
  if (existsSync(mainPath)) {
4330
- await runGitRaw(mainPath, ["worktree", "prune"]);
4331
- await runGitRaw(mainPath, ["branch", "-D", branch]);
4381
+ const prune = await runGitRaw(mainPath, ["worktree", "prune"]);
4382
+ if (prune.code !== 0) {
4383
+ throw new GitError("worktree_prune_failed", "git worktree prune 失败", prune.stderr);
4384
+ }
4385
+ await deleteWorktreeBranch(mainPath, branch, "-D");
4332
4386
  }
4333
4387
  await this.registry.remove(worktreeProjectId);
4334
4388
  this.onProjectsChanged?.();
@@ -4413,6 +4467,41 @@ class WorktreeManager {
4413
4467
  };
4414
4468
  }
4415
4469
  }
4470
+ async function removeMergedWorktree(mainPath, wtPath) {
4471
+ const normal = await runGitRaw(mainPath, ["worktree", "remove", wtPath]);
4472
+ if (existsSync(wtPath)) {
4473
+ await runGitRaw(mainPath, ["worktree", "remove", "--force", wtPath]);
4474
+ }
4475
+ if (existsSync(wtPath)) await trashWorktreePath(wtPath);
4476
+ const prune = await runGitRaw(mainPath, ["worktree", "prune"]);
4477
+ if (prune.code !== 0) {
4478
+ throw new GitError("worktree_prune_failed", "git worktree prune 失败", prune.stderr);
4479
+ }
4480
+ if (normal.code !== 0) {
4481
+ worktreeLog().warn("normal worktree removal required recovery cleanup", {
4482
+ path: wtPath,
4483
+ stderr: normal.stderr
4484
+ });
4485
+ }
4486
+ }
4487
+ async function trashWorktreePath(wtPath) {
4488
+ if (!existsSync(wtPath)) return;
4489
+ try {
4490
+ await trash(wtPath);
4491
+ } catch (error) {
4492
+ const detail = error instanceof Error ? error.message : String(error);
4493
+ throw new GitError("worktree_remove_failed", `worktree 目录删除失败:${wtPath};${detail}`);
4494
+ }
4495
+ if (existsSync(wtPath)) {
4496
+ throw new GitError("worktree_remove_failed", `worktree 目录仍然存在:${wtPath}`);
4497
+ }
4498
+ }
4499
+ async function deleteWorktreeBranch(mainPath, branch, mode) {
4500
+ const result = await runGitRaw(mainPath, ["branch", mode, branch]);
4501
+ if (result.code !== 0) {
4502
+ throw new GitError("branch_remove_failed", `worktree 分支删除失败:${branch}`, result.stderr);
4503
+ }
4504
+ }
4416
4505
  async function detectDefaultBranch(mainPath) {
4417
4506
  const res = await runGitChecked(mainPath, ["rev-parse", "--abbrev-ref", "HEAD"]);
4418
4507
  const name2 = res.stdout.trim();
@@ -4569,6 +4658,14 @@ function createApp(opts) {
4569
4658
  else httpLog.debug("request", meta);
4570
4659
  });
4571
4660
  const api = new Hono();
4661
+ if (opts.shutdown) {
4662
+ api.post("/internal/shutdown", (c) => {
4663
+ const token = c.req.header("x-yorz-shutdown-token");
4664
+ if (token !== opts.shutdown.token) return c.json({ error: "Forbidden" }, 403);
4665
+ setTimeout(opts.shutdown.request, 0);
4666
+ return c.json({ accepted: true }, 202);
4667
+ });
4668
+ }
4572
4669
  const resolveProject = (id) => opts.registry.getOrCreate(id);
4573
4670
  const projectsBus = new RegistryEventBus();
4574
4671
  projectsBus.start(opts.registry.configPath());
@@ -5775,7 +5872,10 @@ class SessionManager {
5775
5872
  running = /* @__PURE__ */ new Set();
5776
5873
  /** Kind of sessions discovered via `listSessions()` but absent from the store. */
5777
5874
  discoveredKinds = /* @__PURE__ */ new Map();
5875
+ /** 后台派发任务;项目关闭必须等待它们完成后才能删除 worktree。 */
5876
+ inFlight = /* @__PURE__ */ new Set();
5778
5877
  statusEmitter = new EventEmitter();
5878
+ disposing = false;
5779
5879
  emitterFor(sid) {
5780
5880
  let e = this.emitters.get(sid);
5781
5881
  if (!e) {
@@ -5909,6 +6009,7 @@ class SessionManager {
5909
6009
  const session = await adapter.resumeSession(sid);
5910
6010
  const ls = { kind, session };
5911
6011
  this.live.set(sid, ls);
6012
+ if (this.disposing) session.abort();
5912
6013
  return ls;
5913
6014
  }
5914
6015
  async send(sid, prompt) {
@@ -5920,7 +6021,7 @@ class SessionManager {
5920
6021
  });
5921
6022
  this.setRunning(sid, true);
5922
6023
  agentLog().info("dispatch start", { sessionId: sid, runId, promptLength: prompt.length });
5923
- void (async () => {
6024
+ const task = (async () => {
5924
6025
  try {
5925
6026
  const ls = await this.ensureLive(sid);
5926
6027
  for await (const ev of ls.session.send(prompt)) {
@@ -5957,6 +6058,11 @@ class SessionManager {
5957
6058
  emitter.emit("done", currentSid);
5958
6059
  }
5959
6060
  })();
6061
+ this.inFlight.add(task);
6062
+ void task.then(
6063
+ () => this.inFlight.delete(task),
6064
+ () => this.inFlight.delete(task)
6065
+ );
5960
6066
  return {
5961
6067
  runId,
5962
6068
  sessionId: sid,
@@ -5999,9 +6105,15 @@ class SessionManager {
5999
6105
  return true;
6000
6106
  }
6001
6107
  async dispose() {
6108
+ this.disposing = true;
6109
+ for (const session of new Set([...this.live.values()].map((item) => item.session))) {
6110
+ session.abort();
6111
+ }
6112
+ await Promise.allSettled([...this.inFlight]);
6002
6113
  await this.adapters.dispose();
6003
6114
  this.live.clear();
6004
6115
  this.emitters.clear();
6116
+ this.running.clear();
6005
6117
  }
6006
6118
  }
6007
6119
  const execFileP = promisify(execFile);
@@ -6143,15 +6255,24 @@ class ProjectRegistry {
6143
6255
  return await addProject(normalized, this.globalConfigPath);
6144
6256
  }
6145
6257
  async remove(id) {
6258
+ await this.release(id).catch(() => {
6259
+ });
6260
+ return await removeProject(id, this.globalConfigPath);
6261
+ }
6262
+ /**
6263
+ * 关闭并移除项目的缓存实例,但保留全局 registry 配置供失败恢复。
6264
+ *
6265
+ * @param id 要释放的项目 id。
6266
+ * @returns 缓存实例关闭完成后结束;关闭失败会原样抛出。
6267
+ */
6268
+ async release(id) {
6146
6269
  const cached = this.cache.get(id);
6147
- if (cached) {
6148
- try {
6149
- await cached.instance.close();
6150
- } catch {
6151
- }
6270
+ if (!cached) return;
6271
+ try {
6272
+ await cached.instance.close();
6273
+ } finally {
6152
6274
  this.cache.delete(id);
6153
6275
  }
6154
- return await removeProject(id, this.globalConfigPath);
6155
6276
  }
6156
6277
  /**
6157
6278
  * Drop the cached instance for a project so the next getOrCreate() rebuilds
@@ -6159,13 +6280,8 @@ class ProjectRegistry {
6159
6280
  * project's `.yorz/config.json` changes (e.g. specsDir or agent override).
6160
6281
  */
6161
6282
  async reload(id) {
6162
- const cached = this.cache.get(id);
6163
- if (!cached) return;
6164
- try {
6165
- await cached.instance.close();
6166
- } catch {
6167
- }
6168
- this.cache.delete(id);
6283
+ await this.release(id).catch(() => {
6284
+ });
6169
6285
  }
6170
6286
  async closeAll() {
6171
6287
  const tasks = [];
@@ -6465,10 +6581,20 @@ const DEFAULT_PORT = 7423;
6465
6581
  const MAX_PORT_TRIES = 10;
6466
6582
  const DEFAULT_HOST = "127.0.0.1";
6467
6583
  function isLoopbackHost(host) {
6468
- return host === "127.0.0.1" || host === "localhost" || host === "::1";
6584
+ const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
6585
+ if (normalized === "localhost") return true;
6586
+ if (isIP(normalized) === 4) return normalized.startsWith("127.");
6587
+ if (isIP(normalized) === 6) {
6588
+ return normalized === "::1" || normalized === "0:0:0:0:0:0:0:1";
6589
+ }
6590
+ return false;
6469
6591
  }
6470
6592
  const log$1 = () => getLogger().child("serve");
6471
6593
  async function start(opts = {}) {
6594
+ const host = opts.host?.trim() || DEFAULT_HOST;
6595
+ if (!isLoopbackHost(host)) {
6596
+ throw new Error(`host must be a loopback address: ${host}`);
6597
+ }
6472
6598
  const registry = new ProjectRegistry({ globalConfigPath: opts.globalConfigPath });
6473
6599
  const cwd = opts.cwd ?? process.cwd();
6474
6600
  if (!opts.noRegisterCwd && existsSync(join(cwd, ".yorz"))) {
@@ -6480,18 +6606,17 @@ async function start(opts = {}) {
6480
6606
  const projects = await registry.list();
6481
6607
  const systemNotifications = new SystemNotificationCenter();
6482
6608
  systemNotifications.startVersionChecks();
6483
- const app = createApp({ registry, guiRoot: opts.guiRoot, systemNotifications });
6484
- const host = opts.host?.trim() || DEFAULT_HOST;
6609
+ const app = createApp({
6610
+ registry,
6611
+ guiRoot: opts.guiRoot,
6612
+ systemNotifications,
6613
+ shutdown: opts.shutdownToken && opts.onShutdownRequest ? { token: opts.shutdownToken, request: opts.onShutdownRequest } : void 0
6614
+ });
6485
6615
  const port = await listen(app.fetch, opts.port ?? DEFAULT_PORT, host);
6486
6616
  const url = `http://localhost:${port.port}/`;
6487
- console.log(`YorZ Service ready at ${url} (${projects.length} project${projects.length === 1 ? "" : "s"})`);
6488
- if (!isLoopbackHost(host)) {
6489
- console.log(` WARNING: bound to ${host} — reachable from the network.`);
6490
- for (const lanUrl of listLanUrls(port.port)) {
6491
- console.log(` LAN: ${lanUrl}`);
6492
- }
6493
- log$1().warn("service bound to a non-loopback address", { host, port: port.port });
6494
- }
6617
+ console.log(
6618
+ `YorZ Service ready at ${url} (${projects.length} project${projects.length === 1 ? "" : "s"})`
6619
+ );
6495
6620
  for (const p of projects) {
6496
6621
  console.log(` - ${p.name} -> ${p.path}`);
6497
6622
  }
@@ -6563,23 +6688,16 @@ async function listen(fetchHandler, preferredPort, hostname) {
6563
6688
  log$1().error("failed to bind any port", { preferredPort, tries: MAX_PORT_TRIES, err: fatal });
6564
6689
  throw fatal;
6565
6690
  }
6566
- function listLanUrls(port) {
6567
- const urls = [];
6568
- const ifaces = networkInterfaces();
6569
- for (const name2 of Object.keys(ifaces)) {
6570
- for (const info of ifaces[name2] ?? []) {
6571
- if (info.family === "IPv4" && !info.internal) {
6572
- urls.push(`http://${info.address}:${port}/`);
6573
- }
6574
- }
6575
- }
6576
- return urls;
6577
- }
6578
6691
  async function tryOpenBrowser(url) {
6579
- const { spawn: spawn2 } = await import("node:child_process");
6580
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
6692
+ const invocation = resolveBrowserOpenInvocation(process.platform, url);
6581
6693
  try {
6582
- spawn2(cmd, [url], { stdio: "ignore", detached: true }).unref();
6694
+ const child = spawnWithoutWindow(invocation.command, invocation.args, {
6695
+ stdio: "ignore",
6696
+ detached: true
6697
+ });
6698
+ child.once("error", () => {
6699
+ });
6700
+ child.unref();
6583
6701
  } catch {
6584
6702
  }
6585
6703
  }
@@ -6632,12 +6750,28 @@ async function runServe(opts) {
6632
6750
  if (!opts.foreground) {
6633
6751
  return startBackgroundServe(opts);
6634
6752
  }
6635
- const handle = await start({
6753
+ const shutdownToken = opts.recordRuntime ? randomUUID() : void 0;
6754
+ let handle = null;
6755
+ let shuttingDown = false;
6756
+ const shutdown = async () => {
6757
+ if (shuttingDown || !handle) return;
6758
+ shuttingDown = true;
6759
+ console.log("\nShutting down YorZ Service…");
6760
+ try {
6761
+ await handle.close();
6762
+ } finally {
6763
+ if (opts.recordRuntime) await removeRuntimeForPid(process.pid);
6764
+ process.exit(0);
6765
+ }
6766
+ };
6767
+ handle = await start({
6636
6768
  port: opts.port,
6637
6769
  host: opts.host,
6638
6770
  open: opts.open,
6639
6771
  cwd: opts.cwd ?? process.cwd(),
6640
- noRegisterCwd: opts.noRegisterCwd
6772
+ noRegisterCwd: opts.noRegisterCwd,
6773
+ shutdownToken,
6774
+ onShutdownRequest: shutdownToken ? () => void shutdown() : void 0
6641
6775
  });
6642
6776
  if (opts.recordRuntime) {
6643
6777
  const snapshot = await readProcessSnapshot(process.pid);
@@ -6648,21 +6782,13 @@ async function runServe(opts) {
6648
6782
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
6649
6783
  execPath: process.execPath,
6650
6784
  argv: process.argv,
6651
- processStartedAt: snapshot?.processStartedAt
6785
+ processStartedAt: snapshot?.processStartedAt,
6786
+ shutdownToken
6652
6787
  });
6653
6788
  console.log(`Stop with: yorz serve stop`);
6654
6789
  } else {
6655
6790
  console.log(`Stop with: Ctrl-C`);
6656
6791
  }
6657
- const shutdown = async () => {
6658
- console.log("\nShutting down YorZ Service…");
6659
- try {
6660
- await handle.close();
6661
- } finally {
6662
- if (opts.recordRuntime) await removeRuntimeForPid(process.pid);
6663
- process.exit(0);
6664
- }
6665
- };
6666
6792
  process.once("SIGINT", () => void shutdown());
6667
6793
  process.once("SIGTERM", () => void shutdown());
6668
6794
  return handle;
@@ -6687,7 +6813,7 @@ function startBackgroundServe(opts) {
6687
6813
  const entry = process.argv[1];
6688
6814
  if (!entry) throw new Error("Cannot resolve CLI entrypoint for background service");
6689
6815
  const stdio = backgroundStdio();
6690
- const child = spawn(process.execPath, [entry, "serve", ...backgroundArgs(opts)], {
6816
+ const child = spawnWithoutWindow(process.execPath, [entry, "serve", ...backgroundArgs(opts)], {
6691
6817
  detached: true,
6692
6818
  stdio: stdio.stdio
6693
6819
  });
@@ -6773,7 +6899,7 @@ async function ensureSkillsInstalledWithLog(cwd) {
6773
6899
  console.log(`[skill][legacy] ${r.skill} kept (not a YorZ skill): ${r.path}`);
6774
6900
  }
6775
6901
  }
6776
- async function runStopServe() {
6902
+ async function runStopServe(opts = {}) {
6777
6903
  silenceConsoleMirror();
6778
6904
  const all = await readAllProcesses();
6779
6905
  if (all.length === 0) {
@@ -6807,16 +6933,38 @@ async function runStopServe() {
6807
6933
  const stoppedPids = [];
6808
6934
  const stoppedUrls = [];
6809
6935
  const failedPids = [];
6936
+ const platform2 = opts.platform ?? process.platform;
6810
6937
  for (const { proc } of alive) {
6811
- try {
6812
- process.kill(proc.pid, "SIGTERM");
6813
- log().info("sent SIGTERM", { pid: proc.pid, url: proc.url });
6814
- } catch (err) {
6815
- log().warn("SIGTERM failed", { pid: proc.pid, err });
6816
- failedPids.push(proc.pid);
6817
- continue;
6938
+ let stopped = false;
6939
+ if (platform2 === "win32") {
6940
+ if (proc.shutdownToken) {
6941
+ try {
6942
+ await requestGracefulShutdown(proc);
6943
+ log().info("requested graceful shutdown", { pid: proc.pid, url: proc.url });
6944
+ stopped = await waitForProcessExit(proc.pid, 5e3);
6945
+ } catch (err) {
6946
+ log().warn("graceful shutdown request failed", { pid: proc.pid, err });
6947
+ stopped = await waitForProcessExit(proc.pid, 2e3);
6948
+ }
6949
+ } else {
6950
+ log().warn("refused to stop unverified legacy Windows runtime pid", { pid: proc.pid });
6951
+ }
6952
+ if (!stopped) {
6953
+ failedPids.push(proc.pid);
6954
+ continue;
6955
+ }
6956
+ }
6957
+ if (!stopped) {
6958
+ try {
6959
+ process.kill(proc.pid, "SIGTERM");
6960
+ log().info("sent SIGTERM", { pid: proc.pid, url: proc.url });
6961
+ } catch (err) {
6962
+ log().warn("SIGTERM failed", { pid: proc.pid, err });
6963
+ failedPids.push(proc.pid);
6964
+ continue;
6965
+ }
6966
+ stopped = await waitForProcessExit(proc.pid, 2e3);
6818
6967
  }
6819
- let stopped = await waitForProcessExit(proc.pid, 2e3);
6820
6968
  if (!stopped) {
6821
6969
  log().warn("still alive after SIGTERM, escalating to SIGKILL", { pid: proc.pid });
6822
6970
  try {
@@ -6855,6 +7003,16 @@ async function runStopServe() {
6855
7003
  message: `Stopped ${stoppedPids.length} YorZ Service process(es): ${pidList}.`
6856
7004
  };
6857
7005
  }
7006
+ async function requestGracefulShutdown(proc) {
7007
+ const response = await fetch(`http://127.0.0.1:${proc.port}/api/internal/shutdown`, {
7008
+ method: "POST",
7009
+ headers: { "x-yorz-shutdown-token": proc.shutdownToken },
7010
+ signal: AbortSignal.timeout(2e3)
7011
+ });
7012
+ if (!response.ok) {
7013
+ throw new Error(`Service rejected graceful shutdown with HTTP ${response.status}`);
7014
+ }
7015
+ }
6858
7016
  function runtimePath() {
6859
7017
  return join(resolveGlobalConfigDir(), RUNTIME_FILE);
6860
7018
  }
@@ -6949,7 +7107,8 @@ function toProcessEntry(obj) {
6949
7107
  startedAt: obj.startedAt,
6950
7108
  execPath: typeof obj.execPath === "string" ? obj.execPath : void 0,
6951
7109
  argv: Array.isArray(obj.argv) && obj.argv.every((v) => typeof v === "string") ? obj.argv : void 0,
6952
- processStartedAt: typeof obj.processStartedAt === "string" ? obj.processStartedAt : void 0
7110
+ processStartedAt: typeof obj.processStartedAt === "string" ? obj.processStartedAt : void 0,
7111
+ shutdownToken: typeof obj.shutdownToken === "string" ? obj.shutdownToken : void 0
6953
7112
  };
6954
7113
  }
6955
7114
  function tryV1Entry(parsed) {
@@ -6963,7 +7122,8 @@ function tryV1Entry(parsed) {
6963
7122
  startedAt: parsed.startedAt,
6964
7123
  execPath: typeof parsed.execPath === "string" ? parsed.execPath : void 0,
6965
7124
  argv: Array.isArray(parsed.argv) && parsed.argv.every((v) => typeof v === "string") ? parsed.argv : void 0,
6966
- processStartedAt: typeof parsed.processStartedAt === "string" ? parsed.processStartedAt : void 0
7125
+ processStartedAt: typeof parsed.processStartedAt === "string" ? parsed.processStartedAt : void 0,
7126
+ shutdownToken: typeof parsed.shutdownToken === "string" ? parsed.shutdownToken : void 0
6967
7127
  };
6968
7128
  }
6969
7129
  async function upsertProcess(entry) {
@@ -1 +1 @@
1
- import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-DenA9g0A.js";import{p as f}from"./chunk-JWPE2WC7-BoSzxOe1.js";import{_ as n,l as o}from"./mermaid.core-bsNbTKFB.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-BXFPytZ_.js";import"./index-DzV2HQw-.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},B={parser:R,db:a,renderer:u,styles:p};export{B as diagram};
1
+ import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-BmLvCJEW.js";import{p as f}from"./chunk-JWPE2WC7-i8qqnqZU.js";import{_ as n,l as o}from"./mermaid.core-wGjSg1h-.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-BLZ43yIC.js";import"./index-DDGLj_Fo.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},B={parser:R,db:a,renderer:u,styles:p};export{B as diagram};