Skip to content

Commit

Permalink
feat: stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
dakshgup committed Nov 26, 2024
1 parent 6bbbccb commit 28c8399
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
GREPTILE_API_KEY=""
GITHUB_TOKEN=""
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
},
"dependencies": {
"consola": "^3.2.3",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"giget": "^1.2.3",
"picocolors": "^1.0.1",
Expand Down
44 changes: 44 additions & 0 deletions src/chameleon_sandbox.ts
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)
76 changes: 76 additions & 0 deletions src/chameleon_utils.ts
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
133 changes: 133 additions & 0 deletions src/commands/cl.ts
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))
}
}
3 changes: 2 additions & 1 deletion src/commands/index.ts
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]
115 changes: 115 additions & 0 deletions src/greptile.ts
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
Loading

0 comments on commit 28c8399

Please sign in to comment.