@coldtea/pr-lens-cli 0.8.1 → 0.10.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.
Files changed (72) hide show
  1. package/dist/canvas/api.d.ts +104 -1
  2. package/dist/canvas/api.d.ts.map +1 -1
  3. package/dist/canvas/api.js +48 -1
  4. package/dist/canvas/api.js.map +1 -1
  5. package/dist/canvas/live.d.ts +15 -0
  6. package/dist/canvas/live.d.ts.map +1 -0
  7. package/dist/canvas/live.js +417 -0
  8. package/dist/canvas/live.js.map +1 -0
  9. package/dist/canvas/registry.d.ts +8 -0
  10. package/dist/canvas/registry.d.ts.map +1 -1
  11. package/dist/canvas/registry.js +2 -0
  12. package/dist/canvas/registry.js.map +1 -1
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +1 -0
  15. package/dist/cli.js.map +1 -1
  16. package/dist/commands/analyze.d.ts.map +1 -1
  17. package/dist/commands/analyze.js +25 -6
  18. package/dist/commands/analyze.js.map +1 -1
  19. package/dist/commands/auth.d.ts +2 -0
  20. package/dist/commands/auth.d.ts.map +1 -1
  21. package/dist/commands/auth.js +1 -1
  22. package/dist/commands/auth.js.map +1 -1
  23. package/dist/commands/canvas.d.ts.map +1 -1
  24. package/dist/commands/canvas.js +29 -2
  25. package/dist/commands/canvas.js.map +1 -1
  26. package/dist/commands/comment.d.ts +1 -1
  27. package/dist/commands/comment.d.ts.map +1 -1
  28. package/dist/commands/comment.js +16 -2
  29. package/dist/commands/comment.js.map +1 -1
  30. package/dist/commands/render.d.ts +1 -1
  31. package/dist/commands/render.d.ts.map +1 -1
  32. package/dist/commands/render.js +9 -4
  33. package/dist/commands/render.js.map +1 -1
  34. package/dist/comment.d.ts +4 -0
  35. package/dist/comment.d.ts.map +1 -1
  36. package/dist/comment.js +193 -33
  37. package/dist/comment.js.map +1 -1
  38. package/dist/config-file.d.ts +7 -2
  39. package/dist/config-file.d.ts.map +1 -1
  40. package/dist/config-file.js +14 -2
  41. package/dist/config-file.js.map +1 -1
  42. package/dist/errors.d.ts +1 -1
  43. package/dist/errors.d.ts.map +1 -1
  44. package/dist/errors.js.map +1 -1
  45. package/dist/git.d.ts +17 -0
  46. package/dist/git.d.ts.map +1 -1
  47. package/dist/git.js +37 -2
  48. package/dist/git.js.map +1 -1
  49. package/dist/skill-content.generated.d.ts +1 -1
  50. package/dist/skill-content.generated.d.ts.map +1 -1
  51. package/dist/skill-content.generated.js +1 -1
  52. package/dist/skill-content.generated.js.map +1 -1
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.d.ts.map +1 -1
  55. package/dist/version.js +1 -1
  56. package/dist/version.js.map +1 -1
  57. package/package.json +3 -3
  58. package/src/canvas/api.ts +109 -0
  59. package/src/canvas/live.ts +523 -0
  60. package/src/canvas/registry.ts +2 -0
  61. package/src/cli.ts +1 -0
  62. package/src/commands/analyze.ts +30 -6
  63. package/src/commands/auth.ts +1 -1
  64. package/src/commands/canvas.ts +36 -2
  65. package/src/commands/comment.ts +18 -2
  66. package/src/commands/render.ts +18 -5
  67. package/src/comment.ts +249 -34
  68. package/src/config-file.ts +14 -2
  69. package/src/errors.ts +3 -0
  70. package/src/git.ts +38 -3
  71. package/src/skill-content.generated.ts +1 -1
  72. package/src/version.ts +1 -1
@@ -1,10 +1,10 @@
1
- import { LENSES, type GraphDocInput, type Lens } from "@coldtea/pr-lens-schema";
1
+ import { LENSES, PROVIDERS, type GraphDocInput, type Lens, type Provider } from "@coldtea/pr-lens-schema";
2
2
  import { dirname } from "node:path";
3
3
  import { parseOptions, readBoolean, readInt, readList, readString } from "../args.js";
4
4
  import { discoverConfig, loadConfig, type LoadedConfig } from "../config-file.js";
5
5
  import { PrLensCliError, usageError } from "../errors.js";
6
6
  import { extractGraph } from "../extract.js";
7
- import { collectDiff, mergeBase, parseRepoSlug, remoteSlug, repositoryRoot, resolveCommit } from "../git.js";
7
+ import { collectDiff, mergeBase, parseRepoSlug, pullRequestUrl, remoteSlug, repositoryRoot, resolveCommit } from "../git.js";
8
8
  import { writeJsonFile } from "../io.js";
9
9
  import { buildExtractionPrompt, SYSTEM_PROMPT } from "../prompt.js";
10
10
  import { completeJson, isProviderId, PROVIDER_IDS, resolveProvider } from "../providers/index.js";
@@ -36,6 +36,8 @@ environment, and the diff goes straight to the provider you name.
36
36
  --pr <number> pull request number, recorded in provenance
37
37
  --repo-slug <owner/name> override the slug read from the git remote
38
38
  --remote <name> remote to read the slug from (default origin)
39
+ --forge <name> github | gitlab | bitbucket — where the repository is
40
+ hosted (default read from the remote host)
39
41
  --max-diff-bytes <n> truncate the diff sent to the model (default ${DEFAULT_MAX_DIFF_BYTES})
40
42
  --max-output-tokens <n> room for the answer (default ${DEFAULT_MAX_OUTPUT_TOKENS})
41
43
  --dry-run report what would be sent, and send nothing
@@ -52,6 +54,16 @@ const readLenses = (values: Record<string, unknown>): Lens[] | undefined => {
52
54
  return LENSES.filter((lens) => requested.includes(lens));
53
55
  };
54
56
 
57
+ const readForge = (values: Record<string, unknown>): Provider | undefined => {
58
+ const forge = readString(values.forge, "forge");
59
+ if (forge === undefined) return undefined;
60
+
61
+ const known = PROVIDERS.find((provider) => provider === forge);
62
+ if (known === undefined)
63
+ throw usageError(`unknown forge ${JSON.stringify(forge)}`, `known forges: ${PROVIDERS.join(", ")}`);
64
+ return known;
65
+ };
66
+
55
67
  const readProviderId = (values: Record<string, unknown>) => {
56
68
  const id = readString(values.provider, "provider") ?? "gemini";
57
69
  if (!isProviderId(id))
@@ -78,6 +90,7 @@ export const analyzeCommand = async (
78
90
  pr: { type: "string" },
79
91
  "repo-slug": { type: "string" },
80
92
  remote: { type: "string" },
93
+ forge: { type: "string" },
81
94
  "max-diff-bytes": { type: "string" },
82
95
  "max-output-tokens": { type: "string" },
83
96
  "dry-run": { type: "boolean" },
@@ -94,17 +107,28 @@ export const analyzeCommand = async (
94
107
  const maxDiffBytes = readInt(values["max-diff-bytes"], "max-diff-bytes", DEFAULT_MAX_DIFF_BYTES);
95
108
 
96
109
  const slugOption = readString(values["repo-slug"], "repo-slug");
97
- const slug = slugOption === undefined
110
+ const detected = slugOption === undefined
98
111
  ? await remoteSlug(repo, readString(values.remote, "remote") ?? "origin")
99
112
  : parseRepoSlug(slugOption);
100
113
 
101
- if (slug === undefined)
114
+ if (detected === undefined)
102
115
  throw new PrLensCliError(
103
116
  "REPOSITORY_UNKNOWN",
104
117
  "cannot tell which repository this is",
105
118
  "pass --repo-slug owner/name, or add a git remote the CLI can read",
106
119
  );
107
120
 
121
+ const forge = readForge(values);
122
+ // An owner/name slug carries no host, and parseRepoSlug fills in
123
+ // github.com — which a non-GitHub forge would then dress in the wrong
124
+ // URLs. Refusing beats a permalink that 404s in the reader's face.
125
+ if (forge !== undefined && forge !== "github" && slugOption !== undefined)
126
+ throw usageError(
127
+ `--forge ${forge} cannot be combined with --repo-slug`,
128
+ "an owner/name slug names no host; let the CLI read the git remote, which carries one",
129
+ );
130
+ const slug = forge === undefined ? detected : { ...detected, provider: forge };
131
+
108
132
  const configPath = readString(values.config, "config");
109
133
  const configured: LoadedConfig | undefined = readBoolean(values["no-config"])
110
134
  ? undefined
@@ -162,10 +186,10 @@ export const analyzeCommand = async (
162
186
  const pullRequest =
163
187
  pr === undefined
164
188
  ? undefined
165
- : { number: pr, url: `https://${slug.host}/${slug.owner}/${slug.name}/pull/${pr}` };
189
+ : { number: pr, url: pullRequestUrl(slug, pr) };
166
190
 
167
191
  const provenance: GraphDocInput["provenance"] = {
168
- repo: slug,
192
+ repo: { owner: slug.owner, name: slug.name, host: slug.host },
169
193
  base: { sha: comparedAgainst, ref: baseCommit.ref },
170
194
  head: { sha: headCommit.sha, ref: headCommit.ref },
171
195
  ...(pullRequest === undefined ? {} : { pullRequest }),
@@ -72,7 +72,7 @@ const sleep = (ms: number): Promise<void> =>
72
72
  const hostOf = (api: string): string => new URL(api).host;
73
73
 
74
74
  /** True means asked, not opened (over SSH it "succeeds" on no screen), so the link is printed either way. */
75
- const askToOpen = (url: string): boolean => {
75
+ export const askToOpen = (url: string): boolean => {
76
76
  // The URL came off the wire: only http(s), and never through a shell
77
77
  // (hence `rundll32` over `cmd /c start`).
78
78
  const scheme = ((): string => {
@@ -36,6 +36,14 @@ import { drawings, WORKSPACE_DIR } from "../workspace.js";
36
36
  import { readToken, requireToken } from "../auth.js";
37
37
  import { claimCommand } from "../canvas/claim.js";
38
38
  import { deleteCommand } from "../canvas/delete.js";
39
+ import {
40
+ answerCommand,
41
+ forkCommand,
42
+ LIVE_USAGE,
43
+ lookCommand,
44
+ openLiveCommand,
45
+ showCommand,
46
+ } from "../canvas/live.js";
39
47
  import { parseOptions, readBoolean, readString } from "../args.js";
40
48
  import { PrLensCliError, usageError } from "../errors.js";
41
49
  import { DEFAULT_API, API_ENV, readApi, requireSameApi, requireWriteToken, settleRotation, settlePendingRotation, writeCredential } from "../canvas/write.js";
@@ -53,13 +61,18 @@ const SUBCOMMANDS = [
53
61
  "claim",
54
62
  "rotate",
55
63
  "delete",
64
+ "open",
65
+ "answer",
66
+ "show",
67
+ "fork",
68
+ "look",
56
69
  ] as const;
57
70
  type Subcommand = (typeof SUBCOMMANDS)[number];
58
71
 
59
72
  const isSubcommand = (value: string): value is Subcommand =>
60
73
  SUBCOMMANDS.some((subcommand) => subcommand === value);
61
74
 
62
- export const USAGE = `pr-lens canvas <list | push | pull | claim | rotate | delete> [options]
75
+ export const USAGE = `pr-lens canvas <list | push | pull | claim | rotate | delete | open | answer | show | fork | look> [options]
63
76
 
64
77
  Keeps a graph document on the PR Lens app as a canvas: a page anyone you share
65
78
  it with can read, and an SVG a README can embed. The write token lands in
@@ -91,6 +104,16 @@ in its fragment, so share the view link and keep the edit link to yourself.
91
104
  pr-lens canvas delete permanently delete the hosted canvas; keep local graph and SVG files
92
105
  --canvas <id|name> which canvas (default the checkout's only canvas)
93
106
 
107
+ Live mode puts your coding agent beside an open canvas: it answers there, with
108
+ the camera, the highlights and the links the page's own agent uses.
109
+
110
+ ${LIVE_USAGE}
111
+
112
+ Every live command takes the drawing it is about:
113
+ --drawing <path> the same drawn.graph.json you opened (open takes it as
114
+ its argument); left out, the checkout's only paired canvas
115
+ --canvas <id|name> a canvas by id or name, instead of a drawing
116
+
94
117
  --api <url> the PR Lens app (default $${API_ENV}, else ${DEFAULT_API})`;
95
118
 
96
119
  type CanvasRef = {
@@ -173,6 +196,7 @@ const recordPull = (current: CanvasRegistry, pull: PullRecord): Recorded => {
173
196
  ...(pending === undefined ? {} : { pending }),
174
197
  ...(kept === undefined ? {} : { writeToken: kept }),
175
198
  rev: pull.fetched?.rev ?? entry?.rev ?? 0,
199
+ ...(entry?.live === undefined ? {} : { live: entry.live }),
176
200
  };
177
201
 
178
202
  return imports
@@ -639,7 +663,7 @@ export const canvasCommand = async (
639
663
  const [name, ...rest] = args;
640
664
  if (name === undefined)
641
665
  throw usageError(
642
- "canvas needs a subcommand: list, push, pull, claim, rotate or delete",
666
+ "canvas needs a subcommand: list, push, pull, claim, rotate, delete, open, answer, show, fork or look",
643
667
  );
644
668
  if (!isSubcommand(name))
645
669
  throw usageError(`unknown canvas subcommand ${JSON.stringify(name)}`);
@@ -657,6 +681,16 @@ export const canvasCommand = async (
657
681
  return deleteCommand(rest, terminal, env);
658
682
  case "rotate":
659
683
  return rotate(rest, terminal, env);
684
+ case "open":
685
+ return openLiveCommand(rest, terminal, env);
686
+ case "answer":
687
+ return answerCommand(rest, terminal, env);
688
+ case "show":
689
+ return showCommand(rest, terminal, env);
690
+ case "fork":
691
+ return forkCommand(rest, terminal, env);
692
+ case "look":
693
+ return lookCommand(rest, terminal, env);
660
694
  default:
661
695
  return assertNever(name, "Unhandled canvas subcommand");
662
696
  }
@@ -1,6 +1,7 @@
1
1
  import { graphContentHash } from "@coldtea/pr-lens-renderer";
2
+ import { PROVIDERS, type Provider } from "@coldtea/pr-lens-schema";
2
3
  import { parseOptions, readBoolean, readString } from "../args.js";
3
- import { composeComment, COMMENT_MARKER } from "../comment.js";
4
+ import { commentMarker, composeComment } from "../comment.js";
4
5
  import { loadConfig } from "../config-file.js";
5
6
  import { readGraphDoc, readRenderManifest } from "../document.js";
6
7
  import { usageError } from "../errors.js";
@@ -20,10 +21,21 @@ goes to stdout, or to a file, for whatever does the posting.
20
21
  manifest records local paths
21
22
  --config <file> read 'branding' from a repository config
22
23
  --no-branding leave off the PR Lens footer
24
+ --target <forge> github | gitlab | bitbucket — who renders the
25
+ comment (default github). GitLab gets no <picture>
26
+ pair; Bitbucket gets plain markdown, no HTML
23
27
  --print-marker print the hidden marker that identifies the
24
28
  comment, and nothing else
25
29
  -o, --out <file> write the markdown here instead of stdout`;
26
30
 
31
+ const readTarget = (values: Record<string, unknown>): Provider => {
32
+ const target = readString(values.target, "target") ?? "github";
33
+ const known = PROVIDERS.find((provider) => provider === target);
34
+ if (known === undefined)
35
+ throw usageError(`unknown target ${JSON.stringify(target)}`, `known targets: ${PROVIDERS.join(", ")}`);
36
+ return known;
37
+ };
38
+
27
39
  export const commentCommand = async (args: readonly string[], terminal: Terminal): Promise<void> => {
28
40
  const { values, positionals } = parseOptions(args, {
29
41
  graph: { type: "string" },
@@ -31,6 +43,7 @@ export const commentCommand = async (args: readonly string[], terminal: Terminal
31
43
  "asset-base-url": { type: "string" },
32
44
  config: { type: "string" },
33
45
  "no-branding": { type: "boolean" },
46
+ target: { type: "string" },
34
47
  "print-marker": { type: "boolean" },
35
48
  out: { type: "string", short: "o" },
36
49
  });
@@ -38,8 +51,10 @@ export const commentCommand = async (args: readonly string[], terminal: Terminal
38
51
  if (positionals.length > 0)
39
52
  throw usageError(`comment takes no positional arguments, got ${positionals.join(" ")}`);
40
53
 
54
+ const target = readTarget(values);
55
+
41
56
  if (readBoolean(values["print-marker"])) {
42
- terminal.out(COMMENT_MARKER);
57
+ terminal.out(commentMarker(target));
43
58
  return;
44
59
  }
45
60
 
@@ -65,6 +80,7 @@ export const commentCommand = async (args: readonly string[], terminal: Terminal
65
80
  manifest,
66
81
  assetBaseUrl: readString(values["asset-base-url"], "asset-base-url"),
67
82
  branding: readBoolean(values["no-branding"]) ? false : (configured?.config.branding ?? true),
83
+ target,
68
84
  });
69
85
 
70
86
  const out = readString(values.out, "out");
@@ -1,4 +1,10 @@
1
- import { applyCorrections, PrLensRenderError, renderAll, THEMES, type Theme } from "@coldtea/pr-lens-renderer";
1
+ import {
2
+ applyCorrections,
3
+ PrLensRenderError,
4
+ renderAll,
5
+ THEME_PAIR,
6
+ type RenderThemes,
7
+ } from "@coldtea/pr-lens-renderer";
2
8
  import { safeParseGraphDoc, type Config, type GraphDoc } from "@coldtea/pr-lens-schema";
3
9
  import { join } from "node:path";
4
10
  import { expectOne, parseOptions, readBoolean, readString } from "../args.js";
@@ -36,20 +42,27 @@ with no sections gets one diagram per lens it declares.
36
42
 
37
43
  -o, --out <dir> where the SVGs, the document and the manifest go
38
44
  (default ${WORKSPACE_DIR}/<title>/, one directory per drawing)
39
- --theme <theme> light | dark | both (default both)
45
+ --theme <theme> light | dark | both | neutral (default both)
46
+ neutral is the single self-contained render for a
47
+ surface that shows one image: GitLab and Bitbucket
40
48
  --config <file> corrections to draw with (default the repository's, if any)
41
49
  --no-config ignore the repository's corrections`;
42
50
 
43
- const readThemes = (value: unknown): readonly Theme[] => {
51
+ const readThemes = (value: unknown): RenderThemes => {
44
52
  const theme = readString(value, "theme") ?? "both";
45
53
  switch (theme) {
46
54
  case "light":
47
55
  case "dark":
56
+ // A surface that shows one image wants this one instead of the pair, not
57
+ // as well as it — so it is a choice of --theme, never an addition to both.
58
+ case "neutral":
48
59
  return [theme];
49
60
  case "both":
50
- return THEMES;
61
+ return THEME_PAIR;
51
62
  default:
52
- throw usageError(`--theme takes light, dark or both, got ${JSON.stringify(theme)}`);
63
+ throw usageError(
64
+ `--theme takes light, dark, both or neutral, got ${JSON.stringify(theme)}`,
65
+ );
53
66
  }
54
67
  };
55
68