Skip to content

Commit a90ac85

Browse files
yamitzkyclaude
andauthored
fix: pass channel_ids as string in users invite (#9)
* fix: pass channel_ids as comma-separated string in users invite SDK type definition expects [string, ...string[]] but the actual Slack API expects a comma-separated string. The SDK's JSON serialization of arrays produces invalid channel IDs, causing failed_to_send_invite errors. Also adds Slack API error detail display and updates CLAUDE.md type safety rules. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add --verbose flag, suppress stack traces by default All errors now show a clean one-line message by default. Use --verbose to see full stack traces for debugging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c1c01ef commit a90ac85

3 files changed

Lines changed: 35 additions & 13 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ skills/ # Agent Skill 定義
6262

6363
### 型安全性ルール
6464

65-
- **`as` キャスト禁止**: 実装コードで `as` を使わない。`satisfies` または設計の見直しで対応
65+
- **`as` キャスト原則禁止**: 実装コードで `as` を使わない。`satisfies` または設計の見直しで対応
66+
- **SDK 型バグの回避**: SDK の型定義が実際の API と異なる場合、`apiCall()` より `as` での回避を優先する
6667
- **discriminated union**: `client.apiCall(methodName, params)``Record<string, unknown>` として渡す(void メソッド)
6768
- **データ返却メソッド**: ブランチパターンで型安全に(`if (opts.teamId) { ... } else if (opts.enterpriseId) { ... }`
6869

src/commands/users/invite.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { WebClient } from "@slack/web-api";
33
interface UsersInviteOptions {
44
teamId: string;
55
email: string;
6-
channelIds: [string, ...string[]];
6+
channelIds: string;
77
customMessage?: string;
88
realName?: string;
99
isRestricted?: boolean;
@@ -13,10 +13,11 @@ interface UsersInviteOptions {
1313
}
1414

1515
export async function executeUsersInvite(client: WebClient, opts: UsersInviteOptions) {
16+
// SDK型定義は channel_ids: [string, ...string[]] だが、実際のAPIはカンマ区切り文字列を期待する
1617
return await client.admin.users.invite({
1718
team_id: opts.teamId,
1819
email: opts.email,
19-
channel_ids: opts.channelIds,
20+
channel_ids: opts.channelIds as unknown as [string, ...string[]],
2021
...(opts.customMessage !== undefined ? { custom_message: opts.customMessage } : {}),
2122
...(opts.realName !== undefined ? { real_name: opts.realName } : {}),
2223
...(opts.isRestricted !== undefined ? { is_restricted: opts.isRestricted } : {}),

src/index.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,19 @@ const discoverabilityValueParser: ValueParser<"sync", TeamDiscoverability> = {
141141
// Global flags (parsed manually from process.argv)
142142
// ---------------------------------------------------------------------------
143143

144-
const GLOBAL_FLAGS = new Set(["--json", "--plain"]);
144+
const GLOBAL_FLAGS = new Set(["--json", "--plain", "--verbose"]);
145145
const GLOBAL_FLAGS_WITH_VALUE = new Set(["--profile"]);
146146

147147
function parseGlobalFlags(argv: string[]): {
148148
json: boolean;
149149
plain: boolean;
150+
verbose: boolean;
150151
profile: string | undefined;
151152
rest: string[];
152153
} {
153154
let json = false;
154155
let plain = false;
156+
let verbose = false;
155157
let profile: string | undefined;
156158
const rest: string[] = [];
157159

@@ -161,6 +163,8 @@ function parseGlobalFlags(argv: string[]): {
161163
json = true;
162164
} else if (arg === "--plain") {
163165
plain = true;
166+
} else if (arg === "--verbose") {
167+
verbose = true;
164168
} else if (arg === "--profile") {
165169
profile = argv[i + 1];
166170
i++;
@@ -169,12 +173,13 @@ function parseGlobalFlags(argv: string[]): {
169173
}
170174
}
171175

172-
return { json, plain, profile, rest };
176+
return { json, plain, verbose, profile, rest };
173177
}
174178

175179
const globalFlags = parseGlobalFlags(process.argv.slice(2));
176180
const jsonFlag = globalFlags.json;
177181
const plainFlag = globalFlags.plain;
182+
const verboseFlag = globalFlags.verbose;
178183
const profileFlag = globalFlags.profile;
179184

180185
const outputFormat: OutputFormat = jsonFlag ? "json" : plainFlag ? "plain" : "table";
@@ -963,16 +968,10 @@ switch (config.cmd) {
963968
}
964969
case "users-invite": {
965970
const client = await createSlackClient(store, profileFlag);
966-
const inviteChannelParts = config.channelIds.split(",");
967-
const inviteFirstChannel = inviteChannelParts[0];
968-
if (inviteFirstChannel === undefined) {
969-
throw new Error("--channel-ids must not be empty");
970-
}
971-
const inviteChannelIds: [string, ...string[]] = [inviteFirstChannel, ...inviteChannelParts.slice(1)];
972971
await executeUsersInvite(client, {
973972
teamId: config.teamId,
974973
email: config.email,
975-
channelIds: inviteChannelIds,
974+
channelIds: config.channelIds,
976975
customMessage: config.customMessage,
977976
realName: config.realName,
978977
isRestricted: config.isRestricted,
@@ -1712,4 +1711,25 @@ switch (config.cmd) {
17121711
}
17131712
}
17141713

1715-
main();
1714+
main().catch((err) => {
1715+
if (verboseFlag) {
1716+
throw err;
1717+
}
1718+
if (err.code === "slack_webapi_platform_error" && err.data) {
1719+
console.error(`Slack API error: ${err.data.error}`);
1720+
if (err.data.needed) {
1721+
console.error(` needed scope: ${err.data.needed}`);
1722+
}
1723+
if (err.data.provided) {
1724+
console.error(` provided scopes: ${err.data.provided}`);
1725+
}
1726+
if (err.data.response_metadata?.messages) {
1727+
for (const msg of err.data.response_metadata.messages) {
1728+
console.error(` ${msg}`);
1729+
}
1730+
}
1731+
} else {
1732+
console.error(`Error: ${err.message ?? err}`);
1733+
}
1734+
process.exit(1);
1735+
});

0 commit comments

Comments
 (0)