-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
376 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
GREPTILE_API_KEY="" | ||
GITHUB_TOKEN="" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* eslint-disable no-console */ | ||
import ChangelogGenerator from './chameleon_utils' | ||
import dotenv from 'dotenv' | ||
import path from 'path' | ||
|
||
// Load environment variables with path relative to project root | ||
dotenv.config({ debug: process.env.DEBUG === 'true', path: path.resolve(__dirname, '../.env') }) | ||
|
||
async function runChangelogDemo() { | ||
// Ensure environment variables are set | ||
const githubToken = process.env.GITHUB_TOKEN | ||
const greptileToken = process.env.GREPTILE_API_KEY | ||
|
||
if (!githubToken || !greptileToken) { | ||
throw new Error('Missing required environment variables: GITHUB_TOKEN and/or GREPTILE_API_KEY') | ||
} | ||
|
||
// Initialize the changelog generator | ||
const generator = new ChangelogGenerator(githubToken, greptileToken) | ||
|
||
// Configure the changelog parameters | ||
const config = { | ||
owner: 'greptileai', | ||
repo: 'greptile', | ||
startDate: new Date('2024-11-18'), // Example start date | ||
endDate: new Date(), // Current date | ||
customInstructions: `Generate a user-friendly changelog with no more than 5 items in the following format: | ||
<Update label="2024-11-26" description="v0.1.0"> | ||
[changelog content here] | ||
</Update>`, | ||
} | ||
|
||
try { | ||
console.log('Generating changelog...') | ||
const changelog = await generator.generateChangelog(config) | ||
console.log('Generated Changelog:') | ||
console.log(changelog) | ||
} catch (error) { | ||
console.error('Error generating changelog:', error) | ||
} | ||
} | ||
|
||
// Run the demo | ||
runChangelogDemo().catch(console.error) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { Octokit } from '@octokit/rest' | ||
import GreptileAPI from './greptile' | ||
|
||
interface ChangelogConfig { | ||
owner: string | ||
repo: string | ||
startDate: Date | ||
endDate: Date | ||
customInstructions: string | ||
} | ||
|
||
class ChangelogGenerator { | ||
private octokit: Octokit | ||
private greptile: GreptileAPI | ||
|
||
constructor(githubToken: string, greptileToken: string) { | ||
this.octokit = new Octokit({ auth: githubToken }) | ||
this.greptile = new GreptileAPI(greptileToken, githubToken) | ||
} | ||
|
||
async generateChangelog({ owner, repo, startDate, endDate, customInstructions }: ChangelogConfig): Promise<string> { | ||
// Get commits between dates | ||
const commits = await this.octokit.repos.compareCommits({ | ||
owner, | ||
repo, | ||
base: await this.getCommitFromDate(owner, repo, startDate), | ||
head: await this.getCommitFromDate(owner, repo, endDate), | ||
}) | ||
|
||
// Index repository in Greptile | ||
await this.greptile.indexRepository({ | ||
remote: 'github', | ||
repository: `${owner}/${repo}`, | ||
branch: 'main', // You might want to make this configurable | ||
}) | ||
// Query Greptile with the diff and custom instructions | ||
const queryResponse = await this.greptile.queryRepository({ | ||
messages: [ | ||
{ | ||
role: 'user', | ||
content: `${customInstructions}\n\n The response should contain only the changelog with nothing before or after it. There should not be a title. Reference the following changes:\n${commits.data.files | ||
?.map((file) => `${file.filename}:\n${file.patch}`) | ||
.join('\n\n')}`, | ||
id: Date.now().toString(), | ||
}, | ||
], | ||
repositories: [ | ||
{ | ||
remote: 'github', | ||
repository: `${owner}/${repo}`, | ||
branch: 'main', | ||
}, | ||
], | ||
sessionId: Date.now().toString(), | ||
}) | ||
|
||
return queryResponse.message | ||
} | ||
|
||
private async getCommitFromDate(owner: string, repo: string, date: Date): Promise<string> { | ||
const commits = await this.octokit.repos.listCommits({ | ||
owner, | ||
repo, | ||
until: date.toISOString(), | ||
per_page: 1, | ||
}) | ||
|
||
if (commits.data.length === 0) { | ||
throw new Error(`No commits found before ${date.toISOString()}`) | ||
} | ||
|
||
return commits.data[0]?.sha || '' | ||
} | ||
} | ||
|
||
export default ChangelogGenerator |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { Argv } from 'yargs' | ||
import { logger } from '../logger' | ||
import { green } from 'picocolors' | ||
import ChangelogGenerator from '../chameleon_utils' | ||
|
||
import { execSync } from 'child_process' | ||
import path from 'path' | ||
import dotenv from 'dotenv' | ||
|
||
dotenv.config({ debug: process.env.DEBUG === 'true', path: path.resolve(__dirname, '../../.env') }) | ||
|
||
interface CLArgv {} | ||
|
||
export const command = 'cl' | ||
export const describe = 'Generate a changelog for a repository' | ||
export const aliases = ['changelog'] | ||
|
||
export function builder(yargs: Argv<CLArgv>): Argv { | ||
return yargs | ||
} | ||
|
||
function getCurrentRepoInfo(): { owner: string; repo: string } | null { | ||
try { | ||
// Get the remote URL | ||
const remoteUrl = execSync('git config --get remote.origin.url', { encoding: 'utf8' }).trim() | ||
|
||
// Extract owner/repo from different Git URL formats | ||
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/) | ||
if (match) { | ||
return { | ||
owner: match[1] || '', | ||
repo: match[2] || '', | ||
} | ||
} | ||
} catch (error) { | ||
return null | ||
} | ||
return null | ||
} | ||
|
||
export async function handler() { | ||
const currentRepo = getCurrentRepoInfo() | ||
|
||
// Get repository information | ||
const repoString = await logger.prompt('Enter repository (owner/repo)', { | ||
type: 'text', | ||
default: currentRepo ? `${currentRepo.owner}/${currentRepo.repo}` : undefined, | ||
}) | ||
|
||
const [owner, repo] = repoString.split('/') | ||
if (!owner || !repo) { | ||
logger.error('Invalid repository format. Please use owner/repo format.') | ||
return | ||
} | ||
|
||
// New date range selection | ||
const dateRange = await logger.prompt('Select date range', { | ||
type: 'select', | ||
options: [ | ||
{ label: 'Last 24 hours', value: '1' }, | ||
{ label: 'Last 7 days', value: '7' }, | ||
{ label: 'Last 14 days', value: '14' }, | ||
{ label: 'Last 30 days', value: '30' }, | ||
], | ||
}) | ||
|
||
const endDate = new Date().toISOString().split('T')[0] | ||
const daysAgo = parseInt(dateRange.toString()) // Will be 1, 7, 14, or 30 | ||
const startDate = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000).toISOString().split('T')[0] | ||
|
||
// Updated changelog type selection with custom option | ||
const changelogType = await logger.prompt('Select changelog type', { | ||
type: 'select', | ||
options: [ | ||
{ label: 'Internal', value: 'internal', hint: 'Technical details for developers' }, | ||
{ label: 'External', value: 'external', hint: 'User-facing changes' }, | ||
{ label: 'Mintlify', value: 'mintlify', hint: 'Changelog format for Mintlify' }, | ||
], | ||
}) | ||
const instructions = await (async () => { | ||
switch (changelogType.toString()) { | ||
case 'internal': | ||
return 'Generate a short technical changelog that contains no more than 5 bullet points in markdown.' | ||
case 'external': | ||
return 'Generate a user-friendly changelog that contains no more than 5 bullet points in markdown.' | ||
case 'mintlify': | ||
return `Generate a user-friendly changelog with no more than 5 items in the following format: | ||
<Update label="${new Date().toISOString().split('T')[0]}" description="[VERSION]"> | ||
[changelog content in markdown here] | ||
</Update>` | ||
default: | ||
return 'Generate a user-friendly changelog that contains no more than 5 bullet points.' | ||
} | ||
})() | ||
|
||
try { | ||
// Load tokens from environment | ||
const githubToken = process.env.GITHUB_TOKEN | ||
const greptileToken = process.env.GREPTILE_API_KEY | ||
|
||
if (!githubToken || !greptileToken) { | ||
logger.error('Missing required environment variables: GITHUB_TOKEN and/or GREPTILE_API_KEY') | ||
return | ||
} | ||
|
||
const generator = new ChangelogGenerator(githubToken, greptileToken) | ||
|
||
const config = { | ||
owner, | ||
repo, | ||
startDate: new Date(startDate as string), | ||
endDate: new Date(endDate as string), | ||
customInstructions: instructions, | ||
} | ||
|
||
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] | ||
let frameIndex = 0 | ||
const spinner = setInterval(() => { | ||
process.stdout.write(`\r${frames[frameIndex]} Generating changelog...`) | ||
frameIndex = (frameIndex + 1) % frames.length | ||
}, 80) | ||
|
||
const changelog = await generator.generateChangelog(config) | ||
|
||
clearInterval(spinner) | ||
process.stdout.write('\r') // Clear the spinner line | ||
|
||
logger.log('\nChangelog:') | ||
logger.log(green(changelog)) | ||
} catch (error) { | ||
logger.error('Failed to generate changelog:', error instanceof Error ? error.message : String(error)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import * as info from './info' | ||
import * as greeting from './greeting' | ||
import * as create from './create' | ||
import * as cl from './cl' | ||
|
||
export const commands = [info, greeting, create] | ||
export const commands = [info, greeting, create, cl] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
interface RepositoryConfig { | ||
remote: string | ||
repository: string | ||
branch: string | ||
reload?: boolean | ||
notify?: boolean | ||
} | ||
|
||
interface Message { | ||
id: string | ||
content: string | ||
role: string | ||
} | ||
|
||
interface QueryConfig { | ||
messages: Message[] | ||
repositories: RepositoryConfig[] | ||
sessionId: string | ||
stream?: boolean | ||
genius?: boolean | ||
} | ||
|
||
interface RepositoryStatus { | ||
repository: string | ||
remote: string | ||
branch: string | ||
private: boolean | ||
status: string | ||
filesProcessed: number | ||
numFiles: number | ||
sha: string | ||
} | ||
|
||
interface Source { | ||
repository: string | ||
remote: string | ||
branch: string | ||
filepath: string | ||
linestart: number | ||
lineend: number | ||
summary: string | ||
} | ||
|
||
interface QueryResponse { | ||
message: string | ||
sources: Source[] | ||
} | ||
|
||
interface IndexResponse { | ||
message: string | ||
statusEndpoint: string | ||
} | ||
|
||
class GreptileAPI { | ||
private baseUrl = 'https://api.greptile.com/v2' | ||
private token: string | ||
private githubToken: string | ||
|
||
constructor(token: string, githubToken: string) { | ||
this.token = token | ||
this.githubToken = githubToken | ||
} | ||
|
||
private getHeaders(): Record<string, string> { | ||
return { | ||
Authorization: `Bearer ${this.token}`, | ||
'X-GitHub-Token': this.githubToken, | ||
'Content-Type': 'application/json', | ||
} | ||
} | ||
|
||
async indexRepository(config: RepositoryConfig): Promise<IndexResponse> { | ||
const response = await fetch(`${this.baseUrl}/repositories`, { | ||
method: 'POST', | ||
headers: this.getHeaders(), | ||
body: JSON.stringify(config), | ||
}) | ||
|
||
if (!response.ok) { | ||
throw new Error(`Failed to index repository: ${response.statusText}`) | ||
} | ||
|
||
return response.json() as Promise<IndexResponse> | ||
} | ||
|
||
async queryRepository(config: QueryConfig): Promise<QueryResponse> { | ||
const response = await fetch(`${this.baseUrl}/query`, { | ||
method: 'POST', | ||
headers: this.getHeaders(), | ||
body: JSON.stringify(config), | ||
}) | ||
|
||
if (!response.ok) { | ||
throw new Error(`Failed to query repository: ${response.statusText}`) | ||
} | ||
|
||
return response.json() as Promise<QueryResponse> | ||
} | ||
|
||
async getRepositoryStatus(repository: RepositoryConfig): Promise<RepositoryStatus> { | ||
const repositoryId = `${encodeURIComponent(repository.remote)}:${encodeURIComponent(repository.branch)}:${encodeURIComponent(repository.repository)}` | ||
const response = await fetch(`${this.baseUrl}/repositories/${repositoryId}`, { | ||
method: 'GET', | ||
headers: this.getHeaders(), | ||
}) | ||
|
||
if (!response.ok) { | ||
throw new Error(`Failed to get repository status: ${response.statusText}`) | ||
} | ||
|
||
return response.json() as Promise<RepositoryStatus> | ||
} | ||
} | ||
|
||
export default GreptileAPI |
Oops, something went wrong.