Skip to content

Commit a29c8b1

Browse files
authored
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.
1 parent 4257916 commit a29c8b1

11 files changed

Lines changed: 34 additions & 13 deletions

eslint.config.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ export default tseslint.config(
7272
'import/namespace': 'off', // Disabled due to https://github.com/import-js/eslint-plugin-import/issues/2866
7373
},
7474
},
75+
{
76+
// `utils/` is the layer every other directory imports, so it must not
77+
// import back into one. The daemon direction is clean and enforced here;
78+
// the remaining `ui/`, `config/`, `i18n/` and `nonInteractive/` edges are
79+
// tracked in #9146 and will be added to this group as they are resolved.
80+
files: ['packages/cli/src/utils/**/*.{ts,tsx}'],
81+
rules: {
82+
'no-restricted-imports': [
83+
'error',
84+
{
85+
patterns: [
86+
{
87+
group: ['**/serve/*', '**/serve/**'],
88+
message:
89+
'packages/cli/src/utils must not import serve/. Move lifecycle-free logic down into utils/ instead (#9146).',
90+
},
91+
],
92+
},
93+
],
94+
},
95+
},
7596
{
7697
// General overrides and rules for the project (TS/TSX files)
7798
files: [

packages/cli/src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
} from 'node:fs';
1515
import { fileURLToPath, pathToFileURL } from 'node:url';
1616
import type { ArgumentsCamelCase, Argv, Options } from 'yargs';
17-
import { normalizeServeFastPathArgv } from './serve/fast-path-argv.js';
17+
import { normalizeServeFastPathArgv } from './utils/serve-fast-path-argv.js';
1818
import { initStartupProfiler } from './utils/startupProfiler.js';
1919
import { initCpuProfiler } from './utils/cpuProfiler.js';
2020
import {

packages/cli/src/serve/fast-path.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
isValidMemoryBudgetMb,
1111
memoryBudgetRangeError,
1212
} from '@qwen-code/acp-bridge/daemonMemoryBudget';
13-
import { normalizeServeFastPathArgv } from './fast-path-argv.js';
13+
import { normalizeServeFastPathArgv } from '../utils/serve-fast-path-argv.js';
1414
import type { ServeFastPathSettings } from './fast-path-settings.js';
1515
import { RUNTIME_STARTUP_CANCELLED_MESSAGE } from './runtime-startup-errors.js';
1616
import type { ServeOptions } from './types.js';

packages/cli/src/ui/AppContainer.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ vi.mock('./utils/terminal-resize-reflow.js', () => ({
2121
buildWakeRepaint: buildWakeRepaintSpy,
2222
}));
2323

24-
vi.mock('../utils/windowTitle.js', async (importOriginal) => {
24+
vi.mock('./utils/windowTitle.js', async (importOriginal) => {
2525
const actual =
26-
await importOriginal<typeof import('../utils/windowTitle.js')>();
26+
await importOriginal<typeof import('./utils/windowTitle.js')>();
2727
return {
2828
...actual,
2929
writeTerminalTitle: (
@@ -62,7 +62,7 @@ import {
6262
import {
6363
formatSessionWindowTitle,
6464
writeTerminalTitle,
65-
} from '../utils/windowTitle.js';
65+
} from './utils/windowTitle.js';
6666
import ansiEscapes from 'ansi-escapes';
6767
import {
6868
type Config,

packages/cli/src/ui/AppContainer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ import {
158158
formatSessionWindowTitle,
159159
titleStatusPrefix,
160160
writeTerminalTitle,
161-
} from '../utils/windowTitle.js';
161+
} from './utils/windowTitle.js';
162162
import { clearScreen } from '../utils/stdioHelpers.js';
163163
import { useTextBuffer } from './components/shared/text-buffer.js';
164164
import { useLogger } from './hooks/useLogger.js';

packages/cli/src/ui/startInteractiveUI.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import { startPostRenderPrefetches } from '../startup/startup-prefetch.js';
5555
import {
5656
computeWindowTitle,
5757
writeTerminalTitle,
58-
} from '../utils/windowTitle.js';
58+
} from './utils/windowTitle.js';
5959
import { getCliVersion } from '../utils/version.js';
6060

6161
const debugLogger = createDebugLogger('STARTUP');

packages/cli/src/utils/windowTitle.test.ts renamed to packages/cli/src/ui/utils/windowTitle.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { describe, it, expect, vi, beforeEach } from 'vitest';
8-
import { StreamingState } from '../ui/types.js';
8+
import { StreamingState } from '../types.js';
99
import {
1010
computeWindowTitle,
1111
writeTerminalTitle,
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import { sanitizeForOsc } from '../ui/utils/osc8.js';
8-
import { ICON } from '../ui/constants.js';
9-
import { StreamingState } from '../ui/types.js';
7+
import { sanitizeForOsc } from './osc8.js';
8+
import { ICON } from '../constants.js';
9+
import { StreamingState } from '../types.js';
1010

1111
export const DEFAULT_WINDOW_TITLE = 'qwen';
1212

File renamed without changes.

packages/cli/src/utils/startupProfiler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import * as path from 'node:path';
3131
import { performance } from 'node:perf_hooks';
3232

3333
import type { StartupEventAttrs } from '@qwen-code/qwen-code-core';
34-
import { isServeFastPathArgv } from '../serve/fast-path-argv.js';
34+
import { isServeFastPathArgv } from './serve-fast-path-argv.js';
3535

3636
interface Checkpoint {
3737
name: string;

0 commit comments

Comments
 (0)