@@ -159,6 +159,12 @@ program
159159 . option ( '--github <username>' , 'Fetch and add a GitHub user\'s GPG public key' )
160160 . option ( '--github-name <name>' , 'Custom name for the GitHub user\'s key (optional)' )
161161 . option ( '--refresh-github-keys' , 'Force refresh of cached GitHub keys (re-fetch from GitHub)' )
162+ // Group management options
163+ . option ( '--group-create <name> <members...>' , 'Create a new recipient group with members' )
164+ . option ( '--group-add <name> <members...>' , 'Add members to an existing group' )
165+ . option ( '--group-remove <name> <members...>' , 'Remove members from a group' )
166+ . option ( '--group-delete <name>' , 'Delete a group entirely' )
167+ . option ( '--group-list' , 'List all groups and their members' )
162168 // Debugging and logging options
163169 . option ( '--verbose' , 'Enable verbose logging (same as --log-level debug)' )
164170 . option ( '--debug' , 'Enable debug mode with extensive logging (same as --log-level trace)' )
@@ -188,6 +194,13 @@ GitHub Integration:
188194 $ dedpaste keys --github username # Add a GitHub user's GPG key
189195 $ dedpaste keys --github username --github-name bob # Add with custom name
190196
197+ Group Management (NEW):
198+ $ dedpaste keys --group-create team gh:alice kb:bob # Create a group named 'team'
199+ $ dedpaste keys --group-add team gh:charlie # Add member to group
200+ $ dedpaste keys --group-remove team gh:bob # Remove member from group
201+ $ dedpaste keys --group-list # List all groups
202+ $ dedpaste keys --group-delete team # Delete a group
203+
191204Key Storage:
192205 - Your keys are stored in ~/.dedpaste/keys/
193206 - Friend keys are stored in ~/.dedpaste/friends/
@@ -686,6 +699,106 @@ Key Storage:
686699 }
687700 return ;
688701 }
702+ // Group management commands
703+ if ( options . groupCreate ) {
704+ const [ groupName , ...members ] = options . groupCreate ;
705+ if ( ! groupName || members . length === 0 ) {
706+ console . error ( 'Error: Group name and at least one member required' ) ;
707+ console . error ( 'Usage: dedpaste keys --group-create <name> <member1> <member2> ...' ) ;
708+ process . exit ( 1 ) ;
709+ }
710+ try {
711+ const { createGroup } = await import ( './groupManager.js' ) ;
712+ await createGroup ( groupName , members ) ;
713+ console . log ( `\n✓ Created group "${ groupName } " with ${ members . length } member(s):` ) ;
714+ for ( const member of members ) {
715+ console . log ( ` - ${ member } ` ) ;
716+ }
717+ }
718+ catch ( error ) {
719+ console . error ( `Error creating group: ${ error . message } ` ) ;
720+ process . exit ( 1 ) ;
721+ }
722+ return ;
723+ }
724+ if ( options . groupAdd ) {
725+ const [ groupName , ...members ] = options . groupAdd ;
726+ if ( ! groupName || members . length === 0 ) {
727+ console . error ( 'Error: Group name and at least one member required' ) ;
728+ console . error ( 'Usage: dedpaste keys --group-add <name> <member1> <member2> ...' ) ;
729+ process . exit ( 1 ) ;
730+ }
731+ try {
732+ const { addToGroup } = await import ( './groupManager.js' ) ;
733+ await addToGroup ( groupName , members ) ;
734+ console . log ( `\n✓ Added ${ members . length } member(s) to group "${ groupName } ":` ) ;
735+ for ( const member of members ) {
736+ console . log ( ` - ${ member } ` ) ;
737+ }
738+ }
739+ catch ( error ) {
740+ console . error ( `Error adding to group: ${ error . message } ` ) ;
741+ process . exit ( 1 ) ;
742+ }
743+ return ;
744+ }
745+ if ( options . groupRemove ) {
746+ const [ groupName , ...members ] = options . groupRemove ;
747+ if ( ! groupName || members . length === 0 ) {
748+ console . error ( 'Error: Group name and at least one member required' ) ;
749+ console . error ( 'Usage: dedpaste keys --group-remove <name> <member1> <member2> ...' ) ;
750+ process . exit ( 1 ) ;
751+ }
752+ try {
753+ const { removeFromGroup } = await import ( './groupManager.js' ) ;
754+ await removeFromGroup ( groupName , members ) ;
755+ console . log ( `\n✓ Removed ${ members . length } member(s) from group "${ groupName } ":` ) ;
756+ for ( const member of members ) {
757+ console . log ( ` - ${ member } ` ) ;
758+ }
759+ }
760+ catch ( error ) {
761+ console . error ( `Error removing from group: ${ error . message } ` ) ;
762+ process . exit ( 1 ) ;
763+ }
764+ return ;
765+ }
766+ if ( options . groupDelete ) {
767+ try {
768+ const { deleteGroup } = await import ( './groupManager.js' ) ;
769+ await deleteGroup ( options . groupDelete ) ;
770+ console . log ( `\n✓ Deleted group "${ options . groupDelete } "` ) ;
771+ }
772+ catch ( error ) {
773+ console . error ( `Error deleting group: ${ error . message } ` ) ;
774+ process . exit ( 1 ) ;
775+ }
776+ return ;
777+ }
778+ if ( options . groupList ) {
779+ try {
780+ const { listGroups } = await import ( './groupManager.js' ) ;
781+ const groups = await listGroups ( ) ;
782+ if ( Object . keys ( groups ) . length === 0 ) {
783+ console . log ( '\nNo groups found. Create one with:' ) ;
784+ console . log ( ' dedpaste keys --group-create <name> <member1> <member2> ...' ) ;
785+ return ;
786+ }
787+ console . log ( '\n📋 Recipient Groups:\n' ) ;
788+ for ( const [ name , members ] of Object . entries ( groups ) ) {
789+ console . log ( ` ${ name } (${ members . length } member${ members . length !== 1 ? 's' : '' } ):` ) ;
790+ for ( const member of members ) {
791+ console . log ( ` - ${ member } ` ) ;
792+ }
793+ console . log ( '' ) ;
794+ }
795+ }
796+ catch ( error ) {
797+ console . error ( `Error listing groups: ${ error . message } ` ) ;
798+ process . exit ( 1 ) ;
799+ }
800+ return ;
801+ }
689802 // Generate a new key pair
690803 if ( options . genKey ) {
691804 logger . debug ( 'Generating new key pair' ) ;
@@ -739,7 +852,7 @@ program
739852 . option ( '-f, --file <path>' , 'Upload a file from the specified path instead of stdin' )
740853 . option ( '-o, --output' , 'Print only the URL (without any additional text, useful for scripts)' )
741854 . option ( '-e, --encrypt' , 'Encrypt the content before uploading (requires key setup)' )
742- . option ( '--for <friend >' , 'Encrypt for a specific friend (requires adding their key first) ' )
855+ . option ( '--for <recipients... >' , 'Encrypt for recipient(s): supports gh:user, kb:user, email@domain, or group names ' )
743856 . option ( '--list-friends' , 'List available friends you can encrypt messages for' )
744857 . option ( '--key-file <path>' , 'Path to public key for encryption (alternative to stored keys)' )
745858 . option ( '--gen-key' , 'Generate a new key pair for encryption if you don\'t have one' )
@@ -761,18 +874,24 @@ Examples:
761874 $ dedpaste send --enhanced --encrypt # Enhanced interactive mode with full key support
762875 $ dedpaste send --list-friends # List available recipients
763876
877+ Short Prefix Support (NEW):
878+ $ dedpaste send --encrypt --for gh:torvalds # GitHub user (short prefix)
879+ $ dedpaste send --encrypt --for kb:username # Keybase user (short prefix)
880+ $ dedpaste send --encrypt --for github:torvalds # GitHub user (full prefix - still works)
881+
882+ Multiple Recipients (NEW):
883+ $ dedpaste send --encrypt --for gh:alice kb:bob charlie@example.com # Multiple recipients (space-separated)
884+
885+ Groups (NEW):
886+ $ dedpaste keys --group-create team gh:alice kb:bob # Create a group
887+ $ dedpaste send --encrypt --for team # Encrypt for all group members
888+
764889PGP Options:
765- $ dedpaste send --encrypt --for alice@example.com --pgp # Encrypt for PGP key (REQUIRED: specify recipient )
890+ $ dedpaste send --encrypt --for alice@example.com # Encrypt for PGP key (auto-detects PGP )
766891 $ dedpaste send --encrypt --for alice --pgp --pgp-key-file friend.asc # Use specific PGP key file
767892 $ dedpaste send --enhanced --encrypt --pgp # Use enhanced mode with GPG keyring support
768893
769- Keybase Integration:
770- $ dedpaste send --encrypt --for keybase:username --pgp # Encrypt for Keybase user
771-
772- GitHub Integration:
773- $ dedpaste send --encrypt --for github:username --pgp # Encrypt for GitHub user (auto-fetches GPG key)
774-
775- IMPORTANT: PGP encryption always requires specifying a recipient with --for
894+ NOTE: PGP encryption is now auto-detected based on recipient type. The --pgp flag is optional.
776895
777896Encryption:
778897 - Standard encryption uses RSA for key exchange and AES-256-GCM for content
@@ -818,7 +937,8 @@ Encryption:
818937 process . exit ( 1 ) ;
819938 }
820939 content = Buffer . from ( result . content || '' ) ;
821- recipientName = result . recipient ?? undefined ;
940+ // Interactive mode returns a single string, wrap in array if provided
941+ recipientName = result . recipient ? [ result . recipient ] : undefined ;
822942 options . temp = result . temp ;
823943 options . pgp = result . pgp ; // Use PGP flag from enhanced mode
824944 contentType = 'text/plain' ;
@@ -827,7 +947,8 @@ Encryption:
827947 // Use standard interactive mode
828948 const result = await interactiveSend ( ) ;
829949 content = Buffer . from ( result . content || '' ) ;
830- recipientName = result . recipient ?? undefined ;
950+ // Interactive mode returns a single string, wrap in array if provided
951+ recipientName = result . recipient ? [ result . recipient ] : undefined ;
831952 options . temp = result . temp ;
832953 contentType = 'text/plain' ;
833954 }
@@ -896,17 +1017,19 @@ Encryption:
8961017 }
8971018 // If PGP key file is provided, read it and use it directly
8981019 if ( options . pgpKeyFile ) {
899- if ( ! recipientName ) {
1020+ if ( ! recipientName || ( Array . isArray ( recipientName ) && recipientName . length === 0 ) ) {
9001021 console . error ( `Error: When using --pgp-key-file, you must specify a recipient with --for` ) ;
9011022 process . exit ( 1 ) ;
9021023 }
1024+ // For pgpKeyFile, use the first recipient
1025+ const firstRecipient = Array . isArray ( recipientName ) ? recipientName [ 0 ] : recipientName ;
9031026 const pgpKeyContent = await fsPromises . readFile ( options . pgpKeyFile , 'utf8' ) ;
904- const pgpResult = await createPgpEncryptedMessage ( content , pgpKeyContent , recipientName ) ;
1027+ const pgpResult = await createPgpEncryptedMessage ( content , pgpKeyContent , firstRecipient ) ;
9051028 content = typeof pgpResult === 'string' ? Buffer . from ( pgpResult ) : pgpResult ;
9061029 }
9071030 else {
9081031 // Use the standard encryption flow with PGP option
909- const encryptResult = await encryptContent ( content . toString ( 'utf8' ) , recipientName , usePgp , options . refreshGithubKeys ) ;
1032+ const encryptResult = await encryptContent ( content . toString ( 'utf8' ) , recipientName ?? null , usePgp , options . refreshGithubKeys ) ;
9101033 content = typeof encryptResult === 'string' ? Buffer . from ( encryptResult ) : encryptResult ;
9111034 }
9121035 // Log PGP mode if used
@@ -1086,14 +1209,15 @@ GPG Keyring Integration:
10861209 // It's a URL
10871210 const url = new URL ( urlOrId ) ;
10881211 const path = url . pathname ;
1089- // Check if it's an encrypted paste
1090- const encryptedMatch = path . match ( / ^ \/ e \/ ( [ a - z A - Z 0 - 9 ] { 8 } ) $ / ) ;
1212+ // Check if it's an encrypted paste (with optional filename)
1213+ const encryptedMatch = path . match ( / ^ \/ e \/ ( [ a - z A - Z 0 - 9 ] { 8 } ) (?: \/ ( . + ) ) ? $ / ) ;
10911214 if ( encryptedMatch ) {
10921215 id = encryptedMatch [ 1 ] ;
10931216 isEncrypted = true ;
10941217 }
10951218 else {
1096- const regularMatch = path . match ( / ^ \/ ( [ a - z A - Z 0 - 9 ] { 8 } ) $ / ) ;
1219+ // Check if it's a regular paste (with optional filename)
1220+ const regularMatch = path . match ( / ^ \/ ( [ a - z A - Z 0 - 9 ] { 8 } ) (?: \/ ( .+ ) ) ? $ / ) ;
10971221 if ( regularMatch ) {
10981222 id = regularMatch [ 1 ] ;
10991223 }
@@ -1118,9 +1242,17 @@ GPG Keyring Integration:
11181242 process . exit ( 1 ) ;
11191243 }
11201244 // Determine the URL to fetch
1121- const fetchUrl = isEncrypted
1122- ? `${ API_URL } /e/${ id } `
1123- : `${ API_URL } /${ id } ` ;
1245+ // If the original URL had a full path with filename, use it
1246+ let fetchUrl ;
1247+ if ( urlOrId . startsWith ( 'http' ) ) {
1248+ const url = new URL ( urlOrId ) ;
1249+ fetchUrl = `${ API_URL } ${ url . pathname } ` ;
1250+ }
1251+ else {
1252+ fetchUrl = isEncrypted
1253+ ? `${ API_URL } /e/${ id } `
1254+ : `${ API_URL } /${ id } ` ;
1255+ }
11241256 console . log ( `Fetching paste from ${ fetchUrl } ...` ) ;
11251257 // Fetch the paste
11261258 const response = await fetch ( fetchUrl ) ;
@@ -1306,11 +1438,14 @@ GPG Keyring Integration:
13061438 }
13071439 }
13081440 console . log ( '\n✓ Paste decrypted successfully:\n' ) ;
1309- process . stdout . write ( result . content ) ;
1441+ const contentToWrite = result . decryptedContent || result . content ;
1442+ process . stdout . write ( contentToWrite ) ;
13101443 // Add a newline at the end if the content doesn't end with one
1311- if ( result . content . length > 0 && result . content [ result . content . length - 1 ] !== 10 ) { // ASCII 10 is newline
1312- process . stdout . write ( Buffer . from ( '\n' ) ) ;
1444+ if ( contentToWrite . length > 0 && contentToWrite [ contentToWrite . length - 1 ] !== '\n' ) {
1445+ process . stdout . write ( '\n' ) ;
13131446 }
1447+ // Exit successfully
1448+ process . exit ( 0 ) ;
13141449 }
13151450 catch ( error ) {
13161451 console . error ( `\n❌ Decryption error: ${ error . message } ` ) ;
@@ -1346,6 +1481,8 @@ GPG Keyring Integration:
13461481 if ( contentBuffer . length > 0 && contentBuffer [ contentBuffer . length - 1 ] !== 10 ) { // ASCII 10 is newline
13471482 process . stdout . write ( Buffer . from ( '\n' ) ) ;
13481483 }
1484+ // Exit successfully
1485+ process . exit ( 0 ) ;
13491486 }
13501487 }
13511488 catch ( error ) {
0 commit comments