diff --git a/scripts/generate-contributors.sh b/scripts/generate-contributors.sh index ba031d5d..340ef497 100755 --- a/scripts/generate-contributors.sh +++ b/scripts/generate-contributors.sh @@ -97,6 +97,34 @@ if not new_prompts and not updated_prompts: import sys sys.exit(2) # Exit code 2 = no changes else: + # Helper function to parse contributors (supports "user1,user2,user3" format) + def parse_contributors(contributor_field): + """Parse contributor field, returns (primary_author, co_authors_list)""" + if not contributor_field: + return 'anonymous', [] + + # Split by comma and clean up + contributors = [c.strip() for c in contributor_field.split(',') if c.strip()] + + if not contributors: + return 'anonymous', [] + + primary = contributors[0] + co_authors = contributors[1:] if len(contributors) > 1 else [] + return primary, co_authors + + def build_commit_message(action, act, co_authors): + """Build commit message with optional co-author trailers""" + msg = f'{action} prompt: {act}' + + if co_authors: + msg += '\n\n' + for co_author in co_authors: + co_email = f"{co_author}@users.noreply.github.com" + msg += f'Co-authored-by: {co_author} <{co_email}>\n' + + return msg + # Process updates first (rewrite entire file with updates applied) if updated_prompts: print("\nApplying updates to existing prompts...") @@ -118,42 +146,41 @@ else: # Commit updates for i, (remote_row, local_row) in enumerate(updated_prompts, 1): - contributor = remote_row.get('contributor', '').strip() + contributor_field = remote_row.get('contributor', '').strip() act = remote_row.get('act', 'Unknown') - if not contributor: - contributor = 'anonymous' - - email = f"{contributor}@users.noreply.github.com" + primary_author, co_authors = parse_contributors(contributor_field) + email = f"{primary_author}@users.noreply.github.com" subprocess.run(['git', 'add', csv_file], check=True) env = os.environ.copy() - env['GIT_AUTHOR_NAME'] = contributor + env['GIT_AUTHOR_NAME'] = primary_author env['GIT_AUTHOR_EMAIL'] = email - env['GIT_COMMITTER_NAME'] = contributor + env['GIT_COMMITTER_NAME'] = primary_author env['GIT_COMMITTER_EMAIL'] = email + commit_msg = build_commit_message('Update', act, co_authors) + subprocess.run([ 'git', 'commit', - '-m', f'Update prompt: {act}', - f'--author={contributor} <{email}>' + '-m', commit_msg, + f'--author={primary_author} <{email}>' ], env=env, check=True) - print(f"[UPDATE {i}/{len(updated_prompts)}] {contributor}: {act}") + co_authors_str = f" (+ {', '.join(co_authors)})" if co_authors else "" + print(f"[UPDATE {i}/{len(updated_prompts)}] {primary_author}{co_authors_str}: {act}") # Process new prompts if new_prompts: print("\nCreating commits for new prompts...") for i, row in enumerate(new_prompts, 1): - contributor = row.get('contributor', '').strip() + contributor_field = row.get('contributor', '').strip() act = row.get('act', 'Unknown') - if not contributor: - contributor = 'anonymous' - - email = f"{contributor}@users.noreply.github.com" + primary_author, co_authors = parse_contributors(contributor_field) + email = f"{primary_author}@users.noreply.github.com" # Append this row to the CSV with open(csv_file, 'a', newline='') as f: @@ -164,18 +191,21 @@ else: subprocess.run(['git', 'add', csv_file], check=True) env = os.environ.copy() - env['GIT_AUTHOR_NAME'] = contributor + env['GIT_AUTHOR_NAME'] = primary_author env['GIT_AUTHOR_EMAIL'] = email - env['GIT_COMMITTER_NAME'] = contributor + env['GIT_COMMITTER_NAME'] = primary_author env['GIT_COMMITTER_EMAIL'] = email + commit_msg = build_commit_message('Add', act, co_authors) + subprocess.run([ 'git', 'commit', - '-m', f'Add prompt: {act}', - f'--author={contributor} <{email}>' + '-m', commit_msg, + f'--author={primary_author} <{email}>' ], env=env, check=True) - print(f"[NEW {i}/{len(new_prompts)}] {contributor}: {act}") + co_authors_str = f" (+ {', '.join(co_authors)})" if co_authors else "" + print(f"[NEW {i}/{len(new_prompts)}] {primary_author}{co_authors_str}: {act}") print(f"\nDone! Created {len(new_prompts)} new commits, {len(updated_prompts)} update commits.") diff --git a/src/app/prompts.csv/route.ts b/src/app/prompts.csv/route.ts index afa67c5e..c8cae623 100644 --- a/src/app/prompts.csv/route.ts +++ b/src/app/prompts.csv/route.ts @@ -14,6 +14,15 @@ function escapeCSVField(field: string): string { return field; } +function getUserIdentifier(user: { email: string; username: string; githubUsername: string | null }): string { + // Determine contributor identifier (immutable to prevent impersonation): + // 1. githubUsername if set (GitHub OAuth users) + // 2. username if email ends with @unclaimed.prompts.chat (imported GitHub contributors) + // 3. email for others (Google, credentials) + const isUnclaimedAccount = user.email.endsWith('@unclaimed.prompts.chat'); + return user.githubUsername || (isUnclaimedAccount ? user.username : user.email); +} + export async function GET() { try { const prompts = await db.prompt.findMany({ @@ -34,6 +43,13 @@ export async function GET() { githubUsername: true, }, }, + contributors: { + select: { + email: true, + username: true, + githubUsername: true, + }, + }, }, orderBy: { createdAt: "asc" }, }); @@ -44,16 +60,18 @@ export async function GET() { const promptContent = escapeCSVField(prompt.content); const forDevs = prompt.category?.slug === "coding" ? "TRUE" : "FALSE"; const type = prompt.type; - // Determine contributor identifier (immutable to prevent impersonation): - // 1. githubUsername if set (GitHub OAuth users) - // 2. username if email ends with @unclaimed.prompts.chat (imported GitHub contributors) - // 3. email for others (Google, credentials) - const isUnclaimedAccount = prompt.author.email.endsWith('@unclaimed.prompts.chat'); - const contributor = escapeCSVField( - prompt.author.githubUsername || (isUnclaimedAccount ? prompt.author.username : prompt.author.email) - ); - return [act, promptContent, forDevs, type, contributor].join(","); + // Build contributor list: author first, then co-contributors + // Format: "@author,@contributor1,@contributor2" or just "@author" + const authorId = getUserIdentifier(prompt.author); + const contributorIds = prompt.contributors + .map((c: { email: string; username: string; githubUsername: string | null }) => getUserIdentifier(c)) + .filter((id: string) => id !== authorId); // Exclude author from contributors list + + const allContributors = [authorId, ...contributorIds]; + const contributorField = escapeCSVField(allContributors.join(",")); + + return [act, promptContent, forDevs, type, contributorField].join(","); }); const csvContent = [headers.join(","), ...rows].join("\n");