poe-code 3.0.337 → 3.0.338

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-code",
3
- "version": "3.0.337",
3
+ "version": "3.0.338",
4
4
  "description": "CLI tool to configure Poe API for developer workflows.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,7 @@
1
1
  export interface ReviewInlineComment {
2
2
  path: string;
3
3
  line: number;
4
+ side?: "LEFT" | "RIGHT";
4
5
  body: string;
5
6
  }
6
7
  export interface ReviewDiffLine {
@@ -23,6 +24,8 @@ export interface ReviewDiffContext {
23
24
  files: ReviewDiffFile[];
24
25
  fileOrder: Map<string, number>;
25
26
  reviewableLinesByPath: Map<string, Set<number>>;
27
+ leftLinesByPath: Map<string, Set<number>>;
28
+ rightContextLinesByPath: Map<string, Set<number>>;
26
29
  rendered: string;
27
30
  }
28
31
  export declare function parseReviewDiff(diffText: string): ReviewDiffContext;
@@ -121,6 +121,8 @@ export function parseReviewDiff(diffText) {
121
121
  status: "modified",
122
122
  hunks: [],
123
123
  reviewableLines: [],
124
+ leftLineSet: new Set(),
125
+ rightContextLineSet: new Set(),
124
126
  reviewableLineSet: new Set()
125
127
  };
126
128
  continue;
@@ -173,11 +175,13 @@ export function parseReviewDiff(diffText) {
173
175
  }
174
176
  if (line.startsWith("-") && !line.startsWith("---")) {
175
177
  currentHunk.lines.push({ side: "LEFT", line: oldLine, text: line });
178
+ currentFile.leftLineSet.add(oldLine);
176
179
  oldLine += 1;
177
180
  continue;
178
181
  }
179
182
  if (line.startsWith(" ")) {
180
183
  currentHunk.lines.push({ side: "RIGHT", line: newLine, text: line });
184
+ currentFile.rightContextLineSet.add(newLine);
181
185
  currentFile.reviewableLineSet.add(newLine);
182
186
  oldLine += 1;
183
187
  newLine += 1;
@@ -195,6 +199,8 @@ export function parseReviewDiff(diffText) {
195
199
  files: publicFiles,
196
200
  fileOrder: new Map(publicFiles.map((file, index) => [file.path, index])),
197
201
  reviewableLinesByPath: new Map(files.map((file) => [file.path, file.reviewableLineSet])),
202
+ leftLinesByPath: new Map(files.map((file) => [file.path, file.leftLineSet])),
203
+ rightContextLinesByPath: new Map(files.map((file) => [file.path, file.rightContextLineSet])),
198
204
  rendered: publicFiles.map(renderReviewFile).join("\n\n")
199
205
  };
200
206
  }
@@ -202,18 +208,31 @@ export function validateInlineComments(comments, context) {
202
208
  const accepted = [];
203
209
  const seen = new Set();
204
210
  for (const comment of comments) {
205
- const path = comment.path.trim();
211
+ const path = comment.path;
206
212
  const body = comment.body.trim();
207
- if (!path || !body || !Number.isInteger(comment.line) || comment.line < 1) {
213
+ if (path.length === 0 || !body || !Number.isInteger(comment.line) || comment.line < 1) {
208
214
  throw new Error("Inline review comments require path, positive line, and body.");
209
215
  }
216
+ if (comment.side === "LEFT") {
217
+ throw new Error(`Inline comment target ${path}:${comment.line} is a left-side diff target; only right-side comments are supported.`);
218
+ }
210
219
  if (!context.reviewableLinesByPath.get(path)?.has(comment.line)) {
211
220
  throw new Error(`Inline comment target ${path}:${comment.line} is not a valid right-side diff target.`);
212
221
  }
222
+ if (!comment.side &&
223
+ context.leftLinesByPath.get(path)?.has(comment.line) &&
224
+ context.rightContextLinesByPath.get(path)?.has(comment.line)) {
225
+ throw new Error(`Inline comment target ${path}:${comment.line} is ambiguous; specify side RIGHT to target the right-side diff line.`);
226
+ }
213
227
  const key = `${path}\0${comment.line}\0${body}`;
214
228
  if (!seen.has(key)) {
215
229
  seen.add(key);
216
- accepted.push({ path, line: comment.line, body });
230
+ accepted.push({
231
+ path,
232
+ line: comment.line,
233
+ ...(comment.side ? { side: comment.side } : {}),
234
+ body
235
+ });
217
236
  }
218
237
  }
219
238
  accepted.sort((left, right) => {
@@ -54,12 +54,13 @@ function parseJson(stdout, action) {
54
54
  }
55
55
  }
56
56
  export function ghPrView(prUrl, fields, options = {}) {
57
- requirePullRequestRef(prUrl);
57
+ const ref = requirePullRequestRef(prUrl);
58
+ const canonicalPrUrl = ref.url;
58
59
  const requestedFields = dedupe([...fields]);
59
- const result = runGh(["pr", "view", prUrl, "--json", requestedFields.join(",")], options);
60
+ const result = runGh(["pr", "view", canonicalPrUrl, "--json", requestedFields.join(",")], options);
60
61
  if (result.code === 0) {
61
62
  const parsed = parseJsonRecord(result.stdout, "gh pr view");
62
- parsed.url ??= prUrl;
63
+ parsed.url ??= canonicalPrUrl;
63
64
  return parsed;
64
65
  }
65
66
  const availableFields = parseGhAvailableFields(result.stderr);
@@ -67,17 +68,17 @@ export function ghPrView(prUrl, fields, options = {}) {
67
68
  if (fallbackFields.length === 0 || fallbackFields.length === requestedFields.length) {
68
69
  throw new Error(result.stderr.trim() || result.stdout.trim() || "gh pr view failed");
69
70
  }
70
- const fallback = runGh(["pr", "view", prUrl, "--json", fallbackFields.join(",")], options);
71
+ const fallback = runGh(["pr", "view", canonicalPrUrl, "--json", fallbackFields.join(",")], options);
71
72
  if (fallback.code !== 0) {
72
73
  throw new Error(fallback.stderr.trim() || fallback.stdout.trim() || "gh pr view failed");
73
74
  }
74
75
  const parsed = parseJsonRecord(fallback.stdout, "gh pr view");
75
- parsed.url ??= prUrl;
76
+ parsed.url ??= canonicalPrUrl;
76
77
  return parsed;
77
78
  }
78
79
  export function ghPrDiff(prUrl, options = {}) {
79
- requirePullRequestRef(prUrl);
80
- return runGhOrThrow(["pr", "diff", prUrl], options);
80
+ const ref = requirePullRequestRef(prUrl);
81
+ return runGhOrThrow(["pr", "diff", ref.url], options);
81
82
  }
82
83
  export function ghApiJson(prUrl, args, payload, options = {}) {
83
84
  const ref = requirePullRequestRef(prUrl);
@@ -1,6 +1,6 @@
1
1
  function parseUrl(value) {
2
2
  try {
3
- return new URL(value);
3
+ return new URL(value.trim());
4
4
  }
5
5
  catch {
6
6
  return null;
@@ -22,6 +22,9 @@ function parsePathPart(value) {
22
22
  return null;
23
23
  }
24
24
  }
25
+ function formatPullRequestUrl(ref) {
26
+ return `https://${ref.host}/${encodeURIComponent(ref.owner)}/${encodeURIComponent(ref.repo)}/pull/${ref.number}`;
27
+ }
25
28
  export function parseGitHubPullRequestRef(prUrl) {
26
29
  const parsed = parseUrl(prUrl);
27
30
  if (!parsed ||
@@ -41,18 +44,18 @@ export function parseGitHubPullRequestRef(prUrl) {
41
44
  if (!owner || !repo || !Number.isSafeInteger(number) || number <= 0) {
42
45
  return null;
43
46
  }
44
- return {
47
+ const ref = {
45
48
  host: parsed.host.toLowerCase(),
46
49
  owner,
47
50
  repo,
48
- number,
49
- url: prUrl
51
+ number
50
52
  };
53
+ return { ...ref, url: formatPullRequestUrl(ref) };
51
54
  }
52
55
  export function canonicalPullRequestUrl(prUrl) {
53
56
  const ref = parseGitHubPullRequestRef(prUrl);
54
57
  if (!ref) {
55
58
  return prUrl;
56
59
  }
57
- return `https://${ref.host}/${encodeURIComponent(ref.owner)}/${encodeURIComponent(ref.repo)}/pull/${ref.number}`;
60
+ return ref.url;
58
61
  }
@@ -76,11 +76,32 @@ function isAtOrAfter(value, since) {
76
76
  return false;
77
77
  return new Date(value).getTime() >= new Date(since).getTime();
78
78
  }
79
- function createdAt(record, kind) {
79
+ function rawCreatedAt(record, kind) {
80
80
  return kind === "review_body"
81
81
  ? (text(record, "submitted_at") ?? text(record, "created_at"))
82
82
  : (text(record, "created_at") ?? text(record, "submitted_at"));
83
83
  }
84
+ function createdAt(record, kind) {
85
+ const value = rawCreatedAt(record, kind);
86
+ if (!value) {
87
+ return null;
88
+ }
89
+ const timestamp = new Date(value);
90
+ if (Number.isNaN(timestamp.getTime())) {
91
+ throw new Error(`Invalid GitHub review-history ${kind} timestamp: ${value}`);
92
+ }
93
+ return timestamp.toISOString();
94
+ }
95
+ function dedupeRepos(repos) {
96
+ const deduped = new Map();
97
+ for (const repo of repos) {
98
+ const key = repo.toLowerCase();
99
+ if (!deduped.has(key)) {
100
+ deduped.set(key, repo);
101
+ }
102
+ }
103
+ return [...deduped.values()];
104
+ }
84
105
  class ReviewHistoryFetcher {
85
106
  options;
86
107
  runner;
@@ -314,7 +335,7 @@ export async function* fetchReviewHistory(options) {
314
335
  (!Number.isInteger(options.maxComments) || options.maxComments < 1)) {
315
336
  throw new Error("Review-history maxComments must be a positive integer.");
316
337
  }
317
- const repos = [...new Set(options.repos)];
338
+ const repos = dedupeRepos(options.repos);
318
339
  for (const repo of repos) {
319
340
  if (!validRepo(repo)) {
320
341
  throw new Error(`Invalid GitHub repository name: ${repo}`);
@@ -35,18 +35,21 @@ function requirePositiveId(commentId) {
35
35
  }
36
36
  return normalized;
37
37
  }
38
- function validateReviewComments(comments) {
39
- for (const comment of comments) {
40
- if (!comment.path.trim()) {
38
+ function normalizeReviewComments(comments) {
39
+ return comments.map((comment) => {
40
+ const path = comment.path.trim();
41
+ const body = comment.body.trim();
42
+ if (!path) {
41
43
  throw new Error("Review comment path must be non-empty.");
42
44
  }
43
45
  if (!Number.isSafeInteger(comment.line) || comment.line <= 0) {
44
46
  throw new Error("Review comment line must be a positive integer.");
45
47
  }
46
- if (!comment.body.trim()) {
48
+ if (!body) {
47
49
  throw new Error("Review comment body must be non-empty.");
48
50
  }
49
- }
51
+ return { path, line: comment.line, body };
52
+ });
50
53
  }
51
54
  function submissionFromResponse(response) {
52
55
  return {
@@ -59,8 +62,7 @@ export function submitPullRequestReview(input) {
59
62
  if (!REVIEW_DECISIONS.has(input.decision)) {
60
63
  throw new Error(`Invalid pull request review decision: ${input.decision}`);
61
64
  }
62
- const comments = input.comments ?? [];
63
- validateReviewComments(comments);
65
+ const comments = normalizeReviewComments(input.comments ?? []);
64
66
  const response = ghApiJson(prUrl, ["--method", "POST", reviewEndpoint(prUrl)], {
65
67
  body: input.summary,
66
68
  event: input.decision,
@@ -75,10 +77,11 @@ export function submitPullRequestReview(input) {
75
77
  }
76
78
  export function editPullRequestReviewComment(input) {
77
79
  const prUrl = inputPullRequestUrl(input);
78
- if (!input.body.trim()) {
80
+ const body = input.body.trim();
81
+ if (!body) {
79
82
  throw new Error("Review comment body must be non-empty.");
80
83
  }
81
- const response = ghApiJson(prUrl, ["--method", "PATCH", commentEndpoint(prUrl, input.commentId)], { body: input.body }, input);
84
+ const response = ghApiJson(prUrl, ["--method", "PATCH", commentEndpoint(prUrl, input.commentId)], { body }, input);
82
85
  return submissionFromResponse(response);
83
86
  }
84
87
  export function deletePullRequestReviewComment(input) {