@weareikko/code-review 0.9.5 → 0.9.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/{cli-tIo5jQvJ.js → cli-D6vNV3kA.js} +186 -28
- package/dist/cli-D6vNV3kA.js.map +1 -0
- package/dist/cli.d.ts +8 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/gitlab-review.d.ts +3 -1
- package/dist/gitlab-review.d.ts.map +1 -1
- package/dist/marketplaces.d.ts.map +1 -1
- package/dist/otel.d.ts.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +2 -2
- package/dist/skill-links.d.ts +52 -0
- package/dist/skill-links.d.ts.map +1 -0
- package/dist/skills.d.ts +35 -2
- package/dist/skills.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/cli-tIo5jQvJ.js.map +0 -1
|
@@ -541,6 +541,10 @@ function summarizeDiff(diff) {
|
|
|
541
541
|
}
|
|
542
542
|
//#endregion
|
|
543
543
|
//#region src/skills.ts
|
|
544
|
+
/** Normalize a filesystem-relative path to the POSIX form used in URLs. */
|
|
545
|
+
function toPosixPath(path) {
|
|
546
|
+
return sep === "/" ? path : path.split(sep).join("/");
|
|
547
|
+
}
|
|
544
548
|
var SKILL_DIRS = [".agents/skills", ".claude/skills"];
|
|
545
549
|
var RESOURCE_DIRS = ["references"];
|
|
546
550
|
function parseFrontmatter(content) {
|
|
@@ -563,7 +567,7 @@ function parseFrontmatter(content) {
|
|
|
563
567
|
description: trimmedDescription
|
|
564
568
|
};
|
|
565
569
|
}
|
|
566
|
-
async function loadSkillFromDir(dirPath,
|
|
570
|
+
async function loadSkillFromDir(dirPath, origin) {
|
|
567
571
|
const skillMdPath = join(dirPath, "SKILL.md");
|
|
568
572
|
let content;
|
|
569
573
|
try {
|
|
@@ -580,14 +584,17 @@ async function loadSkillFromDir(dirPath, source) {
|
|
|
580
584
|
filePath: skillMdPath,
|
|
581
585
|
rootDir: dirPath,
|
|
582
586
|
resourceDirs,
|
|
583
|
-
|
|
587
|
+
origin
|
|
584
588
|
};
|
|
585
589
|
}
|
|
586
590
|
function resolveBuiltinSkillsDir() {
|
|
587
591
|
return join(dirname(fileURLToPath(import.meta.url)), "..", "skills");
|
|
588
592
|
}
|
|
589
593
|
async function loadBuiltinSkill(name) {
|
|
590
|
-
return loadSkillFromDir(join(resolveBuiltinSkillsDir(), name),
|
|
594
|
+
return loadSkillFromDir(join(resolveBuiltinSkillsDir(), name), {
|
|
595
|
+
kind: "builtin",
|
|
596
|
+
name
|
|
597
|
+
});
|
|
591
598
|
}
|
|
592
599
|
async function loadAutoDiscoveredSkills(cwd, gitRoot, warn) {
|
|
593
600
|
const dirs = [];
|
|
@@ -610,7 +617,10 @@ async function loadAutoDiscoveredSkills(cwd, gitRoot, warn) {
|
|
|
610
617
|
}
|
|
611
618
|
for (const entry of entries) {
|
|
612
619
|
const entryPath = join(skillsPath, entry);
|
|
613
|
-
const skill = await loadSkillFromDir(entryPath,
|
|
620
|
+
const skill = await loadSkillFromDir(entryPath, {
|
|
621
|
+
kind: "project",
|
|
622
|
+
path: toPosixPath(relative(gitRoot, entryPath))
|
|
623
|
+
});
|
|
614
624
|
if (skill) found.set(skill.name, skill);
|
|
615
625
|
else if (warn && existsSync(join(entryPath, "SKILL.md"))) warn(`Skill at ${entryPath} has a SKILL.md but is missing required frontmatter fields (name, description) — skill not loaded.`);
|
|
616
626
|
}
|
|
@@ -898,13 +908,20 @@ async function loadNamedSkill(spec, cwd, options = {}) {
|
|
|
898
908
|
const pkgRef = parsed.subpath ? `${parsed.packageName} (subpath "${parsed.subpath}")` : parsed.packageName;
|
|
899
909
|
throw new ConfigError(`Cannot load skill: "${spec}"`, { hint: `Package ${pkgRef} was not found in node_modules. Run \`npm install ${parsed.packageName}\` in the project.` });
|
|
900
910
|
}
|
|
901
|
-
const skill = await loadSkillFromDir(dir,
|
|
911
|
+
const skill = await loadSkillFromDir(dir, {
|
|
912
|
+
kind: "npm",
|
|
913
|
+
packageName: parsed.packageName,
|
|
914
|
+
subpath: parsed.subpath
|
|
915
|
+
});
|
|
902
916
|
if (!skill) throw new ConfigError(`Cannot load skill: "${spec}"`, { hint: `The package at ${dir} does not contain a valid SKILL.md.` });
|
|
903
917
|
return skill;
|
|
904
918
|
}
|
|
905
919
|
if (parsed.protocol === "file") {
|
|
906
920
|
const resolvedPath = parsed.path.startsWith("/") ? parsed.path : join(cwd, parsed.path);
|
|
907
|
-
const skill = await loadSkillFromDir(resolvedPath,
|
|
921
|
+
const skill = await loadSkillFromDir(resolvedPath, {
|
|
922
|
+
kind: "file",
|
|
923
|
+
path: resolvedPath
|
|
924
|
+
});
|
|
908
925
|
if (!skill) throw new ConfigError(`Cannot load skill: "${spec}"`, { hint: `No valid SKILL.md was found at "${resolvedPath}". Check that the path points to a skill directory.` });
|
|
909
926
|
return skill;
|
|
910
927
|
}
|
|
@@ -922,7 +939,12 @@ async function loadNamedSkill(spec, cwd, options = {}) {
|
|
|
922
939
|
hint: `Failed to clone "${redactUrl(parsed.url)}"${atRef}. Check the URL, the ref, and your git credentials. For GitLab, prefer the SSH form: git+ssh://git@host/group/project.git`
|
|
923
940
|
});
|
|
924
941
|
}
|
|
925
|
-
const skill = await loadSkillFromDir(parsed.subpath ? join(repoDir, parsed.subpath) : repoDir,
|
|
942
|
+
const skill = await loadSkillFromDir(parsed.subpath ? join(repoDir, parsed.subpath) : repoDir, {
|
|
943
|
+
kind: "git",
|
|
944
|
+
url: parsed.url,
|
|
945
|
+
ref: parsed.ref,
|
|
946
|
+
path: parsed.subpath
|
|
947
|
+
});
|
|
926
948
|
if (!skill) throw new ConfigError(`Cannot load skill: "${spec}"`, { hint: parsed.subpath ? `The cloned repository has no valid SKILL.md at subpath "${parsed.subpath}".` : "The cloned repository has no valid SKILL.md at its root. If the skill lives in a subdirectory, point at it with \"#<ref>/<subpath>\"." });
|
|
927
949
|
return skill;
|
|
928
950
|
}
|
|
@@ -1061,7 +1083,7 @@ async function resolveAnthropicSkill(repoDir, mp, spec) {
|
|
|
1061
1083
|
const pluginDir = resolvePluginDir(repoDir, manifest, entry.source, mp, spec);
|
|
1062
1084
|
const realPluginDir = await resolveInside(realRoot, pluginDir, ref);
|
|
1063
1085
|
const bases = computeSkillBases(repoDir, pluginDir, entry, entry.strict !== false && realPluginDir ? await readPluginManifest(realRoot, realPluginDir, ref) : null, ref);
|
|
1064
|
-
const skill = await findSkillInBases(realRoot, bases, pluginDir, spec
|
|
1086
|
+
const skill = await findSkillInBases(realRoot, bases, pluginDir, spec, ref, mp);
|
|
1065
1087
|
if (!skill) throw new ConfigError(`Cannot load skill: "${ref}"`, { hint: `No skill "${spec.skill}" found in plugin "${spec.plugin}". Looked under the default skills/ directory${bases.length > 1 ? " and the plugin's custom \"skills\" paths" : ""}. Check the skill name against the marketplace.` });
|
|
1066
1088
|
return skill;
|
|
1067
1089
|
}
|
|
@@ -1121,14 +1143,15 @@ function resolveUnderPlugin(repoDir, pluginDir, rel, ref) {
|
|
|
1121
1143
|
* symlinks and reject any directory that escapes the cloned marketplace before a
|
|
1122
1144
|
* file is read.
|
|
1123
1145
|
*/
|
|
1124
|
-
async function findSkillInBases(realRoot, bases, pluginDir,
|
|
1146
|
+
async function findSkillInBases(realRoot, bases, pluginDir, spec, ref, mp) {
|
|
1147
|
+
const skillName = spec.skill;
|
|
1125
1148
|
for (const base of bases) {
|
|
1126
|
-
const container = await loadSkillIfInside(realRoot, join(base, skillName), ref);
|
|
1149
|
+
const container = await loadSkillIfInside(realRoot, join(base, skillName), ref, mp, spec);
|
|
1127
1150
|
if (container) return container;
|
|
1128
|
-
const single = await loadSkillIfInside(realRoot, base, ref);
|
|
1151
|
+
const single = await loadSkillIfInside(realRoot, base, ref, mp, spec);
|
|
1129
1152
|
if (single && single.name === skillName) return single;
|
|
1130
1153
|
}
|
|
1131
|
-
const root = await loadSkillIfInside(realRoot, pluginDir, ref);
|
|
1154
|
+
const root = await loadSkillIfInside(realRoot, pluginDir, ref, mp, spec);
|
|
1132
1155
|
return root && root.name === skillName ? root : null;
|
|
1133
1156
|
}
|
|
1134
1157
|
/**
|
|
@@ -1173,11 +1196,18 @@ async function readTextInside(realRoot, filePath, ref) {
|
|
|
1173
1196
|
* resolve within `realRoot`. Guarding the file (not just the directory) closes
|
|
1174
1197
|
* the case where a legitimate in-repo dir holds a `SKILL.md` symlinked outside.
|
|
1175
1198
|
*/
|
|
1176
|
-
async function loadSkillIfInside(realRoot, dir, ref) {
|
|
1199
|
+
async function loadSkillIfInside(realRoot, dir, ref, mp, spec) {
|
|
1177
1200
|
const real = await resolveInside(realRoot, dir, ref);
|
|
1178
1201
|
if (!real) return null;
|
|
1179
1202
|
if (await readTextInside(realRoot, join(real, "SKILL.md"), ref) === null) return null;
|
|
1180
|
-
return loadSkillFromDir(real,
|
|
1203
|
+
return loadSkillFromDir(real, {
|
|
1204
|
+
kind: "marketplace",
|
|
1205
|
+
marketplace: mp.name,
|
|
1206
|
+
plugin: spec.plugin,
|
|
1207
|
+
url: mp.url,
|
|
1208
|
+
ref: mp.ref,
|
|
1209
|
+
path: toPosixPath(relative(realRoot, real))
|
|
1210
|
+
});
|
|
1181
1211
|
}
|
|
1182
1212
|
/**
|
|
1183
1213
|
* Resolve a plugin entry's `source` to an absolute directory inside the cloned
|
|
@@ -1304,13 +1334,24 @@ function extractExistingFingerprints(discussions) {
|
|
|
1304
1334
|
for (const discussion of discussions) for (const note of discussion.notes ?? []) for (const match of String(note.body ?? "").matchAll(FINGERPRINT_MARKER_RE$2)) set.add(match[1]);
|
|
1305
1335
|
return set;
|
|
1306
1336
|
}
|
|
1337
|
+
//#endregion
|
|
1338
|
+
//#region src/product.ts
|
|
1339
|
+
/**
|
|
1340
|
+
* The published package name, shown in review footers. Independent of the review
|
|
1341
|
+
* platform: the same tool posts to GitLab and GitHub, so the footer identifies
|
|
1342
|
+
* the tool, not the backend. Kept as a single source of truth so the inline and
|
|
1343
|
+
* summary footers can never drift apart.
|
|
1344
|
+
*/
|
|
1345
|
+
var PRODUCT_NAME = "@weareikko/code-review";
|
|
1346
|
+
/** Canonical project URL used in the footer's markdown link. */
|
|
1347
|
+
var PRODUCT_URL = "https://github.com/weareikko/code-review";
|
|
1307
1348
|
/**
|
|
1308
1349
|
* The `[name](url)` markdown link used verbatim in both the inline comment footer
|
|
1309
1350
|
* ({@link buildCommentBody}) and the reviewed-commit summary footer
|
|
1310
1351
|
* ({@link buildReviewedCommitFooter}). Changing this changes the reviewed-commit
|
|
1311
1352
|
* footer format, which is guarded by a migration test.
|
|
1312
1353
|
*/
|
|
1313
|
-
var PRODUCT_LINK = `[
|
|
1354
|
+
var PRODUCT_LINK = `[${PRODUCT_NAME}](${PRODUCT_URL})`;
|
|
1314
1355
|
//#endregion
|
|
1315
1356
|
//#region src/posting.ts
|
|
1316
1357
|
var SUMMARY_MARKER = "<!-- code-review:summary -->";
|
|
@@ -1390,7 +1431,7 @@ function buildSummaryBody(summary, costFooter, options = {}) {
|
|
|
1390
1431
|
return `${withFooter}\n\n${buildSummaryHistoryBlock(historyEntries)}`;
|
|
1391
1432
|
}
|
|
1392
1433
|
function buildReviewedCommitFooter(commitSha) {
|
|
1393
|
-
return `Reviewed by ${PRODUCT_LINK} v0.9.
|
|
1434
|
+
return `Reviewed by ${PRODUCT_LINK} v0.9.6 for commit ${commitSha}.`;
|
|
1394
1435
|
}
|
|
1395
1436
|
function extractReviewedCommitSha(body) {
|
|
1396
1437
|
return REVIEWED_COMMIT_FOOTER_PATTERN.exec(body)?.[1] ?? null;
|
|
@@ -2035,6 +2076,16 @@ function detectPlatform(args, env, readEventFile = readEventFileSync) {
|
|
|
2035
2076
|
if (hasGitLab && !hasGitHub) return "gitlab";
|
|
2036
2077
|
throw new ConfigError(hasGitHub && hasGitLab ? "Ambiguous review platform: both GitHub and GitLab identifiers are present." : "Could not detect the review platform from the environment.", { hint: `Set --platform (or CODE_REVIEW_PLATFORM) to one of: ${PLATFORMS.join(", ")}.` });
|
|
2037
2078
|
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Web URL of the repository under review, as the CI environment reports it:
|
|
2081
|
+
* GitLab exposes it directly, GitHub composes it from the server and the
|
|
2082
|
+
* `owner/repo` slug. Returns `undefined` outside CI, where no such URL exists.
|
|
2083
|
+
* Shared by the config (skill source links) and the OTel attribute builder.
|
|
2084
|
+
*/
|
|
2085
|
+
function resolveProjectWebUrl(env = process.env) {
|
|
2086
|
+
if (env.CI_PROJECT_URL) return env.CI_PROJECT_URL.replace(/\/$/, "");
|
|
2087
|
+
if (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY) return `${env.GITHUB_SERVER_URL.replace(/\/$/, "")}/${env.GITHUB_REPOSITORY}`;
|
|
2088
|
+
}
|
|
2038
2089
|
function resolveConfig(argv = process.argv.slice(2), env = process.env) {
|
|
2039
2090
|
const args = parseArgs(argv);
|
|
2040
2091
|
const platform = detectPlatform(args, env);
|
|
@@ -2042,6 +2093,7 @@ function resolveConfig(argv = process.argv.slice(2), env = process.env) {
|
|
|
2042
2093
|
const token = resolveGitLabToken(args, env);
|
|
2043
2094
|
const githubApiUrl = String(args.githubApiUrl ?? env.GITHUB_API_URL ?? "https://api.github.com").replace(/\/$/, "");
|
|
2044
2095
|
const githubServerUrl = String(args.githubServerUrl ?? env.GITHUB_SERVER_URL ?? "https://github.com").replace(/\/$/, "");
|
|
2096
|
+
const githubRepository = String(args.githubRepository ?? env.GITHUB_REPOSITORY ?? "");
|
|
2045
2097
|
const model = String(args.model ?? env.CODE_REVIEW_MODEL ?? "");
|
|
2046
2098
|
const apiKey = String(args.apiKey ?? resolveProviderApiKey(model) ?? "");
|
|
2047
2099
|
const baseUrl = String(args.baseUrl ?? first(env.CODE_REVIEW_BASE_URL, resolveOllamaBaseUrl(model, env)) ?? "");
|
|
@@ -2060,11 +2112,12 @@ function resolveConfig(argv = process.argv.slice(2), env = process.env) {
|
|
|
2060
2112
|
gitlabUrl,
|
|
2061
2113
|
gitlabToken: token.token,
|
|
2062
2114
|
gitlabAuthHeader: token.header,
|
|
2063
|
-
githubRepository
|
|
2115
|
+
githubRepository,
|
|
2064
2116
|
githubPr: resolveGitHubPr(args, env),
|
|
2065
2117
|
githubToken: String(args.githubToken ?? env.GITHUB_TOKEN ?? ""),
|
|
2066
2118
|
githubApiUrl,
|
|
2067
2119
|
githubServerUrl,
|
|
2120
|
+
projectWebUrl: resolveProjectWebUrl(env) ?? "",
|
|
2068
2121
|
model,
|
|
2069
2122
|
modelPool: resolveModelPool(args, env),
|
|
2070
2123
|
minSeverity: normalizeChoice(args.minSeverity ?? env.CODE_REVIEW_MIN_SEVERITY ?? "info"),
|
|
@@ -4734,7 +4787,10 @@ async function runReview(config, options) {
|
|
|
4734
4787
|
tokens: aggregated.tokens,
|
|
4735
4788
|
cost: aggregated.cost,
|
|
4736
4789
|
byModel: buildByModelUsage(aggregated),
|
|
4737
|
-
skills: context.skills.map((s) =>
|
|
4790
|
+
skills: context.skills.map((s) => ({
|
|
4791
|
+
name: s.name,
|
|
4792
|
+
origin: s.origin
|
|
4793
|
+
})),
|
|
4738
4794
|
sizeNotice
|
|
4739
4795
|
});
|
|
4740
4796
|
let outputText;
|
|
@@ -5638,7 +5694,7 @@ async function loadDefaultRuntime() {
|
|
|
5638
5694
|
const [sdkNode, resources, semconv] = modules;
|
|
5639
5695
|
const serviceResource = resources.resourceFromAttributes({
|
|
5640
5696
|
[semconv.ATTR_SERVICE_NAME ?? "service.name"]: SERVICE_NAME,
|
|
5641
|
-
[semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.9.
|
|
5697
|
+
[semconv.ATTR_SERVICE_VERSION ?? "service.version"]: "0.9.6"
|
|
5642
5698
|
});
|
|
5643
5699
|
applyOtelExporterDefaults(process.env);
|
|
5644
5700
|
const sdk = new sdkNode.NodeSDK({ resource: resources.defaultResource().merge(serviceResource) });
|
|
@@ -5822,7 +5878,7 @@ function buildCiSpanAttrs(env) {
|
|
|
5822
5878
|
if (taskRunId) attrs["cicd.pipeline.task.run.id"] = taskRunId;
|
|
5823
5879
|
const pipelineRunId = env.CI_PIPELINE_ID ?? env.GITHUB_RUN_ID;
|
|
5824
5880
|
if (pipelineRunId) attrs["cicd.pipeline.run.id"] = pipelineRunId;
|
|
5825
|
-
const repositoryUrl =
|
|
5881
|
+
const repositoryUrl = resolveProjectWebUrl(env);
|
|
5826
5882
|
if (repositoryUrl) attrs["vcs.repository.url.full"] = repositoryUrl;
|
|
5827
5883
|
return attrs;
|
|
5828
5884
|
}
|
|
@@ -6110,7 +6166,7 @@ function boldCommentTitle(body) {
|
|
|
6110
6166
|
*/
|
|
6111
6167
|
function buildCommentBody(body, commitSha, confidence) {
|
|
6112
6168
|
const confidenceLine = `_Confidence: ${confidence}._`;
|
|
6113
|
-
const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.9.
|
|
6169
|
+
const footer = `<sub>Reviewed by ${PRODUCT_LINK} v0.9.6 for commit ${commitSha}.</sub>`;
|
|
6114
6170
|
return `${boldCommentTitle(body.trim())}\n\n${confidenceLine}\n\n---\n\n${footer}`;
|
|
6115
6171
|
}
|
|
6116
6172
|
function buildPayload(comment, body, refs, resolved) {
|
|
@@ -6683,6 +6739,99 @@ function createPlatform(config) {
|
|
|
6683
6739
|
return new GitLabPlatform(config);
|
|
6684
6740
|
}
|
|
6685
6741
|
//#endregion
|
|
6742
|
+
//#region src/skill-links.ts
|
|
6743
|
+
/**
|
|
6744
|
+
* Hosts that serve blobs at `/<repo>/blob/<ref>/<path>`. Everything else is
|
|
6745
|
+
* assumed to be GitLab (`/<repo>/-/blob/<ref>/<path>`), which covers both
|
|
6746
|
+
* gitlab.com and the self-hosted instances this tool mostly runs against.
|
|
6747
|
+
*/
|
|
6748
|
+
var GITHUB_HOSTS = new Set(["github.com", "www.github.com"]);
|
|
6749
|
+
/** Percent-encode each path segment while keeping the `/` separators intact. */
|
|
6750
|
+
function encodePath(path) {
|
|
6751
|
+
return path.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
|
|
6752
|
+
}
|
|
6753
|
+
/**
|
|
6754
|
+
* Build a blob URL for `path` at `ref` inside the repository served at
|
|
6755
|
+
* `repoWebUrl`. An empty ref resolves to `HEAD`, which both GitHub and GitLab
|
|
6756
|
+
* accept, so a skill pinned to a remote's default branch still links.
|
|
6757
|
+
*/
|
|
6758
|
+
function blobUrl(repoWebUrl, ref, path) {
|
|
6759
|
+
let parsed;
|
|
6760
|
+
try {
|
|
6761
|
+
parsed = new URL(repoWebUrl);
|
|
6762
|
+
} catch {
|
|
6763
|
+
return;
|
|
6764
|
+
}
|
|
6765
|
+
return `${`${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`}/${GITHUB_HOSTS.has(parsed.host) ? "blob" : "-/blob"}/${encodeURIComponent(ref || "HEAD")}/${encodePath(path)}`;
|
|
6766
|
+
}
|
|
6767
|
+
/**
|
|
6768
|
+
* Convert a git clone URL to the repository's web URL: drop any transport
|
|
6769
|
+
* marker, embedded credentials, and the `.git` suffix, and serve it over HTTPS.
|
|
6770
|
+
* Accepts `https://`, `ssh://`, `git+<transport>://`, and scp-style
|
|
6771
|
+
* (`git@host:group/repo.git`) forms. Returns `undefined` for anything it cannot
|
|
6772
|
+
* parse, so an exotic remote simply yields an unlinked skill.
|
|
6773
|
+
*/
|
|
6774
|
+
function gitRepoWebUrl(cloneUrl) {
|
|
6775
|
+
let raw = cloneUrl.trim();
|
|
6776
|
+
if (!raw) return void 0;
|
|
6777
|
+
if (raw.startsWith("git+")) raw = raw.slice(4);
|
|
6778
|
+
const scp = /^(?:[^@/]+@)?([^/:]+):(?!\/)(.+)$/.exec(raw);
|
|
6779
|
+
if (scp) raw = `ssh://${scp[1]}/${scp[2]}`;
|
|
6780
|
+
let parsed;
|
|
6781
|
+
try {
|
|
6782
|
+
parsed = new URL(raw);
|
|
6783
|
+
} catch {
|
|
6784
|
+
return;
|
|
6785
|
+
}
|
|
6786
|
+
if (!parsed.host) return void 0;
|
|
6787
|
+
const path = parsed.pathname.replace(/\.git\/?$/, "").replace(/\/+$/, "");
|
|
6788
|
+
if (!path || path === "/") return void 0;
|
|
6789
|
+
return `https://${parsed.host}${path}`;
|
|
6790
|
+
}
|
|
6791
|
+
/**
|
|
6792
|
+
* Resolve a link to a skill's `SKILL.md`, or `undefined` when the source is not
|
|
6793
|
+
* reachable from a browser (a `file:` path, or an in-repo skill on a run with no
|
|
6794
|
+
* CI project URL). Built-in skills link to the published tag of this package, so
|
|
6795
|
+
* the link always shows the skill exactly as the run used it.
|
|
6796
|
+
*/
|
|
6797
|
+
function skillSourceUrl(origin, context = {}) {
|
|
6798
|
+
switch (origin.kind) {
|
|
6799
|
+
case "builtin": return blobUrl(PRODUCT_URL, "0.9.6", `skills/${origin.name}/SKILL.md`);
|
|
6800
|
+
case "project":
|
|
6801
|
+
if (!context.projectWebUrl || !context.commitSha) return void 0;
|
|
6802
|
+
return blobUrl(context.projectWebUrl, context.commitSha, `${origin.path}/SKILL.md`);
|
|
6803
|
+
case "npm": return `https://www.npmjs.com/package/${origin.packageName}`;
|
|
6804
|
+
case "file": return;
|
|
6805
|
+
case "git":
|
|
6806
|
+
case "marketplace": {
|
|
6807
|
+
const repoWebUrl = gitRepoWebUrl(origin.url);
|
|
6808
|
+
if (!repoWebUrl) return void 0;
|
|
6809
|
+
const path = origin.path ? `${origin.path}/SKILL.md` : "SKILL.md";
|
|
6810
|
+
return blobUrl(repoWebUrl, origin.ref, path);
|
|
6811
|
+
}
|
|
6812
|
+
}
|
|
6813
|
+
}
|
|
6814
|
+
/**
|
|
6815
|
+
* The name a skill is shown under in the footer. Marketplace skills carry their
|
|
6816
|
+
* full selector (`<marketplace>:<plugin>/<skill>`) because a bare skill name says
|
|
6817
|
+
* nothing about which marketplace and plugin it came from — and that selector is
|
|
6818
|
+
* exactly what a developer would put in `CODE_REVIEW_SKILLS` to use it. Every
|
|
6819
|
+
* other source shows the skill's own name.
|
|
6820
|
+
*/
|
|
6821
|
+
function skillDisplayName(skill) {
|
|
6822
|
+
return skill.origin.kind === "marketplace" ? `${skill.origin.marketplace}:${skill.origin.plugin}/${skill.name}` : skill.name;
|
|
6823
|
+
}
|
|
6824
|
+
/**
|
|
6825
|
+
* Render one skill for the summary footer: a link to its source when one can be
|
|
6826
|
+
* resolved, otherwise the bare name. The name keeps its code span either way so
|
|
6827
|
+
* linked and unlinked skills read the same.
|
|
6828
|
+
*/
|
|
6829
|
+
function formatSkillLink(skill, context = {}) {
|
|
6830
|
+
const name = skillDisplayName(skill);
|
|
6831
|
+
const url = skillSourceUrl(skill.origin, context);
|
|
6832
|
+
return url ? `[\`${name}\`](${url})` : `\`${name}\``;
|
|
6833
|
+
}
|
|
6834
|
+
//#endregion
|
|
6686
6835
|
//#region src/summary-carryover.ts
|
|
6687
6836
|
var FINGERPRINT_MARKER_GLOBAL_RE = new RegExp(FINGERPRINT_MARKER_PATTERN, "gi");
|
|
6688
6837
|
var RISK_RANK = {
|
|
@@ -7058,7 +7207,10 @@ async function run(config, bridges) {
|
|
|
7058
7207
|
const summaryBody = withCarriedOverFindings(parsed.summary, discussions, currentFingerprints);
|
|
7059
7208
|
const result = await platform.upsertSummary(summaryBody, discussions, {
|
|
7060
7209
|
costFooter: [formatUsageLine(usage), formatPerModelUsage(usage)].filter(Boolean).join("\n\n"),
|
|
7061
|
-
skillsFooter: formatSkillsFooter(usage.skills
|
|
7210
|
+
skillsFooter: formatSkillsFooter(usage.skills, {
|
|
7211
|
+
projectWebUrl: config.projectWebUrl,
|
|
7212
|
+
commitSha: refs.head_sha
|
|
7213
|
+
}),
|
|
7062
7214
|
reviewedCommitSha: refs.head_sha,
|
|
7063
7215
|
runId,
|
|
7064
7216
|
sizeNotice: usage.sizeNotice
|
|
@@ -7125,9 +7277,15 @@ function zeroReviewUsage(model, thinkingLevel) {
|
|
|
7125
7277
|
sizeNotice: { sizeSkippedFiles: [] }
|
|
7126
7278
|
};
|
|
7127
7279
|
}
|
|
7128
|
-
|
|
7280
|
+
/**
|
|
7281
|
+
* The summary footer's skills line. Each skill links to its `SKILL.md` source so
|
|
7282
|
+
* a developer reading the review can check what the reviewer was told; skills
|
|
7283
|
+
* with no reachable source (a `file:` path, or an in-repo skill on a run without
|
|
7284
|
+
* CI project coordinates) stay as plain names.
|
|
7285
|
+
*/
|
|
7286
|
+
function formatSkillsFooter(skills, context = {}) {
|
|
7129
7287
|
if (skills.length === 0) return void 0;
|
|
7130
|
-
return `Skills: ${skills.map((s) =>
|
|
7288
|
+
return `Skills: ${skills.map((s) => formatSkillLink(s, context)).join(", ")}`;
|
|
7131
7289
|
}
|
|
7132
7290
|
function formatUsageLine(usage) {
|
|
7133
7291
|
const formatter = new Intl.NumberFormat("en-US");
|
|
@@ -7217,10 +7375,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
7217
7375
|
return;
|
|
7218
7376
|
}
|
|
7219
7377
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
7220
|
-
console.log("0.9.
|
|
7378
|
+
console.log("0.9.6");
|
|
7221
7379
|
return;
|
|
7222
7380
|
}
|
|
7223
|
-
process.stderr.write(`[code-review] @weareikko/code-review v0.9.
|
|
7381
|
+
process.stderr.write(`[code-review] @weareikko/code-review v0.9.6\n`);
|
|
7224
7382
|
assertNodeVersion();
|
|
7225
7383
|
applyCodeReviewEnvPrefix();
|
|
7226
7384
|
applyDefaultCacheRetention();
|
|
@@ -7241,6 +7399,6 @@ if (isDirectRun()) main().catch((error) => {
|
|
|
7241
7399
|
process.exitCode = 1;
|
|
7242
7400
|
});
|
|
7243
7401
|
//#endregion
|
|
7244
|
-
export {
|
|
7402
|
+
export { normalizeBody as $, SUMMARY_HISTORY_END as A, buildSummaryHistoryEntries as B, createDiagnosticContext as C, traceDiagnosticPhase as D, traceDiagnostic as E, SUMMARY_MARKER as F, findExistingSummaryNoteId as G, extractSummaryHistoryEntries as H, buildArchivedSummaryEntry as I, upsertSummaryNote as J, stripSummaryHistory as K, buildReviewedCommitFooter as L, SUMMARY_HISTORY_ENTRY_START as M, SUMMARY_HISTORY_LIMIT as N, normalizeSeverity as O, SUMMARY_HISTORY_START as P, fingerprints as Q, buildSizeNoticeBlock as R, DIAGNOSTIC_CHANNEL_PREFIX as S, diagnosticChannels as T, findExistingReviewedCommitSha as U, extractReviewedCommitSha as V, findExistingSummaryNote as W, extractDiffHunkContext as X, appendFingerprintMarkers as Y, extractExistingFingerprints as Z, filterDiff as _, main as a, resolveSkillCacheDir as at, parseReviewMarkdownWithWarnings as b, blobUrl as c, skillDisplayName as d, sha256 as et, skillSourceUrl as f, startOtelBridge as g, isOtelEnabled as h, formatUsageLine as i, resolveNpmSkillDir as it, SUMMARY_HISTORY_ENTRY_END as j, toGitLabReviewSeverity as k, formatSkillLink as l, buildPayload as m, formatPerModelUsage as n, loadNamedSkill as nt, run as o, buildGeneratedComments as p, stripSummaryMarker as q, formatSkillsFooter as r, parseSkillSpec as rt, withHttpStamping as s, countPostedBySeverity as t, gitSkillCacheKey as tt, gitRepoWebUrl as u, runReview as v, createDiagnosticRunId as w, DIAGNOSTIC_CHANNEL_NAMES as x, parseReviewMarkdown as y, buildSummaryBody as z };
|
|
7245
7403
|
|
|
7246
|
-
//# sourceMappingURL=cli-
|
|
7404
|
+
//# sourceMappingURL=cli-D6vNV3kA.js.map
|