svn-agent-mcp 1.0.0
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/.editorconfig +12 -0
- package/.gitattributes +7 -0
- package/.github/workflows/ci.yml +39 -0
- package/.svn-mcp-policy.json +9 -0
- package/CHANGELOG.md +248 -0
- package/CONTRIBUTING.md +49 -0
- package/LICENSE +201 -0
- package/README.md +143 -0
- package/SECURITY.md +21 -0
- package/THIRD_PARTY_CHECKSUMS.txt +52 -0
- package/THIRD_PARTY_NOTICES.md +46 -0
- package/bin/SlikSvn-DB44-20-x64.dll +0 -0
- package/bin/SlikSvn-libapr-1.dll +0 -0
- package/bin/SlikSvn-libaprutil-1.dll +0 -0
- package/bin/SlikSvn-libcrypto-3-x64.dll +0 -0
- package/bin/SlikSvn-libintl.dll +0 -0
- package/bin/SlikSvn-libsasl21.dll +0 -0
- package/bin/SlikSvn-libssl-3-x64.dll +0 -0
- package/bin/SlikSvn-libsvn_client-1.dll +0 -0
- package/bin/SlikSvn-libsvn_delta-1.dll +0 -0
- package/bin/SlikSvn-libsvn_diff-1.dll +0 -0
- package/bin/SlikSvn-libsvn_fs-1.dll +0 -0
- package/bin/SlikSvn-libsvn_fs_base-1.dll +0 -0
- package/bin/SlikSvn-libsvn_fs_fs-1.dll +0 -0
- package/bin/SlikSvn-libsvn_fs_util-1.dll +0 -0
- package/bin/SlikSvn-libsvn_fs_x-1.dll +0 -0
- package/bin/SlikSvn-libsvn_ra-1.dll +0 -0
- package/bin/SlikSvn-libsvn_repos-1.dll +0 -0
- package/bin/SlikSvn-libsvn_subr-1.dll +0 -0
- package/bin/SlikSvn-libsvn_wc-1.dll +0 -0
- package/bin/System64/concrt140.dll +0 -0
- package/bin/System64/msvcp140.dll +0 -0
- package/bin/System64/msvcp140_1.dll +0 -0
- package/bin/System64/msvcp140_2.dll +0 -0
- package/bin/System64/msvcp140_atomic_wait.dll +0 -0
- package/bin/System64/msvcp140_codecvt_ids.dll +0 -0
- package/bin/System64/vccorlib140.dll +0 -0
- package/bin/System64/vcruntime140.dll +0 -0
- package/bin/System64/vcruntime140_1.dll +0 -0
- package/bin/dos2unix.exe +0 -0
- package/bin/engines/capi.dll +0 -0
- package/bin/libsvnjavahl-1.dll +0 -0
- package/bin/mac2unix.exe +0 -0
- package/bin/svn-populate-node-origins-index.exe +0 -0
- package/bin/svn.exe +0 -0
- package/bin/svnadmin.exe +0 -0
- package/bin/svnauthz-validate.exe +0 -0
- package/bin/svnauthz.exe +0 -0
- package/bin/svnbench.exe +0 -0
- package/bin/svndumpfilter.exe +0 -0
- package/bin/svnfsfs.exe +0 -0
- package/bin/svnlook.exe +0 -0
- package/bin/svnmucc.exe +0 -0
- package/bin/svnrdump.exe +0 -0
- package/bin/svnserve.exe +0 -0
- package/bin/svnsync.exe +0 -0
- package/bin/svnversion.exe +0 -0
- package/bin/unix2dos.exe +0 -0
- package/bin/unix2mac.exe +0 -0
- package/dist/envelope.d.ts +39 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +137 -0
- package/dist/envelope.js.map +1 -0
- package/dist/eol.d.ts +15 -0
- package/dist/eol.d.ts.map +1 -0
- package/dist/eol.js +119 -0
- package/dist/eol.js.map +1 -0
- package/dist/guards.d.ts +26 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +306 -0
- package/dist/guards.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +221 -0
- package/dist/index.js.map +1 -0
- package/dist/parse/commitText.d.ts +2 -0
- package/dist/parse/commitText.d.ts.map +1 -0
- package/dist/parse/commitText.js +9 -0
- package/dist/parse/commitText.js.map +1 -0
- package/dist/parse/diffText.d.ts +7 -0
- package/dist/parse/diffText.d.ts.map +1 -0
- package/dist/parse/diffText.js +55 -0
- package/dist/parse/diffText.js.map +1 -0
- package/dist/parse/infoXml.d.ts +3 -0
- package/dist/parse/infoXml.d.ts.map +1 -0
- package/dist/parse/infoXml.js +35 -0
- package/dist/parse/infoXml.js.map +1 -0
- package/dist/parse/logXml.d.ts +10 -0
- package/dist/parse/logXml.d.ts.map +1 -0
- package/dist/parse/logXml.js +39 -0
- package/dist/parse/logXml.js.map +1 -0
- package/dist/parse/statusXml.d.ts +6 -0
- package/dist/parse/statusXml.d.ts.map +1 -0
- package/dist/parse/statusXml.js +64 -0
- package/dist/parse/statusXml.js.map +1 -0
- package/dist/parse/updateText.d.ts +6 -0
- package/dist/parse/updateText.d.ts.map +1 -0
- package/dist/parse/updateText.js +67 -0
- package/dist/parse/updateText.js.map +1 -0
- package/dist/runner.d.ts +41 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +270 -0
- package/dist/runner.js.map +1 -0
- package/dist/tools/composite.d.ts +15 -0
- package/dist/tools/composite.d.ts.map +1 -0
- package/dist/tools/composite.js +238 -0
- package/dist/tools/composite.js.map +1 -0
- package/dist/tools/diagnose.d.ts +6 -0
- package/dist/tools/diagnose.d.ts.map +1 -0
- package/dist/tools/diagnose.js +131 -0
- package/dist/tools/diagnose.js.map +1 -0
- package/dist/tools/mutating.d.ts +80 -0
- package/dist/tools/mutating.d.ts.map +1 -0
- package/dist/tools/mutating.js +558 -0
- package/dist/tools/mutating.js.map +1 -0
- package/dist/tools/readonly.d.ts +52 -0
- package/dist/tools/readonly.d.ts.map +1 -0
- package/dist/tools/readonly.js +638 -0
- package/dist/tools/readonly.js.map +1 -0
- package/dist/tools/selfcheck.d.ts +5 -0
- package/dist/tools/selfcheck.d.ts.map +1 -0
- package/dist/tools/selfcheck.js +91 -0
- package/dist/tools/selfcheck.js.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/README.md +11 -0
- package/docs/SPEC.md +964 -0
- package/docs/decisions/ADR-001-use-spec-v1-architecture.md +25 -0
- package/docs/decisions/ADR-002-use-capability-based-generic-svn-policy.md +31 -0
- package/docs/decisions/ADR-003-use-plug-and-play-global-registration.md +39 -0
- package/docs/decisions/ADR-004-borrow-diagnostics-not-external-mcp-surface.md +39 -0
- package/docs/svnrules.md +84 -0
- package/docs/toolrules.md +34 -0
- package/jest.config.cjs +14 -0
- package/package.json +51 -0
- package/scripts/clean.mjs +27 -0
- package/scripts/prepare-release.mjs +84 -0
- package/src/envelope.ts +182 -0
- package/src/eol.ts +137 -0
- package/src/guards.ts +362 -0
- package/src/index.ts +364 -0
- package/src/parse/commitText.ts +9 -0
- package/src/parse/diffText.ts +66 -0
- package/src/parse/infoXml.ts +51 -0
- package/src/parse/logXml.ts +64 -0
- package/src/parse/statusXml.ts +87 -0
- package/src/parse/updateText.ts +76 -0
- package/src/runner.ts +344 -0
- package/src/tools/composite.ts +289 -0
- package/src/tools/diagnose.ts +150 -0
- package/src/tools/mutating.ts +694 -0
- package/src/tools/readonly.ts +786 -0
- package/src/tools/selfcheck.ts +97 -0
- package/src/types.ts +76 -0
- package/tests/README.md +9 -0
- package/tests/diff-redaction.test.ts +146 -0
- package/tests/entrypoint.test.ts +49 -0
- package/tests/guards-eol.test.ts +170 -0
- package/tests/integration-svn.test.ts +709 -0
- package/tests/parsers.test.ts +119 -0
- package/tests/runner.test.ts +159 -0
- package/tests/selfcheck.test.ts +22 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { XMLParser } from "fast-xml-parser";
|
|
4
|
+
import { createEnvelope, envelopeFromRun, failEnvelope, noteFromRun, redactText } from "../envelope.js";
|
|
5
|
+
import { makeEolCheck } from "../eol.js";
|
|
6
|
+
import {
|
|
7
|
+
assertExistingTargets,
|
|
8
|
+
isInsideOrEqual,
|
|
9
|
+
realPathOfNearestExisting,
|
|
10
|
+
repoRelativePath,
|
|
11
|
+
requireExplicitPaths,
|
|
12
|
+
resolveCwd,
|
|
13
|
+
resolveTargetsInsideWc,
|
|
14
|
+
riskySignals,
|
|
15
|
+
statusMap,
|
|
16
|
+
validatePropertyName,
|
|
17
|
+
wcRootFromInfo
|
|
18
|
+
} from "../guards.js";
|
|
19
|
+
import { createDiffAccumulator } from "../parse/diffText.js";
|
|
20
|
+
import { parseInfoXml } from "../parse/infoXml.js";
|
|
21
|
+
import { parseLogXml } from "../parse/logXml.js";
|
|
22
|
+
import { parseStatusXml } from "../parse/statusXml.js";
|
|
23
|
+
import { runSvn, runSvnStreamingLines, runSvnVersion } from "../runner.js";
|
|
24
|
+
import type { DiffSummary, EolCheckResult, Envelope, ToolEnvelope, WcInfo } from "../types.js";
|
|
25
|
+
|
|
26
|
+
export interface ToolInputWithCwd {
|
|
27
|
+
cwd?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const propgetParser = new XMLParser({
|
|
31
|
+
ignoreAttributes: false,
|
|
32
|
+
attributeNamePrefix: "",
|
|
33
|
+
textNodeName: "text"
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export async function getWcContext(cwdInput?: string, pathHints: string[] = []): Promise<{ ok: true; cwd: string; info: WcInfo; wcRoot: string } | { ok: false; envelope: Envelope }> {
|
|
37
|
+
if (!cwdInput && !pathHints.some((hint) => path.isAbsolute(hint))) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
envelope: failEnvelope("svn info --xml", process.cwd(), "cwd or absolute path required")
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let lastRun: Awaited<ReturnType<typeof runSvn>> | null = null;
|
|
45
|
+
|
|
46
|
+
for (const probe of wcProbeCandidates(cwdInput, pathHints)) {
|
|
47
|
+
const run = await runSvn(["info", "--xml", probe.target], probe.execCwd);
|
|
48
|
+
if (run.exitCode !== 0) {
|
|
49
|
+
lastRun = run;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const entries = parseInfoXml(run.stdout);
|
|
54
|
+
const rawWcRoot = wcRootFromInfo(entries);
|
|
55
|
+
if (!rawWcRoot) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
envelope: failEnvelope(run.command, probe.execCwd, "path is not inside a working copy")
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const wcRoot = realPathOfNearestExisting(rawWcRoot);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
cwd: probe.cwd ? realPathOfNearestExisting(probe.cwd) : wcRoot,
|
|
66
|
+
info: entries[0] ?? { url: null, repo_root: null, wc_root: wcRoot, revision: null },
|
|
67
|
+
wcRoot
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (lastRun) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
envelope: envelopeFromRun({
|
|
75
|
+
run: lastRun,
|
|
76
|
+
ok: false,
|
|
77
|
+
note: noteFromRun(lastRun)
|
|
78
|
+
})
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const cwd = resolveCwd(cwdInput);
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
envelope: failEnvelope("svn info --xml", cwd, "path is not inside a working copy")
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function svnStatus(input: { cwd?: string; paths?: string[]; includeIgnored?: boolean; hideNoise?: boolean }): Promise<ToolEnvelope> {
|
|
90
|
+
const context = await getWcContext(input.cwd, input.paths ?? []);
|
|
91
|
+
if (!context.ok) {
|
|
92
|
+
return context.envelope;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const targets = input.paths && input.paths.length > 0 ? input.paths : [];
|
|
96
|
+
const resolved = resolveTargetsInsideWc(context.cwd, context.wcRoot, targets.length > 0 ? targets : [context.cwd]);
|
|
97
|
+
if (!resolved.ok) {
|
|
98
|
+
return failEnvelope("svn status --xml", context.cwd, resolved.note);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const args = ["status", "--xml", ...(input.includeIgnored ? ["--no-ignore"] : []), ...(targets.length > 0 ? resolved.paths : [])];
|
|
102
|
+
const run = await runSvn(args, context.cwd);
|
|
103
|
+
const parsed = run.exitCode === 0 ? parseStatusXml(run.stdout) : { changed_paths: [], conflicts: [] };
|
|
104
|
+
const filtered = input.hideNoise ? filterNoisePaths(parsed.changed_paths, context.cwd, context.wcRoot) : {
|
|
105
|
+
changed_paths: parsed.changed_paths,
|
|
106
|
+
filtered_paths: []
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
...envelopeFromRun({
|
|
110
|
+
run,
|
|
111
|
+
ok: run.exitCode === 0,
|
|
112
|
+
changed_paths: filtered.changed_paths,
|
|
113
|
+
conflicts: parsed.conflicts,
|
|
114
|
+
note: run.exitCode === 0 ? "" : noteFromRun(run)
|
|
115
|
+
}),
|
|
116
|
+
filtered_paths: filtered.filtered_paths
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function svnInfo(input: { cwd?: string; paths?: string[] }): Promise<ToolEnvelope> {
|
|
121
|
+
const context = await getWcContext(input.cwd, input.paths ?? []);
|
|
122
|
+
if (!context.ok) {
|
|
123
|
+
return context.envelope;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const targets = input.paths && input.paths.length > 0 ? input.paths : [context.cwd];
|
|
127
|
+
const resolved = resolveTargetsInsideWc(context.cwd, context.wcRoot, targets);
|
|
128
|
+
if (!resolved.ok) {
|
|
129
|
+
return failEnvelope("svn info --xml", context.cwd, resolved.note);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const run = await runSvn(["info", "--xml", ...resolved.paths], context.cwd);
|
|
133
|
+
const entries = run.exitCode === 0 ? parseInfoXml(run.stdout) : [];
|
|
134
|
+
const versionNotes: string[] = [];
|
|
135
|
+
let mixedRevision = false;
|
|
136
|
+
let modified = false;
|
|
137
|
+
let switched = false;
|
|
138
|
+
let partial = false;
|
|
139
|
+
let revisionRange: { min: number; max: number } | null = null;
|
|
140
|
+
let svnVersion = "";
|
|
141
|
+
|
|
142
|
+
const version = await runSvnVersion(context.wcRoot, context.cwd);
|
|
143
|
+
if (version.exitCode === 0) {
|
|
144
|
+
const value = version.stdout.trim();
|
|
145
|
+
svnVersion = value;
|
|
146
|
+
const parsed = parseSvnVersion(value);
|
|
147
|
+
mixedRevision = mixedRevision || parsed.mixed;
|
|
148
|
+
modified = modified || parsed.modified;
|
|
149
|
+
switched = switched || parsed.switched;
|
|
150
|
+
partial = partial || parsed.partial;
|
|
151
|
+
revisionRange = mergeRevisionRange(revisionRange, parsed.range);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (mixedRevision) {
|
|
155
|
+
versionNotes.push("mixed revision working copy");
|
|
156
|
+
}
|
|
157
|
+
if (modified) {
|
|
158
|
+
versionNotes.push("local modifications present");
|
|
159
|
+
}
|
|
160
|
+
if (switched) {
|
|
161
|
+
versionNotes.push("switched path present");
|
|
162
|
+
}
|
|
163
|
+
if (partial) {
|
|
164
|
+
versionNotes.push("partial working copy");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const first = entries[0] ?? context.info;
|
|
168
|
+
const remoteHeadRevision = await remoteHeadForTargets(context.cwd, resolved.paths);
|
|
169
|
+
if (remoteHeadRevision !== null && revisionRange && remoteHeadRevision > revisionRange.max) {
|
|
170
|
+
versionNotes.push(`remote HEAD newer than working copy (${remoteHeadRevision} > ${revisionRange.max})`);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
...envelopeFromRun({
|
|
174
|
+
run,
|
|
175
|
+
ok: run.exitCode === 0,
|
|
176
|
+
revision: first.revision,
|
|
177
|
+
note: run.exitCode === 0 ? versionNotes.join("; ") : noteFromRun(run)
|
|
178
|
+
}),
|
|
179
|
+
url: first.url,
|
|
180
|
+
repo_root: first.repo_root,
|
|
181
|
+
wc_root: first.wc_root,
|
|
182
|
+
entries,
|
|
183
|
+
mixed_revision: mixedRevision,
|
|
184
|
+
svnversion: svnVersion,
|
|
185
|
+
revision_range: revisionRange,
|
|
186
|
+
local_modifications: modified,
|
|
187
|
+
switched,
|
|
188
|
+
partial,
|
|
189
|
+
remote_head_revision: remoteHeadRevision,
|
|
190
|
+
stale_base: remoteHeadRevision !== null && revisionRange !== null ? remoteHeadRevision > revisionRange.max : false
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function svnDiff(input: {
|
|
195
|
+
cwd?: string;
|
|
196
|
+
paths: string[];
|
|
197
|
+
ignoreEol?: boolean;
|
|
198
|
+
lineLimit?: number;
|
|
199
|
+
}): Promise<ToolEnvelope & DiffSummary> {
|
|
200
|
+
const explicitError = requireExplicitPaths(input.paths);
|
|
201
|
+
const cwd = resolveCwd(input.cwd);
|
|
202
|
+
if (explicitError) {
|
|
203
|
+
return {
|
|
204
|
+
...failEnvelope("svn diff", cwd, explicitError),
|
|
205
|
+
per_file: [],
|
|
206
|
+
diff_excerpt: "",
|
|
207
|
+
truncated: false
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const context = await getWcContext(input.cwd, input.paths);
|
|
212
|
+
if (!context.ok) {
|
|
213
|
+
return {
|
|
214
|
+
...context.envelope,
|
|
215
|
+
per_file: [],
|
|
216
|
+
diff_excerpt: "",
|
|
217
|
+
truncated: context.envelope.truncated
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const resolved = resolveTargetsInsideWc(context.cwd, context.wcRoot, input.paths);
|
|
222
|
+
if (!resolved.ok) {
|
|
223
|
+
return {
|
|
224
|
+
...failEnvelope("svn diff", context.cwd, resolved.note),
|
|
225
|
+
per_file: [],
|
|
226
|
+
diff_excerpt: "",
|
|
227
|
+
truncated: false
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const lineLimit = input.lineLimit ?? defaultDiffLineLimit();
|
|
232
|
+
const ignoreEol = input.ignoreEol ?? true;
|
|
233
|
+
const args = ignoreEol
|
|
234
|
+
? ["diff", "--internal-diff", "-x", "--ignore-eol-style", ...resolved.paths]
|
|
235
|
+
: ["diff", "--internal-diff", ...resolved.paths];
|
|
236
|
+
const diffAccumulator = createDiffAccumulator(lineLimit);
|
|
237
|
+
const run = await runSvnStreamingLines(args, context.cwd, diffAccumulator.pushLine, { stdoutLineLimit: lineLimit });
|
|
238
|
+
const rawDiff = run.exitCode === 0 ? diffAccumulator.summary() : { per_file: [], diff_excerpt: "", truncated: false };
|
|
239
|
+
const diff = { ...rawDiff, diff_excerpt: redactText(rawDiff.diff_excerpt) };
|
|
240
|
+
const eolDiagnostic = run.exitCode !== 0 && isInconsistentEolRun(run)
|
|
241
|
+
? await eolCheck({ cwd: context.cwd, paths: resolved.paths })
|
|
242
|
+
: null;
|
|
243
|
+
return {
|
|
244
|
+
...envelopeFromRun({
|
|
245
|
+
run,
|
|
246
|
+
ok: run.exitCode === 0,
|
|
247
|
+
note: run.exitCode === 0 ? "" : noteFromRun(run),
|
|
248
|
+
truncated: diff.truncated
|
|
249
|
+
}),
|
|
250
|
+
...diff,
|
|
251
|
+
ignore_eol: ignoreEol,
|
|
252
|
+
...(eolDiagnostic
|
|
253
|
+
? {
|
|
254
|
+
recovery_tool: "eol_fix_verified",
|
|
255
|
+
eol_files: eolDiagnostic.files ?? []
|
|
256
|
+
}
|
|
257
|
+
: {})
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function svnLog(input: {
|
|
262
|
+
cwd?: string;
|
|
263
|
+
paths?: string[];
|
|
264
|
+
limit?: number;
|
|
265
|
+
verbose?: boolean;
|
|
266
|
+
}): Promise<ToolEnvelope> {
|
|
267
|
+
const context = await getWcContext(input.cwd, input.paths ?? []);
|
|
268
|
+
if (!context.ok) {
|
|
269
|
+
return context.envelope;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const targets = input.paths && input.paths.length > 0 ? input.paths : [context.cwd];
|
|
273
|
+
const resolved = resolveTargetsInsideWc(context.cwd, context.wcRoot, targets);
|
|
274
|
+
if (!resolved.ok) {
|
|
275
|
+
return failEnvelope("svn log", context.cwd, resolved.note);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const logTargets = await repositoryLogTargets(context.cwd, resolved.paths);
|
|
279
|
+
const args = ["log", "--xml", "-l", String(input.limit ?? 10)];
|
|
280
|
+
if (input.verbose ?? true) {
|
|
281
|
+
args.push("-v");
|
|
282
|
+
}
|
|
283
|
+
args.push(...logTargets.targets);
|
|
284
|
+
|
|
285
|
+
const run = await runSvn(args, context.cwd);
|
|
286
|
+
const entries = run.exitCode === 0 ? parseLogXml(run.stdout) : [];
|
|
287
|
+
return {
|
|
288
|
+
...envelopeFromRun({
|
|
289
|
+
run,
|
|
290
|
+
ok: run.exitCode === 0,
|
|
291
|
+
revision: entries[0]?.rev ?? null,
|
|
292
|
+
note: run.exitCode === 0 ? logTargets.note : noteFromRun(run)
|
|
293
|
+
}),
|
|
294
|
+
entries,
|
|
295
|
+
target_mode: logTargets.mode
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function eolCheck(input: { cwd?: string; paths: string[] }): Promise<ToolEnvelope> {
|
|
300
|
+
const explicitError = requireExplicitPaths(input.paths);
|
|
301
|
+
const cwd = resolveCwd(input.cwd);
|
|
302
|
+
if (explicitError) {
|
|
303
|
+
return {
|
|
304
|
+
...failEnvelope("eol_check", cwd, explicitError),
|
|
305
|
+
files: []
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const context = await getWcContext(input.cwd, input.paths);
|
|
310
|
+
if (!context.ok) {
|
|
311
|
+
return {
|
|
312
|
+
...context.envelope,
|
|
313
|
+
files: []
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const resolved = resolveTargetsInsideWc(context.cwd, context.wcRoot, input.paths);
|
|
318
|
+
if (!resolved.ok) {
|
|
319
|
+
return {
|
|
320
|
+
...failEnvelope("eol_check", context.cwd, resolved.note),
|
|
321
|
+
files: []
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const existsError = assertExistingTargets(resolved.paths);
|
|
326
|
+
if (existsError) {
|
|
327
|
+
return {
|
|
328
|
+
...failEnvelope("eol_check", context.cwd, existsError),
|
|
329
|
+
files: []
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const files: EolCheckResult[] = [];
|
|
334
|
+
const eolStyles = await eolStylesForTargets(context.cwd, resolved.paths);
|
|
335
|
+
for (const target of resolved.paths) {
|
|
336
|
+
files.push(await makeEolCheck(target, eolStyles.get(path.resolve(target).toLowerCase()) ?? null));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
...createEnvelope({
|
|
341
|
+
ok: true,
|
|
342
|
+
command: "eol_check",
|
|
343
|
+
cwd: context.cwd,
|
|
344
|
+
note: files.some((file) => file.mismatch) ? "EOL mismatch detected" : ""
|
|
345
|
+
}),
|
|
346
|
+
files
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function svnPropget(input: { cwd?: string; paths: string[]; name: string }): Promise<ToolEnvelope> {
|
|
351
|
+
const explicitError = requireExplicitPaths(input.paths);
|
|
352
|
+
const cwd = resolveCwd(input.cwd);
|
|
353
|
+
if (explicitError) {
|
|
354
|
+
return {
|
|
355
|
+
...failEnvelope("svn propget", cwd, explicitError),
|
|
356
|
+
properties: [],
|
|
357
|
+
missing_paths: []
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const propertyError = validatePropertyName(input.name);
|
|
362
|
+
if (propertyError) {
|
|
363
|
+
return {
|
|
364
|
+
...failEnvelope("svn propget", cwd, propertyError),
|
|
365
|
+
properties: [],
|
|
366
|
+
missing_paths: []
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const context = await getWcContext(input.cwd, input.paths);
|
|
371
|
+
if (!context.ok) {
|
|
372
|
+
return {
|
|
373
|
+
...context.envelope,
|
|
374
|
+
properties: [],
|
|
375
|
+
missing_paths: []
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const resolved = resolveTargetsInsideWc(context.cwd, context.wcRoot, input.paths);
|
|
380
|
+
if (!resolved.ok) {
|
|
381
|
+
return {
|
|
382
|
+
...failEnvelope("svn propget", context.cwd, resolved.note),
|
|
383
|
+
properties: [],
|
|
384
|
+
missing_paths: []
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const existsError = assertExistingTargets(resolved.paths);
|
|
389
|
+
if (existsError) {
|
|
390
|
+
return {
|
|
391
|
+
...failEnvelope("svn propget", context.cwd, existsError),
|
|
392
|
+
properties: [],
|
|
393
|
+
missing_paths: []
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const run = await runSvn(["propget", input.name, "--xml", ...resolved.paths], context.cwd);
|
|
398
|
+
const properties = parsePropgetProperties(run.stdout, context.cwd, context.wcRoot);
|
|
399
|
+
const missingPaths = propertyMissingPaths(properties, resolved.paths, context.wcRoot);
|
|
400
|
+
if (run.exitCode !== 0 && isMissingPropertyRun(run)) {
|
|
401
|
+
return {
|
|
402
|
+
...createEnvelope({
|
|
403
|
+
ok: true,
|
|
404
|
+
command: run.command,
|
|
405
|
+
cwd: context.cwd,
|
|
406
|
+
stdout: run.stdout,
|
|
407
|
+
stderr: run.stderr,
|
|
408
|
+
note: properties.length > 0 ? "property not set on some paths" : "property not set"
|
|
409
|
+
}),
|
|
410
|
+
properties,
|
|
411
|
+
missing_paths: missingPaths
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
...envelopeFromRun({
|
|
417
|
+
run,
|
|
418
|
+
ok: run.exitCode === 0,
|
|
419
|
+
note: run.exitCode === 0 ? "" : noteFromRun(run)
|
|
420
|
+
}),
|
|
421
|
+
properties,
|
|
422
|
+
missing_paths: missingPaths
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export async function scopedStatusMap(cwd: string, wcRoot: string, paths: string[]): Promise<{
|
|
427
|
+
envelope: ToolEnvelope;
|
|
428
|
+
map: Map<string, string>;
|
|
429
|
+
}> {
|
|
430
|
+
const status = await svnStatus({ cwd, paths });
|
|
431
|
+
return {
|
|
432
|
+
envelope: status,
|
|
433
|
+
map: statusMap(status.changed_paths, cwd)
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function dryRiskSignals(absPaths: string[], wcRoot: string, statusByPath?: Map<string, string>): string[] {
|
|
438
|
+
return riskySignals(absPaths, wcRoot, statusByPath);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function defaultDiffLineLimit(): number {
|
|
442
|
+
const parsed = Number.parseInt(process.env.SVN_AGENT_MAX_DIFF_LINES ?? "", 10);
|
|
443
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 800;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function normalizeStatusLookup(statuses: Map<string, string>, target: string): string | undefined {
|
|
447
|
+
return statuses.get(path.resolve(target).toLowerCase());
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function filterNoisePaths(changedPaths: Array<{ status: string; path: string }>, cwd: string, wcRoot: string): {
|
|
451
|
+
changed_paths: Array<{ status: string; path: string }>;
|
|
452
|
+
filtered_paths: string[];
|
|
453
|
+
} {
|
|
454
|
+
const kept = [];
|
|
455
|
+
const filtered = [];
|
|
456
|
+
|
|
457
|
+
for (const entry of changedPaths) {
|
|
458
|
+
const absPath = path.resolve(cwd, entry.path);
|
|
459
|
+
const relative = repoRelativePath(absPath, wcRoot);
|
|
460
|
+
if ((entry.status === "?" || entry.status === "I") && isNoisePath(relative)) {
|
|
461
|
+
filtered.push(relative);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
kept.push(entry);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return { changed_paths: kept, filtered_paths: filtered };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function isNoisePath(relativePath: string): boolean {
|
|
471
|
+
const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
472
|
+
return [
|
|
473
|
+
"node_modules",
|
|
474
|
+
"dist",
|
|
475
|
+
"current",
|
|
476
|
+
".cache",
|
|
477
|
+
"coverage"
|
|
478
|
+
].some((value) => normalized === value || normalized.startsWith(`${value}/`));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function parseSvnVersion(value: string): {
|
|
482
|
+
range: { min: number; max: number } | null;
|
|
483
|
+
mixed: boolean;
|
|
484
|
+
modified: boolean;
|
|
485
|
+
switched: boolean;
|
|
486
|
+
partial: boolean;
|
|
487
|
+
} {
|
|
488
|
+
const trimmed = value.trim();
|
|
489
|
+
const match = trimmed.match(/^(\d+)(?::(\d+))?/);
|
|
490
|
+
const min = match?.[1] ? Number.parseInt(match[1], 10) : null;
|
|
491
|
+
const max = match?.[2] ? Number.parseInt(match[2], 10) : min;
|
|
492
|
+
const flagString = trimmed.replace(/^\d+(?::\d+)?/, "");
|
|
493
|
+
return {
|
|
494
|
+
range: min !== null && max !== null ? { min, max } : null,
|
|
495
|
+
mixed: trimmed.includes(":"),
|
|
496
|
+
modified: flagString.includes("M"),
|
|
497
|
+
switched: flagString.includes("S"),
|
|
498
|
+
partial: flagString.includes("P")
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function mergeRevisionRange(
|
|
503
|
+
left: { min: number; max: number } | null,
|
|
504
|
+
right: { min: number; max: number } | null
|
|
505
|
+
): { min: number; max: number } | null {
|
|
506
|
+
if (!left) {
|
|
507
|
+
return right;
|
|
508
|
+
}
|
|
509
|
+
if (!right) {
|
|
510
|
+
return left;
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
min: Math.min(left.min, right.min),
|
|
514
|
+
max: Math.max(left.max, right.max)
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function remoteHeadForTargets(cwd: string, targets: string[]): Promise<number | null> {
|
|
519
|
+
let head: number | null = null;
|
|
520
|
+
const run = await runSvn(["info", "--xml", "-r", "HEAD", ...targets], cwd);
|
|
521
|
+
if (run.exitCode !== 0) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
for (const entry of parseInfoXml(run.stdout)) {
|
|
525
|
+
if (entry.revision === null) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
head = head === null ? entry.revision : Math.max(head, entry.revision);
|
|
529
|
+
}
|
|
530
|
+
return head;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function eolStylesForTargets(cwd: string, targets: string[]): Promise<Map<string, string>> {
|
|
534
|
+
const batched = await runSvn(["propget", "svn:eol-style", "--xml", ...targets], cwd);
|
|
535
|
+
if (batched.exitCode === 0) {
|
|
536
|
+
return parsePropgetEolStyles(batched.stdout, cwd);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const styles = new Map<string, string>();
|
|
540
|
+
for (const target of targets) {
|
|
541
|
+
const prop = await runSvn(["propget", "svn:eol-style", target], cwd);
|
|
542
|
+
if (prop.exitCode === 0 && prop.stdout.trim()) {
|
|
543
|
+
styles.set(path.resolve(target).toLowerCase(), prop.stdout.trim());
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return styles;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function parsePropgetEolStyles(xml: string, cwd: string): Map<string, string> {
|
|
550
|
+
const styles = new Map<string, string>();
|
|
551
|
+
if (!xml.trim()) {
|
|
552
|
+
return styles;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const parsed = propgetParser.parse(xml) as {
|
|
556
|
+
properties?: {
|
|
557
|
+
target?: unknown;
|
|
558
|
+
};
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
for (const target of asArray(parsed.properties?.target)) {
|
|
562
|
+
const targetObj = target as { path?: string; property?: unknown };
|
|
563
|
+
const targetPath = targetObj.path ? path.resolve(cwd, targetObj.path).toLowerCase() : null;
|
|
564
|
+
if (!targetPath) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
for (const property of asArray(targetObj.property)) {
|
|
568
|
+
const propertyObj = property as { name?: string; text?: string };
|
|
569
|
+
if (propertyObj.name === "svn:eol-style" && propertyObj.text) {
|
|
570
|
+
styles.set(targetPath, propertyObj.text.trim());
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return styles;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function parsePropgetProperties(xml: string, cwd: string, wcRoot: string): Array<{ path: string; name: string; value: string }> {
|
|
579
|
+
const properties: Array<{ path: string; name: string; value: string }> = [];
|
|
580
|
+
if (!xml.trim()) {
|
|
581
|
+
return properties;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const parsed = propgetParser.parse(xml) as {
|
|
585
|
+
properties?: {
|
|
586
|
+
target?: unknown;
|
|
587
|
+
};
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
for (const target of asArray(parsed.properties?.target)) {
|
|
591
|
+
const targetObj = target as { path?: string; property?: unknown };
|
|
592
|
+
if (!targetObj.path) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const targetPath = displayPropertyPath(targetObj.path, cwd, wcRoot);
|
|
596
|
+
for (const property of asArray(targetObj.property)) {
|
|
597
|
+
const propertyObj = property as { name?: string; text?: unknown };
|
|
598
|
+
if (propertyObj.name) {
|
|
599
|
+
properties.push({
|
|
600
|
+
path: targetPath,
|
|
601
|
+
name: propertyObj.name,
|
|
602
|
+
value: propertyObj.text === undefined ? "" : String(propertyObj.text)
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return properties;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function displayPropertyPath(value: string, cwd: string, wcRoot: string): string {
|
|
612
|
+
const absolute = path.isAbsolute(value) ? path.resolve(value) : path.resolve(cwd, value);
|
|
613
|
+
return isInsideOrEqual(absolute, wcRoot)
|
|
614
|
+
? repoRelativePath(absolute, wcRoot)
|
|
615
|
+
: value;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function isMissingPropertyRun(run: Awaited<ReturnType<typeof runSvn>>): boolean {
|
|
619
|
+
return /W200017|Property '.+' not found/i.test(`${run.stderr}\n${run.stdout}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function propertyMissingPaths(
|
|
623
|
+
properties: Array<{ path: string; name: string; value: string }>,
|
|
624
|
+
resolvedPaths: string[],
|
|
625
|
+
wcRoot: string
|
|
626
|
+
): string[] {
|
|
627
|
+
const pathsWithProperty = new Set(properties.map((property) => property.path.toLowerCase()));
|
|
628
|
+
return resolvedPaths
|
|
629
|
+
.map((target) => repoRelativePath(target, wcRoot))
|
|
630
|
+
.filter((target) => !pathsWithProperty.has(target.toLowerCase()));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function asArray<T>(value: T | T[] | undefined): T[] {
|
|
634
|
+
if (value === undefined) {
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
return Array.isArray(value) ? value : [value];
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function isInconsistentEolRun(run: Awaited<ReturnType<typeof runSvn>>): boolean {
|
|
641
|
+
return /E135000|inconsistent newlines|inconsistent line ending style/i.test(`${run.stderr}\n${run.stdout}`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function repositoryLogTargets(cwd: string, paths: string[]): Promise<{
|
|
645
|
+
targets: string[];
|
|
646
|
+
mode: "repository-url" | "working-copy-path";
|
|
647
|
+
note: string;
|
|
648
|
+
}> {
|
|
649
|
+
const info = await runSvn(["info", "--xml", ...paths], cwd);
|
|
650
|
+
if (info.exitCode !== 0) {
|
|
651
|
+
return { targets: paths, mode: "working-copy-path", note: "" };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const entries = parseInfoXml(info.stdout);
|
|
655
|
+
const urls = entries.map((entry) => entry.url).filter((url): url is string => Boolean(url));
|
|
656
|
+
if (urls.length !== paths.length) {
|
|
657
|
+
return { targets: paths, mode: "working-copy-path", note: "" };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
targets: urls,
|
|
662
|
+
mode: "repository-url",
|
|
663
|
+
note: "queried repository URL at HEAD to avoid working-copy peg revision log gaps"
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
type WcProbe = {
|
|
668
|
+
target: string;
|
|
669
|
+
execCwd: string;
|
|
670
|
+
cwd: string | null;
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
function wcProbeCandidates(cwdInput: string | undefined, pathHints: string[]): WcProbe[] {
|
|
674
|
+
if (cwdInput) {
|
|
675
|
+
const cwd = resolveCwd(cwdInput);
|
|
676
|
+
return [{ target: cwd, execCwd: executableCwdForTarget(cwd), cwd }];
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const probes: WcProbe[] = [];
|
|
680
|
+
for (const hint of pathHints) {
|
|
681
|
+
if (!path.isAbsolute(hint)) {
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
for (const target of probeTargetsForAbsolutePath(hint)) {
|
|
685
|
+
probes.push({ target, execCwd: executableCwdForTarget(target), cwd: null });
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// No launch-directory fallback: in global plug-and-play registration the MCP
|
|
690
|
+
// process cwd is arbitrary and must never silently become the working copy.
|
|
691
|
+
return uniqueProbes(probes);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function probeTargetsForAbsolutePath(value: string): string[] {
|
|
695
|
+
const targets: string[] = [];
|
|
696
|
+
let current = path.resolve(value);
|
|
697
|
+
const startDirectory = existingDirectoryForPath(current);
|
|
698
|
+
const wcMarkerRoot = startDirectory ? nearestSvnMarkerAncestor(startDirectory) : null;
|
|
699
|
+
if (wcMarkerRoot) {
|
|
700
|
+
return [realPathOfNearestExisting(wcMarkerRoot)];
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (fs.existsSync(current)) {
|
|
704
|
+
targets.push(current);
|
|
705
|
+
if (!fs.statSync(current).isDirectory()) {
|
|
706
|
+
current = path.dirname(current);
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
current = path.dirname(current);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
while (true) {
|
|
713
|
+
if (fs.existsSync(current)) {
|
|
714
|
+
targets.push(current);
|
|
715
|
+
}
|
|
716
|
+
const parent = path.dirname(current);
|
|
717
|
+
if (parent === current) {
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
current = parent;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return Array.from(new Set(targets.map((target) => realPathOfNearestExisting(target))));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function existingDirectoryForPath(value: string): string | null {
|
|
727
|
+
let current = path.resolve(value);
|
|
728
|
+
|
|
729
|
+
if (fs.existsSync(current) && !fs.statSync(current).isDirectory()) {
|
|
730
|
+
current = path.dirname(current);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
while (!fs.existsSync(current)) {
|
|
734
|
+
const parent = path.dirname(current);
|
|
735
|
+
if (parent === current) {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
current = parent;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return fs.statSync(current).isDirectory() ? realPathOfNearestExisting(current) : realPathOfNearestExisting(path.dirname(current));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function nearestSvnMarkerAncestor(startDirectory: string): string | null {
|
|
745
|
+
let current = path.resolve(startDirectory);
|
|
746
|
+
|
|
747
|
+
while (true) {
|
|
748
|
+
if (fs.existsSync(path.join(current, ".svn"))) {
|
|
749
|
+
return current;
|
|
750
|
+
}
|
|
751
|
+
const parent = path.dirname(current);
|
|
752
|
+
if (parent === current) {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
current = parent;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function executableCwdForTarget(target: string): string {
|
|
760
|
+
let current = path.resolve(target);
|
|
761
|
+
|
|
762
|
+
while (!fs.existsSync(current)) {
|
|
763
|
+
const parent = path.dirname(current);
|
|
764
|
+
if (parent === current) {
|
|
765
|
+
return process.cwd();
|
|
766
|
+
}
|
|
767
|
+
current = parent;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return fs.statSync(current).isDirectory() ? current : path.dirname(current);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function uniqueProbes(probes: WcProbe[]): WcProbe[] {
|
|
774
|
+
const seen = new Set<string>();
|
|
775
|
+
const unique: WcProbe[] = [];
|
|
776
|
+
|
|
777
|
+
for (const probe of probes) {
|
|
778
|
+
const key = path.resolve(probe.target).toLowerCase();
|
|
779
|
+
if (!seen.has(key)) {
|
|
780
|
+
seen.add(key);
|
|
781
|
+
unique.push(probe);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return unique;
|
|
786
|
+
}
|