prism-review 0.1.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/dist/index.js ADDED
@@ -0,0 +1,388 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/init.ts
4
+ import { writeFile, mkdir } from "fs/promises";
5
+ import { resolve, dirname } from "path";
6
+ import {
7
+ createOpenAIProvider,
8
+ createAnthropicProvider,
9
+ createOpenAICompatProvider,
10
+ createOllamaProvider,
11
+ OLLAMA_RECOMMENDED_MODELS
12
+ } from "prism-core";
13
+
14
+ // src/generators/config.ts
15
+ function generateConfig(options) {
16
+ const config = {
17
+ provider: options.provider,
18
+ model: options.model,
19
+ apiKeyEnv: options.apiKeyEnv,
20
+ profile: options.profile,
21
+ commentMode: options.commentMode,
22
+ maxFiles: options.maxFiles,
23
+ maxDiffBytes: options.maxDiffBytes
24
+ };
25
+ if (options.baseUrl) {
26
+ config.baseUrl = options.baseUrl;
27
+ }
28
+ return JSON.stringify(config, null, 2) + "\n";
29
+ }
30
+
31
+ // src/generators/workflow.ts
32
+ function generateWorkflow(options) {
33
+ const secretName = options.apiKeyEnv.replace(/_/g, "_");
34
+ return `name: PRism Review
35
+
36
+ on:
37
+ pull_request:
38
+ types: [opened, synchronize, reopened, ready_for_review]
39
+
40
+ permissions:
41
+ contents: read
42
+ pull-requests: write
43
+
44
+ jobs:
45
+ review:
46
+ name: PRism
47
+ runs-on: ubuntu-latest
48
+ if: \${{ !github.event.pull_request.draft }}
49
+ steps:
50
+ - name: Checkout
51
+ uses: actions/checkout@v4
52
+
53
+ - name: Run PRism
54
+ uses: ./packages/action
55
+ with:
56
+ config_path: prism.config.json
57
+ env:
58
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
59
+ ${secretName}: \${{ secrets.${secretName} }}
60
+ `;
61
+ }
62
+
63
+ // src/ui.ts
64
+ var RESET = "\x1B[0m";
65
+ var BOLD = "\x1B[1m";
66
+ var DIM = "\x1B[2m";
67
+ var CYAN = "\x1B[36m";
68
+ var GREEN = "\x1B[32m";
69
+ var YELLOW = "\x1B[33m";
70
+ var RED = "\x1B[31m";
71
+ function banner() {
72
+ const lines = [
73
+ "",
74
+ `${CYAN}${BOLD} \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557${RESET}`,
75
+ `${CYAN}${BOLD} \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551${RESET}`,
76
+ `${CYAN}${BOLD} \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551${RESET}`,
77
+ `${CYAN}${BOLD} \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551${RESET}`,
78
+ `${CYAN}${BOLD} \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551${RESET}`,
79
+ `${CYAN}${BOLD} \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D${RESET}`,
80
+ "",
81
+ `${DIM} See through your pull requests.${RESET}`,
82
+ ""
83
+ ];
84
+ console.log(lines.join("\n"));
85
+ }
86
+ function heading(text) {
87
+ console.log(`
88
+ ${CYAN}${BOLD}> ${text}${RESET}
89
+ `);
90
+ }
91
+ function success(text) {
92
+ console.log(` ${GREEN}+${RESET} ${text}`);
93
+ }
94
+ function warn(text) {
95
+ console.log(` ${YELLOW}!${RESET} ${text}`);
96
+ }
97
+ function error(text) {
98
+ console.log(` ${RED}x${RESET} ${text}`);
99
+ }
100
+ function info(text) {
101
+ console.log(` ${DIM}${text}${RESET}`);
102
+ }
103
+ function divider() {
104
+ console.log(`
105
+ ${DIM} ${"\u2500".repeat(48)}${RESET}
106
+ `);
107
+ }
108
+ function nextSteps(steps) {
109
+ console.log(`
110
+ ${CYAN}${BOLD} Next steps:${RESET}
111
+ `);
112
+ steps.forEach((step, i) => {
113
+ console.log(` ${BOLD}${i + 1}.${RESET} ${step}`);
114
+ });
115
+ console.log("");
116
+ }
117
+ function box(title, lines) {
118
+ const maxLen = Math.max(title.length, ...lines.map((l) => l.length));
119
+ const width = maxLen + 4;
120
+ const top = ` \u256D${"\u2500".repeat(width)}\u256E`;
121
+ const bottom = ` \u2570${"\u2500".repeat(width)}\u256F`;
122
+ const titleLine = ` \u2502 ${BOLD}${title}${RESET}${" ".repeat(width - title.length - 2)}\u2502`;
123
+ const separator = ` \u251C${"\u2500".repeat(width)}\u2524`;
124
+ console.log(top);
125
+ console.log(titleLine);
126
+ console.log(separator);
127
+ lines.forEach((line) => {
128
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
129
+ const padding = width - stripped.length - 2;
130
+ console.log(` \u2502 ${line}${" ".repeat(Math.max(0, padding))}\u2502`);
131
+ });
132
+ console.log(bottom);
133
+ console.log("");
134
+ }
135
+
136
+ // src/commands/init-prompts.ts
137
+ import { select, input, search, confirm } from "@inquirer/prompts";
138
+ async function askProvider() {
139
+ return select({
140
+ message: "Which LLM provider do you want to use?",
141
+ choices: [
142
+ { name: "OpenAI", value: "openai", description: "GPT-4o, o1, o3-mini via OpenAI API" },
143
+ { name: "Anthropic", value: "anthropic", description: "Claude 3.5/4 via Anthropic API" },
144
+ { name: "OpenAI-compatible", value: "openai-compat", description: "LM Studio, vLLM, or any /v1/chat/completions endpoint" },
145
+ { name: "Ollama", value: "ollama", description: "Local models via Ollama" }
146
+ ]
147
+ });
148
+ }
149
+ async function askApiKeyEnv(provider) {
150
+ const defaults = {
151
+ openai: "OPENAI_API_KEY",
152
+ anthropic: "ANTHROPIC_API_KEY",
153
+ "openai-compat": "LLM_API_KEY",
154
+ ollama: "OLLAMA_HOST"
155
+ };
156
+ return input({
157
+ message: "Environment variable name for your API key:",
158
+ default: defaults[provider]
159
+ });
160
+ }
161
+ async function askBaseUrl(provider) {
162
+ const defaults = {
163
+ openai: "https://api.openai.com",
164
+ "openai-compat": "http://localhost:1234",
165
+ ollama: "http://localhost:11434"
166
+ };
167
+ if (provider === "anthropic") return void 0;
168
+ const defaultUrl = defaults[provider];
169
+ const needsUrl = provider === "openai-compat" || provider === "ollama";
170
+ if (!needsUrl) {
171
+ const customize = await confirm({
172
+ message: "Do you want to use a custom base URL?",
173
+ default: false
174
+ });
175
+ if (!customize) return defaultUrl;
176
+ }
177
+ return input({
178
+ message: "Base URL for the API:",
179
+ default: defaultUrl
180
+ });
181
+ }
182
+ async function askModelFromList(models) {
183
+ if (models.length === 0) {
184
+ return askModelManual();
185
+ }
186
+ const useList = await confirm({
187
+ message: `Found ${models.length} available models. Select from list?`,
188
+ default: true
189
+ });
190
+ if (!useList) return askModelManual();
191
+ const choices = models.map((m) => ({
192
+ name: m.name !== m.id ? `${m.name} (${m.id})` : m.id,
193
+ value: m.id
194
+ }));
195
+ return search({
196
+ message: "Search and select a model:",
197
+ source: (term) => {
198
+ if (!term) return choices;
199
+ const lower = term.toLowerCase();
200
+ return choices.filter(
201
+ (c) => c.name.toLowerCase().includes(lower) || c.value.toLowerCase().includes(lower)
202
+ );
203
+ }
204
+ });
205
+ }
206
+ async function askModelManual() {
207
+ return input({
208
+ message: "Enter model name:",
209
+ validate: (v) => v.trim().length > 0 ? true : "Model name is required"
210
+ });
211
+ }
212
+ async function askProfile() {
213
+ return select({
214
+ message: "Review profile:",
215
+ choices: [
216
+ { name: "Balanced", value: "balanced", description: "Bugs, security, performance, and quality" },
217
+ { name: "Security-focused", value: "security", description: "Prioritize security vulnerabilities" },
218
+ { name: "Performance-focused", value: "performance", description: "Prioritize performance issues" },
219
+ { name: "Strict", value: "strict", description: "Maximum scrutiny on everything" }
220
+ ]
221
+ });
222
+ }
223
+ async function askCommentMode() {
224
+ return select({
225
+ message: "Comment mode:",
226
+ choices: [
227
+ { name: "Summary only", value: "summary-only", description: "One top-level PR comment" },
228
+ { name: "Inline + Summary", value: "inline+summary", description: "Inline comments plus summary" }
229
+ ]
230
+ });
231
+ }
232
+ async function askMaxFiles() {
233
+ const result = await input({
234
+ message: "Max files to review per PR:",
235
+ default: "30",
236
+ validate: (v) => {
237
+ const n = parseInt(v, 10);
238
+ return n > 0 ? true : "Must be a positive number";
239
+ }
240
+ });
241
+ return parseInt(result, 10);
242
+ }
243
+ async function askMaxDiffBytes() {
244
+ const result = await input({
245
+ message: "Max total diff size (bytes):",
246
+ default: "100000",
247
+ validate: (v) => {
248
+ const n = parseInt(v, 10);
249
+ return n > 0 ? true : "Must be a positive number";
250
+ }
251
+ });
252
+ return parseInt(result, 10);
253
+ }
254
+
255
+ // src/commands/init.ts
256
+ async function runInit() {
257
+ banner();
258
+ heading("Initialize PRism");
259
+ const provider = await askProvider();
260
+ const apiKeyEnv = await askApiKeyEnv(provider);
261
+ const baseUrl = await askBaseUrl(provider);
262
+ heading("Detecting models");
263
+ const models = await fetchModelsForProvider(provider, apiKeyEnv, baseUrl);
264
+ let model;
265
+ if (models.length > 0) {
266
+ success(`Found ${models.length} available models`);
267
+ model = await askModelFromList(models);
268
+ } else {
269
+ warn("Could not fetch model list \u2014 falling back to manual entry");
270
+ if (provider === "ollama") {
271
+ info(`Recommended: ${OLLAMA_RECOMMENDED_MODELS.slice(0, 3).join(", ")}`);
272
+ }
273
+ model = await askModelManual();
274
+ }
275
+ heading("Review preferences");
276
+ const profile = await askProfile();
277
+ const commentMode = await askCommentMode();
278
+ const maxFiles = await askMaxFiles();
279
+ const maxDiffBytes = await askMaxDiffBytes();
280
+ divider();
281
+ heading("Generating configuration");
282
+ const configContent = generateConfig({
283
+ provider,
284
+ model,
285
+ apiKeyEnv,
286
+ baseUrl,
287
+ profile,
288
+ commentMode,
289
+ maxFiles,
290
+ maxDiffBytes
291
+ });
292
+ const workflowContent = generateWorkflow({ provider, apiKeyEnv });
293
+ const configPath = resolve(process.cwd(), "prism.config.json");
294
+ await writeFile(configPath, configContent, "utf-8");
295
+ success("Created prism.config.json");
296
+ const workflowPath = resolve(process.cwd(), ".github/workflows/prism.yml");
297
+ await mkdir(dirname(workflowPath), { recursive: true });
298
+ await writeFile(workflowPath, workflowContent, "utf-8");
299
+ success("Created .github/workflows/prism.yml");
300
+ divider();
301
+ box("PRism Configuration", [
302
+ `Provider: ${provider}`,
303
+ `Model: ${model}`,
304
+ `Profile: ${profile}`,
305
+ `Comment mode: ${commentMode}`,
306
+ `Max files: ${maxFiles}`,
307
+ `Max diff: ${(maxDiffBytes / 1e3).toFixed(0)}KB`
308
+ ]);
309
+ nextSteps([
310
+ `Add your API key as a repository secret named ${apiKeyEnv}`,
311
+ "Commit prism.config.json and .github/workflows/prism.yml",
312
+ "Open a pull request",
313
+ "Watch PRism review your code"
314
+ ]);
315
+ }
316
+ async function fetchModelsForProvider(provider, apiKeyEnv, baseUrl) {
317
+ const apiKey = process.env[apiKeyEnv] ?? "";
318
+ try {
319
+ switch (provider) {
320
+ case "openai": {
321
+ const p = createOpenAIProvider({
322
+ apiKey,
323
+ baseUrl: baseUrl ?? "https://api.openai.com",
324
+ model: ""
325
+ });
326
+ return await p.listModels();
327
+ }
328
+ case "anthropic": {
329
+ const p = createAnthropicProvider({ apiKey, model: "" });
330
+ return await p.listModels();
331
+ }
332
+ case "openai-compat": {
333
+ const p = createOpenAICompatProvider({
334
+ apiKey,
335
+ baseUrl: baseUrl ?? "http://localhost:1234",
336
+ model: ""
337
+ });
338
+ return await p.listModels();
339
+ }
340
+ case "ollama": {
341
+ const p = createOllamaProvider({
342
+ host: baseUrl ?? "http://localhost:11434",
343
+ model: ""
344
+ });
345
+ return await p.listModels();
346
+ }
347
+ }
348
+ } catch {
349
+ return [];
350
+ }
351
+ }
352
+
353
+ // src/index.ts
354
+ var HELP_TEXT = `
355
+ Usage: prism <command>
356
+
357
+ Commands:
358
+ init Set up PRism in your repository
359
+
360
+ Options:
361
+ --help Show this help message
362
+
363
+ Examples:
364
+ npx prism init
365
+ bunx prism init
366
+ `;
367
+ async function main() {
368
+ const args = process.argv.slice(2);
369
+ const command = args[0];
370
+ if (!command || command === "--help" || command === "-h") {
371
+ banner();
372
+ console.log(HELP_TEXT);
373
+ return;
374
+ }
375
+ if (command === "init") {
376
+ await runInit();
377
+ return;
378
+ }
379
+ error(`Unknown command: ${command}`);
380
+ console.log(HELP_TEXT);
381
+ process.exitCode = 1;
382
+ }
383
+ main().catch((err) => {
384
+ const message = err instanceof Error ? err.message : String(err);
385
+ error(message);
386
+ process.exitCode = 1;
387
+ });
388
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/init.ts","../src/generators/config.ts","../src/generators/workflow.ts","../src/ui.ts","../src/commands/init-prompts.ts","../src/index.ts"],"sourcesContent":["import { writeFile, mkdir } from \"node:fs/promises\";\r\nimport { resolve, dirname } from \"node:path\";\r\nimport {\r\n createOpenAIProvider,\r\n createAnthropicProvider,\r\n createOpenAICompatProvider,\r\n createOllamaProvider,\r\n OLLAMA_RECOMMENDED_MODELS,\r\n} from \"prism-core\";\r\nimport type { ModelInfo, ProviderType } from \"prism-core\";\r\nimport { generateConfig } from \"../generators/config.js\";\r\nimport { generateWorkflow } from \"../generators/workflow.js\";\r\nimport * as ui from \"../ui.js\";\r\nimport {\r\n askProvider,\r\n askApiKeyEnv,\r\n askBaseUrl,\r\n askModelFromList,\r\n askModelManual,\r\n askProfile,\r\n askCommentMode,\r\n askMaxFiles,\r\n askMaxDiffBytes,\r\n} from \"./init-prompts.js\";\r\n\r\nexport async function runInit(): Promise<void> {\r\n ui.banner();\r\n ui.heading(\"Initialize PRism\");\r\n\r\n const provider = await askProvider();\r\n const apiKeyEnv = await askApiKeyEnv(provider);\r\n const baseUrl = await askBaseUrl(provider);\r\n\r\n ui.heading(\"Detecting models\");\r\n\r\n const models = await fetchModelsForProvider(provider, apiKeyEnv, baseUrl);\r\n let model: string;\r\n\r\n if (models.length > 0) {\r\n ui.success(`Found ${models.length} available models`);\r\n model = await askModelFromList(models);\r\n } else {\r\n ui.warn(\"Could not fetch model list — falling back to manual entry\");\r\n if (provider === \"ollama\") {\r\n ui.info(`Recommended: ${OLLAMA_RECOMMENDED_MODELS.slice(0, 3).join(\", \")}`);\r\n }\r\n model = await askModelManual();\r\n }\r\n\r\n ui.heading(\"Review preferences\");\r\n\r\n const profile = await askProfile();\r\n const commentMode = await askCommentMode();\r\n const maxFiles = await askMaxFiles();\r\n const maxDiffBytes = await askMaxDiffBytes();\r\n\r\n ui.divider();\r\n ui.heading(\"Generating configuration\");\r\n\r\n const configContent = generateConfig({\r\n provider,\r\n model,\r\n apiKeyEnv,\r\n baseUrl,\r\n profile,\r\n commentMode,\r\n maxFiles,\r\n maxDiffBytes,\r\n });\r\n\r\n const workflowContent = generateWorkflow({ provider, apiKeyEnv });\r\n\r\n const configPath = resolve(process.cwd(), \"prism.config.json\");\r\n await writeFile(configPath, configContent, \"utf-8\");\r\n ui.success(\"Created prism.config.json\");\r\n\r\n const workflowPath = resolve(process.cwd(), \".github/workflows/prism.yml\");\r\n await mkdir(dirname(workflowPath), { recursive: true });\r\n await writeFile(workflowPath, workflowContent, \"utf-8\");\r\n ui.success(\"Created .github/workflows/prism.yml\");\r\n\r\n ui.divider();\r\n\r\n ui.box(\"PRism Configuration\", [\r\n `Provider: ${provider}`,\r\n `Model: ${model}`,\r\n `Profile: ${profile}`,\r\n `Comment mode: ${commentMode}`,\r\n `Max files: ${maxFiles}`,\r\n `Max diff: ${(maxDiffBytes / 1000).toFixed(0)}KB`,\r\n ]);\r\n\r\n ui.nextSteps([\r\n `Add your API key as a repository secret named ${apiKeyEnv}`,\r\n \"Commit prism.config.json and .github/workflows/prism.yml\",\r\n \"Open a pull request\",\r\n \"Watch PRism review your code\",\r\n ]);\r\n}\r\n\r\nasync function fetchModelsForProvider(\r\n provider: ProviderType,\r\n apiKeyEnv: string,\r\n baseUrl?: string,\r\n): Promise<ModelInfo[]> {\r\n const apiKey = process.env[apiKeyEnv] ?? \"\";\r\n\r\n try {\r\n switch (provider) {\r\n case \"openai\": {\r\n const p = createOpenAIProvider({\r\n apiKey,\r\n baseUrl: baseUrl ?? \"https://api.openai.com\",\r\n model: \"\",\r\n });\r\n return await p.listModels();\r\n }\r\n case \"anthropic\": {\r\n const p = createAnthropicProvider({ apiKey, model: \"\" });\r\n return await p.listModels();\r\n }\r\n case \"openai-compat\": {\r\n const p = createOpenAICompatProvider({\r\n apiKey,\r\n baseUrl: baseUrl ?? \"http://localhost:1234\",\r\n model: \"\",\r\n });\r\n return await p.listModels();\r\n }\r\n case \"ollama\": {\r\n const p = createOllamaProvider({\r\n host: baseUrl ?? \"http://localhost:11434\",\r\n model: \"\",\r\n });\r\n return await p.listModels();\r\n }\r\n }\r\n } catch {\r\n return [];\r\n }\r\n}\r\n","import type { PrismConfig, ReviewProfile, CommentMode } from \"prism-core\";\r\n\r\nexport function generateConfig(options: {\r\n provider: PrismConfig[\"provider\"];\r\n model: string;\r\n apiKeyEnv: string;\r\n baseUrl?: string;\r\n profile: ReviewProfile;\r\n commentMode: CommentMode;\r\n maxFiles: number;\r\n maxDiffBytes: number;\r\n}): string {\r\n const config: Record<string, unknown> = {\r\n provider: options.provider,\r\n model: options.model,\r\n apiKeyEnv: options.apiKeyEnv,\r\n profile: options.profile,\r\n commentMode: options.commentMode,\r\n maxFiles: options.maxFiles,\r\n maxDiffBytes: options.maxDiffBytes,\r\n };\r\n\r\n if (options.baseUrl) {\r\n config.baseUrl = options.baseUrl;\r\n }\r\n\r\n return JSON.stringify(config, null, 2) + \"\\n\";\r\n}\r\n","import type { PrismConfig } from \"prism-core\";\r\n\r\nexport function generateWorkflow(options: {\r\n provider: PrismConfig[\"provider\"];\r\n apiKeyEnv: string;\r\n}): string {\r\n const secretName = options.apiKeyEnv.replace(/_/g, \"_\");\r\n\r\n return `name: PRism Review\r\n\r\non:\r\n pull_request:\r\n types: [opened, synchronize, reopened, ready_for_review]\r\n\r\npermissions:\r\n contents: read\r\n pull-requests: write\r\n\r\njobs:\r\n review:\r\n name: PRism\r\n runs-on: ubuntu-latest\r\n if: \\${{ !github.event.pull_request.draft }}\r\n steps:\r\n - name: Checkout\r\n uses: actions/checkout@v4\r\n\r\n - name: Run PRism\r\n uses: ./packages/action\r\n with:\r\n config_path: prism.config.json\r\n env:\r\n GITHUB_TOKEN: \\${{ secrets.GITHUB_TOKEN }}\r\n ${secretName}: \\${{ secrets.${secretName} }}\r\n`;\r\n}\r\n","const RESET = \"\\x1b[0m\";\r\nconst BOLD = \"\\x1b[1m\";\r\nconst DIM = \"\\x1b[2m\";\r\nconst CYAN = \"\\x1b[36m\";\r\nconst GREEN = \"\\x1b[32m\";\r\nconst YELLOW = \"\\x1b[33m\";\r\nconst RED = \"\\x1b[31m\";\r\nconst MAGENTA = \"\\x1b[35m\";\r\nconst WHITE = \"\\x1b[37m\";\r\n\r\nexport function banner(): void {\r\n const lines = [\r\n \"\",\r\n `${CYAN}${BOLD} ██████╗ ██████╗ ██╗███████╗███╗ ███╗${RESET}`,\r\n `${CYAN}${BOLD} ██╔══██╗██╔══██╗██║██╔════╝████╗ ████║${RESET}`,\r\n `${CYAN}${BOLD} ██████╔╝██████╔╝██║███████╗██╔████╔██║${RESET}`,\r\n `${CYAN}${BOLD} ██╔═══╝ ██╔══██╗██║╚════██║██║╚██╔╝██║${RESET}`,\r\n `${CYAN}${BOLD} ██║ ██║ ██║██║███████║██║ ╚═╝ ██║${RESET}`,\r\n `${CYAN}${BOLD} ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝ ╚═╝${RESET}`,\r\n \"\",\r\n `${DIM} See through your pull requests.${RESET}`,\r\n \"\",\r\n ];\r\n console.log(lines.join(\"\\n\"));\r\n}\r\n\r\nexport function heading(text: string): void {\r\n console.log(`\\n${CYAN}${BOLD}> ${text}${RESET}\\n`);\r\n}\r\n\r\nexport function success(text: string): void {\r\n console.log(` ${GREEN}+${RESET} ${text}`);\r\n}\r\n\r\nexport function warn(text: string): void {\r\n console.log(` ${YELLOW}!${RESET} ${text}`);\r\n}\r\n\r\nexport function error(text: string): void {\r\n console.log(` ${RED}x${RESET} ${text}`);\r\n}\r\n\r\nexport function info(text: string): void {\r\n console.log(` ${DIM}${text}${RESET}`);\r\n}\r\n\r\nexport function highlight(label: string, value: string): void {\r\n console.log(` ${WHITE}${label}:${RESET} ${MAGENTA}${value}${RESET}`);\r\n}\r\n\r\nexport function divider(): void {\r\n console.log(`\\n${DIM} ${\"─\".repeat(48)}${RESET}\\n`);\r\n}\r\n\r\nexport function nextSteps(steps: string[]): void {\r\n console.log(`\\n${CYAN}${BOLD} Next steps:${RESET}\\n`);\r\n steps.forEach((step, i) => {\r\n console.log(` ${BOLD}${i + 1}.${RESET} ${step}`);\r\n });\r\n console.log(\"\");\r\n}\r\n\r\nexport function box(title: string, lines: string[]): void {\r\n const maxLen = Math.max(title.length, ...lines.map((l) => l.length));\r\n const width = maxLen + 4;\r\n const top = ` ╭${\"─\".repeat(width)}╮`;\r\n const bottom = ` ╰${\"─\".repeat(width)}╯`;\r\n const titleLine = ` │ ${BOLD}${title}${RESET}${\" \".repeat(width - title.length - 2)}│`;\r\n const separator = ` ├${\"─\".repeat(width)}┤`;\r\n\r\n console.log(top);\r\n console.log(titleLine);\r\n console.log(separator);\r\n lines.forEach((line) => {\r\n const stripped = line.replace(/\\x1b\\[[0-9;]*m/g, \"\");\r\n const padding = width - stripped.length - 2;\r\n console.log(` │ ${line}${\" \".repeat(Math.max(0, padding))}│`);\r\n });\r\n console.log(bottom);\r\n console.log(\"\");\r\n}\r\n","import { select, input, search, confirm } from \"@inquirer/prompts\";\r\nimport type { ProviderType, ReviewProfile, CommentMode, ModelInfo } from \"prism-core\";\r\n\r\nexport async function askProvider(): Promise<ProviderType> {\r\n return select({\r\n message: \"Which LLM provider do you want to use?\",\r\n choices: [\r\n { name: \"OpenAI\", value: \"openai\" as const, description: \"GPT-4o, o1, o3-mini via OpenAI API\" },\r\n { name: \"Anthropic\", value: \"anthropic\" as const, description: \"Claude 3.5/4 via Anthropic API\" },\r\n { name: \"OpenAI-compatible\", value: \"openai-compat\" as const, description: \"LM Studio, vLLM, or any /v1/chat/completions endpoint\" },\r\n { name: \"Ollama\", value: \"ollama\" as const, description: \"Local models via Ollama\" },\r\n ],\r\n });\r\n}\r\n\r\nexport async function askApiKeyEnv(provider: ProviderType): Promise<string> {\r\n const defaults: Record<ProviderType, string> = {\r\n openai: \"OPENAI_API_KEY\",\r\n anthropic: \"ANTHROPIC_API_KEY\",\r\n \"openai-compat\": \"LLM_API_KEY\",\r\n ollama: \"OLLAMA_HOST\",\r\n };\r\n\r\n return input({\r\n message: \"Environment variable name for your API key:\",\r\n default: defaults[provider],\r\n });\r\n}\r\n\r\nexport async function askBaseUrl(provider: ProviderType): Promise<string | undefined> {\r\n const defaults: Record<string, string> = {\r\n openai: \"https://api.openai.com\",\r\n \"openai-compat\": \"http://localhost:1234\",\r\n ollama: \"http://localhost:11434\",\r\n };\r\n\r\n if (provider === \"anthropic\") return undefined;\r\n\r\n const defaultUrl = defaults[provider];\r\n const needsUrl = provider === \"openai-compat\" || provider === \"ollama\";\r\n\r\n if (!needsUrl) {\r\n const customize = await confirm({\r\n message: \"Do you want to use a custom base URL?\",\r\n default: false,\r\n });\r\n if (!customize) return defaultUrl;\r\n }\r\n\r\n return input({\r\n message: \"Base URL for the API:\",\r\n default: defaultUrl,\r\n });\r\n}\r\n\r\nexport async function askModelFromList(models: ModelInfo[]): Promise<string> {\r\n if (models.length === 0) {\r\n return askModelManual();\r\n }\r\n\r\n const useList = await confirm({\r\n message: `Found ${models.length} available models. Select from list?`,\r\n default: true,\r\n });\r\n\r\n if (!useList) return askModelManual();\r\n\r\n const choices = models.map((m) => ({\r\n name: m.name !== m.id ? `${m.name} (${m.id})` : m.id,\r\n value: m.id,\r\n }));\r\n\r\n return search({\r\n message: \"Search and select a model:\",\r\n source: (term) => {\r\n if (!term) return choices;\r\n const lower = term.toLowerCase();\r\n return choices.filter(\r\n (c) => c.name.toLowerCase().includes(lower) || c.value.toLowerCase().includes(lower),\r\n );\r\n },\r\n });\r\n}\r\n\r\nexport async function askModelManual(): Promise<string> {\r\n return input({\r\n message: \"Enter model name:\",\r\n validate: (v) => (v.trim().length > 0 ? true : \"Model name is required\"),\r\n });\r\n}\r\n\r\nexport async function askProfile(): Promise<ReviewProfile> {\r\n return select({\r\n message: \"Review profile:\",\r\n choices: [\r\n { name: \"Balanced\", value: \"balanced\" as const, description: \"Bugs, security, performance, and quality\" },\r\n { name: \"Security-focused\", value: \"security\" as const, description: \"Prioritize security vulnerabilities\" },\r\n { name: \"Performance-focused\", value: \"performance\" as const, description: \"Prioritize performance issues\" },\r\n { name: \"Strict\", value: \"strict\" as const, description: \"Maximum scrutiny on everything\" },\r\n ],\r\n });\r\n}\r\n\r\nexport async function askCommentMode(): Promise<CommentMode> {\r\n return select({\r\n message: \"Comment mode:\",\r\n choices: [\r\n { name: \"Summary only\", value: \"summary-only\" as const, description: \"One top-level PR comment\" },\r\n { name: \"Inline + Summary\", value: \"inline+summary\" as const, description: \"Inline comments plus summary\" },\r\n ],\r\n });\r\n}\r\n\r\nexport async function askMaxFiles(): Promise<number> {\r\n const result = await input({\r\n message: \"Max files to review per PR:\",\r\n default: \"30\",\r\n validate: (v) => {\r\n const n = parseInt(v, 10);\r\n return n > 0 ? true : \"Must be a positive number\";\r\n },\r\n });\r\n return parseInt(result, 10);\r\n}\r\n\r\nexport async function askMaxDiffBytes(): Promise<number> {\r\n const result = await input({\r\n message: \"Max total diff size (bytes):\",\r\n default: \"100000\",\r\n validate: (v) => {\r\n const n = parseInt(v, 10);\r\n return n > 0 ? true : \"Must be a positive number\";\r\n },\r\n });\r\n return parseInt(result, 10);\r\n}\r\n","import { runInit } from \"./commands/init.js\";\r\nimport * as ui from \"./ui.js\";\r\n\r\nconst HELP_TEXT = `\r\n Usage: prism <command>\r\n\r\n Commands:\r\n init Set up PRism in your repository\r\n\r\n Options:\r\n --help Show this help message\r\n\r\n Examples:\r\n npx prism init\r\n bunx prism init\r\n`;\r\n\r\nasync function main(): Promise<void> {\r\n const args = process.argv.slice(2);\r\n const command = args[0];\r\n\r\n if (!command || command === \"--help\" || command === \"-h\") {\r\n ui.banner();\r\n console.log(HELP_TEXT);\r\n return;\r\n }\r\n\r\n if (command === \"init\") {\r\n await runInit();\r\n return;\r\n }\r\n\r\n ui.error(`Unknown command: ${command}`);\r\n console.log(HELP_TEXT);\r\n process.exitCode = 1;\r\n}\r\n\r\nmain().catch((err: unknown) => {\r\n const message = err instanceof Error ? err.message : String(err);\r\n ui.error(message);\r\n process.exitCode = 1;\r\n});\r\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "prism-review",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "prism": "dist/index.js"
7
+ },
8
+ "main": "./dist/index.js",
9
+ "scripts": {
10
+ "build": "tsup",
11
+ "typecheck": "tsc --noEmit",
12
+ "clean": "rm -rf dist",
13
+ "lint": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "prism-core": "^0.1.0",
17
+ "@inquirer/prompts": "^7.2.1"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "^5.7.3",
21
+ "tsup": "^8.3.6",
22
+ "@types/node": "^20.17.12"
23
+ }
24
+ }
@@ -0,0 +1,136 @@
1
+ import { select, input, search, confirm } from "@inquirer/prompts";
2
+ import type { ProviderType, ReviewProfile, CommentMode, ModelInfo } from "prism-core";
3
+
4
+ export async function askProvider(): Promise<ProviderType> {
5
+ return select({
6
+ message: "Which LLM provider do you want to use?",
7
+ choices: [
8
+ { name: "OpenAI", value: "openai" as const, description: "GPT-4o, o1, o3-mini via OpenAI API" },
9
+ { name: "Anthropic", value: "anthropic" as const, description: "Claude 3.5/4 via Anthropic API" },
10
+ { name: "OpenAI-compatible", value: "openai-compat" as const, description: "LM Studio, vLLM, or any /v1/chat/completions endpoint" },
11
+ { name: "Ollama", value: "ollama" as const, description: "Local models via Ollama" },
12
+ ],
13
+ });
14
+ }
15
+
16
+ export async function askApiKeyEnv(provider: ProviderType): Promise<string> {
17
+ const defaults: Record<ProviderType, string> = {
18
+ openai: "OPENAI_API_KEY",
19
+ anthropic: "ANTHROPIC_API_KEY",
20
+ "openai-compat": "LLM_API_KEY",
21
+ ollama: "OLLAMA_HOST",
22
+ };
23
+
24
+ return input({
25
+ message: "Environment variable name for your API key:",
26
+ default: defaults[provider],
27
+ });
28
+ }
29
+
30
+ export async function askBaseUrl(provider: ProviderType): Promise<string | undefined> {
31
+ const defaults: Record<string, string> = {
32
+ openai: "https://api.openai.com",
33
+ "openai-compat": "http://localhost:1234",
34
+ ollama: "http://localhost:11434",
35
+ };
36
+
37
+ if (provider === "anthropic") return undefined;
38
+
39
+ const defaultUrl = defaults[provider];
40
+ const needsUrl = provider === "openai-compat" || provider === "ollama";
41
+
42
+ if (!needsUrl) {
43
+ const customize = await confirm({
44
+ message: "Do you want to use a custom base URL?",
45
+ default: false,
46
+ });
47
+ if (!customize) return defaultUrl;
48
+ }
49
+
50
+ return input({
51
+ message: "Base URL for the API:",
52
+ default: defaultUrl,
53
+ });
54
+ }
55
+
56
+ export async function askModelFromList(models: ModelInfo[]): Promise<string> {
57
+ if (models.length === 0) {
58
+ return askModelManual();
59
+ }
60
+
61
+ const useList = await confirm({
62
+ message: `Found ${models.length} available models. Select from list?`,
63
+ default: true,
64
+ });
65
+
66
+ if (!useList) return askModelManual();
67
+
68
+ const choices = models.map((m) => ({
69
+ name: m.name !== m.id ? `${m.name} (${m.id})` : m.id,
70
+ value: m.id,
71
+ }));
72
+
73
+ return search({
74
+ message: "Search and select a model:",
75
+ source: (term) => {
76
+ if (!term) return choices;
77
+ const lower = term.toLowerCase();
78
+ return choices.filter(
79
+ (c) => c.name.toLowerCase().includes(lower) || c.value.toLowerCase().includes(lower),
80
+ );
81
+ },
82
+ });
83
+ }
84
+
85
+ export async function askModelManual(): Promise<string> {
86
+ return input({
87
+ message: "Enter model name:",
88
+ validate: (v) => (v.trim().length > 0 ? true : "Model name is required"),
89
+ });
90
+ }
91
+
92
+ export async function askProfile(): Promise<ReviewProfile> {
93
+ return select({
94
+ message: "Review profile:",
95
+ choices: [
96
+ { name: "Balanced", value: "balanced" as const, description: "Bugs, security, performance, and quality" },
97
+ { name: "Security-focused", value: "security" as const, description: "Prioritize security vulnerabilities" },
98
+ { name: "Performance-focused", value: "performance" as const, description: "Prioritize performance issues" },
99
+ { name: "Strict", value: "strict" as const, description: "Maximum scrutiny on everything" },
100
+ ],
101
+ });
102
+ }
103
+
104
+ export async function askCommentMode(): Promise<CommentMode> {
105
+ return select({
106
+ message: "Comment mode:",
107
+ choices: [
108
+ { name: "Summary only", value: "summary-only" as const, description: "One top-level PR comment" },
109
+ { name: "Inline + Summary", value: "inline+summary" as const, description: "Inline comments plus summary" },
110
+ ],
111
+ });
112
+ }
113
+
114
+ export async function askMaxFiles(): Promise<number> {
115
+ const result = await input({
116
+ message: "Max files to review per PR:",
117
+ default: "30",
118
+ validate: (v) => {
119
+ const n = parseInt(v, 10);
120
+ return n > 0 ? true : "Must be a positive number";
121
+ },
122
+ });
123
+ return parseInt(result, 10);
124
+ }
125
+
126
+ export async function askMaxDiffBytes(): Promise<number> {
127
+ const result = await input({
128
+ message: "Max total diff size (bytes):",
129
+ default: "100000",
130
+ validate: (v) => {
131
+ const n = parseInt(v, 10);
132
+ return n > 0 ? true : "Must be a positive number";
133
+ },
134
+ });
135
+ return parseInt(result, 10);
136
+ }
@@ -0,0 +1,141 @@
1
+ import { writeFile, mkdir } from "node:fs/promises";
2
+ import { resolve, dirname } from "node:path";
3
+ import {
4
+ createOpenAIProvider,
5
+ createAnthropicProvider,
6
+ createOpenAICompatProvider,
7
+ createOllamaProvider,
8
+ OLLAMA_RECOMMENDED_MODELS,
9
+ } from "prism-core";
10
+ import type { ModelInfo, ProviderType } from "prism-core";
11
+ import { generateConfig } from "../generators/config.js";
12
+ import { generateWorkflow } from "../generators/workflow.js";
13
+ import * as ui from "../ui.js";
14
+ import {
15
+ askProvider,
16
+ askApiKeyEnv,
17
+ askBaseUrl,
18
+ askModelFromList,
19
+ askModelManual,
20
+ askProfile,
21
+ askCommentMode,
22
+ askMaxFiles,
23
+ askMaxDiffBytes,
24
+ } from "./init-prompts.js";
25
+
26
+ export async function runInit(): Promise<void> {
27
+ ui.banner();
28
+ ui.heading("Initialize PRism");
29
+
30
+ const provider = await askProvider();
31
+ const apiKeyEnv = await askApiKeyEnv(provider);
32
+ const baseUrl = await askBaseUrl(provider);
33
+
34
+ ui.heading("Detecting models");
35
+
36
+ const models = await fetchModelsForProvider(provider, apiKeyEnv, baseUrl);
37
+ let model: string;
38
+
39
+ if (models.length > 0) {
40
+ ui.success(`Found ${models.length} available models`);
41
+ model = await askModelFromList(models);
42
+ } else {
43
+ ui.warn("Could not fetch model list — falling back to manual entry");
44
+ if (provider === "ollama") {
45
+ ui.info(`Recommended: ${OLLAMA_RECOMMENDED_MODELS.slice(0, 3).join(", ")}`);
46
+ }
47
+ model = await askModelManual();
48
+ }
49
+
50
+ ui.heading("Review preferences");
51
+
52
+ const profile = await askProfile();
53
+ const commentMode = await askCommentMode();
54
+ const maxFiles = await askMaxFiles();
55
+ const maxDiffBytes = await askMaxDiffBytes();
56
+
57
+ ui.divider();
58
+ ui.heading("Generating configuration");
59
+
60
+ const configContent = generateConfig({
61
+ provider,
62
+ model,
63
+ apiKeyEnv,
64
+ baseUrl,
65
+ profile,
66
+ commentMode,
67
+ maxFiles,
68
+ maxDiffBytes,
69
+ });
70
+
71
+ const workflowContent = generateWorkflow({ provider, apiKeyEnv });
72
+
73
+ const configPath = resolve(process.cwd(), "prism.config.json");
74
+ await writeFile(configPath, configContent, "utf-8");
75
+ ui.success("Created prism.config.json");
76
+
77
+ const workflowPath = resolve(process.cwd(), ".github/workflows/prism.yml");
78
+ await mkdir(dirname(workflowPath), { recursive: true });
79
+ await writeFile(workflowPath, workflowContent, "utf-8");
80
+ ui.success("Created .github/workflows/prism.yml");
81
+
82
+ ui.divider();
83
+
84
+ ui.box("PRism Configuration", [
85
+ `Provider: ${provider}`,
86
+ `Model: ${model}`,
87
+ `Profile: ${profile}`,
88
+ `Comment mode: ${commentMode}`,
89
+ `Max files: ${maxFiles}`,
90
+ `Max diff: ${(maxDiffBytes / 1000).toFixed(0)}KB`,
91
+ ]);
92
+
93
+ ui.nextSteps([
94
+ `Add your API key as a repository secret named ${apiKeyEnv}`,
95
+ "Commit prism.config.json and .github/workflows/prism.yml",
96
+ "Open a pull request",
97
+ "Watch PRism review your code",
98
+ ]);
99
+ }
100
+
101
+ async function fetchModelsForProvider(
102
+ provider: ProviderType,
103
+ apiKeyEnv: string,
104
+ baseUrl?: string,
105
+ ): Promise<ModelInfo[]> {
106
+ const apiKey = process.env[apiKeyEnv] ?? "";
107
+
108
+ try {
109
+ switch (provider) {
110
+ case "openai": {
111
+ const p = createOpenAIProvider({
112
+ apiKey,
113
+ baseUrl: baseUrl ?? "https://api.openai.com",
114
+ model: "",
115
+ });
116
+ return await p.listModels();
117
+ }
118
+ case "anthropic": {
119
+ const p = createAnthropicProvider({ apiKey, model: "" });
120
+ return await p.listModels();
121
+ }
122
+ case "openai-compat": {
123
+ const p = createOpenAICompatProvider({
124
+ apiKey,
125
+ baseUrl: baseUrl ?? "http://localhost:1234",
126
+ model: "",
127
+ });
128
+ return await p.listModels();
129
+ }
130
+ case "ollama": {
131
+ const p = createOllamaProvider({
132
+ host: baseUrl ?? "http://localhost:11434",
133
+ model: "",
134
+ });
135
+ return await p.listModels();
136
+ }
137
+ }
138
+ } catch {
139
+ return [];
140
+ }
141
+ }
@@ -0,0 +1,28 @@
1
+ import type { PrismConfig, ReviewProfile, CommentMode } from "prism-core";
2
+
3
+ export function generateConfig(options: {
4
+ provider: PrismConfig["provider"];
5
+ model: string;
6
+ apiKeyEnv: string;
7
+ baseUrl?: string;
8
+ profile: ReviewProfile;
9
+ commentMode: CommentMode;
10
+ maxFiles: number;
11
+ maxDiffBytes: number;
12
+ }): string {
13
+ const config: Record<string, unknown> = {
14
+ provider: options.provider,
15
+ model: options.model,
16
+ apiKeyEnv: options.apiKeyEnv,
17
+ profile: options.profile,
18
+ commentMode: options.commentMode,
19
+ maxFiles: options.maxFiles,
20
+ maxDiffBytes: options.maxDiffBytes,
21
+ };
22
+
23
+ if (options.baseUrl) {
24
+ config.baseUrl = options.baseUrl;
25
+ }
26
+
27
+ return JSON.stringify(config, null, 2) + "\n";
28
+ }
@@ -0,0 +1,36 @@
1
+ import type { PrismConfig } from "prism-core";
2
+
3
+ export function generateWorkflow(options: {
4
+ provider: PrismConfig["provider"];
5
+ apiKeyEnv: string;
6
+ }): string {
7
+ const secretName = options.apiKeyEnv.replace(/_/g, "_");
8
+
9
+ return `name: PRism Review
10
+
11
+ on:
12
+ pull_request:
13
+ types: [opened, synchronize, reopened, ready_for_review]
14
+
15
+ permissions:
16
+ contents: read
17
+ pull-requests: write
18
+
19
+ jobs:
20
+ review:
21
+ name: PRism
22
+ runs-on: ubuntu-latest
23
+ if: \${{ !github.event.pull_request.draft }}
24
+ steps:
25
+ - name: Checkout
26
+ uses: actions/checkout@v4
27
+
28
+ - name: Run PRism
29
+ uses: ./packages/action
30
+ with:
31
+ config_path: prism.config.json
32
+ env:
33
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
34
+ ${secretName}: \${{ secrets.${secretName} }}
35
+ `;
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { runInit } from "./commands/init.js";
2
+ import * as ui from "./ui.js";
3
+
4
+ const HELP_TEXT = `
5
+ Usage: prism <command>
6
+
7
+ Commands:
8
+ init Set up PRism in your repository
9
+
10
+ Options:
11
+ --help Show this help message
12
+
13
+ Examples:
14
+ npx prism init
15
+ bunx prism init
16
+ `;
17
+
18
+ async function main(): Promise<void> {
19
+ const args = process.argv.slice(2);
20
+ const command = args[0];
21
+
22
+ if (!command || command === "--help" || command === "-h") {
23
+ ui.banner();
24
+ console.log(HELP_TEXT);
25
+ return;
26
+ }
27
+
28
+ if (command === "init") {
29
+ await runInit();
30
+ return;
31
+ }
32
+
33
+ ui.error(`Unknown command: ${command}`);
34
+ console.log(HELP_TEXT);
35
+ process.exitCode = 1;
36
+ }
37
+
38
+ main().catch((err: unknown) => {
39
+ const message = err instanceof Error ? err.message : String(err);
40
+ ui.error(message);
41
+ process.exitCode = 1;
42
+ });
package/src/ui.ts ADDED
@@ -0,0 +1,81 @@
1
+ const RESET = "\x1b[0m";
2
+ const BOLD = "\x1b[1m";
3
+ const DIM = "\x1b[2m";
4
+ const CYAN = "\x1b[36m";
5
+ const GREEN = "\x1b[32m";
6
+ const YELLOW = "\x1b[33m";
7
+ const RED = "\x1b[31m";
8
+ const MAGENTA = "\x1b[35m";
9
+ const WHITE = "\x1b[37m";
10
+
11
+ export function banner(): void {
12
+ const lines = [
13
+ "",
14
+ `${CYAN}${BOLD} ██████╗ ██████╗ ██╗███████╗███╗ ███╗${RESET}`,
15
+ `${CYAN}${BOLD} ██╔══██╗██╔══██╗██║██╔════╝████╗ ████║${RESET}`,
16
+ `${CYAN}${BOLD} ██████╔╝██████╔╝██║███████╗██╔████╔██║${RESET}`,
17
+ `${CYAN}${BOLD} ██╔═══╝ ██╔══██╗██║╚════██║██║╚██╔╝██║${RESET}`,
18
+ `${CYAN}${BOLD} ██║ ██║ ██║██║███████║██║ ╚═╝ ██║${RESET}`,
19
+ `${CYAN}${BOLD} ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝ ╚═╝${RESET}`,
20
+ "",
21
+ `${DIM} See through your pull requests.${RESET}`,
22
+ "",
23
+ ];
24
+ console.log(lines.join("\n"));
25
+ }
26
+
27
+ export function heading(text: string): void {
28
+ console.log(`\n${CYAN}${BOLD}> ${text}${RESET}\n`);
29
+ }
30
+
31
+ export function success(text: string): void {
32
+ console.log(` ${GREEN}+${RESET} ${text}`);
33
+ }
34
+
35
+ export function warn(text: string): void {
36
+ console.log(` ${YELLOW}!${RESET} ${text}`);
37
+ }
38
+
39
+ export function error(text: string): void {
40
+ console.log(` ${RED}x${RESET} ${text}`);
41
+ }
42
+
43
+ export function info(text: string): void {
44
+ console.log(` ${DIM}${text}${RESET}`);
45
+ }
46
+
47
+ export function highlight(label: string, value: string): void {
48
+ console.log(` ${WHITE}${label}:${RESET} ${MAGENTA}${value}${RESET}`);
49
+ }
50
+
51
+ export function divider(): void {
52
+ console.log(`\n${DIM} ${"─".repeat(48)}${RESET}\n`);
53
+ }
54
+
55
+ export function nextSteps(steps: string[]): void {
56
+ console.log(`\n${CYAN}${BOLD} Next steps:${RESET}\n`);
57
+ steps.forEach((step, i) => {
58
+ console.log(` ${BOLD}${i + 1}.${RESET} ${step}`);
59
+ });
60
+ console.log("");
61
+ }
62
+
63
+ export function box(title: string, lines: string[]): void {
64
+ const maxLen = Math.max(title.length, ...lines.map((l) => l.length));
65
+ const width = maxLen + 4;
66
+ const top = ` ╭${"─".repeat(width)}╮`;
67
+ const bottom = ` ╰${"─".repeat(width)}╯`;
68
+ const titleLine = ` │ ${BOLD}${title}${RESET}${" ".repeat(width - title.length - 2)}│`;
69
+ const separator = ` ├${"─".repeat(width)}┤`;
70
+
71
+ console.log(top);
72
+ console.log(titleLine);
73
+ console.log(separator);
74
+ lines.forEach((line) => {
75
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
76
+ const padding = width - stripped.length - 2;
77
+ console.log(` │ ${line}${" ".repeat(Math.max(0, padding))}│`);
78
+ });
79
+ console.log(bottom);
80
+ console.log("");
81
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": [
8
+ "src"
9
+ ]
10
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm"],
6
+ dts: false,
7
+ splitting: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ target: "node20",
11
+ banner: {
12
+ js: "#!/usr/bin/env node",
13
+ },
14
+ });