Skip to content

Commit e60c968

Browse files
committed
Add hook to setup repo when agent starts
Fixes #5605
1 parent 74642ce commit e60c968

3 files changed

Lines changed: 150 additions & 1 deletion

File tree

.github/hooks/setupRepo.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"hooks": {
3+
"SessionStart": [
4+
{
5+
"type": "command",
6+
"command": "npm run agent:setup-repo"
7+
}
8+
]
9+
}
10+
}

bin/agent/setup-repo.mjs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// @ts-check
2+
3+
import { cpSync, existsSync, lstatSync, readFileSync } from 'node:fs';
4+
import { spawnSync } from 'node:child_process';
5+
import { basename, dirname, resolve, sep } from 'node:path';
6+
import { fileURLToPath } from 'node:url';
7+
8+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
9+
const nodeModulesPath = resolve(repoRoot, 'node_modules');
10+
const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
11+
12+
/** @typedef {{ folder: string; reason: string }} Candidate */
13+
14+
/** @param {string} message */
15+
function log(message) {
16+
console.info(`[setup-fast] ${message}`);
17+
}
18+
19+
/** @param {string[]} args */
20+
function runNpm(args) {
21+
log(`Running: npm ${args.join(' ')}`);
22+
const result = spawnSync(npmExecutable, args, {
23+
cwd: repoRoot,
24+
stdio: 'inherit'
25+
});
26+
if (result.error) {
27+
throw result.error;
28+
}
29+
if ((result.status ?? 1) !== 0) {
30+
process.exit(result.status ?? 1);
31+
}
32+
}
33+
34+
/**
35+
* @param {Candidate[]} candidates
36+
* @param {string | undefined} folder
37+
* @param {string} reason
38+
*/
39+
function addCandidate(candidates, folder, reason) {
40+
if (!folder) {
41+
return;
42+
}
43+
if (candidates.some(candidate => candidate.folder === folder)) {
44+
return;
45+
}
46+
candidates.push({ folder, reason });
47+
log(`Candidate found (${reason}): ${folder}`);
48+
}
49+
50+
function detectMainSiblingFolder() {
51+
const currentFolderName = basename(repoRoot);
52+
if (!currentFolderName.startsWith('xterm.js') || currentFolderName === 'xterm.js') {
53+
log(`Current folder is "${currentFolderName}", skipping xterm.js sibling lookup.`);
54+
return undefined;
55+
}
56+
const siblingFolder = resolve(dirname(repoRoot), 'xterm.js');
57+
log(`Current folder is "${currentFolderName}", sibling main folder candidate: ${siblingFolder}`);
58+
return siblingFolder;
59+
}
60+
61+
function detectWorktreeMainFolder() {
62+
const gitPath = resolve(repoRoot, '.git');
63+
if (!existsSync(gitPath)) {
64+
log('No .git entry found at repo root.');
65+
return undefined;
66+
}
67+
const gitStat = lstatSync(gitPath);
68+
if (!gitStat.isFile()) {
69+
log('.git is not a file, this repo does not appear to be a worktree checkout.');
70+
return undefined;
71+
}
72+
const gitFileContent = readFileSync(gitPath, 'utf8').trim();
73+
const gitDirMatch = /^gitdir:\s*(.+)$/m.exec(gitFileContent);
74+
if (!gitDirMatch) {
75+
log('Could not parse gitdir from .git file.');
76+
return undefined;
77+
}
78+
79+
const gitDirPath = resolve(repoRoot, gitDirMatch[1].trim());
80+
log(`Parsed gitdir from .git file: ${gitDirPath}`);
81+
82+
const normalizedGitDirPath = gitDirPath.replace(/\\/g, '/');
83+
const worktreeMarker = '/.git/worktrees/';
84+
const markerIndex = normalizedGitDirPath.indexOf(worktreeMarker);
85+
if (markerIndex === -1) {
86+
log('gitdir path does not contain /.git/worktrees/, skipping worktree main folder lookup.');
87+
return undefined;
88+
}
89+
90+
const normalizedMainFolder = normalizedGitDirPath.slice(0, markerIndex);
91+
const mainFolder = sep === '/' ? normalizedMainFolder : normalizedMainFolder.split('/').join(sep);
92+
log(`Worktree main folder candidate: ${mainFolder}`);
93+
return mainFolder;
94+
}
95+
96+
function resolveSourceFolder() {
97+
/** @type {Candidate[]} */
98+
const candidates = [];
99+
addCandidate(candidates, detectMainSiblingFolder(), 'xterm.js sibling');
100+
addCandidate(candidates, detectWorktreeMainFolder(), 'worktree main repo');
101+
102+
for (const candidate of candidates) {
103+
const candidateNodeModulesPath = resolve(candidate.folder, 'node_modules');
104+
if (existsSync(candidateNodeModulesPath)) {
105+
log(`Using candidate (${candidate.reason}) with node_modules: ${candidate.folder}`);
106+
return candidate.folder;
107+
}
108+
log(`Candidate skipped (${candidate.reason}), node_modules missing: ${candidateNodeModulesPath}`);
109+
}
110+
111+
log('No candidate folder with node_modules was found.');
112+
return undefined;
113+
}
114+
115+
if (!existsSync(nodeModulesPath)) {
116+
log(`node_modules missing: ${nodeModulesPath}`);
117+
const sourceFolder = resolveSourceFolder();
118+
if (sourceFolder) {
119+
const sourceNodeModulesPath = resolve(sourceFolder, 'node_modules');
120+
log(`Copying node_modules from ${sourceNodeModulesPath} to ${nodeModulesPath}`);
121+
try {
122+
cpSync(sourceNodeModulesPath, nodeModulesPath, { recursive: true });
123+
log('node_modules copy completed.');
124+
} catch (error) {
125+
const message = error instanceof Error ? error.message : String(error);
126+
log(`node_modules copy failed: ${message}`);
127+
log('Falling back to npm ci.');
128+
runNpm(['ci']);
129+
}
130+
} else {
131+
log('No source folder available, running npm ci.');
132+
runNpm(['ci']);
133+
}
134+
} else {
135+
log(`node_modules already exists: ${nodeModulesPath}`);
136+
}
137+
138+
runNpm(['run', 'setup']);

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@
7272
"prepackage-headless": "npm run esbuild-package-headless-only",
7373
"package-headless": "webpack --config ./webpack.config.headless.js",
7474
"postpackage-headless": "node ./bin/package_headless.js",
75-
"prepublishOnly": "npm run package"
75+
"prepublishOnly": "npm run package",
76+
"agent:session-start": "node bin/agent/setup-repo.mjs"
7677
},
7778
"devDependencies": {
7879
"@lunapaint/png-codec": "^0.2.0",

0 commit comments

Comments
 (0)