@vitest-agent/cli 1.0.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/LICENSE +21 -0
- package/README.md +46 -0
- package/bin/vitest-agent.js +39 -0
- package/commands/agent.js +159 -0
- package/commands/db.js +123 -0
- package/commands/doctor.js +111 -0
- package/commands/record.js +204 -0
- package/commands/triage.js +40 -0
- package/commands/wrapup.js +48 -0
- package/index.d.ts +169 -0
- package/index.js +18 -0
- package/layers/CliLive.js +30 -0
- package/layers/SidecarLive.js +34 -0
- package/lib/format-db-query.js +26 -0
- package/lib/format-doctor.js +14 -0
- package/lib/internal-end-agent.js +27 -0
- package/lib/internal-register-agent.js +76 -0
- package/lib/record-session.js +33 -0
- package/lib/record-tdd-artifact.js +40 -0
- package/lib/record-turn.js +44 -0
- package/lib/record-workspace-changes.js +64 -0
- package/lib/resolve-session-for-recording.js +71 -0
- package/lib/sidecar-paths.js +101 -0
- package/package.json +54 -0
- package/tsdoc-metadata.json +11 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { DataReader, DataStore, PerClientSessionMapWriter, RunContextService, deriveIdempotencyKey } from "@vitest-agent/sdk";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/internal-register-agent.ts
|
|
5
|
+
const deriveDefaultClientNonce = (input) => {
|
|
6
|
+
const parent = input.parentAgentId ?? "__ROOT__";
|
|
7
|
+
return `${input.hostSessionId}|${input.agentType}|${parent}`;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* End-to-end agent registration.
|
|
11
|
+
*
|
|
12
|
+
* Returns the canonical `agentId` + the conversation it belongs to +
|
|
13
|
+
* the resolved `idempotencyKey`. The `idempotencyHit` flag is `true`
|
|
14
|
+
* when the per-project store recovered an existing agent row instead
|
|
15
|
+
* of inserting a new one.
|
|
16
|
+
*
|
|
17
|
+
* @param input - registration inputs including host identifiers and agent metadata
|
|
18
|
+
* @returns an Effect resolving to `RegisterAgentOutput`
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
const registerAgentEffect = (input) => Effect.gen(function* () {
|
|
22
|
+
const sessionMap = yield* PerClientSessionMapWriter;
|
|
23
|
+
const reader = yield* DataReader;
|
|
24
|
+
const store = yield* DataStore;
|
|
25
|
+
const ctx = yield* RunContextService;
|
|
26
|
+
const conversationId = yield* sessionMap.mapConversation(input.transcriptPath);
|
|
27
|
+
const sessionMapping = yield* sessionMap.mapSession({
|
|
28
|
+
hostSessionId: input.hostSessionId,
|
|
29
|
+
conversationId,
|
|
30
|
+
projectKey: input.projectKey,
|
|
31
|
+
projectDir: input.cwd
|
|
32
|
+
});
|
|
33
|
+
const existingSession = yield* reader.getSessionByChatId(input.hostSessionId);
|
|
34
|
+
const sessionRowId = yield* Option.match(existingSession, {
|
|
35
|
+
onNone: () => store.writeSession({
|
|
36
|
+
chatId: input.hostSessionId,
|
|
37
|
+
project: input.projectKey,
|
|
38
|
+
cwd: input.cwd,
|
|
39
|
+
agentKind: input.parentAgentId === void 0 ? "main" : "subagent",
|
|
40
|
+
agentType: input.agentType,
|
|
41
|
+
triageWasNonEmpty: false,
|
|
42
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
43
|
+
}),
|
|
44
|
+
onSome: (s) => Effect.succeed(s.id)
|
|
45
|
+
});
|
|
46
|
+
const agentContext = yield* ctx.captureAgentContext(input.cwd);
|
|
47
|
+
const clientNonce = input.clientNonce ?? deriveDefaultClientNonce(input);
|
|
48
|
+
const idempotencyKey = deriveIdempotencyKey({
|
|
49
|
+
agentType: input.agentType,
|
|
50
|
+
parentAgentId: input.parentAgentId ?? null,
|
|
51
|
+
clientNonce
|
|
52
|
+
});
|
|
53
|
+
const result = yield* store.registerAgent({
|
|
54
|
+
sessionId: sessionRowId,
|
|
55
|
+
agentType: input.agentType,
|
|
56
|
+
parentAgentId: input.parentAgentId ?? null,
|
|
57
|
+
conversationId,
|
|
58
|
+
startedAt: Math.floor(Date.now() / 1e3),
|
|
59
|
+
...agentContext.startGitBranch !== null && { startGitBranch: agentContext.startGitBranch },
|
|
60
|
+
...agentContext.startGitCommitSha !== null && { startGitCommitSha: agentContext.startGitCommitSha },
|
|
61
|
+
...agentContext.startWorktreeDir !== null && { startWorktreeDir: agentContext.startWorktreeDir },
|
|
62
|
+
idempotencyKey,
|
|
63
|
+
agentId: sessionMapping.mainAgentId
|
|
64
|
+
});
|
|
65
|
+
const isHit = result._tag === "IdempotencyHit";
|
|
66
|
+
return {
|
|
67
|
+
agentId: isHit ? result.existingAgentId : result.agentId,
|
|
68
|
+
conversationId,
|
|
69
|
+
mainAgentId: sessionMapping.mainAgentId,
|
|
70
|
+
idempotencyKey,
|
|
71
|
+
idempotencyHit: isHit
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
export { registerAgentEffect };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DataReader, DataStore } from "@vitest-agent/sdk";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/record-session.ts
|
|
5
|
+
const recordSessionStart = (input) => Effect.gen(function* () {
|
|
6
|
+
const reader = yield* DataReader;
|
|
7
|
+
const store = yield* DataStore;
|
|
8
|
+
let parentSessionId;
|
|
9
|
+
if (input.parentChatId !== void 0) {
|
|
10
|
+
const parent = yield* reader.getSessionByChatId(input.parentChatId);
|
|
11
|
+
if (parent._tag === "Some") parentSessionId = parent.value.id;
|
|
12
|
+
}
|
|
13
|
+
return { sessionId: yield* store.upsertSession({
|
|
14
|
+
chatId: input.chatId,
|
|
15
|
+
project: input.project,
|
|
16
|
+
cwd: input.cwd,
|
|
17
|
+
agentKind: input.agentKind,
|
|
18
|
+
...input.agentType !== void 0 && { agentType: input.agentType },
|
|
19
|
+
...parentSessionId !== void 0 && { parentSessionId },
|
|
20
|
+
triageWasNonEmpty: input.triageWasNonEmpty,
|
|
21
|
+
startedAt: input.startedAt
|
|
22
|
+
}) };
|
|
23
|
+
});
|
|
24
|
+
const recordSessionEnd = (input) => Effect.gen(function* () {
|
|
25
|
+
const reader = yield* DataReader;
|
|
26
|
+
const store = yield* DataStore;
|
|
27
|
+
if ((yield* reader.getSessionByChatId(input.chatId))._tag === "None") return yield* Effect.fail(/* @__PURE__ */ new Error(`Unknown chatId: ${input.chatId}`));
|
|
28
|
+
yield* store.endSession(input.chatId, input.endedAt, input.endReason);
|
|
29
|
+
return { ok: true };
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { recordSessionEnd, recordSessionStart };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { resolveSessionForRecording } from "./resolve-session-for-recording.js";
|
|
2
|
+
import { DataReader, DataStore } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, Option } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/lib/record-tdd-artifact.ts
|
|
6
|
+
const recordTddArtifactEffect = (input) => Effect.gen(function* () {
|
|
7
|
+
const reader = yield* DataReader;
|
|
8
|
+
const store = yield* DataStore;
|
|
9
|
+
const session = yield* resolveSessionForRecording({
|
|
10
|
+
chatId: input.chatId,
|
|
11
|
+
recordedAt: input.recordedAt,
|
|
12
|
+
...input.project !== void 0 && { project: input.project },
|
|
13
|
+
...input.cwd !== void 0 && { cwd: input.cwd }
|
|
14
|
+
});
|
|
15
|
+
const openTdd = (yield* reader.listTddTasksForSession(session.id, { walkParents: true })).find((t) => t.endedAt === null);
|
|
16
|
+
if (openTdd === void 0) return yield* Effect.fail(/* @__PURE__ */ new Error(`No open TDD task under chat_id ${input.chatId}. Call tdd_task start first.`));
|
|
17
|
+
const phaseOpt = yield* reader.getCurrentTddPhase(openTdd.id);
|
|
18
|
+
const phaseId = Option.isSome(phaseOpt) ? phaseOpt.value.id : (yield* store.writeTddPhase({
|
|
19
|
+
tddTaskId: openTdd.id,
|
|
20
|
+
phase: "spike",
|
|
21
|
+
startedAt: input.recordedAt,
|
|
22
|
+
transitionReason: "auto-opened by record tdd-artifact (no prior phase)"
|
|
23
|
+
})).id;
|
|
24
|
+
return {
|
|
25
|
+
id: yield* store.writeTddArtifact({
|
|
26
|
+
phaseId,
|
|
27
|
+
artifactKind: input.artifactKind,
|
|
28
|
+
...input.fileId !== void 0 && { fileId: input.fileId },
|
|
29
|
+
...input.testCaseId !== void 0 && { testCaseId: input.testCaseId },
|
|
30
|
+
...input.testRunId !== void 0 && { testRunId: input.testRunId },
|
|
31
|
+
...input.testFirstFailureRunId !== void 0 && { testFirstFailureRunId: input.testFirstFailureRunId },
|
|
32
|
+
...input.diffExcerpt !== void 0 && { diffExcerpt: input.diffExcerpt },
|
|
33
|
+
recordedAt: input.recordedAt
|
|
34
|
+
}),
|
|
35
|
+
phaseId
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { recordTddArtifactEffect };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { resolveSessionForRecording } from "./resolve-session-for-recording.js";
|
|
2
|
+
import { DataStore, TurnPayload } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/lib/record-turn.ts
|
|
6
|
+
const parseAndValidateTurnPayload = (raw) => {
|
|
7
|
+
let parsed;
|
|
8
|
+
try {
|
|
9
|
+
parsed = JSON.parse(raw);
|
|
10
|
+
} catch (e) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const decoded = Schema.decodeUnknownEither(TurnPayload)(parsed);
|
|
17
|
+
if (decoded._tag === "Left") return {
|
|
18
|
+
ok: false,
|
|
19
|
+
error: `Invalid TurnPayload: ${decoded.left.message}`
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
ok: true,
|
|
23
|
+
payload: decoded.right
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const recordTurnEffect = (input) => Effect.gen(function* () {
|
|
27
|
+
const parse = parseAndValidateTurnPayload(input.payloadJson);
|
|
28
|
+
if (!parse.ok) return yield* Effect.fail(new Error(parse.error));
|
|
29
|
+
const session = yield* resolveSessionForRecording({
|
|
30
|
+
chatId: input.chatId,
|
|
31
|
+
recordedAt: input.occurredAt,
|
|
32
|
+
...input.project !== void 0 && { project: input.project },
|
|
33
|
+
...input.cwd !== void 0 && { cwd: input.cwd }
|
|
34
|
+
});
|
|
35
|
+
return { turnId: yield* (yield* DataStore).writeTurn({
|
|
36
|
+
sessionId: session.id,
|
|
37
|
+
type: parse.payload.type,
|
|
38
|
+
payload: input.payloadJson,
|
|
39
|
+
occurredAt: input.occurredAt
|
|
40
|
+
}) };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { recordTurnEffect };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DataStore } from "@vitest-agent/sdk";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { SqlClient } from "@effect/sql/SqlClient";
|
|
4
|
+
|
|
5
|
+
//#region src/lib/record-workspace-changes.ts
|
|
6
|
+
/**
|
|
7
|
+
* Lib function for the `record run-workspace-changes` CLI subcommand.
|
|
8
|
+
*
|
|
9
|
+
* Driven by the PostToolUse hook on `git commit` / `git push`. Writes a
|
|
10
|
+
* commits row (idempotent on sha via ON CONFLICT DO NOTHING in
|
|
11
|
+
* DataStore.writeCommit) and zero or more run_changed_files rows.
|
|
12
|
+
*
|
|
13
|
+
* Optionally associates the changed files with a test_run_id; for the
|
|
14
|
+
* commit-side write path we don't have a run, so we associate the files
|
|
15
|
+
* with the most-recent test_run for the project (best-effort) or skip
|
|
16
|
+
* the file rows entirely if no test_run exists yet.
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* `ProjectRunSummary` doesn't expose `lastRunId`, so we query it directly
|
|
22
|
+
* via SqlClient. This is the run id of the most-recent test run for a
|
|
23
|
+
* given project (or any project, when `project` is unspecified). Used
|
|
24
|
+
* only as a best-effort association target for `run_changed_files`.
|
|
25
|
+
*/
|
|
26
|
+
const findLatestRunId = (project) => Effect.gen(function* () {
|
|
27
|
+
const sql = yield* SqlClient;
|
|
28
|
+
const rows = project !== void 0 ? yield* sql`
|
|
29
|
+
SELECT id FROM test_runs
|
|
30
|
+
WHERE project = ${project}
|
|
31
|
+
ORDER BY timestamp DESC LIMIT 1
|
|
32
|
+
` : yield* sql`
|
|
33
|
+
SELECT id FROM test_runs
|
|
34
|
+
ORDER BY timestamp DESC LIMIT 1
|
|
35
|
+
`;
|
|
36
|
+
return rows.length === 0 ? null : rows[0].id;
|
|
37
|
+
}).pipe(Effect.orElseSucceed(() => null));
|
|
38
|
+
const recordRunWorkspaceChangesEffect = (input) => Effect.gen(function* () {
|
|
39
|
+
const store = yield* DataStore;
|
|
40
|
+
yield* store.writeCommit({
|
|
41
|
+
sha: input.sha,
|
|
42
|
+
...input.parentSha !== void 0 && { parentSha: input.parentSha },
|
|
43
|
+
...input.message !== void 0 && { message: input.message },
|
|
44
|
+
...input.author !== void 0 && { author: input.author },
|
|
45
|
+
...input.committedAt !== void 0 && { committedAt: input.committedAt },
|
|
46
|
+
...input.branch !== void 0 && { branch: input.branch }
|
|
47
|
+
});
|
|
48
|
+
const runId = yield* findLatestRunId(input.project);
|
|
49
|
+
if (runId !== null && input.files.length > 0) yield* store.writeRunChangedFiles({
|
|
50
|
+
runId,
|
|
51
|
+
files: input.files.map((f) => ({
|
|
52
|
+
filePath: f.filePath,
|
|
53
|
+
changeKind: f.changeKind,
|
|
54
|
+
commitSha: input.sha
|
|
55
|
+
}))
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
sha: input.sha,
|
|
59
|
+
fileRowsWritten: runId !== null ? input.files.length : 0
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { recordRunWorkspaceChangesEffect };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { DataReader, DataStore } from "@vitest-agent/sdk";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
//#region src/lib/resolve-session-for-recording.ts
|
|
6
|
+
/**
|
|
7
|
+
* Shared session-resolution helper for hook-driven recording paths.
|
|
8
|
+
*
|
|
9
|
+
* Background: Claude Code can rotate the chat id (host `session_id`)
|
|
10
|
+
* mid-window — after a continuation, compaction, or `/mcp` reconnect —
|
|
11
|
+
* without the `SessionStart` hook firing for the new id. PostToolUse
|
|
12
|
+
* hooks then deliver tool calls under a `chat_id` that has no row in
|
|
13
|
+
* `sessions`, and the strict `getSessionByChatId` lookup fails with
|
|
14
|
+
* "Unknown chat_id". A second flavour of the same bug: the
|
|
15
|
+
* SubagentStart hook mints a synthetic per-dispatch key
|
|
16
|
+
* (`<parentChatId>-subagent-<ts>-<pid>`) for the subagent row, but
|
|
17
|
+
* subsequent PostToolUse hooks under that subagent receive the bare
|
|
18
|
+
* parent chat id. Exact-match lookup of the bare id misses the
|
|
19
|
+
* synthetic suffix.
|
|
20
|
+
*
|
|
21
|
+
* This resolver applies a three-step fallback that recovers either
|
|
22
|
+
* case without crashing the recording path:
|
|
23
|
+
*
|
|
24
|
+
* 1. Exact match by `chat_id`.
|
|
25
|
+
* 2. Prefix match `<chatId>-subagent-` to recover the synthetic
|
|
26
|
+
* subagent row (most recent first).
|
|
27
|
+
* 3. Idempotent bootstrap of a `main` session row keyed on the
|
|
28
|
+
* input `chat_id`, so future calls in this window resolve via
|
|
29
|
+
* step 1.
|
|
30
|
+
*
|
|
31
|
+
* Step 3 means recording paths can no longer fail with "Unknown
|
|
32
|
+
* chat_id". Bootstrapped rows carry `agent_kind = "main"` and
|
|
33
|
+
* `triage_was_non_empty = false` because the SessionStart triage
|
|
34
|
+
* pipeline never ran for them; downstream metrics consumers should
|
|
35
|
+
* treat triage-empty bootstrapped rows the same as any other
|
|
36
|
+
* SessionStart-less row.
|
|
37
|
+
*
|
|
38
|
+
* @packageDocumentation
|
|
39
|
+
*/
|
|
40
|
+
const resolveSessionForRecording = (input) => Effect.gen(function* () {
|
|
41
|
+
const reader = yield* DataReader;
|
|
42
|
+
const store = yield* DataStore;
|
|
43
|
+
const exact = yield* reader.getSessionByChatId(input.chatId);
|
|
44
|
+
if (Option.isSome(exact)) return exact.value;
|
|
45
|
+
const synthetic = yield* reader.findSessionsByChatPrefix(`${input.chatId}-subagent-`);
|
|
46
|
+
if (synthetic.length > 0) return synthetic[0];
|
|
47
|
+
const cwd = input.cwd ?? process.cwd();
|
|
48
|
+
const project = input.project ?? readPackageName(cwd) ?? "unknown";
|
|
49
|
+
yield* store.upsertSession({
|
|
50
|
+
chatId: input.chatId,
|
|
51
|
+
project,
|
|
52
|
+
cwd,
|
|
53
|
+
agentKind: "main",
|
|
54
|
+
triageWasNonEmpty: false,
|
|
55
|
+
startedAt: input.recordedAt
|
|
56
|
+
});
|
|
57
|
+
const created = yield* reader.getSessionByChatId(input.chatId);
|
|
58
|
+
return Option.getOrThrowWith(created, () => /* @__PURE__ */ new Error(`upsertSession succeeded but row not found for ${input.chatId}`));
|
|
59
|
+
});
|
|
60
|
+
const readPackageName = (cwd) => {
|
|
61
|
+
try {
|
|
62
|
+
const raw = readFileSync(`${cwd}/package.json`, "utf8");
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
return typeof parsed.name === "string" ? parsed.name : void 0;
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
export { resolveSessionForRecording };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ProjectIdentityNotResolvableError } from "@vitest-agent/sdk";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
//#region src/lib/sidecar-paths.ts
|
|
8
|
+
const APP_NAMESPACE = "vitest-agent";
|
|
9
|
+
/**
|
|
10
|
+
* Filename of the per-project test-data SQLite database.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
const DATA_DB_FILENAME = "data.db";
|
|
15
|
+
/**
|
|
16
|
+
* Filename of the per-client session-map SQLite database.
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
const SESSIONS_DB_FILENAME = "sessions.db";
|
|
21
|
+
/**
|
|
22
|
+
* Filename of the global discovery-registry SQLite database.
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
const REGISTRY_DB_FILENAME = "registry.db";
|
|
27
|
+
const resolveXdgDataHome = () => {
|
|
28
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
29
|
+
if (xdg !== void 0 && xdg.length > 0) return xdg;
|
|
30
|
+
return join(homedir(), ".local", "share");
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Resolve (and create) the per-project data directory for the supplied
|
|
34
|
+
* normalized `projectKey`. Returns the directory; callers join
|
|
35
|
+
* {@link DATA_DB_FILENAME} onto it for the `data.db` path.
|
|
36
|
+
*
|
|
37
|
+
* @param projectKey - normalized project key (e.g. `@org__pkg`)
|
|
38
|
+
* @returns absolute path to the resolved (and created) data directory
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
const resolveProjectDataDir = (projectKey) => {
|
|
42
|
+
const dir = join(resolveXdgDataHome(), APP_NAMESPACE, projectKey);
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
return dir;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Resolve (and create) the directory holding the global `registry.db`.
|
|
48
|
+
* Callers join {@link REGISTRY_DB_FILENAME} onto it.
|
|
49
|
+
*
|
|
50
|
+
* @returns absolute path to the resolved (and created) registry directory
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
const resolveRegistryDir = () => {
|
|
54
|
+
const dir = join(resolveXdgDataHome(), APP_NAMESPACE);
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
return dir;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the per-client `sessions.db` path.
|
|
60
|
+
*
|
|
61
|
+
* Precedence: the `CLAUDE_PLUGIN_DATA` env var, then
|
|
62
|
+
* `VITEST_AGENT_SESSION_MAP_DIR`, then `~/.vitest-agent/`. Fails
|
|
63
|
+
* with `ProjectIdentityNotResolvableError` when no home directory
|
|
64
|
+
* is resolvable.
|
|
65
|
+
*
|
|
66
|
+
* @returns an Effect resolving to the absolute `sessions.db` path
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
const resolveSessionMapPath = () => Effect.sync(() => {
|
|
70
|
+
const claudePluginData = process.env.CLAUDE_PLUGIN_DATA;
|
|
71
|
+
if (claudePluginData !== void 0 && claudePluginData.length > 0) {
|
|
72
|
+
mkdirSync(claudePluginData, { recursive: true });
|
|
73
|
+
return join(claudePluginData, SESSIONS_DB_FILENAME);
|
|
74
|
+
}
|
|
75
|
+
const overrideDir = process.env.VITEST_AGENT_SESSION_MAP_DIR;
|
|
76
|
+
if (overrideDir !== void 0 && overrideDir.length > 0) {
|
|
77
|
+
mkdirSync(overrideDir, { recursive: true });
|
|
78
|
+
return join(overrideDir, SESSIONS_DB_FILENAME);
|
|
79
|
+
}
|
|
80
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
81
|
+
if (home === void 0 || home.length === 0) return null;
|
|
82
|
+
const fallbackDir = join(home, ".vitest-agent");
|
|
83
|
+
mkdirSync(fallbackDir, { recursive: true });
|
|
84
|
+
return join(fallbackDir, SESSIONS_DB_FILENAME);
|
|
85
|
+
}).pipe(Effect.flatMap((path) => path !== null ? Effect.succeed(path) : Effect.fail(new ProjectIdentityNotResolvableError({ tried: [
|
|
86
|
+
{
|
|
87
|
+
source: "CLAUDE_PLUGIN_DATA",
|
|
88
|
+
reason: "env var not set"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
source: "VITEST_AGENT_SESSION_MAP_DIR",
|
|
92
|
+
reason: "env var not set"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
source: "HOME",
|
|
96
|
+
reason: "env var not set (USERPROFILE also unset)"
|
|
97
|
+
}
|
|
98
|
+
] }))));
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
export { DATA_DB_FILENAME, REGISTRY_DB_FILENAME, SESSIONS_DB_FILENAME, resolveProjectDataDir, resolveRegistryDir, resolveSessionMapPath };
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vitest-agent/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "On-demand CLI for vitest-agent. Reads cached test data and reports status, overview, coverage, history, trends, and cache health.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"vitest",
|
|
8
|
+
"agent",
|
|
9
|
+
"cli",
|
|
10
|
+
"tests",
|
|
11
|
+
"coverage"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/spencerbeggs/vitest-agent#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/spencerbeggs/vitest-agent/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/spencerbeggs/vitest-agent.git",
|
|
20
|
+
"directory": "packages/cli"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": {
|
|
24
|
+
"name": "C. Spencer Beggs",
|
|
25
|
+
"email": "spencer@beggs.codes",
|
|
26
|
+
"url": "https://spencerbeg.gs"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./index.d.ts",
|
|
32
|
+
"import": "./index.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"bin": {
|
|
37
|
+
"vitest-agent": "bin/vitest-agent.js"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@effect/cli": "^0.75.2",
|
|
41
|
+
"@effect/cluster": "^0.59.0",
|
|
42
|
+
"@effect/platform": "^0.96.2",
|
|
43
|
+
"@effect/platform-node": "^0.107.0",
|
|
44
|
+
"@effect/rpc": "^0.75.1",
|
|
45
|
+
"@effect/sql": "^0.51.1",
|
|
46
|
+
"@effect/sql-sqlite-node": "^0.52.0",
|
|
47
|
+
"@vitest-agent/sdk": "1.0.0",
|
|
48
|
+
"@vitest-agent/sidecar": "1.0.0",
|
|
49
|
+
"effect": "^3.21.4"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=24.11.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.58.9"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|