fix(config/loader): break runtime cycle from normalizers to channels-runtime

The normalizers extraction in #1168 introduced a circular module-load
chain that crashed the built CLI:

  ccs.js -> errors -> services/logging -> log-config -> config-loader-facade
   -> unified-config-loader -> loader/normalizers -> channels/official-channels-runtime
   -> utils/claude-detector -> utils/shell-executor -> utils/websearch-manager
   -> utils/websearch/hook-env -> utils/websearch/trace
   -> services/logging (mid-load - createLogger undefined) -> CRASH

normalizers only needed three pure helpers from official-channels-runtime
(isOfficialChannelId, normalizeOfficialChannelIds, resolveLegacyDiscord
Selection) but pulled in the whole file's claude-detector / shell-
executor / websearch chain.

Fix: extract those three helpers + OFFICIAL_CHANNEL_IDS into a leaf
module src/channels/official-channels-ids.ts with no runtime deps.
Update normalizers.ts and config-getters.ts to import from the leaf.
official-channels-runtime.ts re-exports from the leaf for callers that
still want the bundled API.

Also revert the over-eager facade-import migration in
src/utils/config-manager.ts (it was importing from config-loader-facade
which re-exports from itself, creating a direct cycle).

Verified: dist/ccs.js boots cleanly (--version returns); test:all
1828/1828 pass; typecheck/lint/format clean.

Refs #1135
This commit is contained in:
Tam Nhu Tran
2026-05-03 12:20:32 -04:00
parent 164dcabd20
commit b6a49eeab7
5 changed files with 65 additions and 28 deletions
+48
View File
@@ -0,0 +1,48 @@
/**
* Official Channels IDs (leaf module, no runtime dependencies)
*
* Pure helpers extracted from `official-channels-runtime` so that
* config-loader code can use them without transitively pulling in
* `claude-detector` / `shell-executor` / `websearch-manager`.
*
* The full channel definitions (with display names, plugin specs, env keys,
* etc.) live in `official-channels-runtime`. This module only owns:
* - the canonical ordered list of channel IDs
* - the type-narrowing predicate
* - the order-preserving normalizer
* - the legacy-discord shim
*/
import type { OfficialChannelId } from '../config/unified-config-types';
/** Canonical, ordered list of official channel IDs. */
export const OFFICIAL_CHANNEL_IDS: readonly OfficialChannelId[] = [
'telegram',
'discord',
'imessage',
] as const;
const OFFICIAL_CHANNEL_ID_SET = new Set<string>(OFFICIAL_CHANNEL_IDS);
export function isOfficialChannelId(value: string): value is OfficialChannelId {
return OFFICIAL_CHANNEL_ID_SET.has(value);
}
export function normalizeOfficialChannelIds(values: readonly string[]): OfficialChannelId[] {
const seen = new Set<OfficialChannelId>();
const normalized: OfficialChannelId[] = [];
for (const channelId of OFFICIAL_CHANNEL_IDS) {
if (!values.includes(channelId) || seen.has(channelId)) {
continue;
}
seen.add(channelId);
normalized.push(channelId);
}
return normalized;
}
export function resolveLegacyDiscordSelection(enabled: boolean | undefined): OfficialChannelId[] {
return enabled ? ['discord'] : [];
}
+14 -25
View File
@@ -64,7 +64,20 @@ export const OFFICIAL_CHANNELS: Record<OfficialChannelId, OfficialChannelDefinit
},
};
export const OFFICIAL_CHANNEL_IDS = Object.keys(OFFICIAL_CHANNELS) as OfficialChannelId[];
// Re-export from leaf module so config-loader can use these without pulling
// in this file's claude-detector / shell-executor dep chain.
import {
OFFICIAL_CHANNEL_IDS,
isOfficialChannelId,
normalizeOfficialChannelIds,
resolveLegacyDiscordSelection,
} from './official-channels-ids';
export {
OFFICIAL_CHANNEL_IDS,
isOfficialChannelId,
normalizeOfficialChannelIds,
resolveLegacyDiscordSelection,
};
export const MINIMUM_OFFICIAL_CHANNELS_CLAUDE_VERSION = '2.1.80';
export interface OfficialChannelsVersionSummary {
@@ -163,26 +176,6 @@ export function isDiscordChannelsSessionSupported(
return target === 'claude' && (profileType === 'default' || profileType === 'account');
}
export function isOfficialChannelId(value: string): value is OfficialChannelId {
return value in OFFICIAL_CHANNELS;
}
export function normalizeOfficialChannelIds(values: readonly string[]): OfficialChannelId[] {
const seen = new Set<OfficialChannelId>();
const normalized: OfficialChannelId[] = [];
for (const channelId of OFFICIAL_CHANNEL_IDS) {
if (!values.includes(channelId) || seen.has(channelId)) {
continue;
}
seen.add(channelId);
normalized.push(channelId);
}
return normalized;
}
export function hasExplicitChannelsFlag(args: string[]): boolean {
return args.some((arg) => arg === '--channels' || arg.startsWith('--channels='));
}
@@ -757,10 +750,6 @@ export function isOfficialChannelSelectionValid(selection: string): boolean {
);
}
export function resolveLegacyDiscordSelection(enabled: boolean | undefined): OfficialChannelId[] {
return enabled ? ['discord'] : [];
}
export function getOfficialChannelsSupportedProfiles(): string[] {
return ['default', 'account'];
}
+1 -1
View File
@@ -32,7 +32,7 @@ import type {
} from '../unified-config-types';
import { canonicalizeBrowserConfig } from './normalizers';
import { canonicalizeImageAnalysisConfig } from '../../utils/hooks/image-analysis-backend-resolver';
import { normalizeOfficialChannelIds } from '../../channels/official-channels-runtime';
import { normalizeOfficialChannelIds } from '../../channels/official-channels-ids';
import { normalizeSearxngBaseUrl } from '../../utils/websearch/types';
// ---------------------------------------------------------------------------
+1 -1
View File
@@ -24,7 +24,7 @@ import {
isOfficialChannelId,
normalizeOfficialChannelIds,
resolveLegacyDiscordSelection,
} from '../../channels/official-channels-runtime';
} from '../../channels/official-channels-ids';
import { getRecommendedBrowserUserDataDir } from '../../utils/browser/browser-settings';
import { GO_DURATION_PATTERN, GO_DURATION_SEGMENT } from './io-locks';
+1 -1
View File
@@ -6,7 +6,7 @@ import { isConfig, isSettings } from '../types';
import type { Config, Settings, CLIProxyVariantsConfig, CLIProxyVariantConfig } from '../types';
import { expandPath, error } from './helpers';
import { info } from './ui';
import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/config-loader-facade';
import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
// TODO: Replace with proper imports after converting these files
// const { ErrorManager } = require('./error-manager');