@tamsensedev/runner 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +245 -211
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -187,7 +187,7 @@ function findNpm(nodePath, env, exists) {
|
|
|
187
187
|
}
|
|
188
188
|
return null;
|
|
189
189
|
}
|
|
190
|
-
var runProcess = (command, args, options) => new Promise((
|
|
190
|
+
var runProcess = (command, args, options) => new Promise((resolve2, reject) => {
|
|
191
191
|
const child = spawn(command, args, { cwd: options.cwd, env: options.env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
192
192
|
const tail = [];
|
|
193
193
|
for (const stream of [child.stdout, child.stderr]) {
|
|
@@ -201,7 +201,7 @@ var runProcess = (command, args, options) => new Promise((resolve, reject) => {
|
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
203
|
child.on("error", reject);
|
|
204
|
-
child.on("close", (code) =>
|
|
204
|
+
child.on("close", (code) => resolve2({ code: code ?? 1, output: tail.join("\n") }));
|
|
205
205
|
});
|
|
206
206
|
function failureText(what, result) {
|
|
207
207
|
if (OFFLINE_PATTERN.test(result.output)) {
|
|
@@ -314,8 +314,8 @@ async function probeCapabilities(stop) {
|
|
|
314
314
|
stop?.throwIfAborted();
|
|
315
315
|
let release = () => {
|
|
316
316
|
};
|
|
317
|
-
const released = new Promise((
|
|
318
|
-
release =
|
|
317
|
+
const released = new Promise((resolve2) => {
|
|
318
|
+
release = resolve2;
|
|
319
319
|
});
|
|
320
320
|
const nothing = {
|
|
321
321
|
[Symbol.asyncIterator]: () => ({
|
|
@@ -653,8 +653,8 @@ function inputChannel(options = {}) {
|
|
|
653
653
|
}
|
|
654
654
|
const next = queue.shift();
|
|
655
655
|
if (!next) {
|
|
656
|
-
await new Promise((
|
|
657
|
-
wake =
|
|
656
|
+
await new Promise((resolve2) => {
|
|
657
|
+
wake = resolve2;
|
|
658
658
|
});
|
|
659
659
|
continue;
|
|
660
660
|
}
|
|
@@ -714,9 +714,9 @@ function inputChannel(options = {}) {
|
|
|
714
714
|
onSwitch(apply) {
|
|
715
715
|
applySwitch2 = apply;
|
|
716
716
|
if (waitingSwitch) {
|
|
717
|
-
const { change, resolve } = waitingSwitch;
|
|
717
|
+
const { change, resolve: resolve2 } = waitingSwitch;
|
|
718
718
|
waitingSwitch = null;
|
|
719
|
-
void apply(change).then(() =>
|
|
719
|
+
void apply(change).then(() => resolve2(true), () => resolve2(false));
|
|
720
720
|
}
|
|
721
721
|
},
|
|
722
722
|
close,
|
|
@@ -763,8 +763,8 @@ function inputChannel(options = {}) {
|
|
|
763
763
|
const earlier = waitingSwitch;
|
|
764
764
|
earlier?.resolve(false);
|
|
765
765
|
const merged = { model: change.model ?? earlier?.change.model ?? null, effort: change.effort ?? earlier?.change.effort ?? null };
|
|
766
|
-
return new Promise((
|
|
767
|
-
waitingSwitch = { change: merged, resolve };
|
|
766
|
+
return new Promise((resolve2) => {
|
|
767
|
+
waitingSwitch = { change: merged, resolve: resolve2 };
|
|
768
768
|
});
|
|
769
769
|
}
|
|
770
770
|
};
|
|
@@ -1048,6 +1048,206 @@ function specPromptText(spec) {
|
|
|
1048
1048
|
].filter(Boolean).join("\n\n");
|
|
1049
1049
|
}
|
|
1050
1050
|
|
|
1051
|
+
// src/workspace.ts
|
|
1052
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1053
|
+
import { rm as rm2 } from "fs/promises";
|
|
1054
|
+
import { join as join2, resolve } from "path";
|
|
1055
|
+
|
|
1056
|
+
// src/git.ts
|
|
1057
|
+
import { Buffer } from "buffer";
|
|
1058
|
+
import { spawn as spawn2 } from "child_process";
|
|
1059
|
+
var GITHUB_ORIGIN = "https://github.com/";
|
|
1060
|
+
function githubAuthEnv(token) {
|
|
1061
|
+
const credentials = Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
1062
|
+
return {
|
|
1063
|
+
GIT_CONFIG_COUNT: "1",
|
|
1064
|
+
GIT_CONFIG_KEY_0: `http.${GITHUB_ORIGIN}.extraheader`,
|
|
1065
|
+
GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${credentials}`,
|
|
1066
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
function runGit(args, options) {
|
|
1070
|
+
const env = { ...process.env, ...options.env, ...options.token ? githubAuthEnv(options.token) : {} };
|
|
1071
|
+
return new Promise((resolve2, reject) => {
|
|
1072
|
+
const child = spawn2("git", args, { cwd: options.cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
1073
|
+
let stdout = "";
|
|
1074
|
+
let stderr = "";
|
|
1075
|
+
child.stdout.on("data", (chunk) => {
|
|
1076
|
+
stdout += chunk.toString();
|
|
1077
|
+
});
|
|
1078
|
+
child.stderr.on("data", (chunk) => {
|
|
1079
|
+
stderr += chunk.toString();
|
|
1080
|
+
});
|
|
1081
|
+
child.on("error", reject);
|
|
1082
|
+
child.on("close", (code) => resolve2({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }));
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
async function git(args, options) {
|
|
1086
|
+
const result = await runGit(args, options);
|
|
1087
|
+
if (result.code !== 0) {
|
|
1088
|
+
throw new Error(`git ${args[0]} failed: ${result.stderr || result.stdout || `exit code ${result.code}`}`);
|
|
1089
|
+
}
|
|
1090
|
+
return result.stdout;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/lock.ts
|
|
1094
|
+
function keyedLock() {
|
|
1095
|
+
const tails = /* @__PURE__ */ new Map();
|
|
1096
|
+
return {
|
|
1097
|
+
async run(key, work2) {
|
|
1098
|
+
const previous = tails.get(key) ?? Promise.resolve();
|
|
1099
|
+
let release = () => {
|
|
1100
|
+
};
|
|
1101
|
+
const done2 = new Promise((resolve2) => {
|
|
1102
|
+
release = resolve2;
|
|
1103
|
+
});
|
|
1104
|
+
const tail = previous.then(() => done2);
|
|
1105
|
+
tails.set(key, tail);
|
|
1106
|
+
await previous;
|
|
1107
|
+
try {
|
|
1108
|
+
return await work2();
|
|
1109
|
+
} finally {
|
|
1110
|
+
release();
|
|
1111
|
+
if (tails.get(key) === tail) {
|
|
1112
|
+
tails.delete(key);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/workspace.ts
|
|
1120
|
+
var FALLBACK_IDENTITY = { name: "TAMsense", email: "noreply@tamsense.com" };
|
|
1121
|
+
var cacheLock = keyedLock();
|
|
1122
|
+
function githubRemoteUrl(fullName) {
|
|
1123
|
+
return `https://github.com/${fullName}.git`;
|
|
1124
|
+
}
|
|
1125
|
+
function tamsenseFolder(home, ...parts) {
|
|
1126
|
+
return join2(home, ".tamsense", ...parts);
|
|
1127
|
+
}
|
|
1128
|
+
function folderNames(repositories) {
|
|
1129
|
+
const shortName = (fullName) => fullName.split("/").pop() ?? fullName;
|
|
1130
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1131
|
+
for (const repository of repositories) {
|
|
1132
|
+
counts.set(shortName(repository.fullName), (counts.get(shortName(repository.fullName)) ?? 0) + 1);
|
|
1133
|
+
}
|
|
1134
|
+
return repositories.map((repository) => counts.get(shortName(repository.fullName)) === 1 ? shortName(repository.fullName) : repository.fullName.replace("/", "__"));
|
|
1135
|
+
}
|
|
1136
|
+
async function refreshCache(cache, repository, token, tools) {
|
|
1137
|
+
if (existsSync2(join2(cache, ".git"))) {
|
|
1138
|
+
await git(["fetch", "--prune", "origin"], { cwd: cache, token });
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
await git(["clone", "--no-checkout", tools.remoteUrl(repository.fullName), cache], { cwd: tools.home, token });
|
|
1142
|
+
}
|
|
1143
|
+
async function removeWorktree(cache, path) {
|
|
1144
|
+
await runGit(["worktree", "remove", "--force", path], { cwd: cache });
|
|
1145
|
+
await rm2(path, { recursive: true, force: true });
|
|
1146
|
+
await git(["worktree", "prune"], { cwd: cache });
|
|
1147
|
+
}
|
|
1148
|
+
function stepFolder(job) {
|
|
1149
|
+
const task = `task-${job.taskId}`;
|
|
1150
|
+
return typeof job.stage.columnId === "number" ? [task, `step-${job.stage.columnId}`] : [task];
|
|
1151
|
+
}
|
|
1152
|
+
function isReadOnlyCheck(job) {
|
|
1153
|
+
return job.stage.kind === "review" || job.stage.kind === "test";
|
|
1154
|
+
}
|
|
1155
|
+
function branchHolders(porcelain, branch) {
|
|
1156
|
+
const holders = [];
|
|
1157
|
+
let path = null;
|
|
1158
|
+
for (const line of porcelain.split("\n").map((text2) => text2.trim())) {
|
|
1159
|
+
if (line.startsWith("worktree ")) {
|
|
1160
|
+
path = line.slice("worktree ".length);
|
|
1161
|
+
} else if (path && line === `branch refs/heads/${branch}`) {
|
|
1162
|
+
holders.push(path);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return holders;
|
|
1166
|
+
}
|
|
1167
|
+
async function releaseBranch(cache, branch, keep) {
|
|
1168
|
+
const listed = await runGit(["worktree", "list", "--porcelain"], { cwd: cache });
|
|
1169
|
+
for (const path of branchHolders(listed.stdout, branch).filter((holder) => resolve(holder) !== resolve(keep))) {
|
|
1170
|
+
await removeWorktree(cache, path);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
async function baseRef(cache, job, repository) {
|
|
1174
|
+
const pushed = await runGit(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${job.branch}`], { cwd: cache });
|
|
1175
|
+
return pushed.code === 0 ? `origin/${job.branch}` : `origin/${repository.defaultBranch}`;
|
|
1176
|
+
}
|
|
1177
|
+
async function keptBase(cache, path, job, repository) {
|
|
1178
|
+
if (!job.keepWorkspace || !existsSync2(join2(path, ".git"))) {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path });
|
|
1182
|
+
if (branch.code !== 0 || branch.stdout !== (isReadOnlyCheck(job) ? "HEAD" : job.branch)) {
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
return git(["rev-parse", await baseRef(cache, job, repository)], { cwd: cache });
|
|
1186
|
+
}
|
|
1187
|
+
async function prepareWorkspace(job, token, tools) {
|
|
1188
|
+
const root = tamsenseFolder(tools.home, "work", ...stepFolder(job));
|
|
1189
|
+
const folders = folderNames(job.repositories);
|
|
1190
|
+
const repositories = [];
|
|
1191
|
+
for (const [index, repository] of job.repositories.entries()) {
|
|
1192
|
+
const cache = tamsenseFolder(tools.home, "repos", repository.fullName.replace("/", "__"));
|
|
1193
|
+
const folder = folders[index] ?? repository.fullName;
|
|
1194
|
+
const path = join2(root, folder);
|
|
1195
|
+
const baseSha = await cacheLock.run(repository.fullName, async () => {
|
|
1196
|
+
await refreshCache(cache, repository, token, tools);
|
|
1197
|
+
const kept = await keptBase(cache, path, job, repository);
|
|
1198
|
+
if (kept) {
|
|
1199
|
+
return kept;
|
|
1200
|
+
}
|
|
1201
|
+
await removeWorktree(cache, path);
|
|
1202
|
+
const base = await baseRef(cache, job, repository);
|
|
1203
|
+
if (isReadOnlyCheck(job)) {
|
|
1204
|
+
await git(["worktree", "add", "--force", "--detach", path, base], { cwd: cache });
|
|
1205
|
+
} else {
|
|
1206
|
+
await releaseBranch(cache, job.branch, path);
|
|
1207
|
+
await git(["worktree", "add", "--force", "-B", job.branch, path, base], { cwd: cache });
|
|
1208
|
+
}
|
|
1209
|
+
return git(["rev-parse", "HEAD"], { cwd: path });
|
|
1210
|
+
});
|
|
1211
|
+
repositories.push({ fullName: repository.fullName, folder, path, baseSha });
|
|
1212
|
+
}
|
|
1213
|
+
return { root, repositories };
|
|
1214
|
+
}
|
|
1215
|
+
async function commitIdentityEnv(home) {
|
|
1216
|
+
const configured = await runGit(["config", "--get", "user.email"], { cwd: home });
|
|
1217
|
+
if (configured.code === 0 && configured.stdout) {
|
|
1218
|
+
return {};
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
GIT_AUTHOR_NAME: FALLBACK_IDENTITY.name,
|
|
1222
|
+
GIT_AUTHOR_EMAIL: FALLBACK_IDENTITY.email,
|
|
1223
|
+
GIT_COMMITTER_NAME: FALLBACK_IDENTITY.name,
|
|
1224
|
+
GIT_COMMITTER_EMAIL: FALLBACK_IDENTITY.email
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
async function publishWorkspace(workspace, job, token, identity) {
|
|
1228
|
+
const pushed = [];
|
|
1229
|
+
for (const repository of workspace.repositories) {
|
|
1230
|
+
const options = { cwd: repository.path, env: identity };
|
|
1231
|
+
const sha = await cacheLock.run(repository.fullName, async () => {
|
|
1232
|
+
await git(["add", "--all"], options);
|
|
1233
|
+
const hasStaged = (await runGit(["diff", "--cached", "--quiet"], options)).code === 1;
|
|
1234
|
+
if (hasStaged) {
|
|
1235
|
+
await git(["commit", "--message", `Task #${job.taskId}: ${job.title}`], options);
|
|
1236
|
+
}
|
|
1237
|
+
const newCommits = Number(await git(["rev-list", "--count", `${repository.baseSha}..HEAD`], options));
|
|
1238
|
+
if (newCommits === 0) {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
await git(["push", "origin", `HEAD:refs/heads/${job.branch}`], { ...options, token });
|
|
1242
|
+
return git(["rev-parse", "HEAD"], options);
|
|
1243
|
+
});
|
|
1244
|
+
if (sha) {
|
|
1245
|
+
pushed.push({ repository: repository.fullName, branch: job.branch, sha });
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return pushed;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1051
1251
|
// src/prompt.ts
|
|
1052
1252
|
var VERDICT_TOOL_NAME = "submit_verdict";
|
|
1053
1253
|
var SCREENSHOT_TOOL_NAME = "attach_screenshot";
|
|
@@ -1081,8 +1281,11 @@ var NAMING_RULE = 'Name everything you attach (screenshots, replays, videos) by
|
|
|
1081
1281
|
var RECORDING_RULE = `Every browser you open with Playwright from Node is recorded for the task as a replay people can watch, requests from the tested site to the TAMsense tracker are blocked, so your runs never reach analytics, and requests other than GET, HEAD and OPTIONS to addresses outside this computer are aborted. In every browser script, load the recorder from the TAMSENSE_RECORDER environment variable (require(process.env.TAMSENSE_RECORDER), or createRequire(import.meta.url)(process.env.TAMSENSE_RECORDER) in an .mjs script), open each context with await recorder.newRecordedContext(browser, { title, purpose, context: { viewport } }), call await recorder.mark(page, 'what just happened') at every meaningful step, and end with await recorder.closeRecorded(context). Add video: true when the page has a canvas, a map, a video player or iframes from another site (a replay cannot show them), or when someone asks for a video. Recordings are attached to the task when the run ends; ${REPLAY_TOOL_NAME} and ${VIDEO_TOOL_NAME} attach one earlier with a better title.`;
|
|
1082
1282
|
var BROWSER_RULE = "A headless browser is installed for interface checks. If the project has no Playwright of its own, load playwright-core from the folder in the TAMSENSE_PLAYWRIGHT environment variable, with PLAYWRIGHT_BROWSERS_PATH set to the folder in TAMSENSE_PLAYWRIGHT_BROWSERS.";
|
|
1083
1283
|
var DEPENDENCIES_RULE = "The checkout may have no dependencies installed. To run the app or its tests, install the project's own dependencies from its lockfile with its package manager (pnpm install --frozen-lockfile, npm ci, yarn install --immutable and similar). Never add packages, change a lockfile or download browsers.";
|
|
1084
|
-
function
|
|
1085
|
-
|
|
1284
|
+
function composeProjectName(job) {
|
|
1285
|
+
return ["tamsense", ...stepFolder(job)].join("-");
|
|
1286
|
+
}
|
|
1287
|
+
function localAppRule(job, isCheck, canAsk) {
|
|
1288
|
+
const project = composeProjectName(job);
|
|
1086
1289
|
const cannotRun = isCheck ? `call ${VERDICT_TOOL_NAME} with passed false and couldNotRun true, write the exact reason and what a person should do in the report, and do not hand off: the task waits on this step for a person.` : "say so in your report with the exact reason and what a person should do.";
|
|
1087
1290
|
return [
|
|
1088
1291
|
"When you need the running app, start a local copy from this task folder only.",
|
|
@@ -1258,7 +1461,7 @@ ${job.review}`,
|
|
|
1258
1461
|
teamMessagesText(job.messages)
|
|
1259
1462
|
].filter(Boolean).join("\n\n");
|
|
1260
1463
|
const evidence = [SCREENSHOT_RULE, ...options.recorder ? [RECORDING_RULE] : [], NAMING_RULE];
|
|
1261
|
-
const localApp = localAppRule(job
|
|
1464
|
+
const localApp = localAppRule(job, isCheck, canAsk);
|
|
1262
1465
|
const rules = isCheck ? [...CHECK_RULES, DEPENDENCIES_RULE, localApp, ...evidence] : [...CODE_RULES, ...job.review ? [REWORK_RULE] : [], DEPENDENCIES_RULE, localApp, ...evidence, CODE_REPORT_RULE];
|
|
1263
1466
|
const instructions = [
|
|
1264
1467
|
canAsk ? ASKING_RULE : UNATTENDED_RULE,
|
|
@@ -1267,7 +1470,7 @@ ${job.review}`,
|
|
|
1267
1470
|
stepLine(job.stage),
|
|
1268
1471
|
job.stage.instructions,
|
|
1269
1472
|
contextText(job.projectContext),
|
|
1270
|
-
`The repositories for this task are checked out in the current folder, one subfolder each, on branch ${job.branch}:
|
|
1473
|
+
`The repositories for this task are checked out in the current folder, one subfolder each, ${isCheck ? "at the latest commit of branch" : "on branch"} ${job.branch}:
|
|
1271
1474
|
${bulletList(repositoryLines(job, workspace))}`,
|
|
1272
1475
|
`How to work:
|
|
1273
1476
|
${bulletList(rules)}`,
|
|
@@ -1778,13 +1981,13 @@ function claudeRunner(startQuery) {
|
|
|
1778
1981
|
var runClaude = claudeRunner(query2);
|
|
1779
1982
|
|
|
1780
1983
|
// src/config.ts
|
|
1781
|
-
import { mkdir as mkdir2, readFile as readFile2, rm as
|
|
1984
|
+
import { mkdir as mkdir2, readFile as readFile2, rm as rm3, writeFile as writeFile2 } from "fs/promises";
|
|
1782
1985
|
import { homedir as homedir2 } from "os";
|
|
1783
|
-
import { dirname as dirname2, join as
|
|
1986
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1784
1987
|
var OWNER_ONLY_FILE = 384;
|
|
1785
1988
|
var OWNER_ONLY_FOLDER = 448;
|
|
1786
1989
|
function configPath(home = homedir2()) {
|
|
1787
|
-
return
|
|
1990
|
+
return join3(home, ".tamsense", "runner.json");
|
|
1788
1991
|
}
|
|
1789
1992
|
function isRecord2(value) {
|
|
1790
1993
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -1816,7 +2019,7 @@ async function writeConfig(config, path = configPath()) {
|
|
|
1816
2019
|
`, { mode: OWNER_ONLY_FILE });
|
|
1817
2020
|
}
|
|
1818
2021
|
async function removeConfig(path = configPath()) {
|
|
1819
|
-
await
|
|
2022
|
+
await rm3(path, { force: true });
|
|
1820
2023
|
}
|
|
1821
2024
|
|
|
1822
2025
|
// src/protocol.ts
|
|
@@ -1870,14 +2073,14 @@ async function login(tools, request) {
|
|
|
1870
2073
|
}
|
|
1871
2074
|
|
|
1872
2075
|
// src/recorderEnv.ts
|
|
1873
|
-
import { existsSync as
|
|
1874
|
-
import { join as
|
|
2076
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2077
|
+
import { join as join5 } from "path";
|
|
1875
2078
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1876
2079
|
|
|
1877
2080
|
// src/recorder.ts
|
|
1878
2081
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1879
2082
|
import Module, { createRequire } from "module";
|
|
1880
|
-
import { basename, dirname as dirname3, join as
|
|
2083
|
+
import { basename, dirname as dirname3, join as join4 } from "path";
|
|
1881
2084
|
import process2 from "process";
|
|
1882
2085
|
var RECORDINGS_DIR_ENV = "TAMSENSE_RECORDINGS_DIR";
|
|
1883
2086
|
var SERVER_ENV = "TAMSENSE_SERVER";
|
|
@@ -1958,7 +2161,7 @@ function automaticCaption(env = process2.env, script2 = process2.argv[1]) {
|
|
|
1958
2161
|
}
|
|
1959
2162
|
function rrwebSource() {
|
|
1960
2163
|
const entry = createRequire(import.meta.url).resolve("rrweb");
|
|
1961
|
-
return readFileSync(
|
|
2164
|
+
return readFileSync(join4(dirname3(entry), "rrweb.umd.min.cjs"), "utf8");
|
|
1962
2165
|
}
|
|
1963
2166
|
function pageScript(rrweb) {
|
|
1964
2167
|
return `(() => {
|
|
@@ -2009,8 +2212,8 @@ var Session = class {
|
|
|
2009
2212
|
bytes = 0;
|
|
2010
2213
|
constructor(dir, recording, url) {
|
|
2011
2214
|
const id = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
2012
|
-
this.file =
|
|
2013
|
-
this.metaFile =
|
|
2215
|
+
this.file = join4(dir, `${id}${REPLAY_SUFFIX}`);
|
|
2216
|
+
this.metaFile = join4(dir, `${id}${META_SUFFIX}`);
|
|
2014
2217
|
this.meta = { version: 1, id, ...recording.caption, startedAt: Date.now(), endedAt: null, url, marks: [], video: null, gaps: [], blockedTrackerRequests: 0, blockedExternalWrites: 0, truncated: false, automatic: recording.automatic };
|
|
2015
2218
|
writeFileSync(this.file, "");
|
|
2016
2219
|
this.save();
|
|
@@ -2183,10 +2386,10 @@ if (process2.env[RECORDINGS_DIR_ENV]) {
|
|
|
2183
2386
|
var RECORDER_ENV = "TAMSENSE_RECORDER";
|
|
2184
2387
|
function bundledRecorder() {
|
|
2185
2388
|
const path = fileURLToPath2(new URL("./recorder.cjs", import.meta.url));
|
|
2186
|
-
return
|
|
2389
|
+
return existsSync3(path) ? path : null;
|
|
2187
2390
|
}
|
|
2188
2391
|
function recordingsFolder(home, runId) {
|
|
2189
|
-
return
|
|
2392
|
+
return join5(home, ".tamsense", "recordings", `run-${runId}`);
|
|
2190
2393
|
}
|
|
2191
2394
|
function nodeOptionsWithRecorder(recorder, nodeOptions) {
|
|
2192
2395
|
return [nodeOptions?.trim(), `--require "${recorder.replaceAll("\\", "/")}"`].filter(Boolean).join(" ");
|
|
@@ -2213,7 +2416,7 @@ function describeDevice(identity) {
|
|
|
2213
2416
|
}
|
|
2214
2417
|
|
|
2215
2418
|
// src/system.ts
|
|
2216
|
-
import { spawn as
|
|
2419
|
+
import { spawn as spawn3 } from "child_process";
|
|
2217
2420
|
import { hostname } from "os";
|
|
2218
2421
|
import { delimiter as delimiter2 } from "path";
|
|
2219
2422
|
import { setTimeout as wait } from "timers/promises";
|
|
@@ -2237,7 +2440,7 @@ function browserCommand(url, platform) {
|
|
|
2237
2440
|
}
|
|
2238
2441
|
function openBrowser(url, platform = process.platform) {
|
|
2239
2442
|
const [command, args] = browserCommand(url, platform);
|
|
2240
|
-
const child =
|
|
2443
|
+
const child = spawn3(command, args, { detached: true, stdio: "ignore" });
|
|
2241
2444
|
child.on("error", () => {
|
|
2242
2445
|
});
|
|
2243
2446
|
child.unref();
|
|
@@ -2257,7 +2460,7 @@ async function sleep(ms, signal) {
|
|
|
2257
2460
|
}
|
|
2258
2461
|
|
|
2259
2462
|
// src/update.ts
|
|
2260
|
-
import { spawn as
|
|
2463
|
+
import { spawn as spawn4 } from "child_process";
|
|
2261
2464
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2262
2465
|
import process3 from "process";
|
|
2263
2466
|
var PACKAGE_NAME = "@tamsensedev/runner";
|
|
@@ -2312,8 +2515,8 @@ function relaunchArgs(argv) {
|
|
|
2312
2515
|
}
|
|
2313
2516
|
function relaunch(argv, env) {
|
|
2314
2517
|
const isWindows = process3.platform === "win32";
|
|
2315
|
-
return new Promise((
|
|
2316
|
-
const child =
|
|
2518
|
+
return new Promise((resolve2, reject) => {
|
|
2519
|
+
const child = spawn4(isWindows ? "npx.cmd" : "npx", relaunchArgs(argv), { stdio: "inherit", shell: isWindows, env: { ...env, [RELAUNCHED_ENV]: "1" } });
|
|
2317
2520
|
const ignoreInterrupt = () => {
|
|
2318
2521
|
};
|
|
2319
2522
|
process3.on("SIGINT", ignoreInterrupt);
|
|
@@ -2323,7 +2526,7 @@ function relaunch(argv, env) {
|
|
|
2323
2526
|
});
|
|
2324
2527
|
child.on("close", (code) => {
|
|
2325
2528
|
process3.off("SIGINT", ignoreInterrupt);
|
|
2326
|
-
|
|
2529
|
+
resolve2(code ?? 1);
|
|
2327
2530
|
});
|
|
2328
2531
|
});
|
|
2329
2532
|
}
|
|
@@ -2369,37 +2572,11 @@ function feedQueue(send) {
|
|
|
2369
2572
|
};
|
|
2370
2573
|
}
|
|
2371
2574
|
|
|
2372
|
-
// src/lock.ts
|
|
2373
|
-
function keyedLock() {
|
|
2374
|
-
const tails = /* @__PURE__ */ new Map();
|
|
2375
|
-
return {
|
|
2376
|
-
async run(key, work2) {
|
|
2377
|
-
const previous = tails.get(key) ?? Promise.resolve();
|
|
2378
|
-
let release = () => {
|
|
2379
|
-
};
|
|
2380
|
-
const done2 = new Promise((resolve) => {
|
|
2381
|
-
release = resolve;
|
|
2382
|
-
});
|
|
2383
|
-
const tail = previous.then(() => done2);
|
|
2384
|
-
tails.set(key, tail);
|
|
2385
|
-
await previous;
|
|
2386
|
-
try {
|
|
2387
|
-
return await work2();
|
|
2388
|
-
} finally {
|
|
2389
|
-
release();
|
|
2390
|
-
if (tails.get(key) === tail) {
|
|
2391
|
-
tails.delete(key);
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
};
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
2575
|
// src/recordings.ts
|
|
2399
2576
|
import { randomUUID } from "crypto";
|
|
2400
2577
|
import { createReadStream } from "fs";
|
|
2401
2578
|
import { readdir, readFile as readFile3, stat, writeFile as writeFile3 } from "fs/promises";
|
|
2402
|
-
import { basename as basename2, dirname as dirname4, extname, isAbsolute, join as
|
|
2579
|
+
import { basename as basename2, dirname as dirname4, extname, isAbsolute, join as join6 } from "path";
|
|
2403
2580
|
import { createInterface as createInterface2 } from "readline";
|
|
2404
2581
|
import { Readable } from "stream";
|
|
2405
2582
|
import { promisify } from "util";
|
|
@@ -2471,12 +2648,12 @@ async function readMeta(file) {
|
|
|
2471
2648
|
}
|
|
2472
2649
|
async function metaFor(kind, file) {
|
|
2473
2650
|
if (kind === "replay") {
|
|
2474
|
-
return readMeta(
|
|
2651
|
+
return readMeta(join6(dirname4(file), `${sessionIdOf(file)}${META_SUFFIX}`));
|
|
2475
2652
|
}
|
|
2476
2653
|
const folder = basename2(dirname4(file)) === "video" ? dirname4(dirname4(file)) : dirname4(file);
|
|
2477
2654
|
for (const name of await readdir(folder).catch(() => [])) {
|
|
2478
2655
|
if (name.endsWith(META_SUFFIX) && name !== LEDGER_FILE) {
|
|
2479
|
-
const meta = await readMeta(
|
|
2656
|
+
const meta = await readMeta(join6(folder, name));
|
|
2480
2657
|
if (meta?.video === file) {
|
|
2481
2658
|
return meta;
|
|
2482
2659
|
}
|
|
@@ -2486,7 +2663,7 @@ async function metaFor(kind, file) {
|
|
|
2486
2663
|
}
|
|
2487
2664
|
async function ledger(dir) {
|
|
2488
2665
|
try {
|
|
2489
|
-
return new Set(JSON.parse(await readFile3(
|
|
2666
|
+
return new Set(JSON.parse(await readFile3(join6(dir, LEDGER_FILE), "utf8")));
|
|
2490
2667
|
} catch {
|
|
2491
2668
|
return /* @__PURE__ */ new Set();
|
|
2492
2669
|
}
|
|
@@ -2494,7 +2671,7 @@ async function ledger(dir) {
|
|
|
2494
2671
|
async function remember(dir, file) {
|
|
2495
2672
|
const done2 = await ledger(dir);
|
|
2496
2673
|
done2.add(file);
|
|
2497
|
-
await writeFile3(
|
|
2674
|
+
await writeFile3(join6(dir, LEDGER_FILE), JSON.stringify([...done2]));
|
|
2498
2675
|
}
|
|
2499
2676
|
async function nonEmpty(file) {
|
|
2500
2677
|
return file !== null && ((await stat(file).catch(() => null))?.size ?? 0) > 0;
|
|
@@ -2507,8 +2684,8 @@ async function pendingRecordings(dir) {
|
|
|
2507
2684
|
if (!name.endsWith(META_SUFFIX) || name === LEDGER_FILE) {
|
|
2508
2685
|
continue;
|
|
2509
2686
|
}
|
|
2510
|
-
const meta = await readMeta(
|
|
2511
|
-
const replay =
|
|
2687
|
+
const meta = await readMeta(join6(dir, name));
|
|
2688
|
+
const replay = join6(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
|
|
2512
2689
|
if (!done2.has(replay) && await nonEmpty(replay)) {
|
|
2513
2690
|
pending.push({ kind: "replay", file: replay, meta });
|
|
2514
2691
|
}
|
|
@@ -2650,149 +2827,6 @@ async function readScreenshot(request) {
|
|
|
2650
2827
|
return { name: basename3(request.path), ...caption, contentType, data };
|
|
2651
2828
|
}
|
|
2652
2829
|
|
|
2653
|
-
// src/workspace.ts
|
|
2654
|
-
import { existsSync as existsSync3 } from "fs";
|
|
2655
|
-
import { rm as rm3 } from "fs/promises";
|
|
2656
|
-
import { join as join6 } from "path";
|
|
2657
|
-
|
|
2658
|
-
// src/git.ts
|
|
2659
|
-
import { Buffer } from "buffer";
|
|
2660
|
-
import { spawn as spawn4 } from "child_process";
|
|
2661
|
-
var GITHUB_ORIGIN = "https://github.com/";
|
|
2662
|
-
function githubAuthEnv(token) {
|
|
2663
|
-
const credentials = Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
2664
|
-
return {
|
|
2665
|
-
GIT_CONFIG_COUNT: "1",
|
|
2666
|
-
GIT_CONFIG_KEY_0: `http.${GITHUB_ORIGIN}.extraheader`,
|
|
2667
|
-
GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${credentials}`,
|
|
2668
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
2669
|
-
};
|
|
2670
|
-
}
|
|
2671
|
-
function runGit(args, options) {
|
|
2672
|
-
const env = { ...process.env, ...options.env, ...options.token ? githubAuthEnv(options.token) : {} };
|
|
2673
|
-
return new Promise((resolve, reject) => {
|
|
2674
|
-
const child = spawn4("git", args, { cwd: options.cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
2675
|
-
let stdout = "";
|
|
2676
|
-
let stderr = "";
|
|
2677
|
-
child.stdout.on("data", (chunk) => {
|
|
2678
|
-
stdout += chunk.toString();
|
|
2679
|
-
});
|
|
2680
|
-
child.stderr.on("data", (chunk) => {
|
|
2681
|
-
stderr += chunk.toString();
|
|
2682
|
-
});
|
|
2683
|
-
child.on("error", reject);
|
|
2684
|
-
child.on("close", (code) => resolve({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }));
|
|
2685
|
-
});
|
|
2686
|
-
}
|
|
2687
|
-
async function git(args, options) {
|
|
2688
|
-
const result = await runGit(args, options);
|
|
2689
|
-
if (result.code !== 0) {
|
|
2690
|
-
throw new Error(`git ${args[0]} failed: ${result.stderr || result.stdout || `exit code ${result.code}`}`);
|
|
2691
|
-
}
|
|
2692
|
-
return result.stdout;
|
|
2693
|
-
}
|
|
2694
|
-
|
|
2695
|
-
// src/workspace.ts
|
|
2696
|
-
var FALLBACK_IDENTITY = { name: "TAMsense", email: "noreply@tamsense.com" };
|
|
2697
|
-
var cacheLock = keyedLock();
|
|
2698
|
-
function githubRemoteUrl(fullName) {
|
|
2699
|
-
return `https://github.com/${fullName}.git`;
|
|
2700
|
-
}
|
|
2701
|
-
function tamsenseFolder(home, ...parts) {
|
|
2702
|
-
return join6(home, ".tamsense", ...parts);
|
|
2703
|
-
}
|
|
2704
|
-
function folderNames(repositories) {
|
|
2705
|
-
const shortName = (fullName) => fullName.split("/").pop() ?? fullName;
|
|
2706
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2707
|
-
for (const repository of repositories) {
|
|
2708
|
-
counts.set(shortName(repository.fullName), (counts.get(shortName(repository.fullName)) ?? 0) + 1);
|
|
2709
|
-
}
|
|
2710
|
-
return repositories.map((repository) => counts.get(shortName(repository.fullName)) === 1 ? shortName(repository.fullName) : repository.fullName.replace("/", "__"));
|
|
2711
|
-
}
|
|
2712
|
-
async function refreshCache(cache, repository, token, tools) {
|
|
2713
|
-
if (existsSync3(join6(cache, ".git"))) {
|
|
2714
|
-
await git(["fetch", "--prune", "origin"], { cwd: cache, token });
|
|
2715
|
-
return;
|
|
2716
|
-
}
|
|
2717
|
-
await git(["clone", "--no-checkout", tools.remoteUrl(repository.fullName), cache], { cwd: tools.home, token });
|
|
2718
|
-
}
|
|
2719
|
-
async function removeWorktree(cache, path) {
|
|
2720
|
-
await runGit(["worktree", "remove", "--force", path], { cwd: cache });
|
|
2721
|
-
await rm3(path, { recursive: true, force: true });
|
|
2722
|
-
await git(["worktree", "prune"], { cwd: cache });
|
|
2723
|
-
}
|
|
2724
|
-
async function baseRef(cache, job, repository) {
|
|
2725
|
-
const pushed = await runGit(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${job.branch}`], { cwd: cache });
|
|
2726
|
-
return pushed.code === 0 ? `origin/${job.branch}` : `origin/${repository.defaultBranch}`;
|
|
2727
|
-
}
|
|
2728
|
-
async function keptBase(cache, path, job, repository) {
|
|
2729
|
-
if (!job.keepWorkspace || !existsSync3(join6(path, ".git"))) {
|
|
2730
|
-
return null;
|
|
2731
|
-
}
|
|
2732
|
-
const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path });
|
|
2733
|
-
if (branch.code !== 0 || branch.stdout !== job.branch) {
|
|
2734
|
-
return null;
|
|
2735
|
-
}
|
|
2736
|
-
return git(["rev-parse", await baseRef(cache, job, repository)], { cwd: cache });
|
|
2737
|
-
}
|
|
2738
|
-
async function prepareWorkspace(job, token, tools) {
|
|
2739
|
-
const root = tamsenseFolder(tools.home, "work", `task-${job.taskId}`);
|
|
2740
|
-
const folders = folderNames(job.repositories);
|
|
2741
|
-
const repositories = [];
|
|
2742
|
-
for (const [index, repository] of job.repositories.entries()) {
|
|
2743
|
-
const cache = tamsenseFolder(tools.home, "repos", repository.fullName.replace("/", "__"));
|
|
2744
|
-
const folder = folders[index] ?? repository.fullName;
|
|
2745
|
-
const path = join6(root, folder);
|
|
2746
|
-
const baseSha = await cacheLock.run(repository.fullName, async () => {
|
|
2747
|
-
await refreshCache(cache, repository, token, tools);
|
|
2748
|
-
const kept = await keptBase(cache, path, job, repository);
|
|
2749
|
-
if (kept) {
|
|
2750
|
-
return kept;
|
|
2751
|
-
}
|
|
2752
|
-
await removeWorktree(cache, path);
|
|
2753
|
-
await git(["worktree", "add", "--force", "-B", job.branch, path, await baseRef(cache, job, repository)], { cwd: cache });
|
|
2754
|
-
return git(["rev-parse", "HEAD"], { cwd: path });
|
|
2755
|
-
});
|
|
2756
|
-
repositories.push({ fullName: repository.fullName, folder, path, baseSha });
|
|
2757
|
-
}
|
|
2758
|
-
return { root, repositories };
|
|
2759
|
-
}
|
|
2760
|
-
async function commitIdentityEnv(home) {
|
|
2761
|
-
const configured = await runGit(["config", "--get", "user.email"], { cwd: home });
|
|
2762
|
-
if (configured.code === 0 && configured.stdout) {
|
|
2763
|
-
return {};
|
|
2764
|
-
}
|
|
2765
|
-
return {
|
|
2766
|
-
GIT_AUTHOR_NAME: FALLBACK_IDENTITY.name,
|
|
2767
|
-
GIT_AUTHOR_EMAIL: FALLBACK_IDENTITY.email,
|
|
2768
|
-
GIT_COMMITTER_NAME: FALLBACK_IDENTITY.name,
|
|
2769
|
-
GIT_COMMITTER_EMAIL: FALLBACK_IDENTITY.email
|
|
2770
|
-
};
|
|
2771
|
-
}
|
|
2772
|
-
async function publishWorkspace(workspace, job, token, identity) {
|
|
2773
|
-
const pushed = [];
|
|
2774
|
-
for (const repository of workspace.repositories) {
|
|
2775
|
-
const options = { cwd: repository.path, env: identity };
|
|
2776
|
-
const sha = await cacheLock.run(repository.fullName, async () => {
|
|
2777
|
-
await git(["add", "--all"], options);
|
|
2778
|
-
const hasStaged = (await runGit(["diff", "--cached", "--quiet"], options)).code === 1;
|
|
2779
|
-
if (hasStaged) {
|
|
2780
|
-
await git(["commit", "--message", `Task #${job.taskId}: ${job.title}`], options);
|
|
2781
|
-
}
|
|
2782
|
-
const newCommits = Number(await git(["rev-list", "--count", `${repository.baseSha}..HEAD`], options));
|
|
2783
|
-
if (newCommits === 0) {
|
|
2784
|
-
return null;
|
|
2785
|
-
}
|
|
2786
|
-
await git(["push", "origin", `HEAD:refs/heads/${job.branch}`], { ...options, token });
|
|
2787
|
-
return git(["rev-parse", "HEAD"], options);
|
|
2788
|
-
});
|
|
2789
|
-
if (sha) {
|
|
2790
|
-
pushed.push({ repository: repository.fullName, branch: job.branch, sha });
|
|
2791
|
-
}
|
|
2792
|
-
}
|
|
2793
|
-
return pushed;
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
2830
|
// src/worker.ts
|
|
2797
2831
|
var POLL_INTERVAL_MS = 5e3;
|
|
2798
2832
|
var RETRY_AFTER_ERROR_MS = 15e3;
|
|
@@ -3126,18 +3160,18 @@ async function reportCapabilities(tools, stop) {
|
|
|
3126
3160
|
}
|
|
3127
3161
|
}
|
|
3128
3162
|
function jobTools(job, tools, parallel) {
|
|
3129
|
-
return parallel > 1 ? { ...tools, print: (line) => tools.print(`[#${job.taskId}] ${line}`) } : tools;
|
|
3163
|
+
return parallel > 1 ? { ...tools, print: (line) => tools.print(`[#${job.taskId} ${job.stage.name}] ${line}`) } : tools;
|
|
3130
3164
|
}
|
|
3131
3165
|
async function work(tools, stop, parallel = 1) {
|
|
3132
3166
|
tools.print(parallel > 1 ? `Waiting for tasks, up to ${parallel} at once. Press Ctrl+C to stop.` : "Waiting for tasks. Press Ctrl+C to stop.");
|
|
3133
3167
|
const halt = new AbortController();
|
|
3134
3168
|
const stopAll = AbortSignal.any([stop, halt.signal]);
|
|
3135
3169
|
const running = /* @__PURE__ */ new Set();
|
|
3136
|
-
const
|
|
3170
|
+
const folderLock = keyedLock();
|
|
3137
3171
|
const reporting = reportCapabilities(tools, stopAll);
|
|
3138
3172
|
const start = (job) => {
|
|
3139
3173
|
const runTools = jobTools(job, tools, parallel);
|
|
3140
|
-
const task =
|
|
3174
|
+
const task = folderLock.run(stepFolder(job).join("/"), () => runJob(job, runTools, stopAll)).catch((error) => runTools.print(`The run stopped: ${messageOf3(error)}`)).finally(() => running.delete(task));
|
|
3141
3175
|
running.add(task);
|
|
3142
3176
|
};
|
|
3143
3177
|
try {
|