@yorz/cli 0.3.5 → 0.3.6
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 +17 -17
- package/dist/cli/index.js +657 -201
- package/dist/gui/assets/{abnfDiagram-VRR7QNED-DQM-IUdR.js → abnfDiagram-VRR7QNED-xc0C50rB.js} +1 -1
- package/dist/gui/assets/{arc-WoQS9inO.js → arc-BBPWDS5i.js} +1 -1
- package/dist/gui/assets/{architectureDiagram-ZJ3FMSHR-BSJfE6nY.js → architectureDiagram-ZJ3FMSHR-UfjPkiOc.js} +1 -1
- package/dist/gui/assets/{blockDiagram-677ZJIJ3-BXk0h_Na.js → blockDiagram-677ZJIJ3-D4-uGWRR.js} +1 -1
- package/dist/gui/assets/{c4Diagram-LMCZKHZV-Sgnopqnh.js → c4Diagram-LMCZKHZV-DOsjz28F.js} +1 -1
- package/dist/gui/assets/channel-BUjCRtd2.js +1 -0
- package/dist/gui/assets/{chunk-2Q5K7J3B-sWwRHI31.js → chunk-2Q5K7J3B-D8U1wDI8.js} +1 -1
- package/dist/gui/assets/{chunk-32BRIVSS-6HJXOWs9.js → chunk-32BRIVSS-B_fOO0A_.js} +1 -1
- package/dist/gui/assets/{chunk-5VM5RSS4-De_Y0Itz.js → chunk-5VM5RSS4-BEs0La2r.js} +1 -1
- package/dist/gui/assets/{chunk-EX3LRPZG-CTtRnKWx.js → chunk-EX3LRPZG-tp9GO_-e.js} +1 -1
- package/dist/gui/assets/{chunk-JWPE2WC7-Dp9usWZX.js → chunk-JWPE2WC7-CtyvW3SG.js} +1 -1
- package/dist/gui/assets/{chunk-MOJQB5TN-OoM3V1FE.js → chunk-MOJQB5TN-511Q-LZH.js} +1 -1
- package/dist/gui/assets/{chunk-RYQCIY6F-M8me5HDP.js → chunk-RYQCIY6F-tBbgfa0S.js} +1 -1
- package/dist/gui/assets/{chunk-V7JOEXUC-CLdvRo84.js → chunk-V7JOEXUC-BrxVSk_j.js} +1 -1
- package/dist/gui/assets/{chunk-VR4S4FIN-BMEciG-B.js → chunk-VR4S4FIN-D933YBYm.js} +1 -1
- package/dist/gui/assets/{chunk-XXDRQBXY-DT8b8q8Q.js → chunk-XXDRQBXY-BFQVCR7q.js} +1 -1
- package/dist/gui/assets/classDiagram-OUVF2IWQ-CbHDPN3x.js +1 -0
- package/dist/gui/assets/classDiagram-v2-EOCWNBFH-CbHDPN3x.js +1 -0
- package/dist/gui/assets/{cose-bilkent-JH36ORCC-C1w9npQD.js → cose-bilkent-JH36ORCC-CvG96QUS.js} +1 -1
- package/dist/gui/assets/{cynefin-VYW2F7L2-buyB6Ypi.js → cynefin-VYW2F7L2-D1hO5-bj.js} +1 -1
- package/dist/gui/assets/{cynefinDiagram-TSTJHNR4-DPQmwOX9.js → cynefinDiagram-TSTJHNR4-CGlQfxl6.js} +1 -1
- package/dist/gui/assets/{dagre-VKFMJZFB-DfbmQsg3.js → dagre-VKFMJZFB-DONzuwX_.js} +1 -1
- package/dist/gui/assets/{diagram-FQU43EPY-DZZkoYxF.js → diagram-FQU43EPY-HAAtgUO1.js} +1 -1
- package/dist/gui/assets/{diagram-G47NLZAW-DJyFDLgi.js → diagram-G47NLZAW-BXhu9UFM.js} +1 -1
- package/dist/gui/assets/{diagram-NH7WQ7WH-CeIoika9.js → diagram-NH7WQ7WH-C4ERZVd_.js} +1 -1
- package/dist/gui/assets/{diagram-OA4YK3LP-D3bJmzbA.js → diagram-OA4YK3LP-w9tIMMGB.js} +1 -1
- package/dist/gui/assets/{diagram-WEI45ONY-5whYnN6Z.js → diagram-WEI45ONY-CaAeYUbA.js} +1 -1
- package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-fRcOahPq.js → ebnfDiagram-CCIWWBDH-B6W0V0qu.js} +1 -1
- package/dist/gui/assets/{erDiagram-Q63AITRT-h22ShxWi.js → erDiagram-Q63AITRT-C3sskZRU.js} +1 -1
- package/dist/gui/assets/{flowDiagram-23GEKE2U-CImBEPKl.js → flowDiagram-23GEKE2U-C104zn-S.js} +1 -1
- package/dist/gui/assets/{ganttDiagram-NO4QXBWP-D7GfEoL1.js → ganttDiagram-NO4QXBWP-BTCssrDx.js} +1 -1
- package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-BQlPqwV3.js → gitGraphDiagram-IHSO6WYX-DjOOeH7D.js} +1 -1
- package/dist/gui/assets/{index-BnMXI8QL.js → index-DxqBnHJb.js} +27 -27
- package/dist/gui/assets/index-S1uf4yLT.css +1 -0
- package/dist/gui/assets/{infoDiagram-FWYZ7A6U-dQgAvdfE.js → infoDiagram-FWYZ7A6U-BmFWZBS4.js} +1 -1
- package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-Dsb8okiC.js → ishikawaDiagram-FXEZZL3T-CDxcUzdH.js} +1 -1
- package/dist/gui/assets/{journeyDiagram-5HDEW3XC-BVaJ8kFx.js → journeyDiagram-5HDEW3XC-D-6O7chT.js} +1 -1
- package/dist/gui/assets/{kanban-definition-HUTT4EX6-CtRRLwap.js → kanban-definition-HUTT4EX6-B8x7k6Fv.js} +1 -1
- package/dist/gui/assets/{linear-CtTzoPCE.js → linear-D5Ymya6M.js} +1 -1
- package/dist/gui/assets/{mermaid.core-BO-VJXe-.js → mermaid.core-DufPaTEv.js} +4 -4
- package/dist/gui/assets/{mindmap-definition-LN4V7U3C-Djl0w9Df.js → mindmap-definition-LN4V7U3C-DtvTXeH1.js} +1 -1
- package/dist/gui/assets/{pegDiagram-2B236MQR-D2VRUT1H.js → pegDiagram-2B236MQR-CC4Ckg2m.js} +1 -1
- package/dist/gui/assets/{pieDiagram-ENE6RG2P-Bp7Ytwe1.js → pieDiagram-ENE6RG2P-CbjSsDIy.js} +1 -1
- package/dist/gui/assets/{quadrantDiagram-ABIIQ3AL-Df4S5Uw8.js → quadrantDiagram-ABIIQ3AL-BxPwfoi7.js} +1 -1
- package/dist/gui/assets/{railroadDiagram-RFXS5EU6-1E9T3-eU.js → railroadDiagram-RFXS5EU6-YX-VS0Vh.js} +1 -1
- package/dist/gui/assets/{requirementDiagram-TGXJPOKE-ClWnTEWR.js → requirementDiagram-TGXJPOKE-BEtW3t1o.js} +1 -1
- package/dist/gui/assets/{sankeyDiagram-HTMAVEWB-kV-szT-S.js → sankeyDiagram-HTMAVEWB-ChxRWgoD.js} +1 -1
- package/dist/gui/assets/{sequenceDiagram-DBY2YBRQ-C17izAQ5.js → sequenceDiagram-DBY2YBRQ-_fnT_6l0.js} +1 -1
- package/dist/gui/assets/{sizeCapture-X5ZJPWSS-Qa6aDRpG.js → sizeCapture-X5ZJPWSS-GrOKZWai.js} +1 -1
- package/dist/gui/assets/{stateDiagram-2N3HPSRC-C84qUVRJ.js → stateDiagram-2N3HPSRC-CXQbtXQA.js} +1 -1
- package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-BvJQB9Ga.js +1 -0
- package/dist/gui/assets/{swimlanes-5IMT3BWC-BByVALRE.js → swimlanes-5IMT3BWC-CNBWQS8A.js} +2 -2
- package/dist/gui/assets/swimlanesDiagram-G3AALYLV-CnSljsJi.js +8 -0
- package/dist/gui/assets/{timeline-definition-FHXFAJF6-BD9jOAZN.js → timeline-definition-FHXFAJF6-Kvxm3vNr.js} +1 -1
- package/dist/gui/assets/{vennDiagram-L72KCM5P-BqTRSqDu.js → vennDiagram-L72KCM5P-BzoVFyz9.js} +1 -1
- package/dist/gui/assets/{wardleyDiagram-EHGQE667-DqSSn_TX.js → wardleyDiagram-EHGQE667-BksgQ49c.js} +1 -1
- package/dist/gui/assets/{xychartDiagram-FW5EYKEG-B5GDvD8r.js → xychartDiagram-FW5EYKEG-C4FbK9Sx.js} +1 -1
- package/dist/gui/index.html +2 -2
- package/package.json +1 -1
- package/dist/gui/assets/channel-DAAPjLkv.js +0 -1
- package/dist/gui/assets/classDiagram-OUVF2IWQ-DU6pj67q.js +0 -1
- package/dist/gui/assets/classDiagram-v2-EOCWNBFH-DU6pj67q.js +0 -1
- package/dist/gui/assets/index-B_NGA1xd.css +0 -1
- package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-Ct4b-n_-.js +0 -1
- package/dist/gui/assets/swimlanesDiagram-G3AALYLV-0XnzonY2.js +0 -8
package/dist/cli/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { homedir, networkInterfaces } from "node:os";
|
|
2
|
+
import { homedir, platform, networkInterfaces } from "node:os";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
|
-
import { stat, readFile, writeFile, rm, mkdir, access, rename, readdir,
|
|
4
|
+
import { stat, readFile, writeFile, rm, mkdir, access, rename, readdir, appendFile, copyFile } from "node:fs/promises";
|
|
5
5
|
import { join, dirname, extname, resolve, sep, isAbsolute, basename as basename$1, relative, normalize } from "node:path";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { spawn, execFile } from "node:child_process";
|
|
8
|
-
import { existsSync, statSync, mkdirSync, watch, createReadStream,
|
|
8
|
+
import { existsSync, statSync, mkdirSync, watch, createReadStream, openSync } from "node:fs";
|
|
9
9
|
import { serve } from "@hono/node-server";
|
|
10
10
|
import { Hono } from "hono";
|
|
11
11
|
import { promisify } from "node:util";
|
|
@@ -21,7 +21,7 @@ import { Codex } from "@openai/codex-sdk";
|
|
|
21
21
|
import { createOpencode } from "@opencode-ai/sdk";
|
|
22
22
|
import { createInterface as createInterface$1 } from "node:readline/promises";
|
|
23
23
|
import MarkdownIt from "markdown-it";
|
|
24
|
-
const version = "0.3.
|
|
24
|
+
const version = "0.3.6";
|
|
25
25
|
const pkg = {
|
|
26
26
|
version
|
|
27
27
|
};
|
|
@@ -1096,7 +1096,7 @@ function createSessionsRoutes(resolveProject) {
|
|
|
1096
1096
|
});
|
|
1097
1097
|
return app;
|
|
1098
1098
|
}
|
|
1099
|
-
const execFileP = promisify(execFile);
|
|
1099
|
+
const execFileP$1 = promisify(execFile);
|
|
1100
1100
|
class GitError extends Error {
|
|
1101
1101
|
code;
|
|
1102
1102
|
stderr;
|
|
@@ -1151,7 +1151,7 @@ function assertSafeRelativePath(cwd, p) {
|
|
|
1151
1151
|
}
|
|
1152
1152
|
async function runGit(cwd, args) {
|
|
1153
1153
|
try {
|
|
1154
|
-
const result = await execFileP("git", args, {
|
|
1154
|
+
const result = await execFileP$1("git", args, {
|
|
1155
1155
|
cwd,
|
|
1156
1156
|
encoding: "utf8",
|
|
1157
1157
|
maxBuffer: 16 * 1024 * 1024
|
|
@@ -1170,7 +1170,7 @@ async function runGitChecked(cwd, args) {
|
|
|
1170
1170
|
}
|
|
1171
1171
|
async function runGitRaw(cwd, args) {
|
|
1172
1172
|
try {
|
|
1173
|
-
const result = await execFileP("git", args, {
|
|
1173
|
+
const result = await execFileP$1("git", args, {
|
|
1174
1174
|
cwd,
|
|
1175
1175
|
encoding: "utf8",
|
|
1176
1176
|
maxBuffer: 16 * 1024 * 1024
|
|
@@ -1463,6 +1463,365 @@ function buildGitOpsPrompt(action, specId, specRel, reviewRel) {
|
|
|
1463
1463
|
`;
|
|
1464
1464
|
}
|
|
1465
1465
|
}
|
|
1466
|
+
const CURRENT_VERSION$1 = 1;
|
|
1467
|
+
const DEFAULT_GLOBAL_AGENT = { defaultKind: "claude" };
|
|
1468
|
+
const DEFAULT_NOTIFICATIONS = {
|
|
1469
|
+
sessionEnd: { banner: false, sound: false }
|
|
1470
|
+
};
|
|
1471
|
+
function resolveGlobalConfigDir(env = process.env) {
|
|
1472
|
+
if (env.YORZ_HOME && env.YORZ_HOME.trim()) return env.YORZ_HOME.trim();
|
|
1473
|
+
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
1474
|
+
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config");
|
|
1475
|
+
return join(base, "yorz");
|
|
1476
|
+
}
|
|
1477
|
+
function resolveGlobalConfigPath(env = process.env) {
|
|
1478
|
+
return join(resolveGlobalConfigDir(env), "projects.json");
|
|
1479
|
+
}
|
|
1480
|
+
async function loadGlobalConfig(filePath) {
|
|
1481
|
+
const fp = filePath ?? resolveGlobalConfigPath();
|
|
1482
|
+
if (!existsSync(fp)) return defaultGlobalConfig();
|
|
1483
|
+
let raw;
|
|
1484
|
+
try {
|
|
1485
|
+
raw = await readFile(fp, "utf8");
|
|
1486
|
+
} catch {
|
|
1487
|
+
return defaultGlobalConfig();
|
|
1488
|
+
}
|
|
1489
|
+
if (!raw.trim()) return defaultGlobalConfig();
|
|
1490
|
+
let parsed;
|
|
1491
|
+
try {
|
|
1492
|
+
parsed = JSON.parse(raw);
|
|
1493
|
+
} catch {
|
|
1494
|
+
return defaultGlobalConfig();
|
|
1495
|
+
}
|
|
1496
|
+
return normalizeConfig$1(parsed);
|
|
1497
|
+
}
|
|
1498
|
+
async function saveGlobalConfig(config, filePath) {
|
|
1499
|
+
const fp = filePath ?? resolveGlobalConfigPath();
|
|
1500
|
+
await mkdir(dirname(fp), { recursive: true });
|
|
1501
|
+
const normalized = normalizeConfig$1(config);
|
|
1502
|
+
const body = `${JSON.stringify(normalized, null, 2)}
|
|
1503
|
+
`;
|
|
1504
|
+
const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
1505
|
+
await writeFile(tmp, body, "utf8");
|
|
1506
|
+
await rename(tmp, fp);
|
|
1507
|
+
}
|
|
1508
|
+
function normalizeConfig$1(value) {
|
|
1509
|
+
if (!value || typeof value !== "object") return defaultGlobalConfig();
|
|
1510
|
+
const obj = value;
|
|
1511
|
+
const projectsRaw = Array.isArray(obj.projects) ? obj.projects : [];
|
|
1512
|
+
const projects = [];
|
|
1513
|
+
for (const item of projectsRaw) {
|
|
1514
|
+
if (!item || typeof item !== "object") continue;
|
|
1515
|
+
const it = item;
|
|
1516
|
+
const id = typeof it.id === "string" ? it.id : "";
|
|
1517
|
+
const path = typeof it.path === "string" ? it.path : "";
|
|
1518
|
+
if (!id || !path) continue;
|
|
1519
|
+
const addedAt = typeof it.addedAt === "string" ? it.addedAt : "";
|
|
1520
|
+
const lastActivityAt = typeof it.lastActivityAt === "string" ? it.lastActivityAt : null;
|
|
1521
|
+
const worktree = normalizeWorktree(it.worktree);
|
|
1522
|
+
const entry = { id, path, addedAt, lastActivityAt };
|
|
1523
|
+
if (worktree) entry.worktree = worktree;
|
|
1524
|
+
projects.push(entry);
|
|
1525
|
+
}
|
|
1526
|
+
return {
|
|
1527
|
+
version: CURRENT_VERSION$1,
|
|
1528
|
+
projects,
|
|
1529
|
+
agent: normalizeAgent$1(obj.agent),
|
|
1530
|
+
notifications: normalizeNotifications(obj.notifications)
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
function defaultGlobalConfig() {
|
|
1534
|
+
return {
|
|
1535
|
+
version: CURRENT_VERSION$1,
|
|
1536
|
+
projects: [],
|
|
1537
|
+
agent: { defaultKind: DEFAULT_GLOBAL_AGENT.defaultKind },
|
|
1538
|
+
notifications: {
|
|
1539
|
+
sessionEnd: {
|
|
1540
|
+
banner: DEFAULT_NOTIFICATIONS.sessionEnd.banner,
|
|
1541
|
+
sound: DEFAULT_NOTIFICATIONS.sessionEnd.sound
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
function normalizeAgent$1(value) {
|
|
1547
|
+
if (!value || typeof value !== "object") return { defaultKind: DEFAULT_GLOBAL_AGENT.defaultKind };
|
|
1548
|
+
const obj = value;
|
|
1549
|
+
const defaultKind = obj.defaultKind;
|
|
1550
|
+
if (defaultKind === "opencode" || defaultKind === "codex") return { defaultKind };
|
|
1551
|
+
return { defaultKind: "claude" };
|
|
1552
|
+
}
|
|
1553
|
+
function normalizeNotifications(value) {
|
|
1554
|
+
if (!value || typeof value !== "object") return defaultGlobalConfig().notifications;
|
|
1555
|
+
const obj = value;
|
|
1556
|
+
const sessionEndRaw = obj.sessionEnd;
|
|
1557
|
+
if (!sessionEndRaw || typeof sessionEndRaw !== "object")
|
|
1558
|
+
return defaultGlobalConfig().notifications;
|
|
1559
|
+
const sessionEndObj = sessionEndRaw;
|
|
1560
|
+
return {
|
|
1561
|
+
sessionEnd: {
|
|
1562
|
+
banner: typeof sessionEndObj.banner === "boolean" ? sessionEndObj.banner : DEFAULT_NOTIFICATIONS.sessionEnd.banner,
|
|
1563
|
+
sound: typeof sessionEndObj.sound === "boolean" ? sessionEndObj.sound : DEFAULT_NOTIFICATIONS.sessionEnd.sound
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
function normalizeWorktree(value) {
|
|
1568
|
+
if (!value || typeof value !== "object") return void 0;
|
|
1569
|
+
const it = value;
|
|
1570
|
+
const mainProjectId = typeof it.mainProjectId === "string" ? it.mainProjectId : "";
|
|
1571
|
+
const mainPath = typeof it.mainPath === "string" ? it.mainPath : "";
|
|
1572
|
+
const branch = typeof it.branch === "string" ? it.branch : "";
|
|
1573
|
+
const specId = typeof it.specId === "string" ? it.specId : "";
|
|
1574
|
+
const createdAt = typeof it.createdAt === "string" ? it.createdAt : "";
|
|
1575
|
+
if (!mainProjectId || !mainPath || !branch) return void 0;
|
|
1576
|
+
const cleanSlug = typeof it.cleanSlug === "string" ? it.cleanSlug : void 0;
|
|
1577
|
+
const meta = { mainProjectId, mainPath, branch, specId, createdAt };
|
|
1578
|
+
if (cleanSlug) meta.cleanSlug = cleanSlug;
|
|
1579
|
+
return meta;
|
|
1580
|
+
}
|
|
1581
|
+
function generateProjectId(absPath) {
|
|
1582
|
+
const base = basename$1(absPath);
|
|
1583
|
+
const slug = slugify(base) || "proj";
|
|
1584
|
+
const hash = createHash("sha256").update(absPath).digest("hex").slice(0, 6);
|
|
1585
|
+
return `${slug}-${hash}`;
|
|
1586
|
+
}
|
|
1587
|
+
function slugify(text) {
|
|
1588
|
+
return text.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
|
|
1589
|
+
}
|
|
1590
|
+
async function addProject(absPath, filePath, now = () => /* @__PURE__ */ new Date()) {
|
|
1591
|
+
const config = await loadGlobalConfig(filePath);
|
|
1592
|
+
const existing = config.projects.find((p) => p.path === absPath);
|
|
1593
|
+
if (existing) return { entry: existing, created: false };
|
|
1594
|
+
const entry = {
|
|
1595
|
+
id: generateProjectId(absPath),
|
|
1596
|
+
path: absPath,
|
|
1597
|
+
addedAt: now().toISOString(),
|
|
1598
|
+
lastActivityAt: null
|
|
1599
|
+
};
|
|
1600
|
+
config.projects.push(entry);
|
|
1601
|
+
await saveGlobalConfig(config, filePath);
|
|
1602
|
+
return { entry, created: true };
|
|
1603
|
+
}
|
|
1604
|
+
async function setProjectWorktree(id, worktree, filePath) {
|
|
1605
|
+
const config = await loadGlobalConfig(filePath);
|
|
1606
|
+
const target = config.projects.find((p) => p.id === id);
|
|
1607
|
+
if (!target) return false;
|
|
1608
|
+
if (worktree) target.worktree = worktree;
|
|
1609
|
+
else delete target.worktree;
|
|
1610
|
+
await saveGlobalConfig(config, filePath);
|
|
1611
|
+
return true;
|
|
1612
|
+
}
|
|
1613
|
+
async function removeProject(id, filePath) {
|
|
1614
|
+
const config = await loadGlobalConfig(filePath);
|
|
1615
|
+
const before = config.projects.length;
|
|
1616
|
+
config.projects = config.projects.filter((p) => p.id !== id);
|
|
1617
|
+
if (config.projects.length === before) return false;
|
|
1618
|
+
await saveGlobalConfig(config, filePath);
|
|
1619
|
+
return true;
|
|
1620
|
+
}
|
|
1621
|
+
async function prepareProjectDir(input, cwd) {
|
|
1622
|
+
if (typeof input !== "string" || !input.trim()) {
|
|
1623
|
+
throw new Error("path required");
|
|
1624
|
+
}
|
|
1625
|
+
const trimmed = input.trim();
|
|
1626
|
+
const abs = isAbsolute(trimmed) ? resolve(trimmed) : resolve(cwd ?? process.cwd(), trimmed);
|
|
1627
|
+
if (!existsSync(abs)) {
|
|
1628
|
+
throw new Error(`path does not exist: ${abs}`);
|
|
1629
|
+
}
|
|
1630
|
+
const stats = await stat(abs);
|
|
1631
|
+
if (!stats.isDirectory()) {
|
|
1632
|
+
throw new Error(`path is not a directory: ${abs}`);
|
|
1633
|
+
}
|
|
1634
|
+
await mkdir(join(abs, ".yorz", "specs"), { recursive: true });
|
|
1635
|
+
return abs;
|
|
1636
|
+
}
|
|
1637
|
+
const LEVEL_WEIGHT = {
|
|
1638
|
+
debug: 10,
|
|
1639
|
+
info: 20,
|
|
1640
|
+
warn: 30,
|
|
1641
|
+
error: 40
|
|
1642
|
+
};
|
|
1643
|
+
const LOG_LEVELS = Object.keys(LEVEL_WEIGHT);
|
|
1644
|
+
const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
1645
|
+
const DEFAULT_MAX_ARCHIVES = 1;
|
|
1646
|
+
const DEFAULT_LOG_FILE = "serve.log";
|
|
1647
|
+
const STDIO_LOG_FILE = "serve-stdio.log";
|
|
1648
|
+
function resolveLogDir(env = process.env) {
|
|
1649
|
+
return join(resolveGlobalConfigDir(env), "logs");
|
|
1650
|
+
}
|
|
1651
|
+
function resolveLogLevel(env = process.env) {
|
|
1652
|
+
const raw = env.YORZ_LOG_LEVEL?.trim().toLowerCase();
|
|
1653
|
+
if (raw && LOG_LEVELS.includes(raw)) return raw;
|
|
1654
|
+
return "info";
|
|
1655
|
+
}
|
|
1656
|
+
function defaultOptions(env = process.env) {
|
|
1657
|
+
return {
|
|
1658
|
+
dir: resolveLogDir(env),
|
|
1659
|
+
fileName: DEFAULT_LOG_FILE,
|
|
1660
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
1661
|
+
maxArchives: DEFAULT_MAX_ARCHIVES,
|
|
1662
|
+
level: resolveLogLevel(env),
|
|
1663
|
+
mirrorConsole: true
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
class RotatingFileSink {
|
|
1667
|
+
constructor(dir, fileName, maxBytes, maxArchives) {
|
|
1668
|
+
this.dir = dir;
|
|
1669
|
+
this.fileName = fileName;
|
|
1670
|
+
this.maxBytes = maxBytes;
|
|
1671
|
+
this.maxArchives = maxArchives;
|
|
1672
|
+
}
|
|
1673
|
+
currentSize = 0;
|
|
1674
|
+
initialized = false;
|
|
1675
|
+
queue = Promise.resolve();
|
|
1676
|
+
get filePath() {
|
|
1677
|
+
return join(this.dir, this.fileName);
|
|
1678
|
+
}
|
|
1679
|
+
archivePath(index) {
|
|
1680
|
+
return `${this.filePath}.${index}`;
|
|
1681
|
+
}
|
|
1682
|
+
/** Enqueue a line; returns immediately, never throws. */
|
|
1683
|
+
write(line) {
|
|
1684
|
+
this.queue = this.queue.then(() => this.writeNow(line)).catch(() => {
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
/** Resolve once every queued line has been flushed to disk. */
|
|
1688
|
+
async flush() {
|
|
1689
|
+
await this.queue.catch(() => {
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
async writeNow(line) {
|
|
1693
|
+
try {
|
|
1694
|
+
await this.ensureInit();
|
|
1695
|
+
const bytes = Buffer.byteLength(line, "utf8");
|
|
1696
|
+
if (this.currentSize > 0 && this.currentSize + bytes > this.maxBytes) {
|
|
1697
|
+
await this.rotate();
|
|
1698
|
+
}
|
|
1699
|
+
await appendFile(this.filePath, line, "utf8");
|
|
1700
|
+
this.currentSize += bytes;
|
|
1701
|
+
} catch {
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
/** Lazily create the dir and recover `currentSize` from an existing file. */
|
|
1705
|
+
async ensureInit() {
|
|
1706
|
+
if (this.initialized) return;
|
|
1707
|
+
await mkdir(this.dir, { recursive: true });
|
|
1708
|
+
try {
|
|
1709
|
+
const st = await stat(this.filePath);
|
|
1710
|
+
this.currentSize = st.size;
|
|
1711
|
+
} catch {
|
|
1712
|
+
this.currentSize = 0;
|
|
1713
|
+
}
|
|
1714
|
+
this.initialized = true;
|
|
1715
|
+
}
|
|
1716
|
+
async rotate() {
|
|
1717
|
+
if (this.maxArchives <= 0) {
|
|
1718
|
+
await writeFile(this.filePath, "", "utf8");
|
|
1719
|
+
this.currentSize = 0;
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
for (let i = this.maxArchives - 1; i >= 1; i--) {
|
|
1723
|
+
try {
|
|
1724
|
+
await rename(this.archivePath(i), this.archivePath(i + 1));
|
|
1725
|
+
} catch {
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
await rename(this.filePath, this.archivePath(1));
|
|
1729
|
+
this.currentSize = 0;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
function createSink(options) {
|
|
1733
|
+
return new RotatingFileSink(options.dir, options.fileName, options.maxBytes, options.maxArchives);
|
|
1734
|
+
}
|
|
1735
|
+
function formatMeta(meta) {
|
|
1736
|
+
if (!meta) return "";
|
|
1737
|
+
const keys = Object.keys(meta);
|
|
1738
|
+
if (keys.length === 0) return "";
|
|
1739
|
+
try {
|
|
1740
|
+
return ` ${JSON.stringify(meta, jsonReplacer)}`;
|
|
1741
|
+
} catch {
|
|
1742
|
+
return "";
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
function jsonReplacer(_key, value) {
|
|
1746
|
+
if (value instanceof Error) return { message: value.message, stack: value.stack };
|
|
1747
|
+
if (typeof value === "bigint") return value.toString();
|
|
1748
|
+
return value;
|
|
1749
|
+
}
|
|
1750
|
+
class Logger {
|
|
1751
|
+
constructor(core, scope) {
|
|
1752
|
+
this.core = core;
|
|
1753
|
+
this.scope = scope;
|
|
1754
|
+
}
|
|
1755
|
+
get level() {
|
|
1756
|
+
return this.core.options.level;
|
|
1757
|
+
}
|
|
1758
|
+
get filePath() {
|
|
1759
|
+
return this.core.sink.filePath;
|
|
1760
|
+
}
|
|
1761
|
+
get dir() {
|
|
1762
|
+
return this.core.options.dir;
|
|
1763
|
+
}
|
|
1764
|
+
/** Derive a logger that prefixes every line with `[scope]`. */
|
|
1765
|
+
child(scope) {
|
|
1766
|
+
const next = this.scope && this.scope !== "yorz" ? `${this.scope}:${scope}` : scope;
|
|
1767
|
+
return new Logger(this.core, next);
|
|
1768
|
+
}
|
|
1769
|
+
debug(msg, meta) {
|
|
1770
|
+
this.emit("debug", msg, meta);
|
|
1771
|
+
}
|
|
1772
|
+
info(msg, meta) {
|
|
1773
|
+
this.emit("info", msg, meta);
|
|
1774
|
+
}
|
|
1775
|
+
warn(msg, meta) {
|
|
1776
|
+
this.emit("warn", msg, meta);
|
|
1777
|
+
}
|
|
1778
|
+
error(msg, meta) {
|
|
1779
|
+
this.emit("error", msg, meta);
|
|
1780
|
+
}
|
|
1781
|
+
/** Replace options in place; every existing child logger picks the change up. */
|
|
1782
|
+
configure(patch) {
|
|
1783
|
+
const prev = this.core.options;
|
|
1784
|
+
const next = { ...prev, ...patch };
|
|
1785
|
+
this.core.options = next;
|
|
1786
|
+
const sinkChanged = next.dir !== prev.dir || next.fileName !== prev.fileName || next.maxBytes !== prev.maxBytes || next.maxArchives !== prev.maxArchives;
|
|
1787
|
+
if (sinkChanged) this.core.sink = createSink(next);
|
|
1788
|
+
}
|
|
1789
|
+
async flush() {
|
|
1790
|
+
await this.core.sink.flush();
|
|
1791
|
+
}
|
|
1792
|
+
emit(level, msg, meta) {
|
|
1793
|
+
const { options } = this.core;
|
|
1794
|
+
if (LEVEL_WEIGHT[level] < LEVEL_WEIGHT[options.level]) return;
|
|
1795
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] [${this.scope}] ${msg}${formatMeta(meta)}
|
|
1796
|
+
`;
|
|
1797
|
+
if (options.mirrorConsole) mirror(level, line);
|
|
1798
|
+
this.core.sink.write(line);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function mirror(level, line) {
|
|
1802
|
+
const text = line.endsWith("\n") ? line.slice(0, -1) : line;
|
|
1803
|
+
try {
|
|
1804
|
+
if (level === "error") console.error(text);
|
|
1805
|
+
else if (level === "warn") console.warn(text);
|
|
1806
|
+
else console.log(text);
|
|
1807
|
+
} catch {
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
function createLogger(patch = {}) {
|
|
1811
|
+
const options = { ...defaultOptions(), ...patch };
|
|
1812
|
+
return new Logger({ options, sink: createSink(options) }, "yorz");
|
|
1813
|
+
}
|
|
1814
|
+
let singleton = null;
|
|
1815
|
+
function getLogger() {
|
|
1816
|
+
if (!singleton) singleton = createLogger();
|
|
1817
|
+
return singleton;
|
|
1818
|
+
}
|
|
1819
|
+
function configureLogger(patch) {
|
|
1820
|
+
const logger = getLogger();
|
|
1821
|
+
logger.configure(patch);
|
|
1822
|
+
return logger;
|
|
1823
|
+
}
|
|
1824
|
+
const sseLog = () => getLogger().child("sse");
|
|
1466
1825
|
const HEARTBEAT_INTERVAL_MS = 5e3;
|
|
1467
1826
|
function attachHeartbeat(stream, intervalMs = HEARTBEAT_INTERVAL_MS) {
|
|
1468
1827
|
const timer = setInterval(() => {
|
|
@@ -1626,7 +1985,7 @@ class EventsHub {
|
|
|
1626
1985
|
s.queue.push(frame);
|
|
1627
1986
|
}
|
|
1628
1987
|
} catch (err) {
|
|
1629
|
-
|
|
1988
|
+
sseLog().warn("emit failed", { topic, err });
|
|
1630
1989
|
}
|
|
1631
1990
|
}
|
|
1632
1991
|
async flush(s) {
|
|
@@ -1812,10 +2171,10 @@ function createProjectRoutes(registry, worktreeManager) {
|
|
|
1812
2171
|
});
|
|
1813
2172
|
return app;
|
|
1814
2173
|
}
|
|
1815
|
-
const CURRENT_VERSION
|
|
2174
|
+
const CURRENT_VERSION = 1;
|
|
1816
2175
|
const DEFAULT_SPECS_DIR = ".yorz/specs";
|
|
1817
2176
|
function defaultProjectConfig() {
|
|
1818
|
-
return { version: CURRENT_VERSION
|
|
2177
|
+
return { version: CURRENT_VERSION, agent: { kind: "inherit" }, specsDir: DEFAULT_SPECS_DIR };
|
|
1819
2178
|
}
|
|
1820
2179
|
function configPath(cwd) {
|
|
1821
2180
|
return join(cwd, ".yorz", "config.json");
|
|
@@ -1836,12 +2195,12 @@ async function loadProjectConfig(cwd) {
|
|
|
1836
2195
|
} catch {
|
|
1837
2196
|
return defaultProjectConfig();
|
|
1838
2197
|
}
|
|
1839
|
-
return normalizeConfig
|
|
2198
|
+
return normalizeConfig(parsed);
|
|
1840
2199
|
}
|
|
1841
2200
|
async function saveProjectConfig(cwd, cfg) {
|
|
1842
2201
|
const fp = configPath(cwd);
|
|
1843
2202
|
await mkdir(dirname(fp), { recursive: true });
|
|
1844
|
-
const normalized = normalizeConfig
|
|
2203
|
+
const normalized = normalizeConfig(cfg);
|
|
1845
2204
|
const body = `${JSON.stringify(normalized, null, 2)}
|
|
1846
2205
|
`;
|
|
1847
2206
|
const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
@@ -1861,12 +2220,12 @@ function resolveSpecsDir(cwd, cfg) {
|
|
|
1861
2220
|
async function ensureSpecsDirExists(absSpecsDir) {
|
|
1862
2221
|
await mkdir(absSpecsDir, { recursive: true });
|
|
1863
2222
|
}
|
|
1864
|
-
function normalizeConfig
|
|
2223
|
+
function normalizeConfig(value) {
|
|
1865
2224
|
if (!value || typeof value !== "object") return defaultProjectConfig();
|
|
1866
2225
|
const obj = value;
|
|
1867
2226
|
const agent = normalizeAgent(obj.agent);
|
|
1868
2227
|
const specsDir = normalizeSpecsDir(obj.specsDir);
|
|
1869
|
-
return { version: CURRENT_VERSION
|
|
2228
|
+
return { version: CURRENT_VERSION, agent, specsDir };
|
|
1870
2229
|
}
|
|
1871
2230
|
function normalizeAgent(value) {
|
|
1872
2231
|
if (typeof value === "string") {
|
|
@@ -1877,6 +2236,7 @@ function normalizeAgent(value) {
|
|
|
1877
2236
|
if (!value || typeof value !== "object") return { kind: "claude" };
|
|
1878
2237
|
const obj = value;
|
|
1879
2238
|
const kind = obj.kind;
|
|
2239
|
+
if (kind === "inherit") return { kind: "inherit" };
|
|
1880
2240
|
if (kind === "opencode") return { kind: "opencode" };
|
|
1881
2241
|
if (kind === "codex") return { kind: "codex" };
|
|
1882
2242
|
if (kind === "custom") {
|
|
@@ -1912,7 +2272,7 @@ function createProjectConfigRoutes(registry) {
|
|
|
1912
2272
|
} catch {
|
|
1913
2273
|
return c.json({ error: "invalid JSON body" }, 400);
|
|
1914
2274
|
}
|
|
1915
|
-
const parsed = parseBody(raw);
|
|
2275
|
+
const parsed = parseBody$1(raw);
|
|
1916
2276
|
if ("error" in parsed) return c.json({ error: parsed.error }, 400);
|
|
1917
2277
|
const cfg = {
|
|
1918
2278
|
version: 1,
|
|
@@ -1940,7 +2300,7 @@ function createProjectConfigRoutes(registry) {
|
|
|
1940
2300
|
});
|
|
1941
2301
|
return app;
|
|
1942
2302
|
}
|
|
1943
|
-
function parseBody(value) {
|
|
2303
|
+
function parseBody$1(value) {
|
|
1944
2304
|
if (!value || typeof value !== "object") return { error: "body must be an object" };
|
|
1945
2305
|
const obj = value;
|
|
1946
2306
|
const agent = parseAgent(obj.agent);
|
|
@@ -1955,6 +2315,7 @@ function parseAgent(value) {
|
|
|
1955
2315
|
}
|
|
1956
2316
|
const obj = value;
|
|
1957
2317
|
const kind = obj.kind;
|
|
2318
|
+
if (kind === "inherit") return { value: { kind: "inherit" } };
|
|
1958
2319
|
if (kind === "claude") return { value: { kind: "claude" } };
|
|
1959
2320
|
if (kind === "opencode") return { value: { kind: "opencode" } };
|
|
1960
2321
|
if (kind === "codex") return { value: { kind: "codex" } };
|
|
@@ -1978,7 +2339,7 @@ function parseAgent(value) {
|
|
|
1978
2339
|
}
|
|
1979
2340
|
return { value: { kind: "custom", cmd: cmd.trim(), args } };
|
|
1980
2341
|
}
|
|
1981
|
-
return { error: "agent.kind must be claude | opencode | codex | custom" };
|
|
2342
|
+
return { error: "agent.kind must be inherit | claude | opencode | codex | custom" };
|
|
1982
2343
|
}
|
|
1983
2344
|
function parseSpecsDir(value) {
|
|
1984
2345
|
const fallback = defaultProjectConfig().specsDir;
|
|
@@ -1992,6 +2353,62 @@ function parseSpecsDir(value) {
|
|
|
1992
2353
|
}
|
|
1993
2354
|
return { value: trimmed };
|
|
1994
2355
|
}
|
|
2356
|
+
function createGlobalConfigRoutes(globalConfigPath) {
|
|
2357
|
+
const app = new Hono();
|
|
2358
|
+
app.get("/global-config", async (c) => {
|
|
2359
|
+
const cfg = await loadGlobalConfig(globalConfigPath);
|
|
2360
|
+
return c.json({ agent: cfg.agent, notifications: cfg.notifications });
|
|
2361
|
+
});
|
|
2362
|
+
app.put("/global-config", async (c) => {
|
|
2363
|
+
let raw;
|
|
2364
|
+
try {
|
|
2365
|
+
raw = await c.req.json();
|
|
2366
|
+
} catch {
|
|
2367
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
2368
|
+
}
|
|
2369
|
+
const parsed = parseBody(raw);
|
|
2370
|
+
if ("error" in parsed) return c.json({ error: parsed.error }, 400);
|
|
2371
|
+
const cfg = await loadGlobalConfig(globalConfigPath);
|
|
2372
|
+
cfg.agent = parsed.agent;
|
|
2373
|
+
cfg.notifications = parsed.notifications;
|
|
2374
|
+
await saveGlobalConfig(cfg, globalConfigPath);
|
|
2375
|
+
return c.json({ ok: true, config: { agent: cfg.agent, notifications: cfg.notifications } });
|
|
2376
|
+
});
|
|
2377
|
+
return app;
|
|
2378
|
+
}
|
|
2379
|
+
function parseBody(value) {
|
|
2380
|
+
if (!value || typeof value !== "object") return { error: "body must be an object" };
|
|
2381
|
+
const obj = value;
|
|
2382
|
+
const agent = obj.agent;
|
|
2383
|
+
if (!agent || typeof agent !== "object") {
|
|
2384
|
+
return { error: "agent must be an object" };
|
|
2385
|
+
}
|
|
2386
|
+
const agentObj = agent;
|
|
2387
|
+
const defaultKind = agentObj.defaultKind;
|
|
2388
|
+
if (defaultKind !== "claude" && defaultKind !== "opencode" && defaultKind !== "codex") {
|
|
2389
|
+
return { error: "agent.defaultKind must be claude | opencode | codex" };
|
|
2390
|
+
}
|
|
2391
|
+
const notifications = obj.notifications;
|
|
2392
|
+
if (!notifications || typeof notifications !== "object") {
|
|
2393
|
+
return { error: "notifications must be an object" };
|
|
2394
|
+
}
|
|
2395
|
+
const n = notifications;
|
|
2396
|
+
const sessionEnd = n.sessionEnd;
|
|
2397
|
+
if (!sessionEnd || typeof sessionEnd !== "object") {
|
|
2398
|
+
return { error: "notifications.sessionEnd must be an object" };
|
|
2399
|
+
}
|
|
2400
|
+
const s = sessionEnd;
|
|
2401
|
+
if (typeof s.banner !== "boolean") {
|
|
2402
|
+
return { error: "notifications.sessionEnd.banner must be a boolean" };
|
|
2403
|
+
}
|
|
2404
|
+
if (typeof s.sound !== "boolean") {
|
|
2405
|
+
return { error: "notifications.sessionEnd.sound must be a boolean" };
|
|
2406
|
+
}
|
|
2407
|
+
return {
|
|
2408
|
+
agent: { defaultKind },
|
|
2409
|
+
notifications: { sessionEnd: { banner: s.banner, sound: s.sound } }
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
1995
2412
|
function createSpecDraftsRoutes(resolveProject) {
|
|
1996
2413
|
const app = new Hono();
|
|
1997
2414
|
const need = async (c) => {
|
|
@@ -2393,6 +2810,7 @@ async function serveFile(c, file) {
|
|
|
2393
2810
|
return c.body(body);
|
|
2394
2811
|
}
|
|
2395
2812
|
const DEBOUNCE_MS = 200;
|
|
2813
|
+
const registryLog = () => getLogger().child("registry");
|
|
2396
2814
|
class RegistryEventBus {
|
|
2397
2815
|
subs = /* @__PURE__ */ new Set();
|
|
2398
2816
|
watcher = null;
|
|
@@ -2425,7 +2843,7 @@ class RegistryEventBus {
|
|
|
2425
2843
|
this.scheduleEmit();
|
|
2426
2844
|
});
|
|
2427
2845
|
} catch (err) {
|
|
2428
|
-
|
|
2846
|
+
registryLog().warn("projects.json watch failed", { dir, message: err.message });
|
|
2429
2847
|
}
|
|
2430
2848
|
}
|
|
2431
2849
|
stop() {
|
|
@@ -2445,134 +2863,7 @@ class RegistryEventBus {
|
|
|
2445
2863
|
this.debounceTimer.unref?.();
|
|
2446
2864
|
}
|
|
2447
2865
|
}
|
|
2448
|
-
const
|
|
2449
|
-
function resolveGlobalConfigDir(env = process.env) {
|
|
2450
|
-
if (env.YORZ_HOME && env.YORZ_HOME.trim()) return env.YORZ_HOME.trim();
|
|
2451
|
-
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
2452
|
-
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config");
|
|
2453
|
-
return join(base, "yorz");
|
|
2454
|
-
}
|
|
2455
|
-
function resolveGlobalConfigPath(env = process.env) {
|
|
2456
|
-
return join(resolveGlobalConfigDir(env), "projects.json");
|
|
2457
|
-
}
|
|
2458
|
-
async function loadGlobalConfig(filePath) {
|
|
2459
|
-
const fp = filePath ?? resolveGlobalConfigPath();
|
|
2460
|
-
if (!existsSync(fp)) return { version: CURRENT_VERSION, projects: [] };
|
|
2461
|
-
let raw;
|
|
2462
|
-
try {
|
|
2463
|
-
raw = await readFile(fp, "utf8");
|
|
2464
|
-
} catch {
|
|
2465
|
-
return { version: CURRENT_VERSION, projects: [] };
|
|
2466
|
-
}
|
|
2467
|
-
if (!raw.trim()) return { version: CURRENT_VERSION, projects: [] };
|
|
2468
|
-
let parsed;
|
|
2469
|
-
try {
|
|
2470
|
-
parsed = JSON.parse(raw);
|
|
2471
|
-
} catch {
|
|
2472
|
-
return { version: CURRENT_VERSION, projects: [] };
|
|
2473
|
-
}
|
|
2474
|
-
return normalizeConfig(parsed);
|
|
2475
|
-
}
|
|
2476
|
-
async function saveGlobalConfig(config, filePath) {
|
|
2477
|
-
const fp = filePath ?? resolveGlobalConfigPath();
|
|
2478
|
-
await mkdir(dirname(fp), { recursive: true });
|
|
2479
|
-
const normalized = normalizeConfig(config);
|
|
2480
|
-
const body = `${JSON.stringify(normalized, null, 2)}
|
|
2481
|
-
`;
|
|
2482
|
-
const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
2483
|
-
await writeFile(tmp, body, "utf8");
|
|
2484
|
-
await rename(tmp, fp);
|
|
2485
|
-
}
|
|
2486
|
-
function normalizeConfig(value) {
|
|
2487
|
-
if (!value || typeof value !== "object") return { version: CURRENT_VERSION, projects: [] };
|
|
2488
|
-
const obj = value;
|
|
2489
|
-
const projectsRaw = Array.isArray(obj.projects) ? obj.projects : [];
|
|
2490
|
-
const projects = [];
|
|
2491
|
-
for (const item of projectsRaw) {
|
|
2492
|
-
if (!item || typeof item !== "object") continue;
|
|
2493
|
-
const it = item;
|
|
2494
|
-
const id = typeof it.id === "string" ? it.id : "";
|
|
2495
|
-
const path = typeof it.path === "string" ? it.path : "";
|
|
2496
|
-
if (!id || !path) continue;
|
|
2497
|
-
const addedAt = typeof it.addedAt === "string" ? it.addedAt : "";
|
|
2498
|
-
const lastActivityAt = typeof it.lastActivityAt === "string" ? it.lastActivityAt : null;
|
|
2499
|
-
const worktree = normalizeWorktree(it.worktree);
|
|
2500
|
-
const entry = { id, path, addedAt, lastActivityAt };
|
|
2501
|
-
if (worktree) entry.worktree = worktree;
|
|
2502
|
-
projects.push(entry);
|
|
2503
|
-
}
|
|
2504
|
-
return { version: CURRENT_VERSION, projects };
|
|
2505
|
-
}
|
|
2506
|
-
function normalizeWorktree(value) {
|
|
2507
|
-
if (!value || typeof value !== "object") return void 0;
|
|
2508
|
-
const it = value;
|
|
2509
|
-
const mainProjectId = typeof it.mainProjectId === "string" ? it.mainProjectId : "";
|
|
2510
|
-
const mainPath = typeof it.mainPath === "string" ? it.mainPath : "";
|
|
2511
|
-
const branch = typeof it.branch === "string" ? it.branch : "";
|
|
2512
|
-
const specId = typeof it.specId === "string" ? it.specId : "";
|
|
2513
|
-
const createdAt = typeof it.createdAt === "string" ? it.createdAt : "";
|
|
2514
|
-
if (!mainProjectId || !mainPath || !branch) return void 0;
|
|
2515
|
-
const cleanSlug = typeof it.cleanSlug === "string" ? it.cleanSlug : void 0;
|
|
2516
|
-
const meta = { mainProjectId, mainPath, branch, specId, createdAt };
|
|
2517
|
-
if (cleanSlug) meta.cleanSlug = cleanSlug;
|
|
2518
|
-
return meta;
|
|
2519
|
-
}
|
|
2520
|
-
function generateProjectId(absPath) {
|
|
2521
|
-
const base = basename$1(absPath);
|
|
2522
|
-
const slug = slugify(base) || "proj";
|
|
2523
|
-
const hash = createHash("sha256").update(absPath).digest("hex").slice(0, 6);
|
|
2524
|
-
return `${slug}-${hash}`;
|
|
2525
|
-
}
|
|
2526
|
-
function slugify(text) {
|
|
2527
|
-
return text.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
|
|
2528
|
-
}
|
|
2529
|
-
async function addProject(absPath, filePath, now = () => /* @__PURE__ */ new Date()) {
|
|
2530
|
-
const config = await loadGlobalConfig(filePath);
|
|
2531
|
-
const existing = config.projects.find((p) => p.path === absPath);
|
|
2532
|
-
if (existing) return { entry: existing, created: false };
|
|
2533
|
-
const entry = {
|
|
2534
|
-
id: generateProjectId(absPath),
|
|
2535
|
-
path: absPath,
|
|
2536
|
-
addedAt: now().toISOString(),
|
|
2537
|
-
lastActivityAt: null
|
|
2538
|
-
};
|
|
2539
|
-
config.projects.push(entry);
|
|
2540
|
-
await saveGlobalConfig(config, filePath);
|
|
2541
|
-
return { entry, created: true };
|
|
2542
|
-
}
|
|
2543
|
-
async function setProjectWorktree(id, worktree, filePath) {
|
|
2544
|
-
const config = await loadGlobalConfig(filePath);
|
|
2545
|
-
const target = config.projects.find((p) => p.id === id);
|
|
2546
|
-
if (!target) return false;
|
|
2547
|
-
if (worktree) target.worktree = worktree;
|
|
2548
|
-
else delete target.worktree;
|
|
2549
|
-
await saveGlobalConfig(config, filePath);
|
|
2550
|
-
return true;
|
|
2551
|
-
}
|
|
2552
|
-
async function removeProject(id, filePath) {
|
|
2553
|
-
const config = await loadGlobalConfig(filePath);
|
|
2554
|
-
const before = config.projects.length;
|
|
2555
|
-
config.projects = config.projects.filter((p) => p.id !== id);
|
|
2556
|
-
if (config.projects.length === before) return false;
|
|
2557
|
-
await saveGlobalConfig(config, filePath);
|
|
2558
|
-
return true;
|
|
2559
|
-
}
|
|
2560
|
-
async function prepareProjectDir(input, cwd) {
|
|
2561
|
-
if (typeof input !== "string" || !input.trim()) {
|
|
2562
|
-
throw new Error("path required");
|
|
2563
|
-
}
|
|
2564
|
-
const trimmed = input.trim();
|
|
2565
|
-
const abs = isAbsolute(trimmed) ? resolve(trimmed) : resolve(cwd ?? process.cwd(), trimmed);
|
|
2566
|
-
if (!existsSync(abs)) {
|
|
2567
|
-
throw new Error(`path does not exist: ${abs}`);
|
|
2568
|
-
}
|
|
2569
|
-
const stats = await stat(abs);
|
|
2570
|
-
if (!stats.isDirectory()) {
|
|
2571
|
-
throw new Error(`path is not a directory: ${abs}`);
|
|
2572
|
-
}
|
|
2573
|
-
await mkdir(join(abs, ".yorz", "specs"), { recursive: true });
|
|
2574
|
-
return abs;
|
|
2575
|
-
}
|
|
2866
|
+
const worktreeLog = () => getLogger().child("worktree");
|
|
2576
2867
|
class WorktreeManager {
|
|
2577
2868
|
registry;
|
|
2578
2869
|
globalConfigPath;
|
|
@@ -2620,6 +2911,13 @@ class WorktreeManager {
|
|
|
2620
2911
|
};
|
|
2621
2912
|
await setProjectWorktree(entry.id, meta, this.globalConfigPath);
|
|
2622
2913
|
const updated = await this.registry.findEntry(entry.id) ?? { ...entry, worktree: meta };
|
|
2914
|
+
worktreeLog().info("worktree created", {
|
|
2915
|
+
mainProjectId: main.id,
|
|
2916
|
+
worktreeProjectId: entry.id,
|
|
2917
|
+
branch,
|
|
2918
|
+
path: wtPath,
|
|
2919
|
+
baseRef
|
|
2920
|
+
});
|
|
2623
2921
|
return { entry: updated, branch, path: wtPath, baseRef };
|
|
2624
2922
|
}
|
|
2625
2923
|
/** Update the `specId` on a worktree entry once the spec has been created. */
|
|
@@ -2660,6 +2958,12 @@ class WorktreeManager {
|
|
|
2660
2958
|
globalConfigPath: this.globalConfigPath ?? resolveGlobalConfigPath(),
|
|
2661
2959
|
now: input.now ?? (() => /* @__PURE__ */ new Date())
|
|
2662
2960
|
});
|
|
2961
|
+
worktreeLog().warn("merge back hit conflicts", {
|
|
2962
|
+
worktreeProjectId: entry.id,
|
|
2963
|
+
branch,
|
|
2964
|
+
mainPath,
|
|
2965
|
+
conflictSpecId: "conflictSpecId" in conflict ? conflict.conflictSpecId : void 0
|
|
2966
|
+
});
|
|
2663
2967
|
return conflict;
|
|
2664
2968
|
}
|
|
2665
2969
|
const head = await runGitChecked(mainPath, ["rev-parse", "HEAD"]);
|
|
@@ -2674,6 +2978,12 @@ class WorktreeManager {
|
|
|
2674
2978
|
await this.registry.reload(mainEntry.id);
|
|
2675
2979
|
}
|
|
2676
2980
|
this.onProjectsChanged?.();
|
|
2981
|
+
worktreeLog().info("worktree merged back", {
|
|
2982
|
+
worktreeProjectId: entry.id,
|
|
2983
|
+
mainProjectId: mainEntry?.id ?? entry.worktree.mainProjectId,
|
|
2984
|
+
branch,
|
|
2985
|
+
mergeCommit
|
|
2986
|
+
});
|
|
2677
2987
|
return {
|
|
2678
2988
|
status: "merged",
|
|
2679
2989
|
mainProjectId: mainEntry?.id ?? entry.worktree.mainProjectId,
|
|
@@ -2701,6 +3011,7 @@ class WorktreeManager {
|
|
|
2701
3011
|
}
|
|
2702
3012
|
await this.registry.remove(worktreeProjectId);
|
|
2703
3013
|
this.onProjectsChanged?.();
|
|
3014
|
+
worktreeLog().info("worktree removed", { worktreeProjectId, branch, path: wtPath });
|
|
2704
3015
|
}
|
|
2705
3016
|
async listWorktreesOf(mainProjectId) {
|
|
2706
3017
|
const config = await loadGlobalConfig(this.globalConfigPath);
|
|
@@ -2711,7 +3022,7 @@ class WorktreeManager {
|
|
|
2711
3022
|
const files = unmerged.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2712
3023
|
const commitsByFile = {};
|
|
2713
3024
|
for (const file of files) {
|
|
2714
|
-
const
|
|
3025
|
+
const log2 = await runGitRaw(mainPath, [
|
|
2715
3026
|
"log",
|
|
2716
3027
|
"--since=30 days ago",
|
|
2717
3028
|
"--pretty=format:%h%x09%ad%x09%an%x09%s",
|
|
@@ -2719,7 +3030,7 @@ class WorktreeManager {
|
|
|
2719
3030
|
"--",
|
|
2720
3031
|
file
|
|
2721
3032
|
]);
|
|
2722
|
-
commitsByFile[file] = parseCommitLines(
|
|
3033
|
+
commitsByFile[file] = parseCommitLines(log2.stdout);
|
|
2723
3034
|
}
|
|
2724
3035
|
const merges = await runGitRaw(mainPath, [
|
|
2725
3036
|
"log",
|
|
@@ -2767,9 +3078,10 @@ class WorktreeManager {
|
|
|
2767
3078
|
if (this.triggerConflictAgent) {
|
|
2768
3079
|
await this.triggerConflictAgent(args.mainProjectId, specId);
|
|
2769
3080
|
} else {
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
3081
|
+
worktreeLog().warn("triggerConflictAgent not configured; no Agent launched", {
|
|
3082
|
+
specId,
|
|
3083
|
+
specPath
|
|
3084
|
+
});
|
|
2773
3085
|
}
|
|
2774
3086
|
return {
|
|
2775
3087
|
status: "conflict",
|
|
@@ -2916,8 +3228,25 @@ _暂无_
|
|
|
2916
3228
|
function pad2(n) {
|
|
2917
3229
|
return n < 10 ? `0${n}` : String(n);
|
|
2918
3230
|
}
|
|
3231
|
+
const SLOW_REQUEST_MS = 1e3;
|
|
2919
3232
|
function createApp(opts) {
|
|
2920
3233
|
const app = new Hono();
|
|
3234
|
+
const httpLog = getLogger().child("http");
|
|
3235
|
+
const worktreeLog2 = getLogger().child("worktree");
|
|
3236
|
+
app.use("*", async (c, next) => {
|
|
3237
|
+
const startedAt = Date.now();
|
|
3238
|
+
await next();
|
|
3239
|
+
const durationMs = Date.now() - startedAt;
|
|
3240
|
+
const meta = {
|
|
3241
|
+
method: c.req.method,
|
|
3242
|
+
path: c.req.path,
|
|
3243
|
+
status: c.res.status,
|
|
3244
|
+
durationMs
|
|
3245
|
+
};
|
|
3246
|
+
if (c.res.status >= 400) httpLog.warn("request failed", meta);
|
|
3247
|
+
else if (durationMs >= SLOW_REQUEST_MS) httpLog.warn("slow request", meta);
|
|
3248
|
+
else httpLog.debug("request", meta);
|
|
3249
|
+
});
|
|
2921
3250
|
const api = new Hono();
|
|
2922
3251
|
const resolveProject = (id) => opts.registry.getOrCreate(id);
|
|
2923
3252
|
const projectsBus = new RegistryEventBus();
|
|
@@ -2928,9 +3257,10 @@ function createApp(opts) {
|
|
|
2928
3257
|
triggerConflictAgent: async (mainProjectId, specId) => {
|
|
2929
3258
|
const main = await opts.registry.getOrCreate(mainProjectId);
|
|
2930
3259
|
if (!main) {
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
3260
|
+
worktreeLog2.warn("cannot launch conflict Agent: main project not resolvable", {
|
|
3261
|
+
mainProjectId,
|
|
3262
|
+
specId
|
|
3263
|
+
});
|
|
2934
3264
|
return;
|
|
2935
3265
|
}
|
|
2936
3266
|
const { sessionId } = await main.sessions.ensureSessionForSpec(specId);
|
|
@@ -2941,6 +3271,7 @@ function createApp(opts) {
|
|
|
2941
3271
|
}
|
|
2942
3272
|
});
|
|
2943
3273
|
api.route("/", createProjectRoutes(opts.registry, worktreeManager));
|
|
3274
|
+
api.route("/", createGlobalConfigRoutes(opts.registry.configPath()));
|
|
2944
3275
|
api.route("/", createProjectConfigRoutes(opts.registry));
|
|
2945
3276
|
api.route("/", createSpecsRoutes(resolveProject));
|
|
2946
3277
|
api.route("/", createSessionsRoutes(resolveProject));
|
|
@@ -2952,7 +3283,13 @@ function createApp(opts) {
|
|
|
2952
3283
|
app.route("/api", api);
|
|
2953
3284
|
app.route("/", createStaticRoutes(opts.guiRoot));
|
|
2954
3285
|
app.onError((err, c) => {
|
|
2955
|
-
|
|
3286
|
+
httpLog.error("route error", {
|
|
3287
|
+
method: c.req.method,
|
|
3288
|
+
path: c.req.path,
|
|
3289
|
+
status: 500,
|
|
3290
|
+
message: err.message,
|
|
3291
|
+
stack: err.stack
|
|
3292
|
+
});
|
|
2956
3293
|
return c.json({ error: "Internal Server Error", message: err.message }, 500);
|
|
2957
3294
|
});
|
|
2958
3295
|
return app;
|
|
@@ -3404,6 +3741,7 @@ function placeholderSummary(requirement) {
|
|
|
3404
3741
|
if (!firstParagraph) return "(待 Agent 补全)";
|
|
3405
3742
|
return firstParagraph.length > 200 ? `${firstParagraph.slice(0, 199)}…` : firstParagraph;
|
|
3406
3743
|
}
|
|
3744
|
+
const watcherLog = () => getLogger().child("watcher");
|
|
3407
3745
|
const UNLINK_SETTLE_MS = 80;
|
|
3408
3746
|
class SpecWatcher {
|
|
3409
3747
|
root;
|
|
@@ -3427,13 +3765,15 @@ class SpecWatcher {
|
|
|
3427
3765
|
depth: 2,
|
|
3428
3766
|
...usePolling ? { usePolling: true, interval: 100 } : {}
|
|
3429
3767
|
});
|
|
3430
|
-
this.watcher.on("add", (p) => this.handle(p, "updated"));
|
|
3431
|
-
this.watcher.on("change", (p) => this.handle(p, "updated"));
|
|
3432
|
-
this.watcher.on("unlink", (p) => this.handle(p, "removed"));
|
|
3768
|
+
this.watcher.on("add", (p) => this.handle(p, "updated", "add"));
|
|
3769
|
+
this.watcher.on("change", (p) => this.handle(p, "updated", "change"));
|
|
3770
|
+
this.watcher.on("unlink", (p) => this.handle(p, "removed", "unlink"));
|
|
3771
|
+
this.watcher.on("error", (err) => watcherLog().warn("watch error", { root: this.root, err }));
|
|
3433
3772
|
await new Promise((resolve2, reject) => {
|
|
3434
3773
|
this.watcher.once("ready", () => resolve2());
|
|
3435
3774
|
this.watcher.once("error", (err) => reject(err));
|
|
3436
3775
|
});
|
|
3776
|
+
watcherLog().debug("watching specs dir", { root: this.root, usePolling });
|
|
3437
3777
|
}
|
|
3438
3778
|
async close() {
|
|
3439
3779
|
for (const timer of this.pendingUnlinks.values()) clearTimeout(timer);
|
|
@@ -3462,10 +3802,11 @@ class SpecWatcher {
|
|
|
3462
3802
|
this.listListeners.add(cb);
|
|
3463
3803
|
return () => this.listListeners.delete(cb);
|
|
3464
3804
|
}
|
|
3465
|
-
async handle(filePath, evt) {
|
|
3805
|
+
async handle(filePath, evt, raw) {
|
|
3466
3806
|
if (basename$1(filePath) !== "spec.md") return;
|
|
3467
3807
|
const id = this.idForPath(filePath);
|
|
3468
3808
|
if (!id) return;
|
|
3809
|
+
watcherLog().debug("spec file event", { event: raw, specId: id, path: filePath });
|
|
3469
3810
|
if (evt === "removed") {
|
|
3470
3811
|
this.scheduleUnlink(id, filePath);
|
|
3471
3812
|
return;
|
|
@@ -4072,6 +4413,7 @@ class AdapterRegistry {
|
|
|
4072
4413
|
this.byKind.clear();
|
|
4073
4414
|
}
|
|
4074
4415
|
}
|
|
4416
|
+
const agentLog = () => getLogger().child("agent");
|
|
4075
4417
|
const SESSION_LIST_LIMIT = 30;
|
|
4076
4418
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4077
4419
|
const TITLE_MAX_LENGTH = 64;
|
|
@@ -4086,10 +4428,11 @@ function summarizePromptForTitle(prompt) {
|
|
|
4086
4428
|
return cleaned.length > TITLE_MAX_LENGTH ? `${cleaned.slice(0, TITLE_MAX_LENGTH - 1)}…` : cleaned;
|
|
4087
4429
|
}
|
|
4088
4430
|
class SessionManager {
|
|
4089
|
-
constructor(cwd, defaultKind, store) {
|
|
4431
|
+
constructor(cwd, defaultKind, store, opts = {}) {
|
|
4090
4432
|
this.cwd = cwd;
|
|
4091
4433
|
this.defaultKind = defaultKind;
|
|
4092
4434
|
this.store = store;
|
|
4435
|
+
this.opts = opts;
|
|
4093
4436
|
this.adapters = new AdapterRegistry(cwd);
|
|
4094
4437
|
this.statusEmitter.setMaxListeners(0);
|
|
4095
4438
|
}
|
|
@@ -4215,9 +4558,11 @@ class SessionManager {
|
|
|
4215
4558
|
const runId = randomUUID();
|
|
4216
4559
|
const emitter = this.emitterFor(sid);
|
|
4217
4560
|
let currentSid = sid;
|
|
4561
|
+
const startedAt = Date.now();
|
|
4218
4562
|
await this.maybeUpdateTitleFromPrompt(currentSid, prompt).catch(() => {
|
|
4219
4563
|
});
|
|
4220
4564
|
this.setRunning(sid, true);
|
|
4565
|
+
agentLog().info("dispatch start", { sessionId: sid, runId, promptLength: prompt.length });
|
|
4221
4566
|
void (async () => {
|
|
4222
4567
|
try {
|
|
4223
4568
|
const ls = await this.ensureLive(sid);
|
|
@@ -4230,14 +4575,28 @@ class SessionManager {
|
|
|
4230
4575
|
emitter.emit("event", ev);
|
|
4231
4576
|
}
|
|
4232
4577
|
} catch (err) {
|
|
4578
|
+
agentLog().error("dispatch failed", {
|
|
4579
|
+
sessionId: currentSid,
|
|
4580
|
+
runId,
|
|
4581
|
+
durationMs: Date.now() - startedAt,
|
|
4582
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4583
|
+
stack: err instanceof Error ? err.stack : void 0
|
|
4584
|
+
});
|
|
4233
4585
|
emitter.emit("event", {
|
|
4234
4586
|
type: "error",
|
|
4235
4587
|
message: err instanceof Error ? err.message : String(err)
|
|
4236
4588
|
});
|
|
4237
4589
|
} finally {
|
|
4590
|
+
agentLog().info("dispatch end", {
|
|
4591
|
+
sessionId: currentSid,
|
|
4592
|
+
runId,
|
|
4593
|
+
durationMs: Date.now() - startedAt
|
|
4594
|
+
});
|
|
4238
4595
|
await this.store.touch(currentSid).catch(() => {
|
|
4239
4596
|
});
|
|
4240
4597
|
this.setRunning(currentSid, false);
|
|
4598
|
+
void Promise.resolve(this.opts.onSessionEnd?.(currentSid)).catch(() => {
|
|
4599
|
+
});
|
|
4241
4600
|
emitter.emit("done");
|
|
4242
4601
|
}
|
|
4243
4602
|
})();
|
|
@@ -4261,6 +4620,7 @@ class SessionManager {
|
|
|
4261
4620
|
return () => emitter.off("event", onEvent);
|
|
4262
4621
|
}
|
|
4263
4622
|
async reconcile(oldId, newId) {
|
|
4623
|
+
agentLog().debug("reconcile sessionId", { from: oldId, to: newId });
|
|
4264
4624
|
const ls = this.live.get(oldId);
|
|
4265
4625
|
if (ls) {
|
|
4266
4626
|
this.live.delete(oldId);
|
|
@@ -4365,30 +4725,78 @@ class SessionStore {
|
|
|
4365
4725
|
await this.persist();
|
|
4366
4726
|
}
|
|
4367
4727
|
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4728
|
+
const execFileP = promisify(execFile);
|
|
4729
|
+
const COMMAND_TIMEOUT_MS = 4e3;
|
|
4730
|
+
function createSessionEndNotifier(opts = {}) {
|
|
4731
|
+
return async function notifySessionEnded() {
|
|
4732
|
+
const cfg = await loadGlobalConfig(opts.globalConfigPath);
|
|
4733
|
+
const sessionEnd = cfg.notifications.sessionEnd;
|
|
4734
|
+
if (!sessionEnd.banner && !sessionEnd.sound) return;
|
|
4735
|
+
const os = opts.platform ?? platform();
|
|
4736
|
+
const run = opts.runCommand ?? defaultRunCommand;
|
|
4737
|
+
const tasks = [];
|
|
4738
|
+
if (sessionEnd.banner) tasks.push(showBanner(os, run));
|
|
4739
|
+
if (sessionEnd.sound) tasks.push(playSound(os, run));
|
|
4740
|
+
await Promise.all(tasks.map((task) => task.catch(() => {
|
|
4741
|
+
})));
|
|
4742
|
+
};
|
|
4743
|
+
}
|
|
4744
|
+
async function defaultRunCommand(cmd, args) {
|
|
4745
|
+
await execFileP(cmd, args, {
|
|
4746
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
4747
|
+
windowsHide: true
|
|
4748
|
+
});
|
|
4749
|
+
}
|
|
4750
|
+
async function showBanner(os, run) {
|
|
4751
|
+
if (os === "darwin") {
|
|
4752
|
+
await run("osascript", ["-e", 'display notification "" with title "YorZ"']);
|
|
4753
|
+
return;
|
|
4376
4754
|
}
|
|
4377
|
-
if (
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4755
|
+
if (os === "win32") {
|
|
4756
|
+
await run("powershell.exe", [
|
|
4757
|
+
"-NoProfile",
|
|
4758
|
+
"-NonInteractive",
|
|
4759
|
+
"-Command",
|
|
4760
|
+
[
|
|
4761
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
4762
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
4763
|
+
"$n = New-Object System.Windows.Forms.NotifyIcon",
|
|
4764
|
+
"$n.Icon = [System.Drawing.SystemIcons]::Information",
|
|
4765
|
+
"$n.Visible = $true",
|
|
4766
|
+
"$n.ShowBalloonTip(5000, 'YorZ', '', [System.Windows.Forms.ToolTipIcon]::Info)",
|
|
4767
|
+
"Start-Sleep -Milliseconds 5500",
|
|
4768
|
+
"$n.Dispose()"
|
|
4769
|
+
].join("; ")
|
|
4770
|
+
]);
|
|
4771
|
+
return;
|
|
4383
4772
|
}
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4773
|
+
await run("notify-send", ["YorZ", ""]);
|
|
4774
|
+
}
|
|
4775
|
+
async function playSound(os, run) {
|
|
4776
|
+
if (os === "darwin") {
|
|
4777
|
+
await run("afplay", ["/System/Library/Sounds/Submarine.aiff"]);
|
|
4778
|
+
return;
|
|
4779
|
+
}
|
|
4780
|
+
if (os === "win32") {
|
|
4781
|
+
await run("powershell.exe", [
|
|
4782
|
+
"-NoProfile",
|
|
4783
|
+
"-NonInteractive",
|
|
4784
|
+
"-Command",
|
|
4785
|
+
"[console]::beep(440, 180)"
|
|
4786
|
+
]);
|
|
4787
|
+
return;
|
|
4788
|
+
}
|
|
4789
|
+
for (const [cmd, args] of [
|
|
4790
|
+
["paplay", ["/usr/share/sounds/freedesktop/stereo/complete.oga"]],
|
|
4791
|
+
["canberra-gtk-play", ["-i", "complete"]],
|
|
4792
|
+
["aplay", ["/usr/share/sounds/alsa/Front_Center.wav"]]
|
|
4793
|
+
]) {
|
|
4794
|
+
try {
|
|
4795
|
+
await run(cmd, args);
|
|
4796
|
+
return;
|
|
4797
|
+
} catch {
|
|
4798
|
+
}
|
|
4388
4799
|
}
|
|
4389
|
-
if (!agent || typeof agent !== "object") return "claude";
|
|
4390
|
-
const kind = agent.kind;
|
|
4391
|
-
return kind === "codex" || kind === "opencode" ? kind : "claude";
|
|
4392
4800
|
}
|
|
4393
4801
|
class ProjectRegistry {
|
|
4394
4802
|
globalConfigPath;
|
|
@@ -4488,6 +4896,7 @@ class ProjectRegistry {
|
|
|
4488
4896
|
}
|
|
4489
4897
|
async materialize(input) {
|
|
4490
4898
|
const cfg = await loadProjectConfig(input.path);
|
|
4899
|
+
const globalCfg = await loadGlobalConfig(this.globalConfigPath);
|
|
4491
4900
|
const specsDir = resolveSpecsDir(input.path, cfg);
|
|
4492
4901
|
const specsDirRelative = posixRelative(input.path, specsDir);
|
|
4493
4902
|
await ensureSpecsDirExists(specsDir);
|
|
@@ -4499,7 +4908,11 @@ class ProjectRegistry {
|
|
|
4499
4908
|
});
|
|
4500
4909
|
const attachments = new AttachmentStore({ cwd: input.path });
|
|
4501
4910
|
const sessionStore = new SessionStore(input.path);
|
|
4502
|
-
const
|
|
4911
|
+
const notifySessionEnded = createSessionEndNotifier({ globalConfigPath: this.globalConfigPath });
|
|
4912
|
+
const defaultKind = resolveProjectAgentKind(cfg.agent, globalCfg.agent.defaultKind);
|
|
4913
|
+
const sessions = new SessionManager(input.path, defaultKind, sessionStore, {
|
|
4914
|
+
onSessionEnd: notifySessionEnded
|
|
4915
|
+
});
|
|
4503
4916
|
let closed = false;
|
|
4504
4917
|
const instance = {
|
|
4505
4918
|
id: input.id,
|
|
@@ -4530,6 +4943,11 @@ class ProjectRegistry {
|
|
|
4530
4943
|
return instance;
|
|
4531
4944
|
}
|
|
4532
4945
|
}
|
|
4946
|
+
function resolveProjectAgentKind(projectAgent, globalDefault) {
|
|
4947
|
+
if (projectAgent.kind === "inherit") return globalDefault;
|
|
4948
|
+
if (projectAgent.kind === "codex" || projectAgent.kind === "opencode") return projectAgent.kind;
|
|
4949
|
+
return "claude";
|
|
4950
|
+
}
|
|
4533
4951
|
function basename(p) {
|
|
4534
4952
|
const parts = p.split(/[/\\]/).filter(Boolean);
|
|
4535
4953
|
return parts[parts.length - 1] ?? p;
|
|
@@ -4556,6 +4974,7 @@ async function maxSpecUpdatedAt(projectPath) {
|
|
|
4556
4974
|
}
|
|
4557
4975
|
const DEFAULT_PORT = 7423;
|
|
4558
4976
|
const MAX_PORT_TRIES = 10;
|
|
4977
|
+
const log$1 = () => getLogger().child("serve");
|
|
4559
4978
|
async function start(opts = {}) {
|
|
4560
4979
|
const registry = new ProjectRegistry({ globalConfigPath: opts.globalConfigPath });
|
|
4561
4980
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -4577,6 +4996,15 @@ async function start(opts = {}) {
|
|
|
4577
4996
|
console.log(` - ${p.name} -> ${p.path}`);
|
|
4578
4997
|
}
|
|
4579
4998
|
console.log(`agent heartbeat enabled (interval=${HEARTBEAT_INTERVAL_MS / 1e3}s)`);
|
|
4999
|
+
log$1().info("service ready", {
|
|
5000
|
+
pid: process.pid,
|
|
5001
|
+
port: port.port,
|
|
5002
|
+
url,
|
|
5003
|
+
projects: projects.length,
|
|
5004
|
+
node: process.version,
|
|
5005
|
+
cli: pkg.version,
|
|
5006
|
+
logFile: getLogger().filePath
|
|
5007
|
+
});
|
|
4580
5008
|
installGlobalErrorHandlers();
|
|
4581
5009
|
if (opts.open) await tryOpenBrowser(url);
|
|
4582
5010
|
return {
|
|
@@ -4618,10 +5046,18 @@ async function listen(fetchHandler, preferredPort) {
|
|
|
4618
5046
|
);
|
|
4619
5047
|
} catch (err) {
|
|
4620
5048
|
lastErr = err;
|
|
4621
|
-
|
|
5049
|
+
const code = err.code;
|
|
5050
|
+
if (code === "EADDRINUSE") {
|
|
5051
|
+
log$1().warn("port in use, retrying", { port: tryPort, attempt: i + 1, max: MAX_PORT_TRIES });
|
|
5052
|
+
} else {
|
|
5053
|
+
log$1().warn("listen failed", { port: tryPort, code, err: lastErr });
|
|
5054
|
+
break;
|
|
5055
|
+
}
|
|
4622
5056
|
}
|
|
4623
5057
|
}
|
|
4624
|
-
|
|
5058
|
+
const fatal = lastErr ?? new Error(`failed to bind port near ${preferredPort}`);
|
|
5059
|
+
log$1().error("failed to bind any port", { preferredPort, tries: MAX_PORT_TRIES, err: fatal });
|
|
5060
|
+
throw fatal;
|
|
4625
5061
|
}
|
|
4626
5062
|
function listLanUrls(port) {
|
|
4627
5063
|
const urls = [];
|
|
@@ -4643,23 +5079,9 @@ async function tryOpenBrowser(url) {
|
|
|
4643
5079
|
} catch {
|
|
4644
5080
|
}
|
|
4645
5081
|
}
|
|
4646
|
-
let errorLogReady = false;
|
|
4647
|
-
async function ensureErrorLogDir() {
|
|
4648
|
-
const dir = join(resolveGlobalConfigDir(), "logs");
|
|
4649
|
-
if (!errorLogReady) {
|
|
4650
|
-
await mkdir(dir, { recursive: true });
|
|
4651
|
-
errorLogReady = true;
|
|
4652
|
-
}
|
|
4653
|
-
return join(dir, "serve-errors.log");
|
|
4654
|
-
}
|
|
4655
5082
|
function logCrash(kind, payload) {
|
|
4656
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
4657
5083
|
const body = payload instanceof Error ? payload.stack ?? payload.message : String(payload);
|
|
4658
|
-
|
|
4659
|
-
`;
|
|
4660
|
-
console.error(`[yorz] ${kind}:`, payload);
|
|
4661
|
-
void ensureErrorLogDir().then((fp) => appendFile(fp, line, "utf8")).catch(() => {
|
|
4662
|
-
});
|
|
5084
|
+
log$1().error(kind, { detail: body });
|
|
4663
5085
|
}
|
|
4664
5086
|
function installGlobalErrorHandlers() {
|
|
4665
5087
|
if (process.listenerCount("uncaughtException") > 0) return;
|
|
@@ -4670,10 +5092,25 @@ function installGlobalErrorHandlers() {
|
|
|
4670
5092
|
logCrash("unhandledRejection", reason);
|
|
4671
5093
|
});
|
|
4672
5094
|
}
|
|
5095
|
+
const log = () => getLogger().child("serve");
|
|
5096
|
+
function silenceConsoleMirror() {
|
|
5097
|
+
configureLogger({ mirrorConsole: false });
|
|
5098
|
+
}
|
|
4673
5099
|
const DEFAULT_SERVE_PORT = 7423;
|
|
4674
5100
|
const RUNTIME_FILE = "runtime.json";
|
|
4675
5101
|
const START_LOCK_DIR = "serve.lock";
|
|
4676
5102
|
const START_WAIT_MS = 5e3;
|
|
5103
|
+
function backgroundStdio() {
|
|
5104
|
+
try {
|
|
5105
|
+
const dir = resolveLogDir();
|
|
5106
|
+
mkdirSync(dir, { recursive: true });
|
|
5107
|
+
const filePath = join(dir, STDIO_LOG_FILE);
|
|
5108
|
+
const fd = openSync(filePath, "w");
|
|
5109
|
+
return { stdio: ["ignore", fd, fd], path: filePath };
|
|
5110
|
+
} catch {
|
|
5111
|
+
return { stdio: "ignore", path: null };
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
4677
5114
|
async function runServe(opts) {
|
|
4678
5115
|
if (!opts.skipSkillCheck) {
|
|
4679
5116
|
await ensureSkillsInstalledWithLog(opts.cwd ?? process.cwd());
|
|
@@ -4708,6 +5145,7 @@ async function runServe(opts) {
|
|
|
4708
5145
|
return handle;
|
|
4709
5146
|
}
|
|
4710
5147
|
function startBackgroundServe(opts) {
|
|
5148
|
+
silenceConsoleMirror();
|
|
4711
5149
|
return withStartLock(async () => {
|
|
4712
5150
|
const live = await readLiveProcesses();
|
|
4713
5151
|
if (live.length > 0) {
|
|
@@ -4725,12 +5163,24 @@ function startBackgroundServe(opts) {
|
|
|
4725
5163
|
}
|
|
4726
5164
|
const entry = process.argv[1];
|
|
4727
5165
|
if (!entry) throw new Error("Cannot resolve CLI entrypoint for background service");
|
|
5166
|
+
const stdio = backgroundStdio();
|
|
4728
5167
|
const child = spawn(process.execPath, [entry, "serve", ...backgroundArgs(opts)], {
|
|
4729
5168
|
detached: true,
|
|
4730
|
-
stdio:
|
|
5169
|
+
stdio: stdio.stdio
|
|
4731
5170
|
});
|
|
4732
5171
|
child.unref();
|
|
5172
|
+
log().info("background service spawned", {
|
|
5173
|
+
pid: child.pid,
|
|
5174
|
+
stdioFile: stdio.path,
|
|
5175
|
+
logFile: getLogger().filePath
|
|
5176
|
+
});
|
|
4733
5177
|
const runtime = await waitForRuntime(child.pid, START_WAIT_MS);
|
|
5178
|
+
if (!runtime) {
|
|
5179
|
+
log().warn("timed out waiting for runtime.json", {
|
|
5180
|
+
pid: child.pid,
|
|
5181
|
+
timeoutMs: START_WAIT_MS
|
|
5182
|
+
});
|
|
5183
|
+
}
|
|
4734
5184
|
const port = runtime?.port ?? opts.port ?? DEFAULT_SERVE_PORT;
|
|
4735
5185
|
const url = runtime?.url ?? `http://localhost:${port}/`;
|
|
4736
5186
|
console.log(`YorZ Service started in background (pid=${child.pid ?? "unknown"}).`);
|
|
@@ -4766,6 +5216,7 @@ async function ensureSkillsInstalledWithLog(cwd) {
|
|
|
4766
5216
|
}
|
|
4767
5217
|
}
|
|
4768
5218
|
async function runStopServe() {
|
|
5219
|
+
silenceConsoleMirror();
|
|
4769
5220
|
const all = await readAllProcesses();
|
|
4770
5221
|
if (all.length === 0) {
|
|
4771
5222
|
return {
|
|
@@ -4779,6 +5230,7 @@ async function runStopServe() {
|
|
|
4779
5230
|
const dead = all.filter((p) => !isProcessAlive(p.pid));
|
|
4780
5231
|
if (alive.length === 0) {
|
|
4781
5232
|
await removeRuntime();
|
|
5233
|
+
log().warn("cleared stale runtime records", { pids: dead.map((p) => p.pid) });
|
|
4782
5234
|
const deadPidList = dead.map((p) => `pid=${p.pid}`).join(", ");
|
|
4783
5235
|
return {
|
|
4784
5236
|
stopped: false,
|
|
@@ -4793,12 +5245,15 @@ async function runStopServe() {
|
|
|
4793
5245
|
for (const proc of alive) {
|
|
4794
5246
|
try {
|
|
4795
5247
|
process.kill(proc.pid, "SIGTERM");
|
|
4796
|
-
|
|
5248
|
+
log().info("sent SIGTERM", { pid: proc.pid, url: proc.url });
|
|
5249
|
+
} catch (err) {
|
|
5250
|
+
log().warn("SIGTERM failed", { pid: proc.pid, err });
|
|
4797
5251
|
failedPids.push(proc.pid);
|
|
4798
5252
|
continue;
|
|
4799
5253
|
}
|
|
4800
5254
|
let stopped = await waitForProcessExit(proc.pid, 2e3);
|
|
4801
5255
|
if (!stopped) {
|
|
5256
|
+
log().warn("still alive after SIGTERM, escalating to SIGKILL", { pid: proc.pid });
|
|
4802
5257
|
try {
|
|
4803
5258
|
process.kill(proc.pid, "SIGKILL");
|
|
4804
5259
|
} catch {
|
|
@@ -4809,6 +5264,7 @@ async function runStopServe() {
|
|
|
4809
5264
|
stoppedPids.push(proc.pid);
|
|
4810
5265
|
stoppedUrls.push(proc.url);
|
|
4811
5266
|
} else {
|
|
5267
|
+
log().warn("process did not exit", { pid: proc.pid });
|
|
4812
5268
|
failedPids.push(proc.pid);
|
|
4813
5269
|
}
|
|
4814
5270
|
}
|