@allurereport/plugin-agent 3.10.0 → 3.12.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/README.md +118 -77
- package/dist/capabilities.d.ts +127 -0
- package/dist/capabilities.js +266 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +15 -0
- package/dist/guidance.d.ts +4 -5
- package/dist/guidance.js +223 -60
- package/dist/harness.d.ts +72 -4
- package/dist/harness.js +49 -17
- package/dist/index.d.ts +9 -1
- package/dist/index.js +9 -0
- package/dist/inline-expectations.d.ts +23 -0
- package/dist/inline-expectations.js +186 -0
- package/dist/invalid-output.d.ts +58 -0
- package/dist/invalid-output.js +238 -0
- package/dist/model.d.ts +59 -0
- package/dist/model.js +8 -1
- package/dist/paths.d.ts +3 -0
- package/dist/paths.js +10 -0
- package/dist/plugin.js +916 -137
- package/dist/query.d.ts +195 -0
- package/dist/query.js +177 -0
- package/dist/selection.d.ts +42 -0
- package/dist/selection.js +141 -0
- package/dist/state.d.ts +56 -0
- package/dist/state.js +277 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.js +171 -0
- package/package.json +6 -6
package/dist/state.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { appendFile, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { isFileNotFoundError, listAgentManagedTempOutputDirs, listAgentStatePaths, pathExists, projectStatePath, readPathMtimeMs, tryWithAgentStateLock, withAgentStateLock, writeJsonlAtomic, } from "./utils.js";
|
|
4
|
+
export { ALLURE_AGENT_STATE_DIR_ENV, resolveAgentStateDir } from "./utils.js";
|
|
5
|
+
const AGENT_RUN_STATE_SCHEMA = "allure-agent-run/v1";
|
|
6
|
+
const AGENT_STALE_OUTPUT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
7
|
+
const isAgentRunState = (value) => {
|
|
8
|
+
if (typeof value !== "object" || value === null) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const candidate = value;
|
|
12
|
+
return (candidate.schema === AGENT_RUN_STATE_SCHEMA &&
|
|
13
|
+
typeof candidate.runId === "string" &&
|
|
14
|
+
typeof candidate.cwd === "string" &&
|
|
15
|
+
typeof candidate.outputDir === "string" &&
|
|
16
|
+
typeof candidate.managedOutput === "boolean" &&
|
|
17
|
+
typeof candidate.command === "string" &&
|
|
18
|
+
typeof candidate.startedAt === "number" &&
|
|
19
|
+
Number.isSafeInteger(candidate.startedAt) &&
|
|
20
|
+
(candidate.expectationsPath === undefined || typeof candidate.expectationsPath === "string") &&
|
|
21
|
+
(candidate.finishedAt === undefined ||
|
|
22
|
+
(typeof candidate.finishedAt === "number" && Number.isSafeInteger(candidate.finishedAt))) &&
|
|
23
|
+
(candidate.status === "running" || candidate.status === "finished") &&
|
|
24
|
+
(candidate.exitCode === undefined || typeof candidate.exitCode === "number" || candidate.exitCode === null) &&
|
|
25
|
+
(candidate.pid === undefined || (typeof candidate.pid === "number" && Number.isSafeInteger(candidate.pid))));
|
|
26
|
+
};
|
|
27
|
+
const normalizeAgentRunState = (value) => ({
|
|
28
|
+
schema: AGENT_RUN_STATE_SCHEMA,
|
|
29
|
+
runId: value.runId,
|
|
30
|
+
cwd: resolve(value.cwd),
|
|
31
|
+
outputDir: resolve(value.outputDir),
|
|
32
|
+
managedOutput: value.managedOutput,
|
|
33
|
+
expectationsPath: value.expectationsPath ? resolve(value.expectationsPath) : undefined,
|
|
34
|
+
command: value.command,
|
|
35
|
+
startedAt: value.startedAt,
|
|
36
|
+
finishedAt: value.finishedAt,
|
|
37
|
+
status: value.status,
|
|
38
|
+
exitCode: value.exitCode,
|
|
39
|
+
pid: value.pid,
|
|
40
|
+
});
|
|
41
|
+
const readAgentRunStateFile = async (statePath, cwd) => {
|
|
42
|
+
const normalizedCwd = cwd === undefined ? undefined : resolve(cwd);
|
|
43
|
+
let raw;
|
|
44
|
+
try {
|
|
45
|
+
raw = await readFile(statePath, "utf-8");
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (isFileNotFoundError(error)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
return raw
|
|
54
|
+
.split("\n")
|
|
55
|
+
.map((line) => line.trim())
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.flatMap((line) => {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(line);
|
|
60
|
+
if (!isAgentRunState(parsed)) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
return normalizedCwd === undefined || parsed.cwd === normalizedCwd ? [parsed] : [];
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const readAgentRunStateLines = async (cwd) => {
|
|
71
|
+
const normalizedCwd = resolve(cwd);
|
|
72
|
+
return readAgentRunStateFile(projectStatePath(normalizedCwd), normalizedCwd);
|
|
73
|
+
};
|
|
74
|
+
const foldAgentRunStates = (states) => {
|
|
75
|
+
const order = [];
|
|
76
|
+
const latestByRunId = new Map();
|
|
77
|
+
for (const state of states) {
|
|
78
|
+
if (!latestByRunId.has(state.runId)) {
|
|
79
|
+
order.push(state.runId);
|
|
80
|
+
}
|
|
81
|
+
latestByRunId.set(state.runId, state);
|
|
82
|
+
}
|
|
83
|
+
return order
|
|
84
|
+
.map((runId) => latestByRunId.get(runId))
|
|
85
|
+
.filter((state) => state !== undefined)
|
|
86
|
+
.sort((a, b) => a.startedAt - b.startedAt || (a.finishedAt ?? a.startedAt) - (b.finishedAt ?? b.startedAt));
|
|
87
|
+
};
|
|
88
|
+
const getAgentRunStateAgeTimestamp = (state) => state.finishedAt ?? state.startedAt;
|
|
89
|
+
const isManagedOutputStale = (state, now, staleOutputTtlMs) => state.managedOutput && now - getAgentRunStateAgeTimestamp(state) >= staleOutputTtlMs;
|
|
90
|
+
const isAgentOutputDirectory = async (outputDir) => (await pathExists(join(outputDir, "manifest", "run.json"))) || (await pathExists(join(outputDir, "index.md")));
|
|
91
|
+
const cleanupStaleAgentRunState = async (params) => {
|
|
92
|
+
const states = foldAgentRunStates(await readAgentRunStateLines(params.cwd));
|
|
93
|
+
const retained = [];
|
|
94
|
+
const deleted = [];
|
|
95
|
+
const failed = [];
|
|
96
|
+
for (const state of states) {
|
|
97
|
+
if (!(await pathExists(state.outputDir))) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!isManagedOutputStale(state, params.now, params.staleOutputTtlMs)) {
|
|
101
|
+
retained.push(state);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await rm(state.outputDir, { recursive: true, force: true });
|
|
106
|
+
deleted.push(state);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
retained.push(state);
|
|
110
|
+
failed.push({ state, error });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
await writeJsonlAtomic(projectStatePath(params.cwd), retained);
|
|
114
|
+
return {
|
|
115
|
+
deleted,
|
|
116
|
+
failed,
|
|
117
|
+
retained,
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
const cleanupStaleOrphanAgentOutputs = async (params) => {
|
|
121
|
+
const outputDirs = await listAgentManagedTempOutputDirs();
|
|
122
|
+
const deleted = [];
|
|
123
|
+
const failed = [];
|
|
124
|
+
const retained = [];
|
|
125
|
+
for (const outputDir of outputDirs) {
|
|
126
|
+
const normalizedOutputDir = resolve(outputDir);
|
|
127
|
+
if (params.referencedOutputDirs.has(normalizedOutputDir)) {
|
|
128
|
+
retained.push(normalizedOutputDir);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
let mtimeMs;
|
|
132
|
+
try {
|
|
133
|
+
mtimeMs = await readPathMtimeMs(normalizedOutputDir);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (!isFileNotFoundError(error)) {
|
|
137
|
+
failed.push({ outputDir: normalizedOutputDir, error });
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (params.now - mtimeMs < params.staleOutputTtlMs) {
|
|
142
|
+
retained.push(normalizedOutputDir);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!(await isAgentOutputDirectory(normalizedOutputDir))) {
|
|
146
|
+
retained.push(normalizedOutputDir);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await rm(normalizedOutputDir, { recursive: true, force: true });
|
|
151
|
+
deleted.push(normalizedOutputDir);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
failed.push({ outputDir: normalizedOutputDir, error });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
deleted,
|
|
159
|
+
failed,
|
|
160
|
+
retained,
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
export const writeAgentRunState = async (value) => {
|
|
164
|
+
const normalizedState = normalizeAgentRunState(value);
|
|
165
|
+
await withAgentStateLock(normalizedState.cwd, async () => {
|
|
166
|
+
await appendFile(projectStatePath(normalizedState.cwd), `${JSON.stringify(normalizedState)}\n`, "utf-8");
|
|
167
|
+
});
|
|
168
|
+
return normalizedState;
|
|
169
|
+
};
|
|
170
|
+
export const writeLatestAgentState = writeAgentRunState;
|
|
171
|
+
export const readAgentRunStates = async (cwd) => foldAgentRunStates(await readAgentRunStateLines(cwd));
|
|
172
|
+
export const readLatestAgentState = async (cwd) => {
|
|
173
|
+
const states = await readAgentRunStates(cwd);
|
|
174
|
+
for (let i = states.length - 1; i >= 0; i -= 1) {
|
|
175
|
+
const state = states[i];
|
|
176
|
+
if (await pathExists(state.outputDir)) {
|
|
177
|
+
return state;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
};
|
|
182
|
+
export const cleanupAgentRunState = async (params) => withAgentStateLock(params.cwd, async () => {
|
|
183
|
+
const keepManagedRuns = Math.max(0, params.keepManagedRuns ?? 1);
|
|
184
|
+
const states = foldAgentRunStates(await readAgentRunStateLines(params.cwd));
|
|
185
|
+
const existing = [];
|
|
186
|
+
for (const state of states) {
|
|
187
|
+
if (await pathExists(state.outputDir)) {
|
|
188
|
+
existing.push(state);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const retainedManagedRunIds = new Set(existing
|
|
192
|
+
.filter((state) => state.managedOutput && state.status === "finished")
|
|
193
|
+
.sort((a, b) => (b.finishedAt ?? b.startedAt) - (a.finishedAt ?? a.startedAt) || b.startedAt - a.startedAt)
|
|
194
|
+
.slice(0, keepManagedRuns)
|
|
195
|
+
.map((state) => state.runId));
|
|
196
|
+
if (params.currentRunId) {
|
|
197
|
+
retainedManagedRunIds.add(params.currentRunId);
|
|
198
|
+
}
|
|
199
|
+
const deleted = [];
|
|
200
|
+
const failed = [];
|
|
201
|
+
for (const state of existing) {
|
|
202
|
+
if (!state.managedOutput || state.status !== "finished" || retainedManagedRunIds.has(state.runId)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
await rm(state.outputDir, { recursive: true, force: true });
|
|
207
|
+
deleted.push(state);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
failed.push({ state, error });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const deletedRunIds = new Set(deleted.map((state) => state.runId));
|
|
214
|
+
const retained = existing.filter((state) => !deletedRunIds.has(state.runId));
|
|
215
|
+
await writeJsonlAtomic(projectStatePath(params.cwd), retained);
|
|
216
|
+
return {
|
|
217
|
+
deleted,
|
|
218
|
+
failed,
|
|
219
|
+
retained,
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
export const cleanupStaleAgentRunStates = async (params) => {
|
|
223
|
+
const currentCwd = resolve(params.cwd);
|
|
224
|
+
const currentStatePath = projectStatePath(currentCwd);
|
|
225
|
+
const statePaths = await listAgentStatePaths(currentCwd);
|
|
226
|
+
const staleOutputTtlMs = Math.max(0, params.staleOutputTtlMs ?? AGENT_STALE_OUTPUT_TTL_MS);
|
|
227
|
+
const now = params.now ?? Date.now();
|
|
228
|
+
const staleCwds = new Map();
|
|
229
|
+
const referencedOutputDirs = new Set();
|
|
230
|
+
for (const statePath of statePaths) {
|
|
231
|
+
const states = foldAgentRunStates(await readAgentRunStateFile(statePath));
|
|
232
|
+
for (const state of states) {
|
|
233
|
+
referencedOutputDirs.add(resolve(state.outputDir));
|
|
234
|
+
if (statePath === currentStatePath) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (state.cwd === currentCwd || state.runId === params.currentRunId) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!(await pathExists(state.outputDir)) || isManagedOutputStale(state, now, staleOutputTtlMs)) {
|
|
241
|
+
staleCwds.set(state.cwd, statePath);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const deleted = [];
|
|
247
|
+
const failed = [];
|
|
248
|
+
const retained = [];
|
|
249
|
+
const skipped = [];
|
|
250
|
+
for (const [cwd, statePath] of staleCwds) {
|
|
251
|
+
const lockResult = await tryWithAgentStateLock(cwd, () => cleanupStaleAgentRunState({
|
|
252
|
+
cwd,
|
|
253
|
+
now,
|
|
254
|
+
staleOutputTtlMs,
|
|
255
|
+
}));
|
|
256
|
+
if (!lockResult.acquired) {
|
|
257
|
+
skipped.push({ cwd, statePath, reason: "locked" });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
deleted.push(...lockResult.result.deleted);
|
|
261
|
+
failed.push(...lockResult.result.failed);
|
|
262
|
+
retained.push(...lockResult.result.retained);
|
|
263
|
+
}
|
|
264
|
+
const orphaned = await cleanupStaleOrphanAgentOutputs({
|
|
265
|
+
referencedOutputDirs,
|
|
266
|
+
now,
|
|
267
|
+
staleOutputTtlMs,
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
checked: statePaths.length,
|
|
271
|
+
deleted,
|
|
272
|
+
failed,
|
|
273
|
+
orphaned,
|
|
274
|
+
retained,
|
|
275
|
+
skipped,
|
|
276
|
+
};
|
|
277
|
+
};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const ALLURE_AGENT_STATE_DIR_ENV = "ALLURE_AGENT_STATE_DIR";
|
|
2
|
+
export declare const AGENT_MANAGED_OUTPUT_DIR_PREFIX = "allure-agent-";
|
|
3
|
+
export declare const isFileNotFoundError: (error: unknown) => error is NodeJS.ErrnoException;
|
|
4
|
+
export declare const resolveAgentStateDir: (_cwd?: string) => string;
|
|
5
|
+
export declare const projectStatePath: (cwd: string) => string;
|
|
6
|
+
export declare const listAgentStatePaths: (cwd?: string) => Promise<string[]>;
|
|
7
|
+
export declare const listAgentManagedTempOutputDirs: () => Promise<string[]>;
|
|
8
|
+
export declare const writeJsonlAtomic: (filePath: string, values: readonly unknown[]) => Promise<void>;
|
|
9
|
+
export declare const withAgentStateLock: <T>(cwd: string, operation: () => Promise<T>) => Promise<T>;
|
|
10
|
+
export declare const tryWithAgentStateLock: <T>(cwd: string, operation: () => Promise<T>) => Promise<{
|
|
11
|
+
acquired: true;
|
|
12
|
+
result: T;
|
|
13
|
+
} | {
|
|
14
|
+
acquired: false;
|
|
15
|
+
}>;
|
|
16
|
+
export declare const pathExists: (path: string) => Promise<boolean>;
|
|
17
|
+
export declare const readPathMtimeMs: (path: string) => Promise<number>;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
6
|
+
export const ALLURE_AGENT_STATE_DIR_ENV = "ALLURE_AGENT_STATE_DIR";
|
|
7
|
+
export const AGENT_MANAGED_OUTPUT_DIR_PREFIX = "allure-agent-";
|
|
8
|
+
const AGENT_STATE_LOCK_STALE_MS = 10 * 60 * 1000;
|
|
9
|
+
const AGENT_STATE_LOCK_RETRIES = 100;
|
|
10
|
+
const AGENT_STATE_LOCK_RETRY_MS = 20;
|
|
11
|
+
export const isFileNotFoundError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
12
|
+
const isFileExistsError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
13
|
+
const projectHash = (cwd) => createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
14
|
+
export const resolveAgentStateDir = (_cwd) => {
|
|
15
|
+
const configuredDir = process.env[ALLURE_AGENT_STATE_DIR_ENV]?.trim();
|
|
16
|
+
if (configuredDir) {
|
|
17
|
+
return resolve(configuredDir);
|
|
18
|
+
}
|
|
19
|
+
return join(tmpdir(), "allure-agent-state");
|
|
20
|
+
};
|
|
21
|
+
export const projectStatePath = (cwd) => join(resolveAgentStateDir(cwd), `${projectHash(resolve(cwd))}.jsonl`);
|
|
22
|
+
const projectLockPath = (cwd) => join(resolveAgentStateDir(cwd), `${projectHash(resolve(cwd))}.lock`);
|
|
23
|
+
export const listAgentStatePaths = async (cwd) => {
|
|
24
|
+
const stateDir = resolveAgentStateDir(cwd);
|
|
25
|
+
try {
|
|
26
|
+
const entries = await readdir(stateDir, { withFileTypes: true });
|
|
27
|
+
return entries
|
|
28
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
29
|
+
.map((entry) => join(stateDir, entry.name));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (isFileNotFoundError(error)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
export const listAgentManagedTempOutputDirs = async () => {
|
|
39
|
+
const tempRoot = tmpdir();
|
|
40
|
+
try {
|
|
41
|
+
const entries = await readdir(tempRoot, { withFileTypes: true });
|
|
42
|
+
return entries
|
|
43
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith(AGENT_MANAGED_OUTPUT_DIR_PREFIX))
|
|
44
|
+
.map((entry) => join(tempRoot, entry.name));
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (isFileNotFoundError(error)) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
export const writeJsonlAtomic = async (filePath, values) => {
|
|
54
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
55
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
56
|
+
const content = values.map((value) => JSON.stringify(value)).join("\n") + (values.length ? "\n" : "");
|
|
57
|
+
await writeFile(tempPath, content, "utf-8");
|
|
58
|
+
await rename(tempPath, filePath);
|
|
59
|
+
};
|
|
60
|
+
const removeLockIfStale = async (lockPath) => {
|
|
61
|
+
try {
|
|
62
|
+
const lockStat = await stat(lockPath);
|
|
63
|
+
if (Date.now() - lockStat.mtimeMs > AGENT_STATE_LOCK_STALE_MS) {
|
|
64
|
+
await rm(lockPath, { force: true });
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (isFileNotFoundError(error)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const openAgentStateLock = async (lockPath) => {
|
|
77
|
+
let lockHandle;
|
|
78
|
+
try {
|
|
79
|
+
lockHandle = await open(lockPath, "wx");
|
|
80
|
+
await lockHandle.writeFile(`${process.pid}\n${Date.now()}\n`, "utf-8");
|
|
81
|
+
return lockHandle;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (lockHandle) {
|
|
85
|
+
await lockHandle.close().catch(() => undefined);
|
|
86
|
+
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const runWithAgentStateLock = async (lockPath, lockHandle, operation) => {
|
|
92
|
+
try {
|
|
93
|
+
return await operation();
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
await lockHandle.close().catch(() => undefined);
|
|
97
|
+
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
export const withAgentStateLock = async (cwd, operation) => {
|
|
101
|
+
const normalizedCwd = resolve(cwd);
|
|
102
|
+
const stateDir = resolveAgentStateDir(normalizedCwd);
|
|
103
|
+
const lockPath = projectLockPath(normalizedCwd);
|
|
104
|
+
let lastError;
|
|
105
|
+
await mkdir(stateDir, { recursive: true });
|
|
106
|
+
for (let attempt = 0; attempt < AGENT_STATE_LOCK_RETRIES; attempt += 1) {
|
|
107
|
+
try {
|
|
108
|
+
const lockHandle = await openAgentStateLock(lockPath);
|
|
109
|
+
return await runWithAgentStateLock(lockPath, lockHandle, operation);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (!isFileExistsError(error)) {
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
lastError = error;
|
|
116
|
+
await removeLockIfStale(lockPath);
|
|
117
|
+
await sleep(AGENT_STATE_LOCK_RETRY_MS);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
throw lastError instanceof Error ? lastError : new Error(`Could not acquire agent state lock: ${lockPath}`);
|
|
121
|
+
};
|
|
122
|
+
export const tryWithAgentStateLock = async (cwd, operation) => {
|
|
123
|
+
const normalizedCwd = resolve(cwd);
|
|
124
|
+
const stateDir = resolveAgentStateDir(normalizedCwd);
|
|
125
|
+
const lockPath = projectLockPath(normalizedCwd);
|
|
126
|
+
await mkdir(stateDir, { recursive: true });
|
|
127
|
+
try {
|
|
128
|
+
const lockHandle = await openAgentStateLock(lockPath);
|
|
129
|
+
return {
|
|
130
|
+
acquired: true,
|
|
131
|
+
result: await runWithAgentStateLock(lockPath, lockHandle, operation),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (!isFileExistsError(error)) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
if (!(await removeLockIfStale(lockPath))) {
|
|
139
|
+
return { acquired: false };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const lockHandle = await openAgentStateLock(lockPath);
|
|
144
|
+
return {
|
|
145
|
+
acquired: true,
|
|
146
|
+
result: await runWithAgentStateLock(lockPath, lockHandle, operation),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (isFileExistsError(error)) {
|
|
151
|
+
return { acquired: false };
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
export const pathExists = async (path) => {
|
|
157
|
+
try {
|
|
158
|
+
await stat(path);
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (isFileNotFoundError(error)) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
export const readPathMtimeMs = async (path) => {
|
|
169
|
+
const pathStat = await stat(path);
|
|
170
|
+
return pathStat.mtimeMs;
|
|
171
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allurereport/plugin-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"description": "Allure Agent Plugin – AI-friendly markdown report generator",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -27,16 +27,16 @@
|
|
|
27
27
|
"build": "run clean && tsc --project ./tsconfig.json",
|
|
28
28
|
"clean": "rimraf ./dist",
|
|
29
29
|
"test": "rimraf ./out && vitest run",
|
|
30
|
-
"lint": "oxlint --import-plugin src test features stories",
|
|
31
|
-
"lint:fix": "oxlint --import-plugin --fix src test features stories"
|
|
30
|
+
"lint": "yarn run -T oxlint --import-plugin src test features stories",
|
|
31
|
+
"lint:fix": "yarn run -T oxlint --import-plugin --fix src test features stories"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@allurereport/core-api": "3.
|
|
35
|
-
"@allurereport/plugin-api": "3.
|
|
34
|
+
"@allurereport/core-api": "3.12.0",
|
|
35
|
+
"@allurereport/plugin-api": "3.12.0",
|
|
36
36
|
"yaml": "^2.8.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@allurereport/reader-api": "3.
|
|
39
|
+
"@allurereport/reader-api": "3.12.0",
|
|
40
40
|
"@types/node": "^20",
|
|
41
41
|
"@vitest/runner": "^2",
|
|
42
42
|
"allure-js-commons": "^3",
|