Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ find tests/smoke -name '*.test.ts' | sort

```bash
npm ci # 安装依赖
npm run build # 编译(E2E / smoke 测试需要 dist/main.js)
npm run build # 编译(E2E / smoke 测试需要 dist/src/main.js)
```

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

- opencli 通过 Browser Bridge 扩展连接已运行的 Chrome 浏览器
- E2E 测试通过 `tests/e2e/helpers.ts` 里的 `runCli()` 调用已构建的 `dist/main.js`
- E2E 测试通过 `tests/e2e/helpers.ts` 里的 `runCli()` 调用已构建的 `dist/src/main.js`
- `browser-public.test.ts` 使用 `tryBrowserCommand()`,站点反爬或地域限制导致空数据时会 warn + pass
- `browser-auth.test.ts` 验证 **graceful failure**,重点是不 crash、不 hang、错误信息可控
- 如需测试完整登录态,保持 Chrome 登录态并安装 Browser Bridge 扩展,再手动运行对应测试
Expand Down
2 changes: 1 addition & 1 deletion autoresearch/eval-save.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function judge(criteria: JudgeCriteria, output: string): boolean {

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

/** Run a command, using local dist/main.js instead of global opencli for consistency */
/** Run a command, using the local built entrypoint instead of global opencli for consistency */
function runCommand(cmd: string, timeout = 30000): string {
// Use local build so tests always run against the current source
const localCmd = cmd.replace(/^opencli /, `node dist/src/main.js `);
Expand Down
4 changes: 2 additions & 2 deletions docs/developer/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ find tests/smoke -name '*.test.ts' | sort

```bash
npm ci # 安装依赖
npm run build # 编译(E2E / smoke 测试需要 dist/main.js)
npm run build # 编译(E2E / smoke 测试需要 dist/src/main.js)
```

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

- opencli 通过 Browser Bridge 扩展连接已运行的 Chrome 浏览器
- E2E 测试通过 `tests/e2e/helpers.ts` 里的 `runCli()` 调用已构建的 `dist/main.js`
- E2E 测试通过 `tests/e2e/helpers.ts` 里的 `runCli()` 调用已构建的 `dist/src/main.js`
- `browser-public.test.ts` 使用 `tryBrowserCommand()`,站点反爬或地域限制导致空数据时会 warn + pass
- `browser-auth.test.ts` 验证 **graceful failure**,重点是不 crash、不 hang、错误信息可控
- 如需测试完整登录态,保持 Chrome 登录态并安装 Browser Bridge 扩展,再手动运行对应测试
Expand Down
11 changes: 6 additions & 5 deletions src/build-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* manifest.json for instant cold-start registration (no runtime YAML parsing).
*
* Usage: npx tsx src/build-manifest.ts
* Output: dist/cli-manifest.json
* Output: cli-manifest.json at the package root
*/

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

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

export interface ManifestEntry {
site: string;
Expand Down Expand Up @@ -254,7 +255,7 @@ async function main(): Promise<void> {
// entry-point loses its executable permission, causing "Permission denied".
// See: https://github.com/jackwener/opencli/issues/446
if (process.platform !== 'win32') {
const projectRoot = path.resolve(__dirname, '..', '..');
const projectRoot = PACKAGE_ROOT;
const pkgPath = path.resolve(projectRoot, 'package.json');
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
Expand Down
44 changes: 2 additions & 42 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { Command } from 'commander';
import chalk from 'chalk';
import { findPackageRoot, getBuiltEntryCandidates } from './package-paths.js';
import { type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
import { serializeCommand, formatArgSummary } from './serialization.js';
import { render as renderOutput } from './output.js';
Expand Down Expand Up @@ -1016,48 +1017,7 @@ export interface OperateVerifyInvocation {
shell?: boolean;
}

export function findPackageRoot(startFile: string, fileExists: (path: string) => boolean = fs.existsSync): string {
let dir = path.dirname(startFile);

while (true) {
if (fileExists(path.join(dir, 'package.json'))) return dir;
const parent = path.dirname(dir);
if (parent === dir) {
throw new Error(`Could not find package.json above ${startFile}`);
}
dir = parent;
}
}

function getBuiltEntryCandidates(packageRoot: string, readFile: (path: string) => string): string[] {
const candidates: string[] = [];
try {
const pkg = JSON.parse(readFile(path.join(packageRoot, 'package.json'))) as {
bin?: string | Record<string, string>;
main?: string;
};

if (typeof pkg.bin === 'string') {
candidates.push(path.join(packageRoot, pkg.bin));
} else if (pkg.bin && typeof pkg.bin === 'object' && typeof pkg.bin.opencli === 'string') {
candidates.push(path.join(packageRoot, pkg.bin.opencli));
}

if (typeof pkg.main === 'string') {
candidates.push(path.join(packageRoot, pkg.main));
}
} catch {
// Fall through to compatibility candidates below.
}

// Compatibility fallback for partially-built trees or older layouts.
candidates.push(
path.join(packageRoot, 'dist', 'src', 'main.js'),
path.join(packageRoot, 'dist', 'main.js'),
);

return [...new Set(candidates)];
}
export { findPackageRoot };

export function resolveOperateVerifyInvocation(opts: {
projectRoot?: string;
Expand Down
20 changes: 5 additions & 15 deletions src/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { type CliCommand, type InternalCliCommand, type Arg, Strategy, registerC
import { getErrorMessage } from './errors.js';
import { log } from './logger.js';
import type { ManifestEntry } from './build-manifest.js';
import { findPackageRoot, getCliManifestPath, getFetchAdaptersScriptPath } from './package-paths.js';

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

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

/**
* Find the package root (directory containing package.json).
* Dev: import.meta.url is in src/ → one level up.
* Prod: import.meta.url is in dist/src/ → two levels up.
*/
function findPackageRoot(): string {
let dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
if (!fs.existsSync(path.join(dir, 'package.json'))) {
dir = path.resolve(dir, '..');
}
return dir;
}
const PACKAGE_ROOT = findPackageRoot(fileURLToPath(import.meta.url));

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

// Create node_modules/@jackwener/opencli symlink pointing to the installed package root.
const opencliRoot = findPackageRoot();
const opencliRoot = PACKAGE_ROOT;
const symlinkDir = path.join(baseDir, 'node_modules', '@jackwener');
const symlinkPath = path.join(symlinkDir, 'opencli');
try {
Expand Down Expand Up @@ -118,7 +108,7 @@ export async function ensureUserAdapters(): Promise<void> {
log.info('First run detected — copying adapters (one-time setup)...');
try {
const { execFileSync } = await import('node:child_process');
const scriptPath = path.join(findPackageRoot(), 'scripts', 'fetch-adapters.js');
const scriptPath = getFetchAdaptersScriptPath(PACKAGE_ROOT);
execFileSync(process.execPath, [scriptPath], {
stdio: 'inherit',
env: { ...process.env, _OPENCLI_FIRST_RUN: '1' },
Expand All @@ -137,7 +127,7 @@ export async function ensureUserAdapters(): Promise<void> {
export async function discoverClis(...dirs: string[]): Promise<void> {
// Fast path: try manifest first (production / post-build)
for (const dir of dirs) {
const manifestPath = path.resolve(dir, '..', 'cli-manifest.json');
const manifestPath = getCliManifestPath(dir);
try {
await fs.promises.access(manifestPath);
const loaded = await loadFromManifest(manifestPath, dir);
Expand Down
58 changes: 58 additions & 0 deletions src/package-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as fs from 'node:fs';
import * as path from 'node:path';

export interface PackageJsonLike {
bin?: string | Record<string, string>;
main?: string;
}

export function findPackageRoot(startFile: string, fileExists: (candidate: string) => boolean = fs.existsSync): string {
let dir = path.dirname(startFile);

while (true) {
if (fileExists(path.join(dir, 'package.json'))) return dir;
const parent = path.dirname(dir);
if (parent === dir) {
throw new Error(`Could not find package.json above ${startFile}`);
}
dir = parent;
}
}

export function getBuiltEntryCandidates(
packageRoot: string,
readFile: (filePath: string) => string = (filePath) => fs.readFileSync(filePath, 'utf-8'),
): string[] {
const candidates: string[] = [];
try {
const pkg = JSON.parse(readFile(path.join(packageRoot, 'package.json'))) as PackageJsonLike;

if (typeof pkg.bin === 'string') {
candidates.push(path.join(packageRoot, pkg.bin));
} else if (pkg.bin && typeof pkg.bin === 'object' && typeof pkg.bin.opencli === 'string') {
candidates.push(path.join(packageRoot, pkg.bin.opencli));
}

if (typeof pkg.main === 'string') {
candidates.push(path.join(packageRoot, pkg.main));
}
} catch {
// Fall through to compatibility candidates below.
}

// Compatibility fallback for partially-built trees or older layouts.
candidates.push(
path.join(packageRoot, 'dist', 'src', 'main.js'),
path.join(packageRoot, 'dist', 'main.js'),
);

return [...new Set(candidates)];
}

export function getCliManifestPath(clisDir: string): string {
return path.resolve(clisDir, '..', 'cli-manifest.json');
}

export function getFetchAdaptersScriptPath(packageRoot: string): string {
return path.join(packageRoot, 'scripts', 'fetch-adapters.js');
}