Skip to content

Commit 9469d3d

Browse files
committed
grant credits script
1 parent 78ecb9b commit 9469d3d

File tree

1 file changed

+114
-0
lines changed

1 file changed

+114
-0
lines changed

scripts/grant-credits.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { createInterface } from 'readline'
2+
3+
import { generateCompactId } from '@codebuff/common/util/string'
4+
import db from '@codebuff/internal/db'
5+
import * as schema from '@codebuff/internal/db/schema'
6+
import { eq } from 'drizzle-orm'
7+
8+
function prompt(rl: ReturnType<typeof createInterface>, question: string): Promise<string> {
9+
return new Promise((resolve) => {
10+
rl.question(question, (answer) => resolve(answer.trim()))
11+
})
12+
}
13+
14+
async function lookupUserByEmail(email: string) {
15+
const [user] = await db
16+
.select({ id: schema.user.id, email: schema.user.email, name: schema.user.name })
17+
.from(schema.user)
18+
.where(eq(schema.user.email, email.toLowerCase()))
19+
.limit(1)
20+
return user ?? null
21+
}
22+
23+
async function lookupUserById(userId: string) {
24+
const [user] = await db
25+
.select({ id: schema.user.id, email: schema.user.email, name: schema.user.name })
26+
.from(schema.user)
27+
.where(eq(schema.user.id, userId))
28+
.limit(1)
29+
return user ?? null
30+
}
31+
32+
async function main() {
33+
const rl = createInterface({ input: process.stdin, output: process.stdout })
34+
35+
try {
36+
// 1. Get user by email or ID
37+
const userInput = await prompt(rl, 'Enter user email or user ID: ')
38+
if (!userInput) {
39+
console.error('No input provided.')
40+
process.exit(1)
41+
}
42+
43+
const isEmail = userInput.includes('@')
44+
const user = isEmail
45+
? await lookupUserByEmail(userInput)
46+
: await lookupUserById(userInput)
47+
48+
if (!user) {
49+
console.error(`User not found: ${userInput}`)
50+
process.exit(1)
51+
}
52+
53+
console.log(`\nFound user: ${user.name ?? '(no name)'} <${user.email}> (${user.id})`)
54+
55+
// 2. Get credit amount
56+
const amountStr = await prompt(rl, 'Enter credit amount (integer): ')
57+
const amount = parseInt(amountStr, 10)
58+
if (isNaN(amount) || amount <= 0) {
59+
console.error('Amount must be a positive integer.')
60+
process.exit(1)
61+
}
62+
63+
// 3. Get description
64+
const description = await prompt(rl, 'Enter description: ')
65+
if (!description) {
66+
console.error('Description is required.')
67+
process.exit(1)
68+
}
69+
70+
// 4. Generate operation ID
71+
const operationId = `admin-${user.id}-${generateCompactId()}`
72+
73+
// 5. Confirm
74+
console.log('\n--- Credit Grant Summary ---')
75+
console.log(` User: ${user.name ?? '(no name)'} <${user.email}>`)
76+
console.log(` User ID: ${user.id}`)
77+
console.log(` Amount: ${amount}`)
78+
console.log(` Type: admin`)
79+
console.log(` Priority: 50`)
80+
console.log(` Expires: never`)
81+
console.log(` Description: ${description}`)
82+
console.log(` Operation ID: ${operationId}`)
83+
console.log('----------------------------\n')
84+
85+
const confirm = await prompt(rl, 'Proceed? (y/N): ')
86+
if (!/^[Yy]$/.test(confirm)) {
87+
console.log('Aborted.')
88+
process.exit(0)
89+
}
90+
91+
// 6. Insert into credit_ledger
92+
await db.insert(schema.creditLedger).values({
93+
operation_id: operationId,
94+
user_id: user.id,
95+
principal: amount,
96+
balance: amount,
97+
type: 'admin',
98+
description,
99+
priority: 50,
100+
expires_at: null,
101+
})
102+
103+
console.log(`\n✅ Granted ${amount} credits to ${user.email} (${operationId})`)
104+
} finally {
105+
rl.close()
106+
}
107+
108+
process.exit(0)
109+
}
110+
111+
main().catch((error) => {
112+
console.error('Fatal error:', error)
113+
process.exit(1)
114+
})

0 commit comments

Comments
 (0)