refactor(cli): stop utils/ importing serve/ and the UI layer (#9146) (#9147)

packages/cli/src/utils is the directory every other directory imports
(83 from ui, 30 from the package root, 23 from config), so it only
works as a leaf. It currently imports back into six siblings, which is
what makes the package's directory graph cyclic.

Three moves, each decided by who actually consumes the module:

- serve/fast-path-argv.ts moves down into utils/. It is 25 lines with
  no imports, doing pure argv string manipulation, consumed from three
  layers. A utility filed under the daemon; relocate the target rather
  than rework the caller. Renamed serve-fast-path-argv.ts since
  fast-path-argv is ambiguous among general helpers.
- utils/windowTitle.ts moves up into ui/utils/. Its only consumers are
  AppContainer and startInteractiveUI, and it imports StreamingState,
  ICON and the OSC-8 helper.
- utils/systemInfo.ts imports formatMemoryUsage from core directly
  instead of through ui/utils/formatters.ts, a one-line re-export of
  the same symbol. It cannot move into ui/ because the daemon status
  provider consumes it, so the import is inverted instead.

Adds a no-restricted-imports rule for utils/** rejecting serve/. Scoped
to serve/ only: that direction is now completely clean, so the rule
states something true. The remaining ui/, config/, i18n/ and
nonInteractive/ edges are tracked in #9146.

Runtime upward imports out of cli/src/utils: 25 -> 20; utils -> serve
1 -> 0, eliminating that cycle; utils -> ui 7 -> 3.
This commit is contained in:
易良 2026-08-14 10:30:40 +00:00 committed by GitHub
parent 4257916e7e
commit a29c8b11db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 34 additions and 13 deletions

View file

@ -72,6 +72,27 @@ export default tseslint.config(
'import/namespace': 'off', // Disabled due to https://github.com/import-js/eslint-plugin-import/issues/2866
},
},
{
// `utils/` is the layer every other directory imports, so it must not
// import back into one. The daemon direction is clean and enforced here;
// the remaining `ui/`, `config/`, `i18n/` and `nonInteractive/` edges are
// tracked in #9146 and will be added to this group as they are resolved.
files: ['packages/cli/src/utils/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/serve/*', '**/serve/**'],
message:
'packages/cli/src/utils must not import serve/. Move lifecycle-free logic down into utils/ instead (#9146).',
},
],
},
],
},
},
{
// General overrides and rules for the project (TS/TSX files)
files: [

View file

@ -14,7 +14,7 @@ import {
} from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { ArgumentsCamelCase, Argv, Options } from 'yargs';
import { normalizeServeFastPathArgv } from './serve/fast-path-argv.js';
import { normalizeServeFastPathArgv } from './utils/serve-fast-path-argv.js';
import { initStartupProfiler } from './utils/startupProfiler.js';
import { initCpuProfiler } from './utils/cpuProfiler.js';
import {

View file

@ -10,7 +10,7 @@ import {
isValidMemoryBudgetMb,
memoryBudgetRangeError,
} from '@qwen-code/acp-bridge/daemonMemoryBudget';
import { normalizeServeFastPathArgv } from './fast-path-argv.js';
import { normalizeServeFastPathArgv } from '../utils/serve-fast-path-argv.js';
import type { ServeFastPathSettings } from './fast-path-settings.js';
import { RUNTIME_STARTUP_CANCELLED_MESSAGE } from './runtime-startup-errors.js';
import type { ServeOptions } from './types.js';

View file

@ -21,9 +21,9 @@ vi.mock('./utils/terminal-resize-reflow.js', () => ({
buildWakeRepaint: buildWakeRepaintSpy,
}));
vi.mock('../utils/windowTitle.js', async (importOriginal) => {
vi.mock('./utils/windowTitle.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../utils/windowTitle.js')>();
await importOriginal<typeof import('./utils/windowTitle.js')>();
return {
...actual,
writeTerminalTitle: (
@ -62,7 +62,7 @@ import {
import {
formatSessionWindowTitle,
writeTerminalTitle,
} from '../utils/windowTitle.js';
} from './utils/windowTitle.js';
import ansiEscapes from 'ansi-escapes';
import {
type Config,

View file

@ -158,7 +158,7 @@ import {
formatSessionWindowTitle,
titleStatusPrefix,
writeTerminalTitle,
} from '../utils/windowTitle.js';
} from './utils/windowTitle.js';
import { clearScreen } from '../utils/stdioHelpers.js';
import { useTextBuffer } from './components/shared/text-buffer.js';
import { useLogger } from './hooks/useLogger.js';

View file

@ -55,7 +55,7 @@ import { startPostRenderPrefetches } from '../startup/startup-prefetch.js';
import {
computeWindowTitle,
writeTerminalTitle,
} from '../utils/windowTitle.js';
} from './utils/windowTitle.js';
import { getCliVersion } from '../utils/version.js';
const debugLogger = createDebugLogger('STARTUP');

View file

@ -5,7 +5,7 @@
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { StreamingState } from '../ui/types.js';
import { StreamingState } from '../types.js';
import {
computeWindowTitle,
writeTerminalTitle,

View file

@ -4,9 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { sanitizeForOsc } from '../ui/utils/osc8.js';
import { ICON } from '../ui/constants.js';
import { StreamingState } from '../ui/types.js';
import { sanitizeForOsc } from './osc8.js';
import { ICON } from '../constants.js';
import { StreamingState } from '../types.js';
export const DEFAULT_WINDOW_TITLE = 'qwen';

View file

@ -31,7 +31,7 @@ import * as path from 'node:path';
import { performance } from 'node:perf_hooks';
import type { StartupEventAttrs } from '@qwen-code/qwen-code-core';
import { isServeFastPathArgv } from '../serve/fast-path-argv.js';
import { isServeFastPathArgv } from './serve-fast-path-argv.js';
interface Checkpoint {
name: string;

View file

@ -13,9 +13,9 @@ import {
IdeClient,
AuthType,
createDebugLogger,
formatMemoryUsage,
type LspStatusSnapshot,
} from '@qwen-code/qwen-code-core';
import { formatMemoryUsage } from '../ui/utils/formatters.js';
import { GIT_COMMIT_INFO } from '../generated/git-commit.js';
const debugLogger = createDebugLogger('STATUS');