Skip to content

Commit 738ecf2

Browse files
feat: add --json support to mutating commands (#84)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b01deea commit 738ecf2

File tree

8 files changed

+56
-0
lines changed

8 files changed

+56
-0
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ This is a TypeScript CLI (`tw`) for Twist messaging, built with Commander.js.
5454

5555
- **Implicit view subcommand**: `tw thread <ref>` defaults to `tw thread view <ref>` via Commander's `{ isDefault: true }`. Same for `conversation` and `msg`. Edge case: if a ref matches a subcommand name (e.g., "reply"), the subcommand wins — user must use `tw thread view reply`
5656
- **Named flag aliases**: Where commands accept positional `[workspace-ref]`, the `--workspace` flag is also accepted. Error if both positional and flag are provided
57+
- **JSON output on mutating commands**: Mutating commands (create, update, delete, archive) should support `--json` output where it provides scripting value. Commands that return an object from the API (create/update) should also support `--full`. Commands where the API returns void should output a minimal status object (e.g. `{ id, deleted: true }` or `{ id, isArchived: true }`). Extend `MutationOptions` in `src/lib/options.ts` (which already includes `json` and `full`) rather than adding these fields ad hoc. Use `formatJson()` from `src/lib/output.ts` for the output. See `src/commands/away.ts` as the reference implementation.
5758

5859
## Pre-commit Hooks
5960

src/commands/conversation.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,11 @@ async function replyToConversation(
400400
content: replyContent,
401401
})
402402

403+
if (options.json) {
404+
console.log(formatJson(message, 'message', options.full))
405+
return
406+
}
407+
403408
console.log(`Message sent: ${message.url}`)
404409
}
405410

@@ -413,6 +418,12 @@ async function markConversationDone(ref: string, options: DoneOptions): Promise<
413418

414419
const client = await getTwistClient()
415420
await client.conversations.archiveConversation(conversationId)
421+
422+
if (options.json) {
423+
console.log(formatJson({ id: conversationId, archived: true }))
424+
return
425+
}
426+
416427
console.log(`Conversation ${conversationId} archived.`)
417428
}
418429

@@ -529,11 +540,14 @@ export function registerConversationCommand(program: Command): void {
529540
.command('reply <conversation-ref> [content]')
530541
.description('Send a message in a conversation')
531542
.option('--dry-run', 'Show what would be sent without sending')
543+
.option('--json', 'Output sent message as JSON')
544+
.option('--full', 'Include all fields in JSON output')
532545
.action(replyToConversation)
533546

534547
conversation
535548
.command('done <conversation-ref>')
536549
.description('Archive a conversation')
537550
.option('--dry-run', 'Show what would happen without executing')
551+
.option('--json', 'Output result as JSON')
538552
.action(markConversationDone)
539553
}

src/commands/msg.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ async function updateMessage(
7373
content: newContent,
7474
})
7575

76+
if (options.json) {
77+
console.log(formatJson(message, 'message', options.full))
78+
return
79+
}
80+
7681
console.log(`Message updated: ${message.url}`)
7782
}
7883

@@ -86,6 +91,12 @@ async function deleteMessage(ref: string, options: DeleteOptions): Promise<void>
8691

8792
const client = await getTwistClient()
8893
await client.conversationMessages.deleteMessage(messageId)
94+
95+
if (options.json) {
96+
console.log(formatJson({ id: messageId, deleted: true }))
97+
return
98+
}
99+
89100
console.log(`Message ${messageId} deleted.`)
90101
}
91102

@@ -112,10 +123,13 @@ export function registerMsgCommand(program: Command): void {
112123
msg.command('update <message-ref> [content]')
113124
.description('Edit a conversation message')
114125
.option('--dry-run', 'Show what would be updated without updating')
126+
.option('--json', 'Output updated message as JSON')
127+
.option('--full', 'Include all fields in JSON output')
115128
.action(updateMessage)
116129

117130
msg.command('delete <message-ref>')
118131
.description('Delete a conversation message')
119132
.option('--dry-run', 'Show what would happen without executing')
133+
.option('--json', 'Output result as JSON')
120134
.action(deleteMessage)
121135
}

src/commands/thread.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,14 @@ export function registerThreadCommand(program: Command): void {
4141
),
4242
)
4343
.option('--dry-run', 'Show what would be posted without posting')
44+
.option('--json', 'Output posted comment as JSON')
45+
.option('--full', 'Include all fields in JSON output')
4446
.action(replyToThread)
4547

4648
thread
4749
.command('done <thread-ref>')
4850
.description('Archive a thread (mark as done)')
4951
.option('--dry-run', 'Show what would happen without executing')
52+
.option('--json', 'Output result as JSON')
5053
.action(markThreadDone)
5154
}

src/commands/thread/mutate.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getTwistClient } from '../../lib/api.js'
22
import type { MutationOptions } from '../../lib/options.js'
3+
import { formatJson } from '../../lib/output.js'
34
import { assertChannelIsPublic } from '../../lib/public-channels.js'
45
import { resolveThreadId } from '../../lib/refs.js'
56

@@ -17,5 +18,11 @@ export async function markThreadDone(ref: string, options: DoneOptions): Promise
1718
const thread = await client.threads.getThread(threadId)
1819
await assertChannelIsPublic(thread.channelId, thread.workspaceId)
1920
await client.inbox.archiveThread(threadId)
21+
22+
if (options.json) {
23+
console.log(formatJson({ id: threadId, isArchived: true }))
24+
return
25+
}
26+
2027
console.log(`Thread ${threadId} archived.`)
2128
}

src/commands/thread/reply.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getTwistClient } from '../../lib/api.js'
22
import { openEditor, readStdin } from '../../lib/input.js'
33
import type { MutationOptions } from '../../lib/options.js'
4+
import { formatJson } from '../../lib/output.js'
45
import { assertChannelIsPublic } from '../../lib/public-channels.js'
56
import { extractId, resolveThreadId } from '../../lib/refs.js'
67

@@ -66,5 +67,10 @@ export async function replyToThread(
6667
recipients,
6768
} as Parameters<typeof client.comments.createComment>[0])
6869

70+
if (options.json) {
71+
console.log(formatJson(comment, 'comment', options.full))
72+
return
73+
}
74+
6975
console.log(`Comment posted: ${comment.url}`)
7076
}

src/lib/options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ export type PaginatedViewOptions = ViewOptions & {
1313

1414
export type MutationOptions = {
1515
dryRun?: boolean
16+
json?: boolean
17+
full?: boolean
1618
}

src/lib/skills/content.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ tw thread view <ref> --raw # Show raw markdown
6060
tw thread reply <ref> "content" # Post a comment
6161
tw thread reply <ref> "content" --notify EVERYONE # Notify all workspace members
6262
tw thread reply <ref> "content" --notify 123,id:456 # Notify specific user IDs
63+
tw thread reply <ref> "content" --json # Post and return comment as JSON
64+
tw thread reply <ref> "content" --json --full # Include all comment fields
6365
tw thread done <ref> # Archive thread (mark done)
66+
tw thread done <ref> --json # Archive and return status as JSON
6467
\`\`\`
6568
6669
Default \`--notify\` is EVERYONE_IN_THREAD. Options: EVERYONE, EVERYONE_IN_THREAD, or comma-separated user ID refs.
@@ -75,7 +78,10 @@ tw conversation with <user-ref> # Find your 1:1 DM with a user
7578
tw conversation with <user-ref> --snippet # Include the latest message preview
7679
tw conversation with <user-ref> --include-groups # List any conversations with that user
7780
tw conversation reply <ref> "content" # Send a message
81+
tw conversation reply <ref> "content" --json # Send and return message as JSON
82+
tw conversation reply <ref> "content" --json --full # Include all message fields
7883
tw conversation done <ref> # Archive conversation
84+
tw conversation done <ref> --json # Archive and return status as JSON
7985
\`\`\`
8086
8187
Alias: \`tw convo\` works the same as \`tw conversation\`.
@@ -86,7 +92,10 @@ Alias: \`tw convo\` works the same as \`tw conversation\`.
8692
tw msg <message-ref> # View a message (shorthand for view)
8793
tw msg view <message-ref> # View a single conversation message
8894
tw msg update <ref> "content" # Edit a conversation message
95+
tw msg update <ref> "content" --json # Edit and return updated message as JSON
96+
tw msg update <ref> "content" --json --full # Include all message fields
8997
tw msg delete <ref> # Delete a conversation message
98+
tw msg delete <ref> --json # Delete and return status as JSON
9099
\`\`\`
91100
92101
Alias: \`tw message\` works the same as \`tw msg\`.

0 commit comments

Comments
 (0)