Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
17042d3
ユーザーグループ取得の操作クラスを追加
mimo-3 Jul 23, 2026
936d281
ユーザーグループ関連の型を追加
mimo-3 Jul 23, 2026
4bba44a
SlackApiClientにユーザーグループAPIを追加
mimo-3 Jul 23, 2026
4b10e93
usergroupsコマンドを追加
mimo-3 Jul 23, 2026
48e75bb
usergroupsコマンドを登録
mimo-3 Jul 23, 2026
9afcc76
v0.24.0
mimo-3 Jul 23, 2026
0fc747b
READMEにusergroupsコマンドの説明を追加
mimo-3 Jul 23, 2026
d07654c
package.jsonの整形を修正
mimo-3 Jul 23, 2026
1dfda8c
メンバー取得のメソッド名をfacadeと揃える
mimo-3 Jul 23, 2026
ebc9839
SlackApiClient側の呼び出しも追随
mimo-3 Jul 23, 2026
d82dba0
membersの表示を既存フォーマッタに統一
mimo-3 Jul 23, 2026
f33e118
エラー出力もターミナルサニタイズを通す
mimo-3 Jul 23, 2026
67c6f6e
1行出力用のサニタイズヘルパーを追加
mimo-3 Jul 23, 2026
08b9eb1
メンバー表示の改行・タブを1行に畳む
mimo-3 Jul 23, 2026
873440e
usergroups表示も1行サニタイズに揃える
mimo-3 Jul 23, 2026
2784867
users.infoをレートリミッタ経由にする
mimo-3 Jul 23, 2026
35fa282
v0.24.1
mimo-3 Jul 23, 2026
c464be5
テストの整形を修正
mimo-3 Jul 23, 2026
32052b9
トークン秘匿をサニタイズ後に行う
mimo-3 Jul 23, 2026
f6c39ce
ブックマーク表示も1行サニタイズに揃える
mimo-3 Jul 23, 2026
3ee44bc
リマインダー表示も1行サニタイズに揃える
mimo-3 Jul 23, 2026
3c05a68
検索結果の行表示も1行サニタイズに揃える
mimo-3 Jul 23, 2026
b3af428
ユーザー一覧のsimple表示も1行に畳む
mimo-3 Jul 23, 2026
7b7b839
並列度テストを定数参照にする
mimo-3 Jul 23, 2026
ab9e0f2
チャンネル一覧表示も1行サニタイズに揃える
mimo-3 Jul 23, 2026
64c968b
未読チャンネル表示も1行サニタイズに揃える
mimo-3 Jul 23, 2026
6881348
presence表示も1行に畳む
mimo-3 Jul 23, 2026
6ee9c74
空ブロックのlint指摘を修正
mimo-3 Jul 23, 2026
7263b7e
mainの変更を取り込み
mimo-3 Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mimo-3/slack-cli",
"version": "0.24.0",
"version": "0.24.1",
"description": "A command-line tool for sending messages to Slack",
"main": "dist/index.js",
"bin": {
Expand Down
14 changes: 7 additions & 7 deletions src/commands/usergroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { renderByFormat, withSlackClient } from '../utils/command-support';
import { wrapCommand } from '../utils/command-wrapper';
import { createMembersFormatter, MemberInfo } from '../utils/formatters/members-formatters';
import { parseFormat } from '../utils/option-parsers';
import { sanitizeTerminalData, sanitizeTerminalText } from '../utils/terminal-sanitizer';
import { sanitizeSingleLineText, sanitizeTerminalData } from '../utils/terminal-sanitizer';
import { createValidationHook, optionValidators } from '../utils/validators';

function renderUsergroupTable(usergroups: SlackUsergroup[]) {
const rows = usergroups.map((usergroup) => ({
id: sanitizeTerminalText(usergroup.id || ''),
handle: sanitizeTerminalText(usergroup.handle || ''),
name: sanitizeTerminalText(usergroup.name || ''),
description: sanitizeTerminalText(usergroup.description || ''),
id: sanitizeSingleLineText(usergroup.id || ''),
handle: sanitizeSingleLineText(usergroup.handle || ''),
name: sanitizeSingleLineText(usergroup.name || ''),
description: sanitizeSingleLineText(usergroup.description || ''),
user_count: usergroup.user_count ?? '',
}));

Expand All @@ -23,9 +23,9 @@ function renderUsergroupTable(usergroups: SlackUsergroup[]) {
function renderUsergroupSimple(usergroups: SlackUsergroup[]) {
for (const usergroup of usergroups) {
console.log(
`${sanitizeTerminalText(usergroup.id || '')}\t@${sanitizeTerminalText(
`${sanitizeSingleLineText(usergroup.id || '')}\t@${sanitizeSingleLineText(
usergroup.handle || ''
)}\t${sanitizeTerminalText(usergroup.name || '')}`
)}\t${sanitizeSingleLineText(usergroup.name || '')}`
);
}
}
Expand Down
18 changes: 11 additions & 7 deletions src/commands/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { SlackUser, UserPresence } from '../types/slack';
import { renderByFormat, withSlackClient } from '../utils/command-support';
import { wrapCommand } from '../utils/command-wrapper';
import { parseLimit } from '../utils/option-parsers';
import { sanitizeTerminalData, sanitizeTerminalText } from '../utils/terminal-sanitizer';
import {
sanitizeSingleLineText,
sanitizeTerminalData,
sanitizeTerminalText,
} from '../utils/terminal-sanitizer';
import { createValidationHook, optionValidators } from '../utils/validators';

function renderUserTable(users: SlackUser[]) {
Expand All @@ -27,11 +31,11 @@ function renderUserTable(users: SlackUser[]) {

function renderUserSimple(users: SlackUser[]) {
for (const user of users) {
const email = user.profile?.email ? ` <${sanitizeTerminalText(user.profile.email)}>` : '';
const email = user.profile?.email ? ` <${sanitizeSingleLineText(user.profile.email)}>` : '';
console.log(
`${sanitizeTerminalText(user.id || '')}\t${sanitizeTerminalText(
`${sanitizeSingleLineText(user.id || '')}\t${sanitizeSingleLineText(
user.name || ''
)}\t${sanitizeTerminalText(user.real_name || '')}${email}`
)}\t${sanitizeSingleLineText(user.real_name || '')}${email}`
);
}
}
Expand Down Expand Up @@ -61,15 +65,15 @@ function renderUserInfo(user: SlackUser) {
function renderPresenceTable(userId: string, presence: UserPresence) {
const rows = [
{
user: sanitizeTerminalText(userId),
presence: sanitizeTerminalText(presence.presence),
user: sanitizeSingleLineText(userId),
presence: sanitizeSingleLineText(presence.presence),
},
];
console.table(sanitizeTerminalData(rows));
}

function renderPresenceSimple(userId: string, presence: UserPresence) {
console.log(`${sanitizeTerminalText(userId)}\t${sanitizeTerminalText(presence.presence)}`);
console.log(`${sanitizeSingleLineText(userId)}\t${sanitizeSingleLineText(presence.presence)}`);
}

export function setupUsersCommand(): Command {
Expand Down
9 changes: 7 additions & 2 deletions src/utils/command-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import chalk from 'chalk';
import { extractErrorMessage } from './error-utils';
import { sanitizeTerminalText } from './terminal-sanitizer';
import { redactSlackTokens } from './token-utils';

export type CommandAction<T = unknown> = (options: T) => Promise<void> | void;
Expand All @@ -9,10 +10,14 @@ export function wrapCommand<T = unknown>(action: CommandAction<T>): CommandActio
try {
await action(options);
} catch (error) {
console.error(chalk.red('✗ Error:'), redactSlackTokens(extractErrorMessage(error)));
// Sanitize first so tokens split by injected escape sequences are still redacted.
console.error(
chalk.red('✗ Error:'),
redactSlackTokens(sanitizeTerminalText(extractErrorMessage(error)))
);

if (process.env.NODE_ENV === 'development' && error instanceof Error) {
console.error(chalk.gray(redactSlackTokens(error.stack)));
console.error(chalk.gray(redactSlackTokens(sanitizeTerminalText(error.stack ?? ''))));
}

process.exit(1);
Expand Down
10 changes: 5 additions & 5 deletions src/utils/formatters/bookmark-formatters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sanitizeTerminalText } from '../terminal-sanitizer';
import { sanitizeSingleLineText } from '../terminal-sanitizer';
import { AbstractFormatter, createFormatterFactory, JsonFormatter } from './base-formatter';

export interface BookmarkItem {
Expand Down Expand Up @@ -35,9 +35,9 @@ class BookmarkTableFormatter extends AbstractFormatter<BookmarkFormatterOptions>
console.log('\u2500'.repeat(channelWidth + tsWidth + textWidth + savedAtWidth));

items.forEach((item) => {
const channel = sanitizeTerminalText(item.channel || '').padEnd(channelWidth);
const ts = sanitizeTerminalText(item.message?.ts || '').padEnd(tsWidth);
const text = sanitizeTerminalText(item.message?.text || '')
const channel = sanitizeSingleLineText(item.channel || '').padEnd(channelWidth);
const ts = sanitizeSingleLineText(item.message?.ts || '').padEnd(tsWidth);
const text = sanitizeSingleLineText(item.message?.text || '')
.slice(0, textWidth - 2)
.padEnd(textWidth);
const savedAt = formatDate(item.date_create).padEnd(savedAtWidth);
Expand All @@ -52,7 +52,7 @@ class BookmarkSimpleFormatter extends AbstractFormatter<BookmarkFormatterOptions
items.forEach((item) => {
const savedAt = formatDate(item.date_create);
console.log(
`${sanitizeTerminalText(item.channel || '')}\t${sanitizeTerminalText(item.message?.ts || '')}\t${sanitizeTerminalText(item.message?.text || '')}\t${savedAt}`
`${sanitizeSingleLineText(item.channel || '')}\t${sanitizeSingleLineText(item.message?.ts || '')}\t${sanitizeSingleLineText(item.message?.text || '')}\t${savedAt}`
);
});
}
Expand Down
14 changes: 8 additions & 6 deletions src/utils/formatters/channel-formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import chalk from 'chalk';
import { Channel } from '../../types/slack';
import { formatChannelName } from '../channel-formatter';
import { formatSlackTimestamp } from '../date-utils';
import { sanitizeSingleLineText } from '../terminal-sanitizer';
import { AbstractFormatter, createFormatterFactory, JsonFormatter } from './base-formatter';

function singleLineChannelName(channel: Channel): string {
return sanitizeSingleLineText(channel.display_name || formatChannelName(channel.name));
}

export interface ChannelFormatterOptions {
channels: Channel[];
countOnly?: boolean;
Expand All @@ -15,8 +20,7 @@ class ChannelTableFormatter extends AbstractFormatter<ChannelFormatterOptions> {
console.log('─'.repeat(50));

channels.forEach((channel) => {
const channelName = channel.display_name || formatChannelName(channel.name);
const paddedName = channelName.padEnd(16);
const paddedName = singleLineChannelName(channel).padEnd(16);
const count = (channel.unread_count || 0).toString().padEnd(6);
const lastRead = channel.last_read ? formatSlackTimestamp(channel.last_read) : 'Unknown';
console.log(`${paddedName} ${count} ${lastRead}`);
Expand All @@ -27,8 +31,7 @@ class ChannelTableFormatter extends AbstractFormatter<ChannelFormatterOptions> {
class ChannelSimpleFormatter extends AbstractFormatter<ChannelFormatterOptions> {
format({ channels }: ChannelFormatterOptions): void {
channels.forEach((channel) => {
const channelName = channel.display_name || formatChannelName(channel.name);
console.log(`${channelName} (${channel.unread_count || 0})`);
console.log(`${singleLineChannelName(channel)} (${channel.unread_count || 0})`);
});
}
}
Expand All @@ -49,8 +52,7 @@ class ChannelCountFormatter extends AbstractFormatter<ChannelFormatterOptions> {
channels.forEach((channel) => {
const count = channel.unread_count || 0;
totalUnread += count;
const channelName = channel.display_name || formatChannelName(channel.name);
console.log(`${channelName}: ${count}`);
console.log(`${singleLineChannelName(channel)}: ${count}`);
});
console.log(chalk.bold(`Total: ${totalUnread} unread messages`));
}
Expand Down
8 changes: 4 additions & 4 deletions src/utils/formatters/channels-list-formatters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChannelInfo } from '../channel-formatter';
import { sanitizeTerminalText } from '../terminal-sanitizer';
import { sanitizeSingleLineText } from '../terminal-sanitizer';
import { AbstractFormatter, createFormatterFactory, JsonFormatter } from './base-formatter';

export interface ChannelsListFormatterOptions {
Expand All @@ -14,8 +14,8 @@ class ChannelsTableFormatter extends AbstractFormatter<ChannelsListFormatterOpti

// Print channel rows
channels.forEach((channel) => {
const safeName = sanitizeTerminalText(channel.name);
const safePurpose = sanitizeTerminalText(channel.purpose);
const safeName = sanitizeSingleLineText(channel.name);
const safePurpose = sanitizeSingleLineText(channel.purpose);
const name = safeName.padEnd(17);
const type = channel.type.padEnd(9);
const members = channel.members.toString().padEnd(8);
Expand All @@ -29,7 +29,7 @@ class ChannelsTableFormatter extends AbstractFormatter<ChannelsListFormatterOpti

class ChannelsSimpleFormatter extends AbstractFormatter<ChannelsListFormatterOptions> {
format({ channels }: ChannelsListFormatterOptions): void {
channels.forEach((channel) => console.log(sanitizeTerminalText(channel.name)));
channels.forEach((channel) => console.log(sanitizeSingleLineText(channel.name)));
}
}

Expand Down
10 changes: 5 additions & 5 deletions src/utils/formatters/members-formatters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sanitizeTerminalText } from '../terminal-sanitizer';
import { sanitizeSingleLineText } from '../terminal-sanitizer';
import { AbstractFormatter, createFormatterFactory, JsonFormatter } from './base-formatter';

export interface MemberInfo {
Expand All @@ -17,9 +17,9 @@ class MembersTableFormatter extends AbstractFormatter<MembersFormatterOptions> {
console.log('\u2500'.repeat(60));

members.forEach((member) => {
const id = sanitizeTerminalText(member.id || '').padEnd(17);
const name = sanitizeTerminalText(member.name || '').padEnd(17);
const realName = sanitizeTerminalText(member.realName || '');
const id = sanitizeSingleLineText(member.id || '').padEnd(17);
const name = sanitizeSingleLineText(member.name || '').padEnd(17);
const realName = sanitizeSingleLineText(member.realName || '');

console.log(`${id} ${name} ${realName}`);
});
Expand All @@ -30,7 +30,7 @@ class MembersSimpleFormatter extends AbstractFormatter<MembersFormatterOptions>
format({ members }: MembersFormatterOptions): void {
members.forEach((member) => {
console.log(
`${sanitizeTerminalText(member.id || '')}\t${sanitizeTerminalText(member.name || '')}\t${sanitizeTerminalText(member.realName || '')}`
`${sanitizeSingleLineText(member.id || '')}\t${sanitizeSingleLineText(member.name || '')}\t${sanitizeSingleLineText(member.realName || '')}`
);
});
}
Expand Down
8 changes: 4 additions & 4 deletions src/utils/formatters/reminder-formatters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sanitizeTerminalText } from '../terminal-sanitizer';
import { sanitizeSingleLineText } from '../terminal-sanitizer';
import { AbstractFormatter, createFormatterFactory, JsonFormatter } from './base-formatter';

export interface ReminderInfo {
Expand Down Expand Up @@ -37,8 +37,8 @@ class ReminderTableFormatter extends AbstractFormatter<ReminderFormatterOptions>
console.log('\u2500'.repeat(idWidth + textWidth + timeWidth + statusWidth));

reminders.forEach((reminder) => {
const id = sanitizeTerminalText(reminder.id || '').padEnd(idWidth);
const text = sanitizeTerminalText(reminder.text || '')
const id = sanitizeSingleLineText(reminder.id || '').padEnd(idWidth);
const text = sanitizeSingleLineText(reminder.text || '')
.slice(0, textWidth - 2)
.padEnd(textWidth);
const time = formatTime(reminder.time).padEnd(timeWidth);
Expand All @@ -55,7 +55,7 @@ class ReminderSimpleFormatter extends AbstractFormatter<ReminderFormatterOptions
const time = formatTime(reminder.time);
const status = getStatus(reminder.complete_ts);
console.log(
`${sanitizeTerminalText(reminder.id || '')}\t${sanitizeTerminalText(reminder.text || '')}\t${time}\t${status}`
`${sanitizeSingleLineText(reminder.id || '')}\t${sanitizeSingleLineText(reminder.text || '')}\t${time}\t${status}`
);
});
}
Expand Down
18 changes: 9 additions & 9 deletions src/utils/formatters/search-formatters.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import chalk from 'chalk';
import { SearchMatch } from '../../types/slack';
import { formatTimestampFixed } from '../date-utils';
import { sanitizeTerminalText } from '../terminal-sanitizer';
import { sanitizeSingleLineText, sanitizeTerminalText } from '../terminal-sanitizer';
import { AbstractFormatter, createFormatterFactory, JsonFormatter } from './base-formatter';

export interface SearchFormatterOptions {
Expand Down Expand Up @@ -30,16 +30,16 @@ class TableSearchFormatter extends AbstractFormatter<SearchFormatterOptions> {
console.log('');
matches.forEach((match) => {
const channel = match.channel.name
? `#${sanitizeTerminalText(match.channel.name)}`
: sanitizeTerminalText(match.channel.id || 'unknown');
const username = sanitizeTerminalText(match.username || match.user || 'Unknown');
? `#${sanitizeSingleLineText(match.channel.name)}`
: sanitizeSingleLineText(match.channel.id || 'unknown');
const username = sanitizeSingleLineText(match.username || match.user || 'Unknown');
const timestamp = match.ts ? formatTimestampFixed(match.ts) : '';
const text = sanitizeTerminalText(match.text || '(no text)');

console.log(`${chalk.gray(`[${timestamp}]`)} ${chalk.blue(channel)} ${chalk.cyan(username)}`);
console.log(text);
if (match.permalink) {
console.log(chalk.gray(sanitizeTerminalText(match.permalink)));
console.log(chalk.gray(sanitizeSingleLineText(match.permalink)));
}
console.log('');
});
Expand All @@ -59,11 +59,11 @@ class SimpleSearchFormatter extends AbstractFormatter<SearchFormatterOptions> {

matches.forEach((match) => {
const channel = match.channel.name
? `#${sanitizeTerminalText(match.channel.name)}`
: sanitizeTerminalText(match.channel.id || 'unknown');
const username = sanitizeTerminalText(match.username || match.user || 'Unknown');
? `#${sanitizeSingleLineText(match.channel.name)}`
: sanitizeSingleLineText(match.channel.id || 'unknown');
const username = sanitizeSingleLineText(match.username || match.user || 'Unknown');
const timestamp = match.ts ? formatTimestampFixed(match.ts) : '';
const text = sanitizeTerminalText(match.text || '(no text)');
const text = sanitizeSingleLineText(match.text || '(no text)');
console.log(`[${channel}] ${username} (${timestamp}): ${text}`);
});

Expand Down
2 changes: 1 addition & 1 deletion src/utils/slack-operations/user-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class UserOperations extends BaseSlackClient {
}

async getUserInfo(userId: string): Promise<SlackUser> {
const response = await this.client.users.info({ user: userId });
const response = await this.rateLimiter(() => this.client.users.info({ user: userId }));
return response.user as SlackUser;
}

Expand Down
9 changes: 9 additions & 0 deletions src/utils/terminal-sanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ export function sanitizeTerminalText(value: string): string {
return sanitized;
}

/**
* Sanitize a value for single-line output (TSV rows, table cells).
* Collapses whitespace runs (including newlines and tabs) into single spaces
* so untrusted values cannot forge extra rows or columns.
*/
export function sanitizeSingleLineText(value: string): string {
return sanitizeTerminalText(value).replace(/\s+/g, ' ').trim();
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
if (!value || typeof value !== 'object') {
return false;
Expand Down
23 changes: 23 additions & 0 deletions tests/commands/usergroups.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,29 @@ describe('usergroups command', () => {
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('engineers'));
});

it('should collapse newlines in simple format fields', async () => {
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
token: 'test-token',
updatedAt: new Date().toISOString(),
});
vi.mocked(mockSlackClient.listUsergroups).mockResolvedValue([
{
id: 'S123',
name: 'Engineering\nfake-row',
handle: 'eng\tineers',
description: 'team',
user_count: 10,
},
]);

await program.parseAsync(['node', 'slack-cli', 'usergroups', 'list', '--format', 'simple']);

const output = mockConsole.logSpy.mock.calls[0][0] as string;
expect(output).not.toContain('\n');
expect(output).toContain('Engineering fake-row');
expect(output).toContain('eng ineers');
});

it('should show message when no usergroups found', async () => {
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
token: 'test-token',
Expand Down
Loading
Loading