@andyqiu/codeforge 0.5.5 → 0.5.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/dist/index.js +250 -96
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8205,11 +8205,11 @@ function shouldStopByStuck(history, cfg) {
|
|
|
8205
8205
|
async function withTimeout4(p, timeoutMs) {
|
|
8206
8206
|
if (timeoutMs <= 0)
|
|
8207
8207
|
return await p;
|
|
8208
|
-
return await new Promise((
|
|
8208
|
+
return await new Promise((resolve16, reject) => {
|
|
8209
8209
|
const timer = setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
8210
8210
|
Promise.resolve(p).then((v) => {
|
|
8211
8211
|
clearTimeout(timer);
|
|
8212
|
-
|
|
8212
|
+
resolve16(v);
|
|
8213
8213
|
}, (err) => {
|
|
8214
8214
|
clearTimeout(timer);
|
|
8215
8215
|
reject(err);
|
|
@@ -14676,8 +14676,8 @@ async function sendParentNotice(client, sessionID, text, opts = {}) {
|
|
|
14676
14676
|
id: makePartId(),
|
|
14677
14677
|
type: "text",
|
|
14678
14678
|
text,
|
|
14679
|
-
synthetic:
|
|
14680
|
-
ignored:
|
|
14679
|
+
synthetic: true,
|
|
14680
|
+
ignored: true
|
|
14681
14681
|
}
|
|
14682
14682
|
]
|
|
14683
14683
|
}
|
|
@@ -15879,7 +15879,12 @@ var codeforgeToolsServer = async (ctx) => {
|
|
|
15879
15879
|
...sid ? { currentSessionId: sid } : {},
|
|
15880
15880
|
...sid ? {
|
|
15881
15881
|
sendProgress: async (state, detail) => {
|
|
15882
|
-
|
|
15882
|
+
const client = ctx.client;
|
|
15883
|
+
if (typeof client?.tui?.showToast === "function") {
|
|
15884
|
+
await client.tui.showToast({
|
|
15885
|
+
body: { message: `[merge-loop] ${state}: ${detail}`, variant: "default", duration: 4000, title: "CodeForge" }
|
|
15886
|
+
});
|
|
15887
|
+
}
|
|
15883
15888
|
}
|
|
15884
15889
|
} : {}
|
|
15885
15890
|
});
|
|
@@ -17856,9 +17861,103 @@ var handler12 = modelFallbackServer;
|
|
|
17856
17861
|
|
|
17857
17862
|
// plugins/subtask-heartbeat.ts
|
|
17858
17863
|
import { promises as fsPromises } from "node:fs";
|
|
17859
|
-
import * as
|
|
17864
|
+
import * as path19 from "node:path";
|
|
17860
17865
|
init_runtime_paths();
|
|
17861
17866
|
init_global_config();
|
|
17867
|
+
|
|
17868
|
+
// lib/parent-map-store.ts
|
|
17869
|
+
init_runtime_paths();
|
|
17870
|
+
import { promises as fs15 } from "node:fs";
|
|
17871
|
+
import * as path18 from "node:path";
|
|
17872
|
+
var PARENT_MAP_VERSION = 1;
|
|
17873
|
+
var PARENT_MAP_LOCK_TIMEOUT_MS = 2000;
|
|
17874
|
+
function parentMapDir(mainRoot) {
|
|
17875
|
+
return path18.join(runtimeDir(path18.resolve(mainRoot), { ensure: false }), "session-worktrees");
|
|
17876
|
+
}
|
|
17877
|
+
function parentMapPath(mainRoot) {
|
|
17878
|
+
return path18.join(parentMapDir(mainRoot), "parent-map.json");
|
|
17879
|
+
}
|
|
17880
|
+
function parentMapLockPath(mainRoot) {
|
|
17881
|
+
return path18.join(parentMapDir(mainRoot), "parent-map.lock");
|
|
17882
|
+
}
|
|
17883
|
+
async function readParentMapFile(mainRoot) {
|
|
17884
|
+
const file = parentMapPath(mainRoot);
|
|
17885
|
+
try {
|
|
17886
|
+
const raw = await fs15.readFile(file, "utf8");
|
|
17887
|
+
const parsed = JSON.parse(raw);
|
|
17888
|
+
if (parsed.version !== PARENT_MAP_VERSION || !Array.isArray(parsed.entries)) {
|
|
17889
|
+
return { version: PARENT_MAP_VERSION, entries: [] };
|
|
17890
|
+
}
|
|
17891
|
+
return parsed;
|
|
17892
|
+
} catch (err) {
|
|
17893
|
+
const e = err;
|
|
17894
|
+
if (e.code === "ENOENT")
|
|
17895
|
+
return { version: PARENT_MAP_VERSION, entries: [] };
|
|
17896
|
+
return { version: PARENT_MAP_VERSION, entries: [] };
|
|
17897
|
+
}
|
|
17898
|
+
}
|
|
17899
|
+
async function writeParentMapFile(mainRoot, payload) {
|
|
17900
|
+
const file = parentMapPath(mainRoot);
|
|
17901
|
+
await fs15.mkdir(path18.dirname(file), { recursive: true });
|
|
17902
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
17903
|
+
await fs15.writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
|
|
17904
|
+
await fs15.rename(tmp, file);
|
|
17905
|
+
}
|
|
17906
|
+
async function loadParentMap(mainRoot) {
|
|
17907
|
+
const out = new Map;
|
|
17908
|
+
let file;
|
|
17909
|
+
try {
|
|
17910
|
+
file = await readParentMapFile(mainRoot);
|
|
17911
|
+
} catch {
|
|
17912
|
+
return out;
|
|
17913
|
+
}
|
|
17914
|
+
for (const e of file.entries) {
|
|
17915
|
+
if (!e || typeof e.childID !== "string" || typeof e.parentID !== "string")
|
|
17916
|
+
continue;
|
|
17917
|
+
if (!e.childID || !e.parentID)
|
|
17918
|
+
continue;
|
|
17919
|
+
const ts = typeof e.ts === "number" ? e.ts : Date.now();
|
|
17920
|
+
out.set(e.childID, { parentID: e.parentID, ts });
|
|
17921
|
+
}
|
|
17922
|
+
return out;
|
|
17923
|
+
}
|
|
17924
|
+
async function writeParentMap(mainRoot, snapshot, opts = {}) {
|
|
17925
|
+
const lockPath = parentMapLockPath(mainRoot);
|
|
17926
|
+
await fs15.mkdir(path18.dirname(lockPath), { recursive: true });
|
|
17927
|
+
const lockOpts = {
|
|
17928
|
+
timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
|
|
17929
|
+
...opts
|
|
17930
|
+
};
|
|
17931
|
+
await withFileLock(lockPath, async () => {
|
|
17932
|
+
const entries = [];
|
|
17933
|
+
for (const [childID, v] of snapshot.entries()) {
|
|
17934
|
+
entries.push({ childID, parentID: v.parentID, ts: v.ts });
|
|
17935
|
+
}
|
|
17936
|
+
await writeParentMapFile(mainRoot, { version: PARENT_MAP_VERSION, entries });
|
|
17937
|
+
}, lockOpts);
|
|
17938
|
+
}
|
|
17939
|
+
async function appendParentEntry(mainRoot, childID, parentID, ts, opts = {}) {
|
|
17940
|
+
if (!childID || !parentID)
|
|
17941
|
+
return;
|
|
17942
|
+
const lockPath = parentMapLockPath(mainRoot);
|
|
17943
|
+
await fs15.mkdir(path18.dirname(lockPath), { recursive: true });
|
|
17944
|
+
const lockOpts = {
|
|
17945
|
+
timeoutMs: opts.timeoutMs ?? PARENT_MAP_LOCK_TIMEOUT_MS,
|
|
17946
|
+
...opts
|
|
17947
|
+
};
|
|
17948
|
+
await withFileLock(lockPath, async () => {
|
|
17949
|
+
const current = await readParentMapFile(mainRoot);
|
|
17950
|
+
const idx = current.entries.findIndex((e) => e.childID === childID);
|
|
17951
|
+
if (idx >= 0) {
|
|
17952
|
+
current.entries[idx] = { childID, parentID, ts };
|
|
17953
|
+
} else {
|
|
17954
|
+
current.entries.push({ childID, parentID, ts });
|
|
17955
|
+
}
|
|
17956
|
+
await writeParentMapFile(mainRoot, current);
|
|
17957
|
+
}, lockOpts);
|
|
17958
|
+
}
|
|
17959
|
+
|
|
17960
|
+
// plugins/subtask-heartbeat.ts
|
|
17862
17961
|
var PLUGIN_NAME13 = "subtask-heartbeat";
|
|
17863
17962
|
logLifecycle(PLUGIN_NAME13, "import", {});
|
|
17864
17963
|
var HEARTBEAT_INTERVAL_MS2 = 30000;
|
|
@@ -17876,6 +17975,14 @@ var inflight3 = new Map;
|
|
|
17876
17975
|
var pendingTask = new Map;
|
|
17877
17976
|
var sessionParentMap = new Map;
|
|
17878
17977
|
var _parentParseFailLogged = 0;
|
|
17978
|
+
var _persistRoot = null;
|
|
17979
|
+
function _bulkInjectSessionParentMap(entries) {
|
|
17980
|
+
for (const e of entries) {
|
|
17981
|
+
if (!e.childID || !e.parentID)
|
|
17982
|
+
continue;
|
|
17983
|
+
sessionParentMap.set(e.childID, { parentID: e.parentID, ts: e.ts });
|
|
17984
|
+
}
|
|
17985
|
+
}
|
|
17879
17986
|
function _snapshotInflight() {
|
|
17880
17987
|
return [...inflight3.values()].map((r) => ({ ...r }));
|
|
17881
17988
|
}
|
|
@@ -17884,21 +17991,29 @@ function recordSessionParent(childID, parentID, now = Date.now()) {
|
|
|
17884
17991
|
return;
|
|
17885
17992
|
if (sessionParentMap.has(childID)) {
|
|
17886
17993
|
sessionParentMap.set(childID, { parentID, ts: now });
|
|
17887
|
-
|
|
17888
|
-
|
|
17889
|
-
|
|
17890
|
-
|
|
17891
|
-
|
|
17892
|
-
|
|
17893
|
-
|
|
17894
|
-
|
|
17895
|
-
|
|
17994
|
+
} else {
|
|
17995
|
+
if (sessionParentMap.size >= SESSION_PARENT_MAP_MAX_SIZE) {
|
|
17996
|
+
let oldestKey = null;
|
|
17997
|
+
let oldestTs = Number.POSITIVE_INFINITY;
|
|
17998
|
+
for (const [k, v] of sessionParentMap.entries()) {
|
|
17999
|
+
if (v.ts < oldestTs) {
|
|
18000
|
+
oldestTs = v.ts;
|
|
18001
|
+
oldestKey = k;
|
|
18002
|
+
}
|
|
17896
18003
|
}
|
|
18004
|
+
if (oldestKey !== null)
|
|
18005
|
+
sessionParentMap.delete(oldestKey);
|
|
17897
18006
|
}
|
|
17898
|
-
|
|
17899
|
-
|
|
18007
|
+
sessionParentMap.set(childID, { parentID, ts: now });
|
|
18008
|
+
}
|
|
18009
|
+
if (_persistRoot) {
|
|
18010
|
+
appendParentEntry(_persistRoot, childID, parentID, now).catch((err) => {
|
|
18011
|
+
log7.warn("appendParentEntry 失败(已隔离)", {
|
|
18012
|
+
error: err instanceof Error ? err.message : String(err),
|
|
18013
|
+
childID
|
|
18014
|
+
});
|
|
18015
|
+
});
|
|
17900
18016
|
}
|
|
17901
|
-
sessionParentMap.set(childID, { parentID, ts: now });
|
|
17902
18017
|
}
|
|
17903
18018
|
function lookupParentSessionId(childID, now = Date.now()) {
|
|
17904
18019
|
const entry = sessionParentMap.get(childID);
|
|
@@ -17912,6 +18027,15 @@ function lookupParentSessionId(childID, now = Date.now()) {
|
|
|
17912
18027
|
}
|
|
17913
18028
|
function deleteSessionParent(childID) {
|
|
17914
18029
|
sessionParentMap.delete(childID);
|
|
18030
|
+
if (_persistRoot) {
|
|
18031
|
+
const snapshot = new Map(sessionParentMap);
|
|
18032
|
+
writeParentMap(_persistRoot, snapshot).catch((err) => {
|
|
18033
|
+
log7.warn("writeParentMap (delete) 失败(已隔离)", {
|
|
18034
|
+
error: err instanceof Error ? err.message : String(err),
|
|
18035
|
+
childID
|
|
18036
|
+
});
|
|
18037
|
+
});
|
|
18038
|
+
}
|
|
17915
18039
|
}
|
|
17916
18040
|
function sweepExpiredSessionParents(now = Date.now()) {
|
|
17917
18041
|
let removed = 0;
|
|
@@ -17921,6 +18045,15 @@ function sweepExpiredSessionParents(now = Date.now()) {
|
|
|
17921
18045
|
removed++;
|
|
17922
18046
|
}
|
|
17923
18047
|
}
|
|
18048
|
+
if (removed > 0 && _persistRoot) {
|
|
18049
|
+
const snapshot = new Map(sessionParentMap);
|
|
18050
|
+
writeParentMap(_persistRoot, snapshot).catch((err) => {
|
|
18051
|
+
log7.warn("writeParentMap (sweep) 失败(已隔离)", {
|
|
18052
|
+
error: err instanceof Error ? err.message : String(err),
|
|
18053
|
+
removed
|
|
18054
|
+
});
|
|
18055
|
+
});
|
|
18056
|
+
}
|
|
17924
18057
|
return removed;
|
|
17925
18058
|
}
|
|
17926
18059
|
function detectUnparsedParentID(event) {
|
|
@@ -18218,7 +18351,7 @@ function buildFailureNotice(r, endedType, logPath, worktreePath, now = Date.now(
|
|
|
18218
18351
|
}
|
|
18219
18352
|
async function appendSubagentLog(filePath, line, log7) {
|
|
18220
18353
|
try {
|
|
18221
|
-
await fsPromises.mkdir(
|
|
18354
|
+
await fsPromises.mkdir(path19.dirname(filePath), { recursive: true });
|
|
18222
18355
|
await fsPromises.appendFile(filePath, line + `
|
|
18223
18356
|
`, "utf8");
|
|
18224
18357
|
} catch (err) {
|
|
@@ -18277,6 +18410,27 @@ var subtaskHeartbeatServer = async (ctx) => {
|
|
|
18277
18410
|
});
|
|
18278
18411
|
const client = ctx.client;
|
|
18279
18412
|
const cwd = ctx.directory;
|
|
18413
|
+
_persistRoot = cwd;
|
|
18414
|
+
try {
|
|
18415
|
+
const restored = await loadParentMap(cwd);
|
|
18416
|
+
if (restored.size > 0) {
|
|
18417
|
+
const entries = [...restored.entries()].map(([childID, v]) => ({
|
|
18418
|
+
childID,
|
|
18419
|
+
parentID: v.parentID,
|
|
18420
|
+
ts: v.ts
|
|
18421
|
+
}));
|
|
18422
|
+
_bulkInjectSessionParentMap(entries);
|
|
18423
|
+
safeWriteLog(PLUGIN_NAME13, {
|
|
18424
|
+
hook: "activate",
|
|
18425
|
+
type: "parent-map.restore",
|
|
18426
|
+
restored: restored.size
|
|
18427
|
+
});
|
|
18428
|
+
}
|
|
18429
|
+
} catch (err) {
|
|
18430
|
+
log7.warn("loadParentMap 失败(已隔离),降级为空表", {
|
|
18431
|
+
error: err instanceof Error ? err.message : String(err)
|
|
18432
|
+
});
|
|
18433
|
+
}
|
|
18280
18434
|
const interval = setInterval(() => {
|
|
18281
18435
|
safeAsync(PLUGIN_NAME13, "interval", async () => {
|
|
18282
18436
|
const swept = sweepExpiredPendingTasks();
|
|
@@ -18551,7 +18705,7 @@ var handler14 = parallelStatusServer;
|
|
|
18551
18705
|
|
|
18552
18706
|
// plugins/parallel-tool-nudge.ts
|
|
18553
18707
|
import { readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
|
|
18554
|
-
import { join as
|
|
18708
|
+
import { join as join16 } from "node:path";
|
|
18555
18709
|
import { homedir as homedir6 } from "node:os";
|
|
18556
18710
|
var PLUGIN_NAME15 = "parallel-tool-nudge";
|
|
18557
18711
|
logLifecycle(PLUGIN_NAME15, "import", {});
|
|
@@ -18607,10 +18761,10 @@ function loadAgentToolsMap(rootDir, opts = {}) {
|
|
|
18607
18761
|
const reader = opts.reader ?? defaultReader2;
|
|
18608
18762
|
const dirReader = opts.dirReader ?? defaultDirReader2;
|
|
18609
18763
|
const dirExists = opts.dirExists ?? defaultDirExists2;
|
|
18610
|
-
const homeAgentsDir = opts.homeAgentsDir ??
|
|
18764
|
+
const homeAgentsDir = opts.homeAgentsDir ?? join16(homedir6(), ".config", "opencode", "agents");
|
|
18611
18765
|
const candidateDirs = [
|
|
18612
|
-
|
|
18613
|
-
|
|
18766
|
+
join16(rootDir, ".codeforge", "agents"),
|
|
18767
|
+
join16(rootDir, "agents"),
|
|
18614
18768
|
homeAgentsDir
|
|
18615
18769
|
];
|
|
18616
18770
|
const result = new Map;
|
|
@@ -18633,20 +18787,20 @@ function loadAgentToolsMap(rootDir, opts = {}) {
|
|
|
18633
18787
|
for (const entry of entries) {
|
|
18634
18788
|
if (!entry.endsWith(".md"))
|
|
18635
18789
|
continue;
|
|
18636
|
-
const
|
|
18790
|
+
const path20 = join16(dir, entry);
|
|
18637
18791
|
let content;
|
|
18638
18792
|
try {
|
|
18639
|
-
content = reader(
|
|
18793
|
+
content = reader(path20);
|
|
18640
18794
|
} catch (err) {
|
|
18641
18795
|
log8.warn(`agent.md 读取失败(已跳过)`, {
|
|
18642
|
-
path:
|
|
18796
|
+
path: path20,
|
|
18643
18797
|
error: err instanceof Error ? err.message : String(err)
|
|
18644
18798
|
});
|
|
18645
18799
|
continue;
|
|
18646
18800
|
}
|
|
18647
18801
|
const parsed = parseAgentFrontmatter(content);
|
|
18648
18802
|
if (!parsed) {
|
|
18649
|
-
log8.warn(`agent frontmatter 解析失败(已跳过)`, { path:
|
|
18803
|
+
log8.warn(`agent frontmatter 解析失败(已跳过)`, { path: path20 });
|
|
18650
18804
|
continue;
|
|
18651
18805
|
}
|
|
18652
18806
|
if (result.has(parsed.name))
|
|
@@ -18837,19 +18991,19 @@ var handler16 = async (_ctx3) => {
|
|
|
18837
18991
|
};
|
|
18838
18992
|
|
|
18839
18993
|
// lib/event-stream.ts
|
|
18840
|
-
import { promises as
|
|
18994
|
+
import { promises as fs16 } from "node:fs";
|
|
18841
18995
|
init_runtime_paths();
|
|
18842
|
-
import * as
|
|
18996
|
+
import * as path20 from "node:path";
|
|
18843
18997
|
async function loadSession(id, opts = {}) {
|
|
18844
18998
|
const file = resolveSessionFile(id, opts);
|
|
18845
|
-
const raw = await
|
|
18999
|
+
const raw = await fs16.readFile(file, "utf8");
|
|
18846
19000
|
return parseJsonl(id, raw);
|
|
18847
19001
|
}
|
|
18848
19002
|
async function listSessions(opts = {}) {
|
|
18849
19003
|
const dir = resolveDir(opts);
|
|
18850
19004
|
let entries;
|
|
18851
19005
|
try {
|
|
18852
|
-
entries = await
|
|
19006
|
+
entries = await fs16.readdir(dir, { withFileTypes: true });
|
|
18853
19007
|
} catch (err) {
|
|
18854
19008
|
if (err.code === "ENOENT")
|
|
18855
19009
|
return [];
|
|
@@ -18859,10 +19013,10 @@ async function listSessions(opts = {}) {
|
|
|
18859
19013
|
for (const e of entries) {
|
|
18860
19014
|
if (!e.isFile() || !e.name.endsWith(".jsonl"))
|
|
18861
19015
|
continue;
|
|
18862
|
-
const file =
|
|
19016
|
+
const file = path20.join(dir, e.name);
|
|
18863
19017
|
const id = e.name.replace(/\.jsonl$/, "");
|
|
18864
19018
|
try {
|
|
18865
|
-
const stat = await
|
|
19019
|
+
const stat = await fs16.stat(file);
|
|
18866
19020
|
const headerLine = await readFirstLine(file);
|
|
18867
19021
|
let started_at = stat.birthtimeMs;
|
|
18868
19022
|
if (headerLine) {
|
|
@@ -18886,11 +19040,11 @@ async function listSessions(opts = {}) {
|
|
|
18886
19040
|
return out;
|
|
18887
19041
|
}
|
|
18888
19042
|
function resolveDir(opts = {}) {
|
|
18889
|
-
const root =
|
|
18890
|
-
return opts.sessions_dir ?
|
|
19043
|
+
const root = path20.resolve(opts.root ?? process.cwd());
|
|
19044
|
+
return opts.sessions_dir ? path20.resolve(root, opts.sessions_dir) : path20.join(runtimeDir(root), "sessions");
|
|
18891
19045
|
}
|
|
18892
19046
|
function resolveSessionFile(id, opts = {}) {
|
|
18893
|
-
return
|
|
19047
|
+
return path20.join(resolveDir(opts), `${id}.jsonl`);
|
|
18894
19048
|
}
|
|
18895
19049
|
function parseJsonl(id, raw) {
|
|
18896
19050
|
const events = [];
|
|
@@ -18925,7 +19079,7 @@ function isEvent(obj) {
|
|
|
18925
19079
|
}
|
|
18926
19080
|
async function readFirstLine(file) {
|
|
18927
19081
|
const buf = Buffer.alloc(4096);
|
|
18928
|
-
const fh = await
|
|
19082
|
+
const fh = await fs16.open(file, "r");
|
|
18929
19083
|
try {
|
|
18930
19084
|
const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
|
|
18931
19085
|
const s = buf.subarray(0, bytesRead).toString("utf8");
|
|
@@ -19154,11 +19308,11 @@ function isRecoveryWorthShowing(plan) {
|
|
|
19154
19308
|
|
|
19155
19309
|
// lib/block-pending.ts
|
|
19156
19310
|
init_runtime_paths();
|
|
19157
|
-
import { promises as
|
|
19158
|
-
import * as
|
|
19311
|
+
import { promises as fs17 } from "node:fs";
|
|
19312
|
+
import * as path21 from "node:path";
|
|
19159
19313
|
function blockPendingFilePath(absRoot) {
|
|
19160
19314
|
const rd = runtimeDir(absRoot, { ensure: false });
|
|
19161
|
-
return
|
|
19315
|
+
return path21.join(rd, "sessions", "autonomous-blocks.ndjson");
|
|
19162
19316
|
}
|
|
19163
19317
|
function consumeLockPath(absRoot) {
|
|
19164
19318
|
return blockPendingFilePath(absRoot) + ".consume.lock";
|
|
@@ -19167,7 +19321,7 @@ async function scanBlockPending(absRoot, filterSessionId) {
|
|
|
19167
19321
|
const file = blockPendingFilePath(absRoot);
|
|
19168
19322
|
let raw;
|
|
19169
19323
|
try {
|
|
19170
|
-
raw = await
|
|
19324
|
+
raw = await fs17.readFile(file, "utf8");
|
|
19171
19325
|
} catch {
|
|
19172
19326
|
return [];
|
|
19173
19327
|
}
|
|
@@ -19223,7 +19377,7 @@ async function markBlocksConsumed(absRoot, entries) {
|
|
|
19223
19377
|
if (entries.length === 0)
|
|
19224
19378
|
return;
|
|
19225
19379
|
const file = blockPendingFilePath(absRoot);
|
|
19226
|
-
await
|
|
19380
|
+
await fs17.mkdir(path21.dirname(file), { recursive: true });
|
|
19227
19381
|
const now = new Date().toISOString();
|
|
19228
19382
|
const lines = entries.map((e) => ({
|
|
19229
19383
|
type: "consume",
|
|
@@ -19234,7 +19388,7 @@ async function markBlocksConsumed(absRoot, entries) {
|
|
|
19234
19388
|
`) + `
|
|
19235
19389
|
`;
|
|
19236
19390
|
await withFileLock(consumeLockPath(absRoot), async () => {
|
|
19237
|
-
await
|
|
19391
|
+
await fs17.appendFile(file, lines, "utf8");
|
|
19238
19392
|
});
|
|
19239
19393
|
}
|
|
19240
19394
|
|
|
@@ -19372,8 +19526,8 @@ var sessionRecoveryServer = async (ctx) => {
|
|
|
19372
19526
|
var handler17 = sessionRecoveryServer;
|
|
19373
19527
|
|
|
19374
19528
|
// plugins/subtasks.ts
|
|
19375
|
-
import { promises as
|
|
19376
|
-
import * as
|
|
19529
|
+
import { promises as fs18 } from "node:fs";
|
|
19530
|
+
import * as path22 from "node:path";
|
|
19377
19531
|
|
|
19378
19532
|
// lib/parallel-merge.ts
|
|
19379
19533
|
init_autonomy();
|
|
@@ -20191,14 +20345,14 @@ function describe8(err) {
|
|
|
20191
20345
|
}
|
|
20192
20346
|
}
|
|
20193
20347
|
function sleep2(ms) {
|
|
20194
|
-
return new Promise((
|
|
20348
|
+
return new Promise((resolve16) => setTimeout(resolve16, ms));
|
|
20195
20349
|
}
|
|
20196
20350
|
|
|
20197
20351
|
// plugins/subtasks.ts
|
|
20198
20352
|
init_runtime_paths();
|
|
20199
20353
|
var PLUGIN_NAME18 = "subtasks";
|
|
20200
20354
|
function getLogFile(root = process.cwd()) {
|
|
20201
|
-
return
|
|
20355
|
+
return path22.join(runtimeDir(root), "logs", "subtasks.log");
|
|
20202
20356
|
}
|
|
20203
20357
|
var VERB_RE = /^([a-zA-Z]{3,12})/;
|
|
20204
20358
|
var CN_VERBS = [
|
|
@@ -20503,8 +20657,8 @@ async function writeLog(level, msg, data) {
|
|
|
20503
20657
|
`;
|
|
20504
20658
|
try {
|
|
20505
20659
|
const logFile = getLogFile();
|
|
20506
|
-
await
|
|
20507
|
-
await
|
|
20660
|
+
await fs18.mkdir(path22.dirname(logFile), { recursive: true });
|
|
20661
|
+
await fs18.appendFile(logFile, line, "utf8");
|
|
20508
20662
|
} catch {}
|
|
20509
20663
|
}
|
|
20510
20664
|
logLifecycle(PLUGIN_NAME18, "import");
|
|
@@ -21020,12 +21174,12 @@ var tokenManagerServer = async (ctx) => {
|
|
|
21020
21174
|
var handler20 = tokenManagerServer;
|
|
21021
21175
|
|
|
21022
21176
|
// plugins/tool-policy.ts
|
|
21023
|
-
import { promises as
|
|
21024
|
-
import * as
|
|
21177
|
+
import { promises as fs19 } from "node:fs";
|
|
21178
|
+
import * as path24 from "node:path";
|
|
21025
21179
|
init_autonomy();
|
|
21026
21180
|
|
|
21027
21181
|
// lib/file-regex-acl.ts
|
|
21028
|
-
import * as
|
|
21182
|
+
import * as path23 from "node:path";
|
|
21029
21183
|
function compileRule(r) {
|
|
21030
21184
|
if (r instanceof RegExp)
|
|
21031
21185
|
return r;
|
|
@@ -21091,7 +21245,7 @@ function normalizePath2(p) {
|
|
|
21091
21245
|
let s = p.replace(/\\/g, "/");
|
|
21092
21246
|
if (s.startsWith("./"))
|
|
21093
21247
|
s = s.slice(2);
|
|
21094
|
-
s =
|
|
21248
|
+
s = path23.posix.normalize(s);
|
|
21095
21249
|
return s;
|
|
21096
21250
|
}
|
|
21097
21251
|
function checkFileAccess(acl, file, op) {
|
|
@@ -21201,11 +21355,11 @@ function decideToolCall(ctx, cfg = {}, currentAgent) {
|
|
|
21201
21355
|
action = "deny";
|
|
21202
21356
|
return { action, reasons, autonomy: a, acl: aclResults };
|
|
21203
21357
|
}
|
|
21204
|
-
var POLICY_PATH =
|
|
21358
|
+
var POLICY_PATH = path24.join(".codeforge", "policy.json");
|
|
21205
21359
|
async function loadPolicy(root = process.cwd()) {
|
|
21206
|
-
const file =
|
|
21360
|
+
const file = path24.join(root, POLICY_PATH);
|
|
21207
21361
|
try {
|
|
21208
|
-
const raw = await
|
|
21362
|
+
const raw = await fs19.readFile(file, "utf8");
|
|
21209
21363
|
const data = JSON.parse(raw);
|
|
21210
21364
|
return data;
|
|
21211
21365
|
} catch {
|
|
@@ -21315,7 +21469,7 @@ var handler21 = toolPolicyServer;
|
|
|
21315
21469
|
// plugins/update-checker.ts
|
|
21316
21470
|
import { existsSync as existsSync5 } from "node:fs";
|
|
21317
21471
|
import { homedir as homedir8 } from "node:os";
|
|
21318
|
-
import { join as
|
|
21472
|
+
import { join as join22 } from "node:path";
|
|
21319
21473
|
|
|
21320
21474
|
// lib/update-checker-impl.ts
|
|
21321
21475
|
import { createHash as createHash5 } from "node:crypto";
|
|
@@ -21332,7 +21486,7 @@ import {
|
|
|
21332
21486
|
writeFileSync as writeFileSync2
|
|
21333
21487
|
} from "node:fs";
|
|
21334
21488
|
import { homedir as homedir7, tmpdir } from "node:os";
|
|
21335
|
-
import { dirname as
|
|
21489
|
+
import { dirname as dirname13, join as join21 } from "node:path";
|
|
21336
21490
|
import { fileURLToPath } from "node:url";
|
|
21337
21491
|
import * as https from "node:https";
|
|
21338
21492
|
import * as zlib from "node:zlib";
|
|
@@ -21340,7 +21494,7 @@ import * as zlib from "node:zlib";
|
|
|
21340
21494
|
// lib/version-injected.ts
|
|
21341
21495
|
function getInjectedVersion() {
|
|
21342
21496
|
try {
|
|
21343
|
-
const v = "0.5.
|
|
21497
|
+
const v = "0.5.6";
|
|
21344
21498
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
21345
21499
|
return v;
|
|
21346
21500
|
}
|
|
@@ -21429,18 +21583,18 @@ function readLocalVersion() {
|
|
|
21429
21583
|
return injected;
|
|
21430
21584
|
try {
|
|
21431
21585
|
const here = fileURLToPath(import.meta.url);
|
|
21432
|
-
const root =
|
|
21433
|
-
const pkg = JSON.parse(readFileSync5(
|
|
21586
|
+
const root = dirname13(dirname13(here));
|
|
21587
|
+
const pkg = JSON.parse(readFileSync5(join21(root, "package.json"), "utf8"));
|
|
21434
21588
|
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
21435
21589
|
} catch {
|
|
21436
21590
|
return "0.0.0";
|
|
21437
21591
|
}
|
|
21438
21592
|
}
|
|
21439
21593
|
function defaultCacheDir() {
|
|
21440
|
-
return process.env["CODEFORGE_CACHE_DIR"] ??
|
|
21594
|
+
return process.env["CODEFORGE_CACHE_DIR"] ?? join21(homedir7(), ".cache", "codeforge");
|
|
21441
21595
|
}
|
|
21442
21596
|
function defaultCacheFile() {
|
|
21443
|
-
return
|
|
21597
|
+
return join21(defaultCacheDir(), "update-check.json");
|
|
21444
21598
|
}
|
|
21445
21599
|
function readCache(file) {
|
|
21446
21600
|
try {
|
|
@@ -21458,7 +21612,7 @@ function readCache(file) {
|
|
|
21458
21612
|
}
|
|
21459
21613
|
function writeCache(file, entry) {
|
|
21460
21614
|
try {
|
|
21461
|
-
mkdirSync3(
|
|
21615
|
+
mkdirSync3(dirname13(file), { recursive: true });
|
|
21462
21616
|
writeFileSync2(file, JSON.stringify(entry, null, 2), "utf8");
|
|
21463
21617
|
} catch {}
|
|
21464
21618
|
}
|
|
@@ -21477,7 +21631,7 @@ function fetchLatestTagFromGitHub(repo) {
|
|
|
21477
21631
|
});
|
|
21478
21632
|
}
|
|
21479
21633
|
function getJsonWithRedirect(url, hopsLeft) {
|
|
21480
|
-
return new Promise((
|
|
21634
|
+
return new Promise((resolve16, reject) => {
|
|
21481
21635
|
const u = new URL(url);
|
|
21482
21636
|
const headers = {
|
|
21483
21637
|
"User-Agent": "codeforge-update-checker",
|
|
@@ -21501,12 +21655,12 @@ function getJsonWithRedirect(url, hopsLeft) {
|
|
|
21501
21655
|
return;
|
|
21502
21656
|
}
|
|
21503
21657
|
const next = new URL(res.headers.location, url).toString();
|
|
21504
|
-
getJsonWithRedirect(next, hopsLeft - 1).then(
|
|
21658
|
+
getJsonWithRedirect(next, hopsLeft - 1).then(resolve16, reject);
|
|
21505
21659
|
return;
|
|
21506
21660
|
}
|
|
21507
21661
|
if (status === 404) {
|
|
21508
21662
|
res.resume();
|
|
21509
|
-
|
|
21663
|
+
resolve16(null);
|
|
21510
21664
|
return;
|
|
21511
21665
|
}
|
|
21512
21666
|
if (status >= 400) {
|
|
@@ -21517,7 +21671,7 @@ function getJsonWithRedirect(url, hopsLeft) {
|
|
|
21517
21671
|
let body = "";
|
|
21518
21672
|
res.setEncoding("utf8");
|
|
21519
21673
|
res.on("data", (chunk) => body += chunk);
|
|
21520
|
-
res.on("end", () =>
|
|
21674
|
+
res.on("end", () => resolve16(body));
|
|
21521
21675
|
});
|
|
21522
21676
|
req.on("timeout", () => {
|
|
21523
21677
|
req.destroy();
|
|
@@ -21557,7 +21711,7 @@ async function fetchLatestFromNpm(opts) {
|
|
|
21557
21711
|
return { version, tarballUrl, integrity };
|
|
21558
21712
|
}
|
|
21559
21713
|
function defaultHttpFetcher(url, timeoutMs) {
|
|
21560
|
-
return new Promise((
|
|
21714
|
+
return new Promise((resolve16, reject) => {
|
|
21561
21715
|
const u = new URL(url);
|
|
21562
21716
|
const headers = {
|
|
21563
21717
|
"User-Agent": "codeforge-update-checker",
|
|
@@ -21574,7 +21728,7 @@ function defaultHttpFetcher(url, timeoutMs) {
|
|
|
21574
21728
|
const status = res.statusCode ?? 0;
|
|
21575
21729
|
if (status === 404) {
|
|
21576
21730
|
res.resume();
|
|
21577
|
-
|
|
21731
|
+
resolve16(null);
|
|
21578
21732
|
return;
|
|
21579
21733
|
}
|
|
21580
21734
|
if (status >= 400) {
|
|
@@ -21585,7 +21739,7 @@ function defaultHttpFetcher(url, timeoutMs) {
|
|
|
21585
21739
|
let body = "";
|
|
21586
21740
|
res.setEncoding("utf8");
|
|
21587
21741
|
res.on("data", (chunk) => body += chunk);
|
|
21588
|
-
res.on("end", () =>
|
|
21742
|
+
res.on("end", () => resolve16(body));
|
|
21589
21743
|
});
|
|
21590
21744
|
req.on("timeout", () => {
|
|
21591
21745
|
req.destroy();
|
|
@@ -21596,14 +21750,14 @@ function defaultHttpFetcher(url, timeoutMs) {
|
|
|
21596
21750
|
});
|
|
21597
21751
|
}
|
|
21598
21752
|
async function downloadAndExtractBundle(opts) {
|
|
21599
|
-
const tmpRoot = opts.tmpDir ?? mkdtempSync(
|
|
21753
|
+
const tmpRoot = opts.tmpDir ?? mkdtempSync(join21(tmpdir(), "codeforge-update-"));
|
|
21600
21754
|
mkdirSync3(tmpRoot, { recursive: true });
|
|
21601
21755
|
const fetcher = opts.tarballFetcher ?? defaultBinaryFetcher;
|
|
21602
21756
|
const tarballBuf = await fetcher(opts.tarballUrl);
|
|
21603
21757
|
verifyIntegrity(tarballBuf, opts.expectedIntegrity);
|
|
21604
21758
|
const tarBuf = zlib.gunzipSync(tarballBuf);
|
|
21605
21759
|
extractTarToDir(tarBuf, tmpRoot);
|
|
21606
|
-
const bundlePath =
|
|
21760
|
+
const bundlePath = join21(tmpRoot, "package", "dist", "index.js");
|
|
21607
21761
|
if (!existsSync4(bundlePath)) {
|
|
21608
21762
|
throw new Error(`bundle_not_found: ${bundlePath}`);
|
|
21609
21763
|
}
|
|
@@ -21643,11 +21797,11 @@ function extractTarToDir(tarBuf, destRoot) {
|
|
|
21643
21797
|
offset += 512;
|
|
21644
21798
|
if (typeFlag === "0" || typeFlag === "" || typeFlag === "\x00") {
|
|
21645
21799
|
const fileBuf = tarBuf.subarray(offset, offset + size);
|
|
21646
|
-
const dest =
|
|
21647
|
-
mkdirSync3(
|
|
21800
|
+
const dest = join21(destRoot, fullName);
|
|
21801
|
+
mkdirSync3(dirname13(dest), { recursive: true });
|
|
21648
21802
|
writeFileSync2(dest, fileBuf);
|
|
21649
21803
|
} else if (typeFlag === "5") {
|
|
21650
|
-
mkdirSync3(
|
|
21804
|
+
mkdirSync3(join21(destRoot, fullName), { recursive: true });
|
|
21651
21805
|
}
|
|
21652
21806
|
offset += Math.ceil(size / 512) * 512;
|
|
21653
21807
|
}
|
|
@@ -21656,7 +21810,7 @@ function defaultBinaryFetcher(url) {
|
|
|
21656
21810
|
return downloadBinary(url, 3);
|
|
21657
21811
|
}
|
|
21658
21812
|
function downloadBinary(url, hopsLeft) {
|
|
21659
|
-
return new Promise((
|
|
21813
|
+
return new Promise((resolve16, reject) => {
|
|
21660
21814
|
const u = new URL(url);
|
|
21661
21815
|
const req = https.request({
|
|
21662
21816
|
host: u.hostname,
|
|
@@ -21674,7 +21828,7 @@ function downloadBinary(url, hopsLeft) {
|
|
|
21674
21828
|
return;
|
|
21675
21829
|
}
|
|
21676
21830
|
const next = new URL(res.headers.location, url).toString();
|
|
21677
|
-
downloadBinary(next, hopsLeft - 1).then(
|
|
21831
|
+
downloadBinary(next, hopsLeft - 1).then(resolve16, reject);
|
|
21678
21832
|
return;
|
|
21679
21833
|
}
|
|
21680
21834
|
if (status >= 400) {
|
|
@@ -21684,7 +21838,7 @@ function downloadBinary(url, hopsLeft) {
|
|
|
21684
21838
|
}
|
|
21685
21839
|
const chunks = [];
|
|
21686
21840
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
21687
|
-
res.on("end", () =>
|
|
21841
|
+
res.on("end", () => resolve16(Buffer.concat(chunks)));
|
|
21688
21842
|
});
|
|
21689
21843
|
req.on("timeout", () => {
|
|
21690
21844
|
req.destroy();
|
|
@@ -21700,7 +21854,7 @@ function atomicReplaceBundle(opts) {
|
|
|
21700
21854
|
if (!existsSync4(source)) {
|
|
21701
21855
|
throw new Error(`atomic_source_missing: ${source}`);
|
|
21702
21856
|
}
|
|
21703
|
-
mkdirSync3(
|
|
21857
|
+
mkdirSync3(dirname13(target), { recursive: true });
|
|
21704
21858
|
const newPath = `${target}.new`;
|
|
21705
21859
|
const backupPath = `${target}.bak.${oldVersion}`;
|
|
21706
21860
|
let strategy = "rename";
|
|
@@ -21752,11 +21906,11 @@ function cleanupOldBackups(target, keep) {
|
|
|
21752
21906
|
if (keep <= 0)
|
|
21753
21907
|
return;
|
|
21754
21908
|
try {
|
|
21755
|
-
const dir =
|
|
21909
|
+
const dir = dirname13(target);
|
|
21756
21910
|
const base = target.substring(dir.length + 1);
|
|
21757
21911
|
const prefix = `${base}.bak.`;
|
|
21758
21912
|
const all = readdirSync3(dir).filter((f) => f.startsWith(prefix)).map((f) => {
|
|
21759
|
-
const full =
|
|
21913
|
+
const full = join21(dir, f);
|
|
21760
21914
|
let mtimeMs = 0;
|
|
21761
21915
|
try {
|
|
21762
21916
|
mtimeMs = statSync5(full).mtimeMs;
|
|
@@ -21778,7 +21932,7 @@ function loadCompatibility(opts) {
|
|
|
21778
21932
|
const root = opts?.cwd ?? inferPluginRoot();
|
|
21779
21933
|
if (!root)
|
|
21780
21934
|
return null;
|
|
21781
|
-
file =
|
|
21935
|
+
file = join21(root, "compatibility.json");
|
|
21782
21936
|
}
|
|
21783
21937
|
if (!existsSync4(file))
|
|
21784
21938
|
return null;
|
|
@@ -21803,7 +21957,7 @@ function loadCompatibility(opts) {
|
|
|
21803
21957
|
function inferPluginRoot() {
|
|
21804
21958
|
try {
|
|
21805
21959
|
const here = fileURLToPath(import.meta.url);
|
|
21806
|
-
return
|
|
21960
|
+
return dirname13(dirname13(here));
|
|
21807
21961
|
} catch {
|
|
21808
21962
|
return null;
|
|
21809
21963
|
}
|
|
@@ -21998,14 +22152,14 @@ function detectOpencodeVersion() {
|
|
|
21998
22152
|
}
|
|
21999
22153
|
function getOpencodeBundlePath() {
|
|
22000
22154
|
const candidates = [];
|
|
22001
|
-
candidates.push(
|
|
22155
|
+
candidates.push(join22(homedir8(), ".config", "opencode", "codeforge", "index.js"));
|
|
22002
22156
|
if (process.platform === "win32") {
|
|
22003
22157
|
const appData = process.env["APPDATA"];
|
|
22004
22158
|
if (appData)
|
|
22005
|
-
candidates.push(
|
|
22159
|
+
candidates.push(join22(appData, "opencode", "codeforge", "index.js"));
|
|
22006
22160
|
const localAppData = process.env["LOCALAPPDATA"];
|
|
22007
22161
|
if (localAppData)
|
|
22008
|
-
candidates.push(
|
|
22162
|
+
candidates.push(join22(localAppData, "opencode", "codeforge", "index.js"));
|
|
22009
22163
|
}
|
|
22010
22164
|
for (const c of candidates) {
|
|
22011
22165
|
if (existsSync5(c))
|
|
@@ -22066,11 +22220,11 @@ async function postToast(ctx, message) {
|
|
|
22066
22220
|
var handler22 = updateCheckerServer;
|
|
22067
22221
|
|
|
22068
22222
|
// plugins/workflow-engine.ts
|
|
22069
|
-
import * as
|
|
22223
|
+
import * as path26 from "node:path";
|
|
22070
22224
|
|
|
22071
22225
|
// lib/workflow-loader.ts
|
|
22072
|
-
import { promises as
|
|
22073
|
-
import * as
|
|
22226
|
+
import { promises as fs20 } from "node:fs";
|
|
22227
|
+
import * as path25 from "node:path";
|
|
22074
22228
|
import { z as z32 } from "zod";
|
|
22075
22229
|
var ActionSchema = z32.object({
|
|
22076
22230
|
tool: z32.string().min(1, "action.tool 不能为空"),
|
|
@@ -22156,7 +22310,7 @@ function parseWorkflowYaml(yaml, sourcePath = "<inline>") {
|
|
|
22156
22310
|
async function loadWorkflowFromFile(filePath) {
|
|
22157
22311
|
let txt;
|
|
22158
22312
|
try {
|
|
22159
|
-
txt = await
|
|
22313
|
+
txt = await fs20.readFile(filePath, "utf8");
|
|
22160
22314
|
} catch (err) {
|
|
22161
22315
|
return {
|
|
22162
22316
|
ok: false,
|
|
@@ -22171,7 +22325,7 @@ async function loadWorkflowsFromDir(dir) {
|
|
|
22171
22325
|
const failed = [];
|
|
22172
22326
|
let entries;
|
|
22173
22327
|
try {
|
|
22174
|
-
entries = await
|
|
22328
|
+
entries = await fs20.readdir(dir);
|
|
22175
22329
|
} catch (err) {
|
|
22176
22330
|
const e = err;
|
|
22177
22331
|
if (e.code === "ENOENT")
|
|
@@ -22183,7 +22337,7 @@ async function loadWorkflowsFromDir(dir) {
|
|
|
22183
22337
|
continue;
|
|
22184
22338
|
if (!/\.ya?ml$/i.test(name))
|
|
22185
22339
|
continue;
|
|
22186
|
-
const full =
|
|
22340
|
+
const full = path25.join(dir, name);
|
|
22187
22341
|
const r = await loadWorkflowFromFile(full);
|
|
22188
22342
|
if (r.ok)
|
|
22189
22343
|
loaded.push(r);
|
|
@@ -22573,7 +22727,7 @@ async function handleCommandInvoked(raw, workflowsDir = "workflows") {
|
|
|
22573
22727
|
}
|
|
22574
22728
|
var workflowEngineServer = async (ctx) => {
|
|
22575
22729
|
const directory = ctx.directory ?? process.cwd();
|
|
22576
|
-
const workflowsDir =
|
|
22730
|
+
const workflowsDir = path26.join(directory, "workflows");
|
|
22577
22731
|
ensureRegistry(workflowsDir).catch((err) => fallbackLog2.warn(`[${PLUGIN_NAME23}] preload workflows failed`, {
|
|
22578
22732
|
error: err instanceof Error ? err.message : String(err)
|
|
22579
22733
|
}));
|
|
@@ -22617,7 +22771,7 @@ var workflowEngineServer = async (ctx) => {
|
|
|
22617
22771
|
var handler23 = workflowEngineServer;
|
|
22618
22772
|
|
|
22619
22773
|
// plugins/session-worktree-guard.ts
|
|
22620
|
-
import
|
|
22774
|
+
import path27 from "node:path";
|
|
22621
22775
|
var PLUGIN_NAME24 = "session-worktree-guard";
|
|
22622
22776
|
logLifecycle(PLUGIN_NAME24, "import", {});
|
|
22623
22777
|
var WRITE_INTENT_RE = />(?!=)|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
|
|
@@ -22646,7 +22800,7 @@ var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
|
|
|
22646
22800
|
function rewritePath(value, mainRoot, worktreeRoot) {
|
|
22647
22801
|
if (!value)
|
|
22648
22802
|
return null;
|
|
22649
|
-
const resolved =
|
|
22803
|
+
const resolved = path27.isAbsolute(value) ? value : path27.resolve(mainRoot, value);
|
|
22650
22804
|
if (resolved === mainRoot)
|
|
22651
22805
|
return worktreeRoot;
|
|
22652
22806
|
const prefix = mainRoot.endsWith("/") ? mainRoot : mainRoot + "/";
|