-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add co-host management (#24) #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,131 @@ | ||||||||||||||
| /** | ||||||||||||||
| * Cohosts commands: list, add, remove | ||||||||||||||
| */ | ||||||||||||||
|
|
||||||||||||||
| import { loadConfig, getValidToken, wrapPayload } from '../lib/auth.js'; | ||||||||||||||
| import { apiRequest } from '../lib/http.js'; | ||||||||||||||
| import { resolveCohostNames, getCohostIds, setCohostIds } from '../lib/cohosts.js'; | ||||||||||||||
| import { jsonOutput, jsonError } from '../lib/output.js'; | ||||||||||||||
| import { PartifulError } from '../lib/errors.js'; | ||||||||||||||
|
|
||||||||||||||
| export function registerCohostsCommands(program) { | ||||||||||||||
| const cohosts = program.command('cohosts').description('Manage event co-hosts'); | ||||||||||||||
|
|
||||||||||||||
| cohosts | ||||||||||||||
| .command('list') | ||||||||||||||
| .description('List co-hosts for an event') | ||||||||||||||
| .argument('<eventId>', 'Event ID') | ||||||||||||||
| .action(async (eventId, opts, cmd) => { | ||||||||||||||
| const globalOpts = cmd.optsWithGlobals(); | ||||||||||||||
| try { | ||||||||||||||
| const config = loadConfig(); | ||||||||||||||
| const token = await getValidToken(config); | ||||||||||||||
|
|
||||||||||||||
| const ids = await getCohostIds(eventId, token, globalOpts.verbose); | ||||||||||||||
| if (ids.length === 0) { | ||||||||||||||
| jsonOutput([], { eventId, count: 0 }); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Cross-reference with contacts for names | ||||||||||||||
| const contactsPayload = { data: wrapPayload(config, { params: {}, amplitudeSessionId: Date.now(), userId: config.userId }) }; | ||||||||||||||
| const contactsResult = await apiRequest('POST', '/getContacts', token, contactsPayload, globalOpts.verbose); | ||||||||||||||
| const allContacts = contactsResult.result?.data || []; | ||||||||||||||
|
|
||||||||||||||
| const result = ids.map(id => { | ||||||||||||||
| const contact = allContacts.find(c => c.userId === id); | ||||||||||||||
| return { userId: id, name: contact?.name || null }; | ||||||||||||||
| }); | ||||||||||||||
|
Comment on lines
+35
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 35-Line 38 only returns 🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| jsonOutput(result, { eventId, count: result.length }); | ||||||||||||||
| } catch (e) { | ||||||||||||||
| if (e instanceof PartifulError) jsonError(e.message, e.exitCode, e.type, e.details); | ||||||||||||||
| else jsonError(e.message); | ||||||||||||||
| } | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| cohosts | ||||||||||||||
| .command('add') | ||||||||||||||
| .description('Add co-hosts to an event') | ||||||||||||||
| .argument('<eventId>', 'Event ID') | ||||||||||||||
| .option('--name <names...>', 'Co-host names (resolved from contacts)') | ||||||||||||||
| .option('--user-id <userIds...>', 'Co-host user IDs (direct)') | ||||||||||||||
|
Comment on lines
+51
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Only Suggested command-surface extension cohosts
.command('add')
.description('Add co-hosts to an event')
.argument('<eventId>', 'Event ID')
.option('--name <names...>', 'Co-host names (resolved from contacts)')
+ .option('--phone <phones...>', 'Co-host phone numbers')
+ .option('--username <usernames...>', 'Co-host Partiful usernames')
.option('--user-id <userIds...>', 'Co-host user IDs (direct)')📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| .action(async (eventId, opts, cmd) => { | ||||||||||||||
| const globalOpts = cmd.optsWithGlobals(); | ||||||||||||||
| try { | ||||||||||||||
| if (!opts.name && !opts.userId) { | ||||||||||||||
| jsonError('Provide --name or --user-id to specify co-hosts', 3, 'validation_error'); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const config = loadConfig(); | ||||||||||||||
| const token = await getValidToken(config); | ||||||||||||||
|
|
||||||||||||||
| const currentIds = await getCohostIds(eventId, token, globalOpts.verbose); | ||||||||||||||
| const newIds = [...currentIds]; | ||||||||||||||
|
|
||||||||||||||
| // Resolve names | ||||||||||||||
| const resolved = await resolveCohostNames(opts.name, token, config, globalOpts.verbose); | ||||||||||||||
| for (const id of resolved) { | ||||||||||||||
| if (!newIds.includes(id)) newIds.push(id); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Add direct user IDs | ||||||||||||||
| for (const id of (opts.userId || [])) { | ||||||||||||||
| if (!newIds.includes(id)) newIds.push(id); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const added = newIds.filter(id => !currentIds.includes(id)); | ||||||||||||||
| if (added.length === 0) { | ||||||||||||||
| jsonOutput({ eventId, added: [], total: currentIds.length, message: 'No new co-hosts to add' }); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if (globalOpts.dryRun) { | ||||||||||||||
| jsonOutput({ dryRun: true, eventId, currentCohosts: currentIds, newCohosts: newIds }); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| await setCohostIds(eventId, newIds, token, globalOpts.verbose); | ||||||||||||||
|
|
||||||||||||||
| jsonOutput({ eventId, added, total: newIds.length, url: `https://partiful.com/e/${eventId}` }); | ||||||||||||||
| } catch (e) { | ||||||||||||||
| if (e instanceof PartifulError) jsonError(e.message, e.exitCode, e.type, e.details); | ||||||||||||||
| else jsonError(e.message); | ||||||||||||||
| } | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| cohosts | ||||||||||||||
| .command('remove') | ||||||||||||||
| .description('Remove a co-host from an event') | ||||||||||||||
| .argument('<eventId>', 'Event ID') | ||||||||||||||
| .requiredOption('--user-id <userId>', 'User ID of the co-host to remove') | ||||||||||||||
| .action(async (eventId, opts, cmd) => { | ||||||||||||||
| const globalOpts = cmd.optsWithGlobals(); | ||||||||||||||
| try { | ||||||||||||||
| const config = loadConfig(); | ||||||||||||||
| const token = await getValidToken(config); | ||||||||||||||
|
|
||||||||||||||
| const currentIds = await getCohostIds(eventId, token, globalOpts.verbose); | ||||||||||||||
|
|
||||||||||||||
| if (!currentIds.includes(opts.userId)) { | ||||||||||||||
| jsonError(`User ${opts.userId} is not a co-host of this event`, 4, 'not_found'); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const newIds = currentIds.filter(id => id !== opts.userId); | ||||||||||||||
|
|
||||||||||||||
| if (globalOpts.dryRun) { | ||||||||||||||
| jsonOutput({ dryRun: true, eventId, removing: opts.userId, remaining: newIds }); | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| await setCohostIds(eventId, newIds, token, globalOpts.verbose); | ||||||||||||||
|
|
||||||||||||||
| jsonOutput({ eventId, removed: opts.userId, remaining: newIds.length, url: `https://partiful.com/e/${eventId}` }); | ||||||||||||||
| } catch (e) { | ||||||||||||||
| if (e instanceof PartifulError) jsonError(e.message, e.exitCode, e.type, e.details); | ||||||||||||||
| else jsonError(e.message); | ||||||||||||||
| } | ||||||||||||||
| }); | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| */ | ||
|
|
||
| import { loadConfig, getValidToken, wrapPayload } from '../lib/auth.js'; | ||
| import { resolveCohostNames } from '../lib/cohosts.js'; | ||
| import { fetchCatalog, searchPosters, buildPosterImage } from '../lib/posters.js'; | ||
| import { apiRequest, firestoreRequest } from '../lib/http.js'; | ||
| import { parseDateTime, stripMarkdown, formatDate } from '../lib/dates.js'; | ||
|
|
@@ -183,6 +184,7 @@ export function registerEventsCommands(program) { | |
| .option('--link-text <text...>', 'Display text for link (paired with --link by position)') | ||
| .option('--template <name>', 'Create from a saved template') | ||
| .option('--var <vars...>', 'Template variables (key=value)') | ||
| .option('--cohost <names...>', 'Co-host names (resolved from contacts)') | ||
| .action(async (opts, cmd) => { | ||
| const globalOpts = cmd.optsWithGlobals(); | ||
| try { | ||
|
|
@@ -331,16 +333,18 @@ export function registerEventsCommands(program) { | |
| } | ||
| } | ||
|
|
||
| const cohostIds = await resolveCohostNames(opts.cohost, token, config, globalOpts.verbose); | ||
|
|
||
| const payload = { | ||
| data: wrapPayload(config, { | ||
| params: { event, cohostIds: [] }, | ||
| params: { event, cohostIds }, | ||
| amplitudeSessionId: Date.now(), | ||
| userId: config.userId, | ||
| }), | ||
| }; | ||
|
|
||
| if (globalOpts.dryRun) { | ||
| jsonOutput({ dryRun: true, endpoint: '/createEvent', payload, ...(opts.repeat ? { series: { repeat: opts.repeat, count: opts.count } } : {}) }); | ||
| jsonOutput({ dryRun: true, endpoint: '/createEvent', payload, cohostsResolved: cohostIds.length, ...(opts.repeat ? { series: { repeat: opts.repeat, count: opts.count } } : {}) }); | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -360,7 +364,7 @@ export function registerEventsCommands(program) { | |
| const seriesEvent = { ...event, startDate: d.toISOString() }; | ||
| const seriesPayload = { | ||
| data: wrapPayload(config, { | ||
| params: { event: seriesEvent, cohostIds: [] }, | ||
| params: { event: seriesEvent, cohostIds }, | ||
| amplitudeSessionId: Date.now(), | ||
| userId: config.userId, | ||
| }), | ||
|
|
@@ -409,6 +413,7 @@ export function registerEventsCommands(program) { | |
| .option('--image <path>', 'Upload and set custom image') | ||
| .option('--link <url...>', 'Link URL (repeatable)') | ||
| .option('--link-text <text...>', 'Display text for link (paired with --link by position)') | ||
| .option('--cohost <names...>', 'Co-host names (resolved from contacts)') | ||
| .action(async (eventId, opts, cmd) => { | ||
| const globalOpts = cmd.optsWithGlobals(); | ||
| try { | ||
|
|
@@ -510,8 +515,18 @@ export function registerEventsCommands(program) { | |
| } | ||
| } | ||
|
|
||
| if (opts.cohost && opts.cohost.length > 0) { | ||
| const resolvedIds = await resolveCohostNames(opts.cohost, token, config, globalOpts.verbose); | ||
| if (resolvedIds.length > 0) { | ||
| fields.cohostIds = { | ||
| arrayValue: { values: resolvedIds.map(id => ({ stringValue: id })) } | ||
| }; | ||
| updateFields.push('cohostIds'); | ||
| } | ||
| } | ||
|
Comment on lines
+518
to
+526
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same deduplication gap in update command. The 🔧 Proposed fix if (match && match.userId) {
- resolvedIds.push(match.userId);
+ if (!resolvedIds.includes(match.userId)) resolvedIds.push(match.userId);
} else {🤖 Prompt for AI Agents |
||
|
|
||
| if (updateFields.length === 0) { | ||
| jsonError('No fields to update. Use --title, --location, --description, --date, --end-date, --capacity, --link, --poster, --poster-search, or --image', 3, 'validation_error'); | ||
| jsonError('No fields to update. Use --title, --location, --description, --date, --end-date, --capacity, --link, --poster, --poster-search, --image, or --cohost', 3, 'validation_error'); | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -553,6 +568,7 @@ export function registerEventsCommands(program) { | |
| .option('--image <path>', 'Override with custom image') | ||
| .option('--link <url...>', 'Override links (repeatable)') | ||
| .option('--link-text <text...>', 'Display text for links') | ||
| .option('--cohost <names...>', 'Co-host names (resolved from contacts)') | ||
| .action(async (eventId, opts, cmd) => { | ||
| const globalOpts = cmd.optsWithGlobals(); | ||
| try { | ||
|
|
@@ -717,10 +733,12 @@ export function registerEventsCommands(program) { | |
| event.image = src.image; | ||
| } | ||
|
|
||
| const cohostIds = await resolveCohostNames(opts.cohost, token, config, globalOpts.verbose); | ||
|
|
||
| // 4. Build API payload | ||
| const payload = { | ||
| data: wrapPayload(config, { | ||
| params: { event, cohostIds: [] }, | ||
| params: { event, cohostIds }, | ||
| amplitudeSessionId: Date.now(), | ||
| userId: config.userId, | ||
| }), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| /** | ||
| * Shared co-host helpers: contact resolution, Firestore read/write. | ||
| */ | ||
|
|
||
| import { apiRequest, firestoreRequest } from './http.js'; | ||
| import { wrapPayload } from './auth.js'; | ||
|
|
||
| /** | ||
| * Resolve co-host names to Partiful user IDs via the contacts API. | ||
| * Tries exact match first, then substring. Warns on stderr for misses. | ||
| * @returns {string[]} resolved user IDs | ||
| */ | ||
| export async function resolveCohostNames(names, token, config, verbose = false) { | ||
| if (!names || names.length === 0) return []; | ||
|
|
||
| const payload = { | ||
| data: wrapPayload(config, { | ||
| params: {}, | ||
| amplitudeSessionId: Date.now(), | ||
| userId: config.userId, | ||
| }), | ||
| }; | ||
| const result = await apiRequest('POST', '/getContacts', token, payload, verbose); | ||
| const contacts = result.result?.data || []; | ||
|
|
||
| const ids = []; | ||
| for (const name of names) { | ||
| const q = name.toLowerCase(); | ||
| const match = | ||
| contacts.find(c => (c.name || '').toLowerCase() === q) || | ||
| contacts.find(c => (c.name || '').toLowerCase().includes(q)); | ||
| if (match?.userId) { | ||
| if (!ids.includes(match.userId)) ids.push(match.userId); | ||
| } else { | ||
| process.stderr.write(`Warning: could not resolve co-host "${name}" from contacts — skipping\n`); | ||
| } | ||
| } | ||
| return ids; | ||
| } | ||
|
|
||
| /** | ||
| * Read cohostIds array from a Firestore event document. | ||
| * @returns {string[]} | ||
| */ | ||
| export async function getCohostIds(eventId, token, verbose = false) { | ||
| const doc = await firestoreRequest('GET', eventId, null, token, [], verbose); | ||
| const values = doc.fields?.cohostIds?.arrayValue?.values || []; | ||
| return values.map(v => v.stringValue).filter(Boolean); | ||
| } | ||
|
|
||
| /** | ||
| * Write cohostIds array to a Firestore event document. | ||
| */ | ||
| export async function setCohostIds(eventId, ids, token, verbose = false) { | ||
| const unique = [...new Set(ids.filter(Boolean))]; | ||
| const fields = { | ||
| cohostIds: { | ||
| arrayValue: { values: unique.map(id => ({ stringValue: id })) }, | ||
| }, | ||
| }; | ||
| await firestoreRequest('PATCH', eventId, { fields }, token, ['cohostIds'], verbose); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pass global options into every
jsonOutputcall in this file.On Line 26, Line 40, Line 79, Line 86, Line 114, and Line 120,
jsonOutputis called withoutglobalOpts, so global flags like--outputare ignored for cohost commands.Proposed patch
Also applies to: 40-40, 79-79, 86-86, 114-114, 120-120
🤖 Prompt for AI Agents