mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 00:17:47 +00:00
feat(channels): auto-enable official Claude channels
This commit is contained in:
+15
-36
@@ -36,17 +36,11 @@ import { fail, info, warn } from './utils/ui';
|
||||
import { isCopilotSubcommandToken } from './copilot/constants';
|
||||
import {
|
||||
buildOfficialChannelsArgs,
|
||||
getOfficialChannelDisplayName,
|
||||
getOfficialChannelTokenIds,
|
||||
isBunAvailable,
|
||||
getOfficialChannelsEnvironmentStatus,
|
||||
officialChannelRequiresMacOS,
|
||||
resolveOfficialChannelsLaunchPlan,
|
||||
resolveOfficialChannelsSyncConfigDir,
|
||||
} from './channels/official-channels-runtime';
|
||||
import {
|
||||
getOfficialChannelReadiness,
|
||||
syncOfficialChannelEnvToConfigDir,
|
||||
} from './channels/official-channels-store';
|
||||
import { getOfficialChannelReadiness } from './channels/official-channels-store';
|
||||
|
||||
// Import centralized error handling
|
||||
import { handleError, runCleanup } from './errors';
|
||||
@@ -149,6 +143,9 @@ function resolveNativeClaudeLaunchArgs(
|
||||
targetConfigDir?: string
|
||||
): string[] {
|
||||
const config = getOfficialChannelsConfig();
|
||||
const environment = getOfficialChannelsEnvironmentStatus(
|
||||
targetConfigDir ? { CLAUDE_CONFIG_DIR: targetConfigDir } : undefined
|
||||
);
|
||||
const channelReadiness = {
|
||||
telegram: getOfficialChannelReadiness('telegram'),
|
||||
discord: getOfficialChannelReadiness('discord'),
|
||||
@@ -159,7 +156,7 @@ function resolveNativeClaudeLaunchArgs(
|
||||
config,
|
||||
target: 'claude',
|
||||
profileType,
|
||||
bunAvailable: isBunAvailable(),
|
||||
environment,
|
||||
channelReadiness,
|
||||
});
|
||||
|
||||
@@ -167,37 +164,19 @@ function resolveNativeClaudeLaunchArgs(
|
||||
console.error(warn(message));
|
||||
}
|
||||
|
||||
if (
|
||||
config.selected.length > 0 &&
|
||||
environment.auth.state === 'eligible' &&
|
||||
environment.auth.orgRequirementMessage
|
||||
) {
|
||||
console.error(warn(environment.auth.orgRequirementMessage));
|
||||
}
|
||||
|
||||
if (!plan.applied) {
|
||||
return args;
|
||||
}
|
||||
|
||||
const activeConfigDir = resolveOfficialChannelsSyncConfigDir(targetConfigDir);
|
||||
const syncedChannels = [...plan.appliedChannels];
|
||||
|
||||
if (activeConfigDir) {
|
||||
for (const channelId of [...syncedChannels]) {
|
||||
if (!getOfficialChannelTokenIds().includes(channelId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const syncResult = syncOfficialChannelEnvToConfigDir(channelId, activeConfigDir);
|
||||
if (!syncResult.synced && syncResult.reason !== 'already_current') {
|
||||
const suffix = syncResult.error ? ` (${syncResult.error})` : '';
|
||||
console.error(
|
||||
warn(
|
||||
`${getOfficialChannelDisplayName(channelId)} auto-enable skipped: failed to sync channel env to ${syncResult.targetPath}${suffix}`
|
||||
)
|
||||
);
|
||||
syncedChannels.splice(syncedChannels.indexOf(channelId), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (syncedChannels.length === 0) {
|
||||
return args;
|
||||
}
|
||||
|
||||
return buildOfficialChannelsArgs(args, syncedChannels, plan.wantsPermissionBypass);
|
||||
return buildOfficialChannelsArgs(args, plan.appliedChannels, plan.wantsPermissionBypass);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
|
||||
@@ -2,6 +2,13 @@ import { spawnSync } from 'child_process';
|
||||
import type { TargetType } from '../targets/target-adapter';
|
||||
import type { ProfileType } from '../types/profile';
|
||||
import type { OfficialChannelId, OfficialChannelsConfig } from '../config/unified-config-types';
|
||||
import type { OfficialChannelTokenSource } from './official-channels-store';
|
||||
import {
|
||||
getClaudeAuthStatus,
|
||||
getClaudeCliVersion,
|
||||
isClaudeCliVersionAtLeast,
|
||||
type ClaudeAuthStatus,
|
||||
} from '../utils/claude-detector';
|
||||
|
||||
export interface OfficialChannelDefinition {
|
||||
id: OfficialChannelId;
|
||||
@@ -9,6 +16,7 @@ export interface OfficialChannelDefinition {
|
||||
pluginSpec: string;
|
||||
envKey?: string;
|
||||
envDir: string;
|
||||
stateDirEnvKey: string;
|
||||
requiresMacOS?: boolean;
|
||||
manualSetupCommands: string[];
|
||||
}
|
||||
@@ -20,6 +28,7 @@ export const OFFICIAL_CHANNELS: Record<OfficialChannelId, OfficialChannelDefinit
|
||||
pluginSpec: 'plugin:telegram@claude-plugins-official',
|
||||
envKey: 'TELEGRAM_BOT_TOKEN',
|
||||
envDir: 'telegram',
|
||||
stateDirEnvKey: 'TELEGRAM_STATE_DIR',
|
||||
manualSetupCommands: [
|
||||
'/plugin install telegram@claude-plugins-official',
|
||||
'/telegram:configure <token>',
|
||||
@@ -33,6 +42,7 @@ export const OFFICIAL_CHANNELS: Record<OfficialChannelId, OfficialChannelDefinit
|
||||
pluginSpec: 'plugin:discord@claude-plugins-official',
|
||||
envKey: 'DISCORD_BOT_TOKEN',
|
||||
envDir: 'discord',
|
||||
stateDirEnvKey: 'DISCORD_STATE_DIR',
|
||||
manualSetupCommands: [
|
||||
'/plugin install discord@claude-plugins-official',
|
||||
'/discord:configure <token>',
|
||||
@@ -45,6 +55,7 @@ export const OFFICIAL_CHANNELS: Record<OfficialChannelId, OfficialChannelDefinit
|
||||
displayName: 'iMessage',
|
||||
pluginSpec: 'plugin:imessage@claude-plugins-official',
|
||||
envDir: 'imessage',
|
||||
stateDirEnvKey: 'IMESSAGE_STATE_DIR',
|
||||
requiresMacOS: true,
|
||||
manualSetupCommands: [
|
||||
'/plugin install imessage@claude-plugins-official',
|
||||
@@ -54,6 +65,71 @@ export const OFFICIAL_CHANNELS: Record<OfficialChannelId, OfficialChannelDefinit
|
||||
};
|
||||
|
||||
export const OFFICIAL_CHANNEL_IDS = Object.keys(OFFICIAL_CHANNELS) as OfficialChannelId[];
|
||||
export const MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION = '2.1.80';
|
||||
|
||||
export interface OfficialChannelsVersionSummary {
|
||||
current: string | null;
|
||||
minimum: string;
|
||||
state: 'supported' | 'unsupported' | 'unknown';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface OfficialChannelsAuthSummary {
|
||||
checked: boolean;
|
||||
loggedIn: boolean;
|
||||
authMethod: string | null;
|
||||
subscriptionType: string | null;
|
||||
state: 'eligible' | 'ineligible' | 'unknown';
|
||||
eligible: boolean;
|
||||
message: string;
|
||||
orgRequirementMessage?: string;
|
||||
}
|
||||
|
||||
export interface OfficialChannelsEnvironmentStatus {
|
||||
bunInstalled: boolean;
|
||||
supportedProfiles: string[];
|
||||
stateScopeMessage: string;
|
||||
claudeVersion: OfficialChannelsVersionSummary;
|
||||
auth: OfficialChannelsAuthSummary;
|
||||
}
|
||||
|
||||
export interface OfficialChannelSetupSummary {
|
||||
state: 'not_selected' | 'ready' | 'needs_token' | 'needs_claude_setup' | 'unavailable';
|
||||
label: string;
|
||||
detail: string;
|
||||
nextStep: string;
|
||||
}
|
||||
|
||||
export interface OfficialChannelsReadinessSummary {
|
||||
state: 'ready' | 'needs_setup' | 'limited';
|
||||
title: string;
|
||||
message: string;
|
||||
nextStep: string;
|
||||
blockers: string[];
|
||||
}
|
||||
|
||||
export interface OfficialChannelsLaunchPreview {
|
||||
state: 'disabled' | 'blocked' | 'partial' | 'ready';
|
||||
title: string;
|
||||
detail: string;
|
||||
command: string;
|
||||
appendedArgs: string[];
|
||||
appliedChannels: OfficialChannelId[];
|
||||
permissionBypassIncluded: boolean;
|
||||
skippedMessages: string[];
|
||||
}
|
||||
|
||||
export interface OfficialChannelsStatusChannelInput {
|
||||
id: OfficialChannelId;
|
||||
displayName: string;
|
||||
selected: boolean;
|
||||
requiresToken: boolean;
|
||||
tokenAvailable: boolean;
|
||||
tokenSource?: OfficialChannelTokenSource;
|
||||
savedInClaudeState?: boolean;
|
||||
processEnvAvailable?: boolean;
|
||||
unavailableReason?: string;
|
||||
}
|
||||
|
||||
export interface DiscordChannelsLaunchPlan {
|
||||
applied: boolean;
|
||||
@@ -67,7 +143,7 @@ interface DiscordChannelsLaunchInput {
|
||||
config: OfficialChannelsConfig;
|
||||
target: TargetType;
|
||||
profileType: ProfileType;
|
||||
bunAvailable: boolean;
|
||||
environment: OfficialChannelsEnvironmentStatus;
|
||||
channelReadiness: Record<OfficialChannelId, boolean>;
|
||||
}
|
||||
|
||||
@@ -114,14 +190,125 @@ export function hasExplicitChannelsFlag(args: string[]): boolean {
|
||||
export function hasExplicitPermissionOverride(args: string[]): boolean {
|
||||
return args.some(
|
||||
(arg) =>
|
||||
arg === '--allow-dangerously-skip-permissions' ||
|
||||
arg === '--dangerously-skip-permissions' ||
|
||||
arg === '--permission-mode' ||
|
||||
arg.startsWith('--permission-mode=')
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveOfficialChannelsSyncConfigDir(targetConfigDir?: string): string | undefined {
|
||||
return targetConfigDir ?? process.env.CLAUDE_CONFIG_DIR;
|
||||
function isTeamOrEnterpriseSubscription(subscriptionType: string | null): boolean {
|
||||
const normalized = subscriptionType?.trim().toLowerCase() ?? '';
|
||||
return normalized.includes('team') || normalized.includes('enterprise');
|
||||
}
|
||||
|
||||
export function resolveOfficialChannelsVersionSummary(
|
||||
version: string | null
|
||||
): OfficialChannelsVersionSummary {
|
||||
if (!version) {
|
||||
return {
|
||||
current: null,
|
||||
minimum: MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION,
|
||||
state: 'unknown',
|
||||
message: `Unable to detect Claude Code version. Official Channels require v${MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION}+.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (isClaudeCliVersionAtLeast(version, MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION)) {
|
||||
return {
|
||||
current: version,
|
||||
minimum: MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION,
|
||||
state: 'supported',
|
||||
message: `Claude Code v${version}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
current: version,
|
||||
minimum: MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION,
|
||||
state: 'unsupported',
|
||||
message: `Official Channels require Claude Code v${MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION}+ (found v${version}).`,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveOfficialChannelsAuthSummary(
|
||||
authStatus: ClaudeAuthStatus | null
|
||||
): OfficialChannelsAuthSummary {
|
||||
if (!authStatus) {
|
||||
return {
|
||||
checked: false,
|
||||
loggedIn: false,
|
||||
authMethod: null,
|
||||
subscriptionType: null,
|
||||
state: 'unknown',
|
||||
eligible: false,
|
||||
message: 'Unable to verify Claude auth status. Official Channels require claude.ai login.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!authStatus.loggedIn) {
|
||||
return {
|
||||
checked: true,
|
||||
loggedIn: false,
|
||||
authMethod: authStatus.authMethod ?? null,
|
||||
subscriptionType: authStatus.subscriptionType ?? null,
|
||||
state: 'ineligible',
|
||||
eligible: false,
|
||||
message: 'Official Channels require claude.ai login. Run `claude auth login` first.',
|
||||
};
|
||||
}
|
||||
|
||||
if (authStatus.authMethod !== 'claude.ai') {
|
||||
return {
|
||||
checked: true,
|
||||
loggedIn: true,
|
||||
authMethod: authStatus.authMethod ?? null,
|
||||
subscriptionType: authStatus.subscriptionType ?? null,
|
||||
state: 'ineligible',
|
||||
eligible: false,
|
||||
message: `Official Channels require claude.ai login. Current auth method: ${authStatus.authMethod ?? 'unknown'}.`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
checked: true,
|
||||
loggedIn: true,
|
||||
authMethod: authStatus.authMethod,
|
||||
subscriptionType: authStatus.subscriptionType ?? null,
|
||||
state: 'eligible',
|
||||
eligible: true,
|
||||
message: 'Authenticated with claude.ai.',
|
||||
...(isTeamOrEnterpriseSubscription(authStatus.subscriptionType ?? null)
|
||||
? {
|
||||
orgRequirementMessage:
|
||||
'Team and Enterprise orgs also need channels enabled by an admin before messages will arrive.',
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function getOfficialChannelsStateScopeMessage(): string {
|
||||
return "Telegram and Discord tokens live in Claude's machine-level channel state under ~/.claude/channels/. Native Claude sessions share that state unless you manually override the official *_STATE_DIR variables.";
|
||||
}
|
||||
|
||||
export function getOfficialChannelsSupportMessage(): string {
|
||||
return 'Works only for native Claude default/account sessions. It does not apply to API, OAuth, or Droid targets such as `ccs glm`, `ccs gemini`, `ccs codex`, or `ccs --target droid`.';
|
||||
}
|
||||
|
||||
export function getOfficialChannelsAccountStatusCaveat(): string {
|
||||
return 'Dashboard status reflects the base Claude install visible to the current CCS process. Isolated native account sessions can still differ until that account signs in with claude.ai.';
|
||||
}
|
||||
|
||||
export function getOfficialChannelsEnvironmentStatus(
|
||||
authEnvOverrides?: NodeJS.ProcessEnv
|
||||
): OfficialChannelsEnvironmentStatus {
|
||||
return {
|
||||
bunInstalled: isBunAvailable(),
|
||||
supportedProfiles: getOfficialChannelsSupportedProfiles(),
|
||||
stateScopeMessage: getOfficialChannelsStateScopeMessage(),
|
||||
claudeVersion: resolveOfficialChannelsVersionSummary(getClaudeCliVersion()),
|
||||
auth: resolveOfficialChannelsAuthSummary(getClaudeAuthStatus(authEnvOverrides)),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOfficialChannelsArgs(
|
||||
@@ -145,7 +332,7 @@ export function buildOfficialChannelsArgs(
|
||||
export function resolveOfficialChannelsLaunchPlan(
|
||||
input: DiscordChannelsLaunchInput
|
||||
): DiscordChannelsLaunchPlan {
|
||||
const { args, config, target, profileType, bunAvailable, channelReadiness } = input;
|
||||
const { args, config, target, profileType, environment, channelReadiness } = input;
|
||||
const skippedMessages: string[] = [];
|
||||
|
||||
if (config.selected.length === 0) {
|
||||
@@ -162,9 +349,7 @@ export function resolveOfficialChannelsLaunchPlan(
|
||||
applied: false,
|
||||
wantsPermissionBypass: false,
|
||||
appliedChannels: [],
|
||||
skippedMessages: [
|
||||
'Official Channels auto-enable only applies to native Claude default/account sessions.',
|
||||
],
|
||||
skippedMessages: [getOfficialChannelsCompatibilityMessage()],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -177,7 +362,7 @@ export function resolveOfficialChannelsLaunchPlan(
|
||||
};
|
||||
}
|
||||
|
||||
if (!bunAvailable) {
|
||||
if (!environment.bunInstalled) {
|
||||
return {
|
||||
applied: false,
|
||||
wantsPermissionBypass: false,
|
||||
@@ -186,6 +371,24 @@ export function resolveOfficialChannelsLaunchPlan(
|
||||
};
|
||||
}
|
||||
|
||||
if (environment.claudeVersion.state !== 'supported') {
|
||||
return {
|
||||
applied: false,
|
||||
wantsPermissionBypass: false,
|
||||
appliedChannels: [],
|
||||
skippedMessages: [environment.claudeVersion.message],
|
||||
};
|
||||
}
|
||||
|
||||
if (environment.auth.state !== 'eligible') {
|
||||
return {
|
||||
applied: false,
|
||||
wantsPermissionBypass: false,
|
||||
appliedChannels: [],
|
||||
skippedMessages: [environment.auth.message],
|
||||
};
|
||||
}
|
||||
|
||||
const appliedChannels: OfficialChannelId[] = [];
|
||||
|
||||
for (const channelId of normalizeOfficialChannelIds(config.selected)) {
|
||||
@@ -244,15 +447,19 @@ export function getOfficialChannelEnvDir(channelId: OfficialChannelId): string {
|
||||
return OFFICIAL_CHANNELS[channelId].envDir;
|
||||
}
|
||||
|
||||
export function getOfficialChannelStateDirEnvKey(channelId: OfficialChannelId): string {
|
||||
return OFFICIAL_CHANNELS[channelId].stateDirEnvKey;
|
||||
}
|
||||
|
||||
export function getOfficialChannelSummary(channelId: OfficialChannelId): string {
|
||||
if (channelId === 'telegram') {
|
||||
return 'Bot token required. Polls your Telegram bot while Claude is running.';
|
||||
return 'Bot token required. Runtime-only while Claude is running; Telegram pairing and access policy still happen in Claude.';
|
||||
}
|
||||
if (channelId === 'discord') {
|
||||
return 'Bot token required. Receives DMs and allowed server messages while Claude is running.';
|
||||
return 'Bot token required. Runtime-only while Claude is running; Discord pairing and access policy still happen in Claude.';
|
||||
}
|
||||
|
||||
return 'macOS-only. No bot token required, but Messages permissions are required.';
|
||||
return 'macOS-only. Runtime-only while Claude is running; plugin install, Full Disk Access, and the first-reply Automation approval are still required.';
|
||||
}
|
||||
|
||||
export function getOfficialChannelUnavailableReason(
|
||||
@@ -268,14 +475,258 @@ export function getOfficialChannelUnavailableReason(
|
||||
export function getOfficialChannelReadyMessage(channelId: OfficialChannelId): string {
|
||||
if (channelId === 'imessage') {
|
||||
return isMacOS()
|
||||
? 'Ready after Claude-side install and macOS permissions.'
|
||||
? 'Needs Claude-side install plus Full Disk Access and the first-reply Automation prompt.'
|
||||
: 'Unavailable on this platform.';
|
||||
}
|
||||
|
||||
const envKey = getOfficialChannelEnvKey(channelId);
|
||||
return envKey
|
||||
? `${envKey} must be configured before CCS can auto-enable this channel.`
|
||||
: 'Ready.';
|
||||
? `${envKey} must be configured before CCS can auto-enable this channel. Claude-side pairing and access policy are still required.`
|
||||
: 'Claude-side setup required.';
|
||||
}
|
||||
|
||||
export function buildOfficialChannelSetupSummary(
|
||||
channel: OfficialChannelsStatusChannelInput
|
||||
): OfficialChannelSetupSummary {
|
||||
if (!channel.selected) {
|
||||
return {
|
||||
state: 'not_selected',
|
||||
label: 'Not selected',
|
||||
detail: 'CCS will not auto-add this channel until you turn it on here.',
|
||||
nextStep: 'Turn this channel on if you want CCS to add it on supported native Claude runs.',
|
||||
};
|
||||
}
|
||||
|
||||
if (channel.unavailableReason) {
|
||||
return {
|
||||
state: 'unavailable',
|
||||
label: channel.unavailableReason,
|
||||
detail: `${channel.displayName} is selected, but this machine cannot use it right now.`,
|
||||
nextStep: 'Turn it off here, or switch to a supported machine before relying on it.',
|
||||
};
|
||||
}
|
||||
|
||||
if (channel.id === 'imessage') {
|
||||
return {
|
||||
state: 'needs_claude_setup',
|
||||
label: 'Claude-side setup remaining',
|
||||
detail:
|
||||
'CCS can add iMessage on the next native Claude run, but plugin install, sender allowlist, Full Disk Access, and the first-reply Automation prompt are still local steps.',
|
||||
nextStep: 'Complete the one-time Claude and macOS setup below before relying on iMessage.',
|
||||
};
|
||||
}
|
||||
|
||||
const envKey = getOfficialChannelEnvKey(channel.id);
|
||||
if (channel.requiresToken && !channel.tokenAvailable) {
|
||||
return {
|
||||
state: 'needs_token',
|
||||
label: 'Needs token',
|
||||
detail: `${envKey} is missing. CCS cannot auto-add ${channel.displayName} until you save it here or provide it in the current CCS process env.`,
|
||||
nextStep: `Save ${envKey} below, or export it before launching CCS.`,
|
||||
};
|
||||
}
|
||||
|
||||
const sourceDetail = channel.savedInClaudeState
|
||||
? `${envKey} is saved in Claude channel state.`
|
||||
: `${envKey} is available from the current CCS process env.`;
|
||||
|
||||
return {
|
||||
state: 'ready',
|
||||
label: channel.savedInClaudeState
|
||||
? 'Ready for next native run'
|
||||
: 'Ready from current CCS process env',
|
||||
detail: channel.savedInClaudeState
|
||||
? `${sourceDetail}${channel.processEnvAvailable ? ` The current CCS process env also provides ${envKey}.` : ''} CCS can auto-add ${channel.displayName} on the next supported native Claude run. Claude-side pairing and access policy still happen in Claude.`
|
||||
: `${sourceDetail} CCS can auto-add ${channel.displayName} on the next supported native Claude run. Claude-side pairing and access policy still happen in Claude.`,
|
||||
nextStep: channel.savedInClaudeState
|
||||
? 'Run `ccs` or a native Claude account profile. Claude-side pairing and access policy may still be required.'
|
||||
: 'Run CCS from this same env, or save the token here if you want persistent Claude state.',
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOfficialChannelsReadinessSummary(input: {
|
||||
config: OfficialChannelsConfig;
|
||||
environment: OfficialChannelsEnvironmentStatus;
|
||||
channels: OfficialChannelsStatusChannelInput[];
|
||||
}): OfficialChannelsReadinessSummary {
|
||||
const { config, environment, channels } = input;
|
||||
|
||||
if (config.selected.length === 0) {
|
||||
return {
|
||||
state: 'needs_setup',
|
||||
title: 'No channels selected yet',
|
||||
message:
|
||||
'Choose at least one official channel before CCS can auto-add it on supported native Claude runs.',
|
||||
nextStep: 'Turn on Telegram, Discord, and/or iMessage below.',
|
||||
blockers: ['Select at least one channel for auto-enable.'],
|
||||
};
|
||||
}
|
||||
|
||||
const blockers: string[] = [];
|
||||
if (!environment.bunInstalled) {
|
||||
blockers.push('Install Bun to use Anthropic official channel plugins.');
|
||||
}
|
||||
if (environment.claudeVersion.state !== 'supported') {
|
||||
blockers.push(environment.claudeVersion.message);
|
||||
}
|
||||
if (environment.auth.state !== 'eligible') {
|
||||
blockers.push(environment.auth.message);
|
||||
}
|
||||
|
||||
const selectedChannels = channels.filter((channel) => channel.selected);
|
||||
const missingTokenChannels = selectedChannels.filter(
|
||||
(channel) => channel.requiresToken && !channel.tokenAvailable
|
||||
);
|
||||
if (missingTokenChannels.length > 0) {
|
||||
blockers.push(
|
||||
`Missing bot token for ${missingTokenChannels.map((channel) => channel.displayName).join(', ')}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (blockers.length > 0) {
|
||||
return {
|
||||
state: 'needs_setup',
|
||||
title: 'Needs setup before CCS can auto-add these channels',
|
||||
message: blockers[0] ?? 'Official Channels still need setup.',
|
||||
nextStep: 'Resolve the blockers below, then launch a supported native Claude session again.',
|
||||
blockers,
|
||||
};
|
||||
}
|
||||
|
||||
const limitedNotes: string[] = [];
|
||||
const unavailableSelectedChannels = selectedChannels.filter((channel) =>
|
||||
Boolean(channel.unavailableReason)
|
||||
);
|
||||
if (unavailableSelectedChannels.length > 0) {
|
||||
limitedNotes.push(
|
||||
`${unavailableSelectedChannels.map((channel) => channel.displayName).join(', ')} cannot run on this machine.`
|
||||
);
|
||||
}
|
||||
if (selectedChannels.some((channel) => channel.id === 'imessage')) {
|
||||
limitedNotes.push(
|
||||
'iMessage still needs Claude-side install plus local macOS permissions before it is dependable.'
|
||||
);
|
||||
}
|
||||
|
||||
if (limitedNotes.length > 0) {
|
||||
return {
|
||||
state: 'limited',
|
||||
title: 'Selected, but some channels still need manual setup',
|
||||
message: limitedNotes[0] ?? 'Some selected channels still need additional setup.',
|
||||
nextStep: 'Review the channel cards below before relying on this from a native Claude run.',
|
||||
blockers: limitedNotes,
|
||||
};
|
||||
}
|
||||
|
||||
const selectedLabels = selectedChannels.map((channel) => channel.displayName).join(', ');
|
||||
const envOnlyChannels = selectedChannels.filter(
|
||||
(channel) => channel.processEnvAvailable && !channel.savedInClaudeState
|
||||
);
|
||||
return {
|
||||
state: 'ready',
|
||||
title: 'Ready for the next native Claude run',
|
||||
message:
|
||||
envOnlyChannels.length === 0
|
||||
? `CCS can auto-add ${selectedLabels} the next time you run \`ccs\` or a native Claude account profile.`
|
||||
: envOnlyChannels.length === selectedChannels.length
|
||||
? `CCS can auto-add ${selectedLabels} on the next supported native Claude run from this same CCS process env.`
|
||||
: `CCS can auto-add ${selectedLabels} on the next supported native Claude run. ${envOnlyChannels.map((channel) => channel.displayName).join(', ')} currently depends on this same CCS process env.`,
|
||||
nextStep:
|
||||
envOnlyChannels.length === 0
|
||||
? 'Claude-side pairing and access policy may still be required inside Claude, but CCS-side prerequisites are ready.'
|
||||
: envOnlyChannels.length === selectedChannels.length
|
||||
? 'Run CCS from this same env, or save the token here first if you want persistent Claude channel state.'
|
||||
: 'Save env-only tokens here if you want persistent Claude channel state across shells.',
|
||||
blockers: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOfficialChannelsLaunchPreview(input: {
|
||||
config: OfficialChannelsConfig;
|
||||
environment: OfficialChannelsEnvironmentStatus;
|
||||
channels: OfficialChannelsStatusChannelInput[];
|
||||
}): OfficialChannelsLaunchPreview {
|
||||
const { config, environment, channels } = input;
|
||||
|
||||
if (config.selected.length === 0) {
|
||||
return {
|
||||
state: 'disabled',
|
||||
title: 'Nothing will be auto-added yet',
|
||||
detail: 'Turn on at least one channel below before `ccs` can add official channel flags.',
|
||||
command: 'ccs',
|
||||
appendedArgs: [],
|
||||
appliedChannels: [],
|
||||
permissionBypassIncluded: false,
|
||||
skippedMessages: [],
|
||||
};
|
||||
}
|
||||
|
||||
const channelReadiness = Object.fromEntries(
|
||||
channels.map((channel) => [
|
||||
channel.id,
|
||||
!channel.unavailableReason &&
|
||||
(channel.id === 'imessage' || !channel.requiresToken || channel.tokenAvailable),
|
||||
])
|
||||
) as Record<OfficialChannelId, boolean>;
|
||||
|
||||
const plan = resolveOfficialChannelsLaunchPlan({
|
||||
args: [],
|
||||
config,
|
||||
target: 'claude',
|
||||
profileType: 'default',
|
||||
environment,
|
||||
channelReadiness,
|
||||
});
|
||||
|
||||
const appendedArgs = plan.applied
|
||||
? buildOfficialChannelsArgs([], plan.appliedChannels, plan.wantsPermissionBypass)
|
||||
: [];
|
||||
|
||||
if (!plan.applied) {
|
||||
return {
|
||||
state: 'blocked',
|
||||
title: 'Running `ccs` now will not auto-add channels',
|
||||
detail:
|
||||
plan.skippedMessages[0] ??
|
||||
'Official Channels are selected, but this machine is not ready to auto-add them yet.',
|
||||
command: 'ccs',
|
||||
appendedArgs: [],
|
||||
appliedChannels: [],
|
||||
permissionBypassIncluded: false,
|
||||
skippedMessages: plan.skippedMessages,
|
||||
};
|
||||
}
|
||||
|
||||
const appliedLabels = plan.appliedChannels.map((channelId) =>
|
||||
getOfficialChannelDisplayName(channelId)
|
||||
);
|
||||
|
||||
if (plan.skippedMessages.length > 0) {
|
||||
return {
|
||||
state: 'partial',
|
||||
title: `CCS will auto-add ${appliedLabels.join(', ')}`,
|
||||
detail:
|
||||
'Some selected channels are still skipped. Review the notes below before relying on the rest.',
|
||||
command: 'ccs',
|
||||
appendedArgs,
|
||||
appliedChannels: plan.appliedChannels,
|
||||
permissionBypassIncluded: plan.wantsPermissionBypass,
|
||||
skippedMessages: plan.skippedMessages,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
state: 'ready',
|
||||
title: `CCS will auto-add ${appliedLabels.join(', ')}`,
|
||||
detail: plan.wantsPermissionBypass
|
||||
? 'Running `ccs` will add the selected official channels and skip permission prompts for that launch.'
|
||||
: 'Running `ccs` will add the selected official channels automatically on this machine.',
|
||||
command: 'ccs',
|
||||
appendedArgs,
|
||||
appliedChannels: plan.appliedChannels,
|
||||
permissionBypassIncluded: plan.wantsPermissionBypass,
|
||||
skippedMessages: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function expandOfficialChannelSelection(selection: string): OfficialChannelId[] {
|
||||
@@ -341,11 +792,11 @@ export function getOfficialChannelConfiguredPlaceholder(channelId: OfficialChann
|
||||
}
|
||||
|
||||
export function getOfficialChannelsSectionDescription(): string {
|
||||
return 'Auto-enable Anthropic official channels for compatible Claude sessions. Tokens stay in Claude channel env files rather than config.yaml.';
|
||||
return 'Auto-enable Anthropic official channels for compatible Claude sessions. CCS only stores selection in config.yaml; Claude keeps machine-level channel state under ~/.claude/channels/.';
|
||||
}
|
||||
|
||||
export function getOfficialChannelsRuntimeNote(): string {
|
||||
return 'CCS does not persist a global Claude channels default. It only injects runtime flags when the selected channels are supported and ready.';
|
||||
return 'CCS does not persist a global Claude channels default. It only injects runtime flags for the current Claude session when prerequisites are met.';
|
||||
}
|
||||
|
||||
export function getOfficialChannelsSetHelp(): string {
|
||||
@@ -399,7 +850,7 @@ export function getOfficialChannelsBunMissingMessage(): string {
|
||||
}
|
||||
|
||||
export function getOfficialChannelsCompatibilityMessage(): string {
|
||||
return 'Official Channels auto-enable only applies to native Claude default/account sessions.';
|
||||
return 'Official Channels auto-enable only works for native Claude default/account sessions. It does not apply to `ccs glm`, other API/OAuth profiles, or Droid targets.';
|
||||
}
|
||||
|
||||
export function getOfficialChannelsNoSelectionMessage(): string {
|
||||
|
||||
@@ -6,22 +6,42 @@ import type { OfficialChannelId } from '../config/unified-config-types';
|
||||
import {
|
||||
getOfficialChannelEnvDir,
|
||||
getOfficialChannelEnvKey,
|
||||
getOfficialChannelStateDirEnvKey,
|
||||
getOfficialChannelTokenIds,
|
||||
isOfficialChannelTokenRequired,
|
||||
} from './official-channels-runtime';
|
||||
|
||||
export interface DiscordChannelsSyncResult {
|
||||
synced: boolean;
|
||||
targetPath: string;
|
||||
reason?: 'missing_env' | 'missing_token' | 'already_current' | 'write_failed';
|
||||
error?: string;
|
||||
export type OfficialChannelTokenSource = 'saved_env' | 'process_env' | 'missing';
|
||||
|
||||
export interface OfficialChannelTokenStatus {
|
||||
available: boolean;
|
||||
source: OfficialChannelTokenSource;
|
||||
envKey?: string;
|
||||
tokenPath?: string;
|
||||
savedInClaudeState: boolean;
|
||||
processEnvAvailable: boolean;
|
||||
}
|
||||
|
||||
function getResolvedStateDirOverride(
|
||||
channelId: OfficialChannelId,
|
||||
envOverrides?: NodeJS.ProcessEnv | null
|
||||
): string | null {
|
||||
const env = envOverrides === undefined ? process.env : envOverrides;
|
||||
const rawStateDir = env?.[getOfficialChannelStateDirEnvKey(channelId)]?.trim();
|
||||
|
||||
return rawStateDir ? path.resolve(rawStateDir) : null;
|
||||
}
|
||||
|
||||
export function getOfficialChannelEnvPath(
|
||||
channelId: OfficialChannelId,
|
||||
configDir = getDefaultClaudeConfigDir()
|
||||
configDir = getDefaultClaudeConfigDir(),
|
||||
envOverrides?: NodeJS.ProcessEnv | null
|
||||
): string {
|
||||
return path.join(configDir, 'channels', getOfficialChannelEnvDir(channelId), '.env');
|
||||
const overrideStateDir = getResolvedStateDirOverride(channelId, envOverrides);
|
||||
const stateDir =
|
||||
overrideStateDir ?? path.join(configDir, 'channels', getOfficialChannelEnvDir(channelId));
|
||||
|
||||
return path.join(stateDir, '.env');
|
||||
}
|
||||
|
||||
function readFileIfExists(filePath: string): string | null {
|
||||
@@ -137,6 +157,19 @@ function listManagedClaudeConfigDirs(): string[] {
|
||||
return [...dirs];
|
||||
}
|
||||
|
||||
function listManagedOfficialChannelEnvPaths(channelId: OfficialChannelId): string[] {
|
||||
const envPaths = new Set<string>([
|
||||
getOfficialChannelEnvPath(channelId, getDefaultClaudeConfigDir(), null),
|
||||
getOfficialChannelEnvPath(channelId),
|
||||
]);
|
||||
|
||||
for (const configDir of listManagedClaudeConfigDirs()) {
|
||||
envPaths.add(getOfficialChannelEnvPath(channelId, configDir, null));
|
||||
}
|
||||
|
||||
return [...envPaths];
|
||||
}
|
||||
|
||||
export function normalizeDiscordBotToken(value: string): string | null {
|
||||
const normalized = value.trim();
|
||||
if (!normalized || /[\r\n]/.test(normalized)) {
|
||||
@@ -171,10 +204,82 @@ export function readConfiguredOfficialChannelToken(channelId: OfficialChannelId)
|
||||
return content ? readOfficialChannelTokenFromEnvContent(channelId, content) : null;
|
||||
}
|
||||
|
||||
export function readOfficialChannelTokenFromProcessEnv(
|
||||
channelId: OfficialChannelId,
|
||||
envOverrides?: NodeJS.ProcessEnv | null
|
||||
): string | null {
|
||||
const envKey = getOfficialChannelEnvKey(channelId);
|
||||
if (!envKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawValue = (envOverrides === undefined ? process.env : envOverrides)?.[envKey];
|
||||
if (typeof rawValue !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizeDiscordBotToken(rawValue);
|
||||
}
|
||||
|
||||
export function hasConfiguredOfficialChannelToken(channelId: OfficialChannelId): boolean {
|
||||
return readConfiguredOfficialChannelToken(channelId) !== null;
|
||||
}
|
||||
|
||||
export function getOfficialChannelTokenStatus(
|
||||
channelId: OfficialChannelId,
|
||||
envOverrides?: NodeJS.ProcessEnv | null
|
||||
): OfficialChannelTokenStatus {
|
||||
const envKey = getOfficialChannelEnvKey(channelId);
|
||||
if (!envKey) {
|
||||
return {
|
||||
available: true,
|
||||
source: 'saved_env',
|
||||
savedInClaudeState: true,
|
||||
processEnvAvailable: false,
|
||||
};
|
||||
}
|
||||
|
||||
const processEnvToken = readOfficialChannelTokenFromProcessEnv(channelId, envOverrides);
|
||||
const tokenPath = getOfficialChannelEnvPath(channelId);
|
||||
const savedToken = readConfiguredOfficialChannelToken(channelId);
|
||||
|
||||
if (savedToken !== null) {
|
||||
return {
|
||||
available: true,
|
||||
source: 'saved_env',
|
||||
envKey,
|
||||
tokenPath,
|
||||
savedInClaudeState: true,
|
||||
processEnvAvailable: processEnvToken !== null,
|
||||
};
|
||||
}
|
||||
|
||||
if (processEnvToken !== null) {
|
||||
return {
|
||||
available: true,
|
||||
source: 'process_env',
|
||||
envKey,
|
||||
savedInClaudeState: false,
|
||||
processEnvAvailable: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
available: false,
|
||||
source: 'missing',
|
||||
envKey,
|
||||
tokenPath,
|
||||
savedInClaudeState: false,
|
||||
processEnvAvailable: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function getOfficialChannelReadiness(channelId: OfficialChannelId): boolean {
|
||||
return isOfficialChannelTokenRequired(channelId)
|
||||
? getOfficialChannelTokenStatus(channelId).available
|
||||
: true;
|
||||
}
|
||||
|
||||
export function setConfiguredOfficialChannelToken(
|
||||
channelId: OfficialChannelId,
|
||||
token: string
|
||||
@@ -207,9 +312,8 @@ export function clearConfiguredOfficialChannelTokensEverywhere(
|
||||
const clearedPaths: string[] = [];
|
||||
const channels = channelId ? [channelId] : getOfficialChannelTokenIds();
|
||||
|
||||
for (const configDir of listManagedClaudeConfigDirs()) {
|
||||
for (const tokenChannelId of channels) {
|
||||
const envPath = getOfficialChannelEnvPath(tokenChannelId, configDir);
|
||||
for (const tokenChannelId of channels) {
|
||||
for (const envPath of listManagedOfficialChannelEnvPaths(tokenChannelId)) {
|
||||
if (clearOfficialChannelTokenAtPath(tokenChannelId, envPath)) {
|
||||
clearedPaths.push(envPath);
|
||||
}
|
||||
@@ -218,52 +322,3 @@ export function clearConfiguredOfficialChannelTokensEverywhere(
|
||||
|
||||
return clearedPaths;
|
||||
}
|
||||
|
||||
export function syncOfficialChannelEnvToConfigDir(
|
||||
channelId: OfficialChannelId,
|
||||
targetConfigDir: string
|
||||
): DiscordChannelsSyncResult {
|
||||
const envKey = getOfficialChannelEnvKey(channelId);
|
||||
if (!envKey) {
|
||||
return {
|
||||
synced: false,
|
||||
targetPath: getOfficialChannelEnvPath(channelId, targetConfigDir),
|
||||
reason: 'missing_token',
|
||||
};
|
||||
}
|
||||
|
||||
const sourcePath = getOfficialChannelEnvPath(channelId);
|
||||
const targetPath = getOfficialChannelEnvPath(channelId, targetConfigDir);
|
||||
const token = readConfiguredOfficialChannelToken(channelId);
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
return { synced: false, targetPath, reason: 'missing_env' };
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return { synced: false, targetPath, reason: 'missing_token' };
|
||||
}
|
||||
|
||||
if (path.resolve(sourcePath) === path.resolve(targetPath)) {
|
||||
return { synced: false, targetPath, reason: 'already_current' };
|
||||
}
|
||||
|
||||
try {
|
||||
const targetContent = readFileIfExists(targetPath) ?? '';
|
||||
writeSecureFile(targetPath, upsertEnvValue(targetContent, envKey, token));
|
||||
return { synced: true, targetPath };
|
||||
} catch (error) {
|
||||
return {
|
||||
synced: false,
|
||||
targetPath,
|
||||
reason: 'write_failed',
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getOfficialChannelReadiness(channelId: OfficialChannelId): boolean {
|
||||
return isOfficialChannelTokenRequired(channelId)
|
||||
? hasConfiguredOfficialChannelToken(channelId)
|
||||
: true;
|
||||
}
|
||||
|
||||
@@ -8,18 +8,22 @@ import type { OfficialChannelId } from '../config/unified-config-types';
|
||||
import { DEFAULT_OFFICIAL_CHANNELS_CONFIG } from '../config/unified-config-types';
|
||||
import {
|
||||
clearConfiguredOfficialChannelTokensEverywhere,
|
||||
getOfficialChannelEnvPath,
|
||||
getOfficialChannelTokenStatus,
|
||||
hasConfiguredOfficialChannelToken,
|
||||
setConfiguredOfficialChannelToken,
|
||||
} from '../channels/official-channels-store';
|
||||
import {
|
||||
buildOfficialChannelsLaunchPreview,
|
||||
buildOfficialChannelsReadinessSummary,
|
||||
buildOfficialChannelSetupSummary,
|
||||
expandOfficialChannelSelection,
|
||||
getChannelConfigSelectionLabel,
|
||||
getOfficialChannelChoices,
|
||||
getOfficialChannelsAccountStatusCaveat,
|
||||
getOfficialChannelsSupportMessage,
|
||||
getOfficialChannelDisplayName,
|
||||
getOfficialChannelEnvKey,
|
||||
getOfficialChannelManualSetupCommands,
|
||||
getOfficialChannelReadyMessage,
|
||||
getOfficialChannelsCompatibilityMessage,
|
||||
getOfficialChannelsDocsSummary,
|
||||
getOfficialChannelsLegacyEnableHelp,
|
||||
@@ -28,12 +32,12 @@ import {
|
||||
getOfficialChannelClearTokenHelp,
|
||||
getOfficialChannelMacOSHelp,
|
||||
getOfficialChannelSummary,
|
||||
getOfficialChannelsEnvironmentStatus,
|
||||
getOfficialChannelsRuntimeNote,
|
||||
getOfficialChannelsSectionDescription,
|
||||
getOfficialChannelsSupportedProfiles,
|
||||
getOfficialChannelUnavailableReason,
|
||||
getOfficialChannelTokenIds,
|
||||
isBunAvailable,
|
||||
isOfficialChannelId,
|
||||
isOfficialChannelSelectionValid,
|
||||
} from '../channels/official-channels-runtime';
|
||||
@@ -128,6 +132,9 @@ function showHelp(): void {
|
||||
console.log('');
|
||||
console.log(` ${getOfficialChannelsSectionDescription()}`);
|
||||
console.log(` ${dim(getOfficialChannelsDocsSummary())}`);
|
||||
console.log(
|
||||
` ${dim('Fastest path: run `ccs config`, open Settings -> Channels, turn on the channel, save the token if needed, then run `ccs`.')}`
|
||||
);
|
||||
console.log('');
|
||||
console.log(subheader('Usage:'));
|
||||
console.log(` ${color('ccs config channels', 'command')} [options]`);
|
||||
@@ -135,8 +142,12 @@ function showHelp(): void {
|
||||
console.log(subheader('Options:'));
|
||||
console.log(` ${color('--set <csv|all>', 'command')} ${getOfficialChannelsSetHelp()}`);
|
||||
console.log(` ${color('--clear', 'command')} Clear all selected channels`);
|
||||
console.log(` ${color('--enable', 'command')} Legacy alias: add Discord`);
|
||||
console.log(` ${color('--disable', 'command')} Legacy alias: remove Discord`);
|
||||
console.log(
|
||||
` ${color('--enable', 'command')} Legacy compatibility alias: add Discord`
|
||||
);
|
||||
console.log(
|
||||
` ${color('--disable', 'command')} Legacy compatibility alias: remove Discord`
|
||||
);
|
||||
console.log(
|
||||
` ${color('--unattended', 'command')} Also add --dangerously-skip-permissions`
|
||||
);
|
||||
@@ -148,6 +159,9 @@ function showHelp(): void {
|
||||
console.log(` ${color('--help, -h', 'command')} Show this help`);
|
||||
console.log('');
|
||||
console.log(subheader('Examples:'));
|
||||
console.log(
|
||||
` $ ${color('ccs config', 'command')} ${dim('# Dashboard -> Settings -> Channels (fastest path)')}`
|
||||
);
|
||||
console.log(
|
||||
` $ ${color('ccs config channels', 'command')} ${dim('# Show status')}`
|
||||
);
|
||||
@@ -163,60 +177,173 @@ function showHelp(): void {
|
||||
console.log(
|
||||
` $ ${color('ccs config channels --clear-token discord', 'command')} ${dim('# Clear one token')}`
|
||||
);
|
||||
console.log(
|
||||
` ${dim('Official Channels only work on native Claude default/account sessions, not on ccs glm or other API/OAuth/Droid targets.')}`
|
||||
);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
function showStatus(): void {
|
||||
const config = getOfficialChannelsConfig();
|
||||
const selected = config.selected;
|
||||
const bunReady = isBunAvailable();
|
||||
const environment = getOfficialChannelsEnvironmentStatus();
|
||||
const channelRows = expandOfficialChannelSelection('all').map((channelId) => {
|
||||
const selectedForLaunch = selected.includes(channelId);
|
||||
const tokenStatus = getOfficialChannelTokenStatus(channelId);
|
||||
|
||||
return {
|
||||
id: channelId,
|
||||
displayName: getOfficialChannelDisplayName(channelId),
|
||||
selected: selectedForLaunch,
|
||||
requiresToken: getOfficialChannelTokenIds().includes(channelId),
|
||||
tokenConfigured: hasConfiguredOfficialChannelToken(channelId),
|
||||
tokenStatus,
|
||||
unavailableReason: getOfficialChannelUnavailableReason(channelId),
|
||||
setup: buildOfficialChannelSetupSummary({
|
||||
id: channelId,
|
||||
displayName: getOfficialChannelDisplayName(channelId),
|
||||
selected: selectedForLaunch,
|
||||
requiresToken: getOfficialChannelTokenIds().includes(channelId),
|
||||
tokenAvailable: tokenStatus.available,
|
||||
tokenSource: tokenStatus.source,
|
||||
savedInClaudeState: tokenStatus.savedInClaudeState,
|
||||
processEnvAvailable: tokenStatus.processEnvAvailable,
|
||||
unavailableReason: getOfficialChannelUnavailableReason(channelId),
|
||||
}),
|
||||
};
|
||||
});
|
||||
const summary = buildOfficialChannelsReadinessSummary({
|
||||
config,
|
||||
environment,
|
||||
channels: channelRows.map((channel) => ({
|
||||
id: channel.id,
|
||||
displayName: channel.displayName,
|
||||
selected: channel.selected,
|
||||
requiresToken: channel.requiresToken,
|
||||
tokenAvailable: channel.tokenStatus.available,
|
||||
tokenSource: channel.tokenStatus.source,
|
||||
savedInClaudeState: channel.tokenStatus.savedInClaudeState,
|
||||
processEnvAvailable: channel.tokenStatus.processEnvAvailable,
|
||||
unavailableReason: channel.unavailableReason,
|
||||
})),
|
||||
});
|
||||
const launchPreview = buildOfficialChannelsLaunchPreview({
|
||||
config,
|
||||
environment,
|
||||
channels: channelRows.map((channel) => ({
|
||||
id: channel.id,
|
||||
displayName: channel.displayName,
|
||||
selected: channel.selected,
|
||||
requiresToken: channel.requiresToken,
|
||||
tokenAvailable: channel.tokenStatus.available,
|
||||
tokenSource: channel.tokenStatus.source,
|
||||
savedInClaudeState: channel.tokenStatus.savedInClaudeState,
|
||||
processEnvAvailable: channel.tokenStatus.processEnvAvailable,
|
||||
unavailableReason: channel.unavailableReason,
|
||||
})),
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log(header('Official Channels Configuration'));
|
||||
console.log('');
|
||||
console.log(
|
||||
` Status: ${
|
||||
summary.state === 'ready'
|
||||
? ok(summary.title)
|
||||
: summary.state === 'limited'
|
||||
? warn(summary.title)
|
||||
: warn(summary.title)
|
||||
}`
|
||||
);
|
||||
console.log(` ${dim(summary.message)}`);
|
||||
console.log(` ${dim(summary.nextStep)}`);
|
||||
console.log('');
|
||||
console.log(` Launch: ${info(launchPreview.title)}`);
|
||||
console.log(` ${dim(launchPreview.detail)}`);
|
||||
if (launchPreview.appendedArgs.length > 0) {
|
||||
console.log(` ${dim(`ccs adds: ${launchPreview.appendedArgs.join(' ')}`)}`);
|
||||
}
|
||||
if (launchPreview.skippedMessages.length > 0) {
|
||||
console.log(` ${dim(`Skipped: ${launchPreview.skippedMessages.join(' | ')}`)}`);
|
||||
}
|
||||
console.log('');
|
||||
console.log(
|
||||
` Channels: ${selected.length > 0 ? ok(getChannelConfigSelectionLabel(selected)) : warn('Disabled')}`
|
||||
);
|
||||
console.log(` Unattended: ${config.unattended ? warn('Enabled') : info('Disabled')}`);
|
||||
console.log(` Bun: ${bunReady ? ok('Installed') : warn('Missing')}`);
|
||||
console.log(` Bun: ${environment.bunInstalled ? ok('Installed') : warn('Missing')}`);
|
||||
console.log(
|
||||
` Claude Code: ${
|
||||
environment.claudeVersion.state === 'supported'
|
||||
? ok(environment.claudeVersion.message)
|
||||
: environment.claudeVersion.state === 'unsupported'
|
||||
? warn(environment.claudeVersion.message)
|
||||
: info(environment.claudeVersion.message)
|
||||
}`
|
||||
);
|
||||
console.log(
|
||||
` Claude Auth: ${
|
||||
environment.auth.state === 'eligible'
|
||||
? ok(environment.auth.message)
|
||||
: environment.auth.state === 'ineligible'
|
||||
? warn(environment.auth.message)
|
||||
: info(environment.auth.message)
|
||||
}`
|
||||
);
|
||||
console.log('');
|
||||
console.log(subheader('Applies To:'));
|
||||
console.log(` ${dim(getOfficialChannelsCompatibilityMessage())}`);
|
||||
console.log(` ${dim(getOfficialChannelsSupportMessage())}`);
|
||||
console.log(
|
||||
` ${dim(`Supported profiles: ${getOfficialChannelsSupportedProfiles().join(', ')}`)}`
|
||||
);
|
||||
console.log(` ${dim(environment.stateScopeMessage)}`);
|
||||
console.log(` ${dim(getOfficialChannelsAccountStatusCaveat())}`);
|
||||
if (environment.auth.orgRequirementMessage) {
|
||||
console.log(` ${dim(environment.auth.orgRequirementMessage)}`);
|
||||
}
|
||||
console.log('');
|
||||
console.log(subheader('Channels:'));
|
||||
for (const channelId of expandOfficialChannelSelection('all')) {
|
||||
const displayName = getOfficialChannelDisplayName(channelId);
|
||||
const enabled = selected.includes(channelId);
|
||||
const envKey = getOfficialChannelEnvKey(channelId);
|
||||
const tokenConfigured = envKey ? hasConfiguredOfficialChannelToken(channelId) : true;
|
||||
const unavailableReason = getOfficialChannelUnavailableReason(channelId);
|
||||
const status = unavailableReason
|
||||
? warn(unavailableReason)
|
||||
: envKey
|
||||
? tokenConfigured
|
||||
? ok('Ready')
|
||||
: warn(`${envKey} missing`)
|
||||
: ok('Ready');
|
||||
console.log(` ${enabled ? '[x]' : '[ ]'} ${displayName}: ${status}`);
|
||||
console.log(` ${dim(getOfficialChannelSummary(channelId))}`);
|
||||
if (envKey) {
|
||||
console.log(
|
||||
` ${dim(`${envKey}: ${tokenConfigured ? 'configured' : 'not configured'}`)}`
|
||||
);
|
||||
console.log(` ${dim(getOfficialChannelEnvPath(channelId))}`);
|
||||
for (const channel of channelRows) {
|
||||
const status =
|
||||
channel.setup.state === 'ready'
|
||||
? ok(channel.setup.label)
|
||||
: channel.setup.state === 'not_selected'
|
||||
? info(channel.setup.label)
|
||||
: warn(channel.setup.label);
|
||||
console.log(` ${channel.selected ? '[x]' : '[ ]'} ${channel.displayName}: ${status}`);
|
||||
console.log(` ${dim(getOfficialChannelSummary(channel.id))}`);
|
||||
console.log(` ${dim(channel.setup.detail)}`);
|
||||
console.log(` ${dim(channel.setup.nextStep)}`);
|
||||
if (channel.requiresToken) {
|
||||
const envKey = getOfficialChannelEnvKey(channel.id) ?? '';
|
||||
if (channel.tokenStatus.source === 'saved_env') {
|
||||
console.log(` ${dim(`${envKey}: saved in Claude channel state`)}`);
|
||||
if (channel.tokenStatus.processEnvAvailable) {
|
||||
console.log(` ${dim(`${envKey}: also available from current CCS process env`)}`);
|
||||
}
|
||||
if (channel.tokenStatus.tokenPath) {
|
||||
console.log(` ${dim(channel.tokenStatus.tokenPath)}`);
|
||||
}
|
||||
} else if (channel.tokenStatus.source === 'process_env') {
|
||||
console.log(` ${dim(`${envKey}: available from current CCS process env`)}`);
|
||||
} else {
|
||||
console.log(` ${dim(`${envKey}: missing`)}`);
|
||||
if (channel.tokenStatus.tokenPath) {
|
||||
console.log(` ${dim(channel.tokenStatus.tokenPath)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(` ${dim(getOfficialChannelReadyMessage(channelId))}`);
|
||||
}
|
||||
console.log('');
|
||||
console.log(subheader('Notes:'));
|
||||
console.log(` ${dim(getOfficialChannelsLegacyEnableHelp())}`);
|
||||
console.log(` ${dim(environment.stateScopeMessage)}`);
|
||||
console.log(` ${dim(getOfficialChannelMacOSHelp())}`);
|
||||
console.log(` ${dim(getOfficialChannelsRuntimeNote())}`);
|
||||
console.log(` ${dim(getOfficialChannelsCompatibilityMessage())}`);
|
||||
console.log(` ${dim(getOfficialChannelsAccountStatusCaveat())}`);
|
||||
console.log('');
|
||||
console.log(subheader('Manual Claude Setup:'));
|
||||
console.log(subheader('Claude-side Setup:'));
|
||||
for (const channelId of expandOfficialChannelSelection('all')) {
|
||||
console.log(` ${dim(`${getOfficialChannelDisplayName(channelId)}:`)}`);
|
||||
for (const command of getOfficialChannelManualSetupCommands(channelId)) {
|
||||
|
||||
@@ -92,6 +92,8 @@ export function showConfigCommandHelp(): void {
|
||||
console.log(' --set-token <s> Save channel token (telegram=<t> or discord=<t>)');
|
||||
console.log(' --clear-token Remove all saved channel tokens');
|
||||
console.log(' --clear-token <c> Remove one saved channel token');
|
||||
console.log(' Works only for native Claude default/account sessions');
|
||||
console.log(' Not for ccs glm, other API/OAuth profiles, or Droid targets');
|
||||
console.log('');
|
||||
console.log(' auth Manage dashboard authentication');
|
||||
console.log(' auth setup Configure username and password');
|
||||
|
||||
@@ -312,7 +312,10 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
['ccs config auth setup', 'Configure dashboard login'],
|
||||
['ccs config auth show', 'Show dashboard auth status'],
|
||||
['ccs config channels', 'Show Official Channels status'],
|
||||
['ccs config channels --set telegram,discord', 'Auto-enable Telegram + Discord'],
|
||||
[
|
||||
'ccs config channels --set telegram,discord',
|
||||
'Auto-add Telegram + Discord on supported native Claude runs',
|
||||
],
|
||||
['ccs config channels --set-token telegram=<token>', 'Save TELEGRAM_BOT_TOKEN'],
|
||||
['ccs config image-analysis', 'Show image analysis settings'],
|
||||
['ccs config image-analysis --enable', 'Enable image analysis'],
|
||||
@@ -470,10 +473,11 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
]);
|
||||
|
||||
printSubSection('Official Channels (official Claude plugins)', [
|
||||
['ccs config', 'Dashboard -> Settings -> Channels (fastest path)'],
|
||||
['ccs config channels', 'Show current status'],
|
||||
[
|
||||
'ccs config channels --set telegram,discord',
|
||||
'Auto-add selected channels on native Claude sessions',
|
||||
'Auto-add selected channels on native Claude default/account sessions',
|
||||
],
|
||||
['ccs config channels --set all', 'Enable Telegram, Discord, and iMessage'],
|
||||
['ccs config channels --unattended', 'Also add --dangerously-skip-permissions'],
|
||||
@@ -481,8 +485,11 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
['ccs config channels --set-token discord=<token>', 'Save DISCORD_BOT_TOKEN'],
|
||||
['ccs config channels --clear-token [channel]', 'Remove one or all saved channel tokens'],
|
||||
['', ''],
|
||||
['Note:', 'Runtime-only. Applies to native Claude default/account sessions.'],
|
||||
['', 'Fastest path: turn on the channel, save the token if needed, then run ccs.'],
|
||||
['Note:', 'Runtime-only. Applies to native Claude default/account sessions only.'],
|
||||
['', 'Not supported for ccs glm, other API/OAuth profiles, or Droid targets.'],
|
||||
['', 'Telegram/Discord tokens live in ~/.claude/channels/<channel>/.env.'],
|
||||
['', 'Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work for that launch.'],
|
||||
['', 'iMessage is macOS-only and requires local OS permissions instead of a bot token.'],
|
||||
]);
|
||||
|
||||
@@ -554,6 +561,4 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
// License
|
||||
console.log(dim('License: MIT'));
|
||||
console.log('');
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -79,10 +79,15 @@ function getLockFilePath(): string {
|
||||
|
||||
function acquireLock(): string | null {
|
||||
const lockPath = getLockFilePath();
|
||||
const lockDir = path.dirname(lockPath);
|
||||
const lockToken = crypto.randomUUID();
|
||||
const lockData = `${process.pid}\n${Date.now()}\n${lockToken}`;
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(lockDir)) {
|
||||
fs.mkdirSync(lockDir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
// Check if lock exists
|
||||
if (fs.existsSync(lockPath)) {
|
||||
const content = fs.readFileSync(lockPath, 'utf8');
|
||||
@@ -304,17 +309,21 @@ interface LegacyDiscordChannelsConfig {
|
||||
function normalizeOfficialChannelsConfig(
|
||||
partial: Partial<UnifiedConfig> & { discord_channels?: LegacyDiscordChannelsConfig }
|
||||
): OfficialChannelsConfig {
|
||||
const rawSelected = Array.isArray(partial.channels?.selected)
|
||||
? partial.channels.selected.filter((value): value is OfficialChannelId =>
|
||||
isOfficialChannelId(value)
|
||||
)
|
||||
: [];
|
||||
const hasCanonicalChannelsSection = partial.channels !== undefined;
|
||||
const hasExplicitSelectedField =
|
||||
hasCanonicalChannelsSection &&
|
||||
Object.prototype.hasOwnProperty.call(partial.channels, 'selected');
|
||||
const rawSelected =
|
||||
hasExplicitSelectedField && Array.isArray(partial.channels?.selected)
|
||||
? partial.channels.selected.filter((value): value is OfficialChannelId =>
|
||||
isOfficialChannelId(value)
|
||||
)
|
||||
: [];
|
||||
|
||||
return {
|
||||
selected:
|
||||
rawSelected.length > 0
|
||||
? normalizeOfficialChannelIds(rawSelected)
|
||||
: resolveLegacyDiscordSelection(partial.discord_channels?.enabled),
|
||||
selected: hasCanonicalChannelsSection
|
||||
? normalizeOfficialChannelIds(rawSelected)
|
||||
: resolveLegacyDiscordSelection(partial.discord_channels?.enabled),
|
||||
unattended:
|
||||
partial.channels?.unattended ??
|
||||
partial.discord_channels?.unattended ??
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import * as fs from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { execFileSync, execSync } from 'child_process';
|
||||
import { expandPath } from './helpers';
|
||||
import { ClaudeCliInfo } from '../types';
|
||||
import type { ClaudeCliInfo } from '../types';
|
||||
import { escapeShellArg, stripAnthropicEnv, stripClaudeCodeEnv } from './shell-executor';
|
||||
|
||||
export interface ClaudeAuthStatus {
|
||||
loggedIn: boolean;
|
||||
authMethod?: string | null;
|
||||
apiProvider?: string | null;
|
||||
email?: string | null;
|
||||
orgId?: string | null;
|
||||
orgName?: string | null;
|
||||
subscriptionType?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows installation paths for Claude CLI
|
||||
@@ -127,6 +138,120 @@ export function getClaudeCliInfo(): ClaudeCliInfo | null {
|
||||
};
|
||||
}
|
||||
|
||||
function runClaudeCliCommand(args: string[], envOverrides?: NodeJS.ProcessEnv): string | null {
|
||||
const cliInfo = getClaudeCliInfo();
|
||||
if (!cliInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const env = stripClaudeCodeEnv(stripAnthropicEnv({ ...process.env, ...envOverrides }));
|
||||
const { path: claudePath, needsShell } = cliInfo;
|
||||
const isWindows = process.platform === 'win32';
|
||||
const isPowerShellScript = isWindows && /\.ps1$/i.test(claudePath);
|
||||
|
||||
try {
|
||||
if (isPowerShellScript) {
|
||||
return execFileSync(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', claudePath, ...args],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 5000,
|
||||
env,
|
||||
}
|
||||
).trim();
|
||||
}
|
||||
|
||||
if (needsShell) {
|
||||
return execSync([claudePath, ...args].map(escapeShellArg).join(' '), {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 5000,
|
||||
shell: process.env.ComSpec || 'cmd.exe',
|
||||
env,
|
||||
}).trim();
|
||||
}
|
||||
|
||||
return execFileSync(claudePath, args, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 5000,
|
||||
env,
|
||||
}).trim();
|
||||
} catch (error) {
|
||||
if (typeof error === 'object' && error !== null && 'stdout' in error) {
|
||||
const stdout = (error as { stdout?: string | Buffer | null }).stdout;
|
||||
if (typeof stdout === 'string') {
|
||||
const trimmed = stdout.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
if (Buffer.isBuffer(stdout)) {
|
||||
const trimmed = stdout.toString('utf8').trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getClaudeCliVersion(): string | null {
|
||||
const output = runClaudeCliCommand(['--version']);
|
||||
const versionMatch = output?.match(/(\d+\.\d+\.\d+)/);
|
||||
return versionMatch ? versionMatch[1] : null;
|
||||
}
|
||||
|
||||
export function compareClaudeCliVersions(left: string, right: string): number {
|
||||
const leftParts = left.split('.').map((value) => Number.parseInt(value, 10) || 0);
|
||||
const rightParts = right.split('.').map((value) => Number.parseInt(value, 10) || 0);
|
||||
const maxLength = Math.max(leftParts.length, rightParts.length);
|
||||
|
||||
for (let index = 0; index < maxLength; index += 1) {
|
||||
const leftValue = leftParts[index] ?? 0;
|
||||
const rightValue = rightParts[index] ?? 0;
|
||||
|
||||
if (leftValue !== rightValue) {
|
||||
return leftValue > rightValue ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function isClaudeCliVersionAtLeast(
|
||||
currentVersion: string | null,
|
||||
minimumVersion: string
|
||||
): boolean {
|
||||
return currentVersion !== null && compareClaudeCliVersions(currentVersion, minimumVersion) >= 0;
|
||||
}
|
||||
|
||||
export function getClaudeAuthStatus(envOverrides?: NodeJS.ProcessEnv): ClaudeAuthStatus | null {
|
||||
const output = runClaudeCliCommand(['auth', 'status'], envOverrides);
|
||||
if (!output) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(output) as Partial<ClaudeAuthStatus>;
|
||||
if (typeof parsed.loggedIn !== 'boolean') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
loggedIn: parsed.loggedIn,
|
||||
authMethod: parsed.authMethod ?? null,
|
||||
apiProvider: parsed.apiProvider ?? null,
|
||||
email: parsed.email ?? null,
|
||||
orgId: parsed.orgId ?? null,
|
||||
orgName: parsed.orgName ?? null,
|
||||
subscriptionType: parsed.subscriptionType ?? null,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Claude not found error
|
||||
*/
|
||||
|
||||
@@ -2,48 +2,110 @@ import { Router, type Request, type Response } from 'express';
|
||||
import { getOfficialChannelsConfig, mutateUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import {
|
||||
clearConfiguredOfficialChannelTokensEverywhere,
|
||||
getOfficialChannelEnvPath,
|
||||
getOfficialChannelReadiness,
|
||||
getOfficialChannelTokenStatus,
|
||||
hasConfiguredOfficialChannelToken,
|
||||
setConfiguredOfficialChannelToken,
|
||||
} from '../../channels/official-channels-store';
|
||||
import {
|
||||
buildOfficialChannelsLaunchPreview,
|
||||
buildOfficialChannelsReadinessSummary,
|
||||
buildOfficialChannelSetupSummary,
|
||||
expandOfficialChannelSelection,
|
||||
getOfficialChannelsAccountStatusCaveat,
|
||||
getOfficialChannelDisplayName,
|
||||
getOfficialChannelEnvKey,
|
||||
getOfficialChannelPluginSpec,
|
||||
getOfficialChannelSummary,
|
||||
getOfficialChannelsSupportMessage,
|
||||
getOfficialChannelUnavailableReason,
|
||||
getOfficialChannelsSupportedProfiles,
|
||||
getOfficialChannelsEnvironmentStatus,
|
||||
getOfficialChannelManualSetupCommands,
|
||||
getOfficialChannelTokenIds,
|
||||
isBunAvailable,
|
||||
isOfficialChannelId,
|
||||
} from '../../channels/official-channels-runtime';
|
||||
import { requireLocalAccessWhenAuthDisabled } from '../middleware/auth-middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
function buildChannelsStatus() {
|
||||
return {
|
||||
bunInstalled: isBunAvailable(),
|
||||
supportedProfiles: getOfficialChannelsSupportedProfiles(),
|
||||
channels: expandOfficialChannelSelection('all').map((channelId) => ({
|
||||
function buildChannelsStatus(config = getOfficialChannelsConfig()) {
|
||||
const environment = getOfficialChannelsEnvironmentStatus();
|
||||
const channels = expandOfficialChannelSelection('all').map((channelId) => {
|
||||
const tokenStatus = getOfficialChannelTokenIds().includes(channelId)
|
||||
? getOfficialChannelTokenStatus(channelId)
|
||||
: undefined;
|
||||
const selected = config.selected.includes(channelId);
|
||||
|
||||
return {
|
||||
id: channelId,
|
||||
selected,
|
||||
displayName: getOfficialChannelDisplayName(channelId),
|
||||
pluginSpec: getOfficialChannelPluginSpec(channelId),
|
||||
summary: getOfficialChannelSummary(channelId),
|
||||
requiresToken: getOfficialChannelTokenIds().includes(channelId),
|
||||
envKey: getOfficialChannelEnvKey(channelId),
|
||||
tokenConfigured: getOfficialChannelTokenIds().includes(channelId)
|
||||
? hasConfiguredOfficialChannelToken(channelId)
|
||||
: getOfficialChannelReadiness(channelId),
|
||||
tokenPath: getOfficialChannelTokenIds().includes(channelId)
|
||||
? getOfficialChannelEnvPath(channelId)
|
||||
: undefined,
|
||||
tokenConfigured:
|
||||
getOfficialChannelTokenIds().includes(channelId) &&
|
||||
hasConfiguredOfficialChannelToken(channelId),
|
||||
tokenAvailable: tokenStatus?.available ?? false,
|
||||
tokenSource: tokenStatus?.source,
|
||||
tokenPath: tokenStatus?.tokenPath,
|
||||
savedInClaudeState: tokenStatus?.savedInClaudeState ?? false,
|
||||
processEnvAvailable: tokenStatus?.processEnvAvailable ?? false,
|
||||
unavailableReason: getOfficialChannelUnavailableReason(channelId),
|
||||
manualSetupCommands: getOfficialChannelManualSetupCommands(channelId),
|
||||
})),
|
||||
setup: buildOfficialChannelSetupSummary({
|
||||
id: channelId,
|
||||
displayName: getOfficialChannelDisplayName(channelId),
|
||||
selected,
|
||||
requiresToken: getOfficialChannelTokenIds().includes(channelId),
|
||||
tokenAvailable: tokenStatus?.available ?? false,
|
||||
tokenSource: tokenStatus?.source,
|
||||
savedInClaudeState: tokenStatus?.savedInClaudeState ?? false,
|
||||
processEnvAvailable: tokenStatus?.processEnvAvailable ?? false,
|
||||
unavailableReason: getOfficialChannelUnavailableReason(channelId),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
bunInstalled: environment.bunInstalled,
|
||||
supportedProfiles: environment.supportedProfiles,
|
||||
supportMessage: getOfficialChannelsSupportMessage(),
|
||||
accountStatusCaveat: getOfficialChannelsAccountStatusCaveat(),
|
||||
stateScopeMessage: environment.stateScopeMessage,
|
||||
claudeVersion: environment.claudeVersion,
|
||||
auth: environment.auth,
|
||||
summary: buildOfficialChannelsReadinessSummary({
|
||||
config,
|
||||
environment,
|
||||
channels: channels.map((channel) => ({
|
||||
id: channel.id,
|
||||
displayName: channel.displayName,
|
||||
selected: channel.selected,
|
||||
requiresToken: channel.requiresToken,
|
||||
tokenAvailable: channel.tokenAvailable,
|
||||
tokenSource: channel.tokenSource,
|
||||
savedInClaudeState: channel.savedInClaudeState,
|
||||
processEnvAvailable: channel.processEnvAvailable,
|
||||
unavailableReason: channel.unavailableReason,
|
||||
})),
|
||||
}),
|
||||
launchPreview: buildOfficialChannelsLaunchPreview({
|
||||
config,
|
||||
environment,
|
||||
channels: channels.map((channel) => ({
|
||||
id: channel.id,
|
||||
displayName: channel.displayName,
|
||||
selected: channel.selected,
|
||||
requiresToken: channel.requiresToken,
|
||||
tokenAvailable: channel.tokenAvailable,
|
||||
tokenSource: channel.tokenSource,
|
||||
savedInClaudeState: channel.savedInClaudeState,
|
||||
processEnvAvailable: channel.processEnvAvailable,
|
||||
unavailableReason: channel.unavailableReason,
|
||||
})),
|
||||
}),
|
||||
channels,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,9 +122,10 @@ router.use((req: Request, res: Response, next) => {
|
||||
});
|
||||
|
||||
router.get('/', (_req: Request, res: Response): void => {
|
||||
const config = getOfficialChannelsConfig();
|
||||
res.json({
|
||||
config: getOfficialChannelsConfig(),
|
||||
status: buildChannelsStatus(),
|
||||
config,
|
||||
status: buildChannelsStatus(config),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,7 +148,8 @@ router.put('/', (req: Request, res: Response): void => {
|
||||
try {
|
||||
const updated = mutateUnifiedConfig((config) => {
|
||||
config.channels = {
|
||||
selected: selected ? [...new Set(selected)] : (config.channels?.selected ?? []),
|
||||
selected:
|
||||
selected !== undefined ? [...new Set(selected)] : (config.channels?.selected ?? []),
|
||||
unattended: unattended ?? config.channels?.unattended ?? false,
|
||||
};
|
||||
});
|
||||
@@ -132,7 +196,6 @@ router.delete('/:channelId/token', (req: Request, res: Response): void => {
|
||||
res.json({
|
||||
success: true,
|
||||
tokenConfigured: false,
|
||||
tokenPath: getOfficialChannelEnvPath(channelId),
|
||||
clearedPaths,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user