<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Summary Improves reliability and correctness of `bin/` maintenance scripts used for local development and CI. ## Behavioral fixes - **Exit codes**: `test_integration.js` exits with code 1 when `--suite` names an unknown suite; `test_unit.js` uses a non-zero fallback (`-1`) when the coverage or mocha child process has no status; `esbuild_all.mjs` exits non-zero if any parallel esbuild child fails. - **Repo root**: `setup-full.mjs` resolves the repository root one directory above `bin/` (was incorrectly using `bin/` as root). - **esbuild_all**: Propagates child build failures instead of always exiting 0. - **Integration suites**: When `--suite` is omitted, all configured Playwright suites (core + addons) run; when provided, only the matching suite runs. - **test_unit.js**: Declares the `files` filter variable so file arguments from `npm run test-unit` work without a ReferenceError. - **test_mousemodes.js**: Uses the correct cursor-home CSI (`ESC [ H`) when clearing the menu screen. - **Cleanup**: Removes debug logging from the test-unit coverage path; adds a missing semicolon after the suite regex match. ## Affected scripts - `bin/test_integration.js` - `bin/test_unit.js` - `bin/esbuild_all.mjs` - `bin/setup-full.mjs` - `bin/test_mousemodes.js` ## Testing - Syntax-checked affected scripts with `node --check` - Verified `node bin/test_integration.js --suite=unknown` exits 1 with an error message - `npm run lint-changes` (no TypeScript files changed) <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-94b28f5d-740c-4cb6-8cd9-40f35bc6086c"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-94b28f5d-740c-4cb6-8cd9-40f35bc6086c"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
139 lines
4.5 KiB
JavaScript
139 lines
4.5 KiB
JavaScript
// @ts-check
|
|
|
|
import { cpSync, existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { basename, dirname, resolve, sep } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const nodeModulesPath = resolve(repoRoot, 'node_modules');
|
|
const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
|
|
/** @typedef {{ folder: string; reason: string }} Candidate */
|
|
|
|
/** @param {string} message */
|
|
function log(message) {
|
|
console.info(`[setup-full] ${message}`);
|
|
}
|
|
|
|
/** @param {string[]} args */
|
|
function runNpm(args) {
|
|
log(`Running: npm ${args.join(' ')}`);
|
|
const result = spawnSync(npmExecutable, args, {
|
|
cwd: repoRoot,
|
|
stdio: 'inherit'
|
|
});
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
if ((result.status ?? 1) !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Candidate[]} candidates
|
|
* @param {string | undefined} folder
|
|
* @param {string} reason
|
|
*/
|
|
function addCandidate(candidates, folder, reason) {
|
|
if (!folder) {
|
|
return;
|
|
}
|
|
if (candidates.some(candidate => candidate.folder === folder)) {
|
|
return;
|
|
}
|
|
candidates.push({ folder, reason });
|
|
log(`Candidate found (${reason}): ${folder}`);
|
|
}
|
|
|
|
function detectMainSiblingFolder() {
|
|
const currentFolderName = basename(repoRoot);
|
|
if (!currentFolderName.startsWith('xterm.js') || currentFolderName === 'xterm.js') {
|
|
log(`Current folder is "${currentFolderName}", skipping xterm.js sibling lookup.`);
|
|
return undefined;
|
|
}
|
|
const siblingFolder = resolve(dirname(repoRoot), 'xterm.js');
|
|
log(`Current folder is "${currentFolderName}", sibling main folder candidate: ${siblingFolder}`);
|
|
return siblingFolder;
|
|
}
|
|
|
|
function detectWorktreeMainFolder() {
|
|
const gitPath = resolve(repoRoot, '.git');
|
|
if (!existsSync(gitPath)) {
|
|
log('No .git entry found at repo root.');
|
|
return undefined;
|
|
}
|
|
const gitStat = lstatSync(gitPath);
|
|
if (!gitStat.isFile()) {
|
|
log('.git is not a file, this repo does not appear to be a worktree checkout.');
|
|
return undefined;
|
|
}
|
|
const gitFileContent = readFileSync(gitPath, 'utf8').trim();
|
|
const gitDirMatch = /^gitdir:\s*(.+)$/m.exec(gitFileContent);
|
|
if (!gitDirMatch) {
|
|
log('Could not parse gitdir from .git file.');
|
|
return undefined;
|
|
}
|
|
|
|
const gitDirPath = resolve(repoRoot, gitDirMatch[1].trim());
|
|
log(`Parsed gitdir from .git file: ${gitDirPath}`);
|
|
|
|
const normalizedGitDirPath = gitDirPath.replace(/\\/g, '/');
|
|
const worktreeMarker = '/.git/worktrees/';
|
|
const markerIndex = normalizedGitDirPath.indexOf(worktreeMarker);
|
|
if (markerIndex === -1) {
|
|
log('gitdir path does not contain /.git/worktrees/, skipping worktree main folder lookup.');
|
|
return undefined;
|
|
}
|
|
|
|
const normalizedMainFolder = normalizedGitDirPath.slice(0, markerIndex);
|
|
const mainFolder = sep === '/' ? normalizedMainFolder : normalizedMainFolder.split('/').join(sep);
|
|
log(`Worktree main folder candidate: ${mainFolder}`);
|
|
return mainFolder;
|
|
}
|
|
|
|
function resolveSourceFolder() {
|
|
/** @type {Candidate[]} */
|
|
const candidates = [];
|
|
addCandidate(candidates, detectMainSiblingFolder(), 'xterm.js sibling');
|
|
addCandidate(candidates, detectWorktreeMainFolder(), 'worktree main repo');
|
|
|
|
for (const candidate of candidates) {
|
|
const candidateNodeModulesPath = resolve(candidate.folder, 'node_modules');
|
|
if (existsSync(candidateNodeModulesPath)) {
|
|
log(`Using candidate (${candidate.reason}) with node_modules: ${candidate.folder}`);
|
|
return candidate.folder;
|
|
}
|
|
log(`Candidate skipped (${candidate.reason}), node_modules missing: ${candidateNodeModulesPath}`);
|
|
}
|
|
|
|
log('No candidate folder with node_modules was found.');
|
|
return undefined;
|
|
}
|
|
|
|
if (!existsSync(nodeModulesPath)) {
|
|
log(`node_modules missing: ${nodeModulesPath}`);
|
|
const sourceFolder = resolveSourceFolder();
|
|
if (sourceFolder) {
|
|
const sourceNodeModulesPath = resolve(sourceFolder, 'node_modules');
|
|
log(`Copying node_modules from ${sourceNodeModulesPath} to ${nodeModulesPath}`);
|
|
try {
|
|
cpSync(sourceNodeModulesPath, nodeModulesPath, { recursive: true });
|
|
log('node_modules copy completed.');
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
log(`node_modules copy failed: ${message}`);
|
|
log('Falling back to npm ci.');
|
|
runNpm(['ci']);
|
|
}
|
|
} else {
|
|
log('No source folder available, running npm ci.');
|
|
runNpm(['ci']);
|
|
}
|
|
} else {
|
|
log(`node_modules already exists: ${nodeModulesPath}`);
|
|
}
|
|
|
|
runNpm(['run', 'setup']);
|