Skip to content

Commit a3efdc1

Browse files
authored
refactor: centralize build path resolution (#807)
1 parent d51338c commit a3efdc1

7 files changed

Lines changed: 76 additions & 67 deletions

File tree

TESTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ find tests/smoke -name '*.test.ts' | sort
9494

9595
```bash
9696
npm ci # 安装依赖
97-
npm run build # 编译(E2E / smoke 测试需要 dist/main.js)
97+
npm run build # 编译(E2E / smoke 测试需要 dist/src/main.js)
9898
```
9999

100100
### 运行命令
@@ -123,7 +123,7 @@ npx vitest src/
123123
### 浏览器命令本地测试须知
124124

125125
- opencli 通过 Browser Bridge 扩展连接已运行的 Chrome 浏览器
126-
- E2E 测试通过 `tests/e2e/helpers.ts` 里的 `runCli()` 调用已构建的 `dist/main.js`
126+
- E2E 测试通过 `tests/e2e/helpers.ts` 里的 `runCli()` 调用已构建的 `dist/src/main.js`
127127
- `browser-public.test.ts` 使用 `tryBrowserCommand()`,站点反爬或地域限制导致空数据时会 warn + pass
128128
- `browser-auth.test.ts` 验证 **graceful failure**,重点是不 crash、不 hang、错误信息可控
129129
- 如需测试完整登录态,保持 Chrome 登录态并安装 Browser Bridge 扩展,再手动运行对应测试

autoresearch/eval-save.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ function judge(criteria: JudgeCriteria, output: string): boolean {
8080

8181
const PROJECT_ROOT = join(__dirname, '..');
8282

83-
/** Run a command, using local dist/main.js instead of global opencli for consistency */
83+
/** Run a command, using the local built entrypoint instead of global opencli for consistency */
8484
function runCommand(cmd: string, timeout = 30000): string {
8585
// Use local build so tests always run against the current source
8686
const localCmd = cmd.replace(/^opencli /, `node dist/src/main.js `);

docs/developer/testing.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ find tests/smoke -name '*.test.ts' | sort
9595

9696
```bash
9797
npm ci # 安装依赖
98-
npm run build # 编译(E2E / smoke 测试需要 dist/main.js)
98+
npm run build # 编译(E2E / smoke 测试需要 dist/src/main.js)
9999
```
100100

101101
### 运行命令
@@ -127,7 +127,7 @@ npx vitest src/
127127
### 浏览器命令本地测试须知
128128

129129
- opencli 通过 Browser Bridge 扩展连接已运行的 Chrome 浏览器
130-
- E2E 测试通过 `tests/e2e/helpers.ts` 里的 `runCli()` 调用已构建的 `dist/main.js`
130+
- E2E 测试通过 `tests/e2e/helpers.ts` 里的 `runCli()` 调用已构建的 `dist/src/main.js`
131131
- `browser-public.test.ts` 使用 `tryBrowserCommand()`,站点反爬或地域限制导致空数据时会 warn + pass
132132
- `browser-auth.test.ts` 验证 **graceful failure**,重点是不 crash、不 hang、错误信息可控
133133
- 如需测试完整登录态,保持 Chrome 登录态并安装 Browser Bridge 扩展,再手动运行对应测试

src/build-manifest.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* manifest.json for instant cold-start registration (no runtime YAML parsing).
77
*
88
* Usage: npx tsx src/build-manifest.ts
9-
* Output: dist/cli-manifest.json
9+
* Output: cli-manifest.json at the package root
1010
*/
1111

1212
import * as fs from 'node:fs';
@@ -15,10 +15,11 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
1515
import yaml from 'js-yaml';
1616
import { getErrorMessage } from './errors.js';
1717
import { fullName, getRegistry, type CliCommand } from './registry.js';
18+
import { findPackageRoot, getCliManifestPath } from './package-paths.js';
1819

19-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
20-
const CLIS_DIR = path.resolve(__dirname, '..', 'clis');
21-
const OUTPUT = path.resolve(__dirname, '..', 'cli-manifest.json');
20+
const PACKAGE_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
21+
const CLIS_DIR = path.join(PACKAGE_ROOT, 'clis');
22+
const OUTPUT = getCliManifestPath(CLIS_DIR);
2223

2324
export interface ManifestEntry {
2425
site: string;
@@ -254,7 +255,7 @@ async function main(): Promise<void> {
254255
// entry-point loses its executable permission, causing "Permission denied".
255256
// See: https://github.com/jackwener/opencli/issues/446
256257
if (process.platform !== 'win32') {
257-
const projectRoot = path.resolve(__dirname, '..', '..');
258+
const projectRoot = PACKAGE_ROOT;
258259
const pkgPath = path.resolve(projectRoot, 'package.json');
259260
try {
260261
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));

src/cli.ts

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as path from 'node:path';
1010
import { fileURLToPath } from 'node:url';
1111
import { Command } from 'commander';
1212
import chalk from 'chalk';
13+
import { findPackageRoot, getBuiltEntryCandidates } from './package-paths.js';
1314
import { type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
1415
import { serializeCommand, formatArgSummary } from './serialization.js';
1516
import { render as renderOutput } from './output.js';
@@ -1016,48 +1017,7 @@ export interface OperateVerifyInvocation {
10161017
shell?: boolean;
10171018
}
10181019

1019-
export function findPackageRoot(startFile: string, fileExists: (path: string) => boolean = fs.existsSync): string {
1020-
let dir = path.dirname(startFile);
1021-
1022-
while (true) {
1023-
if (fileExists(path.join(dir, 'package.json'))) return dir;
1024-
const parent = path.dirname(dir);
1025-
if (parent === dir) {
1026-
throw new Error(`Could not find package.json above ${startFile}`);
1027-
}
1028-
dir = parent;
1029-
}
1030-
}
1031-
1032-
function getBuiltEntryCandidates(packageRoot: string, readFile: (path: string) => string): string[] {
1033-
const candidates: string[] = [];
1034-
try {
1035-
const pkg = JSON.parse(readFile(path.join(packageRoot, 'package.json'))) as {
1036-
bin?: string | Record<string, string>;
1037-
main?: string;
1038-
};
1039-
1040-
if (typeof pkg.bin === 'string') {
1041-
candidates.push(path.join(packageRoot, pkg.bin));
1042-
} else if (pkg.bin && typeof pkg.bin === 'object' && typeof pkg.bin.opencli === 'string') {
1043-
candidates.push(path.join(packageRoot, pkg.bin.opencli));
1044-
}
1045-
1046-
if (typeof pkg.main === 'string') {
1047-
candidates.push(path.join(packageRoot, pkg.main));
1048-
}
1049-
} catch {
1050-
// Fall through to compatibility candidates below.
1051-
}
1052-
1053-
// Compatibility fallback for partially-built trees or older layouts.
1054-
candidates.push(
1055-
path.join(packageRoot, 'dist', 'src', 'main.js'),
1056-
path.join(packageRoot, 'dist', 'main.js'),
1057-
);
1058-
1059-
return [...new Set(candidates)];
1060-
}
1020+
export { findPackageRoot };
10611021

10621022
export function resolveOperateVerifyInvocation(opts: {
10631023
projectRoot?: string;

src/discovery.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { type CliCommand, type InternalCliCommand, type Arg, Strategy, registerC
1717
import { getErrorMessage } from './errors.js';
1818
import { log } from './logger.js';
1919
import type { ManifestEntry } from './build-manifest.js';
20+
import { findPackageRoot, getCliManifestPath, getFetchAdaptersScriptPath } from './package-paths.js';
2021

2122
/** User runtime directory: ~/.opencli */
2223
export const USER_OPENCLI_DIR = path.join(os.homedir(), '.opencli');
@@ -37,18 +38,7 @@ function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Str
3738

3839
import { isRecord } from './utils.js';
3940

40-
/**
41-
* Find the package root (directory containing package.json).
42-
* Dev: import.meta.url is in src/ → one level up.
43-
* Prod: import.meta.url is in dist/src/ → two levels up.
44-
*/
45-
function findPackageRoot(): string {
46-
let dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
47-
if (!fs.existsSync(path.join(dir, 'package.json'))) {
48-
dir = path.resolve(dir, '..');
49-
}
50-
return dir;
51-
}
41+
const PACKAGE_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
5242

5343
/**
5444
* Ensure ~/.opencli/node_modules/@jackwener/opencli symlink exists so that
@@ -72,7 +62,7 @@ export async function ensureUserCliCompatShims(baseDir: string = USER_OPENCLI_DI
7262
}
7363

7464
// Create node_modules/@jackwener/opencli symlink pointing to the installed package root.
75-
const opencliRoot = findPackageRoot();
65+
const opencliRoot = PACKAGE_ROOT;
7666
const symlinkDir = path.join(baseDir, 'node_modules', '@jackwener');
7767
const symlinkPath = path.join(symlinkDir, 'opencli');
7868
try {
@@ -118,7 +108,7 @@ export async function ensureUserAdapters(): Promise<void> {
118108
log.info('First run detected — copying adapters (one-time setup)...');
119109
try {
120110
const { execFileSync } = await import('node:child_process');
121-
const scriptPath = path.join(findPackageRoot(), 'scripts', 'fetch-adapters.js');
111+
const scriptPath = getFetchAdaptersScriptPath(PACKAGE_ROOT);
122112
execFileSync(process.execPath, [scriptPath], {
123113
stdio: 'inherit',
124114
env: { ...process.env, _OPENCLI_FIRST_RUN: '1' },
@@ -137,7 +127,7 @@ export async function ensureUserAdapters(): Promise<void> {
137127
export async function discoverClis(...dirs: string[]): Promise<void> {
138128
// Fast path: try manifest first (production / post-build)
139129
for (const dir of dirs) {
140-
const manifestPath = path.resolve(dir, '..', 'cli-manifest.json');
130+
const manifestPath = getCliManifestPath(dir);
141131
try {
142132
await fs.promises.access(manifestPath);
143133
const loaded = await loadFromManifest(manifestPath, dir);

src/package-paths.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as fs from 'node:fs';
2+
import * as path from 'node:path';
3+
4+
export interface PackageJsonLike {
5+
bin?: string | Record<string, string>;
6+
main?: string;
7+
}
8+
9+
export function findPackageRoot(startFile: string, fileExists: (candidate: string) => boolean = fs.existsSync): string {
10+
let dir = path.dirname(startFile);
11+
12+
while (true) {
13+
if (fileExists(path.join(dir, 'package.json'))) return dir;
14+
const parent = path.dirname(dir);
15+
if (parent === dir) {
16+
throw new Error(`Could not find package.json above ${startFile}`);
17+
}
18+
dir = parent;
19+
}
20+
}
21+
22+
export function getBuiltEntryCandidates(
23+
packageRoot: string,
24+
readFile: (filePath: string) => string = (filePath) => fs.readFileSync(filePath, 'utf-8'),
25+
): string[] {
26+
const candidates: string[] = [];
27+
try {
28+
const pkg = JSON.parse(readFile(path.join(packageRoot, 'package.json'))) as PackageJsonLike;
29+
30+
if (typeof pkg.bin === 'string') {
31+
candidates.push(path.join(packageRoot, pkg.bin));
32+
} else if (pkg.bin && typeof pkg.bin === 'object' && typeof pkg.bin.opencli === 'string') {
33+
candidates.push(path.join(packageRoot, pkg.bin.opencli));
34+
}
35+
36+
if (typeof pkg.main === 'string') {
37+
candidates.push(path.join(packageRoot, pkg.main));
38+
}
39+
} catch {
40+
// Fall through to compatibility candidates below.
41+
}
42+
43+
// Compatibility fallback for partially-built trees or older layouts.
44+
candidates.push(
45+
path.join(packageRoot, 'dist', 'src', 'main.js'),
46+
path.join(packageRoot, 'dist', 'main.js'),
47+
);
48+
49+
return [...new Set(candidates)];
50+
}
51+
52+
export function getCliManifestPath(clisDir: string): string {
53+
return path.resolve(clisDir, '..', 'cli-manifest.json');
54+
}
55+
56+
export function getFetchAdaptersScriptPath(packageRoot: string): string {
57+
return path.join(packageRoot, 'scripts', 'fetch-adapters.js');
58+
}

0 commit comments

Comments
 (0)