@vitest-agent/engine 0.2.10 → 0.3.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/index.d.ts +45 -1
- package/index.js +3 -3
- package/layers/DataReaderLive.js +26 -10
- package/layers/PathResolutionLive.js +14 -2
- package/package.json +5 -5
- package/programs/hook-paths.js +2 -8
- package/programs/session-env.js +15 -6
- package/utils/resolve-data-path.js +2 -2
- package/utils/resolve-project-key-from-cwd.js +62 -12
- package/version.js +1 -1
package/index.d.ts
CHANGED
|
@@ -1370,6 +1370,15 @@ export declare const OutputRendererLive: Layer.Layer<OutputRenderer, never, neve
|
|
|
1370
1370
|
* @public
|
|
1371
1371
|
*/
|
|
1372
1372
|
export declare const APP_NAMESPACE = "vitest-agent";
|
|
1373
|
+
/**
|
|
1374
|
+
* Where the XDG data root lands when `XDG_DATA_HOME` is unset:
|
|
1375
|
+
* `$HOME/.local/share/vitest-agent` — the XDG spec default. `AppDirs` alone
|
|
1376
|
+
* would fall back to `$HOME/.vitest-agent`. Shared by `PathResolutionLive`
|
|
1377
|
+
* (reporter/MCP route) and `resolveHookPaths` (hook/sidecar route) so both
|
|
1378
|
+
* routes land in the same directory.
|
|
1379
|
+
* @public
|
|
1380
|
+
*/
|
|
1381
|
+
export declare const DATA_FALLBACK_DIR = ".local/share/vitest-agent";
|
|
1373
1382
|
/**
|
|
1374
1383
|
* Composite layer providing every service `resolveDataPath` requires:
|
|
1375
1384
|
* `AppDirs` (XDG path resolution), `VitestAgentConfigFile` (TOML
|
|
@@ -2418,6 +2427,21 @@ export interface SessionContext {
|
|
|
2418
2427
|
* @public
|
|
2419
2428
|
*/
|
|
2420
2429
|
export declare const parseSessionEnvExports: (content: string) => Record<string, string>;
|
|
2430
|
+
/**
|
|
2431
|
+
* The synchronous filesystem reads {@link recoverSessionContextFromSessionEnv}
|
|
2432
|
+
* performs. Each member throws on a missing or unreadable path, exactly as
|
|
2433
|
+
* the `node:fs` call it stands in for; recovery treats a throw as "skip".
|
|
2434
|
+
*
|
|
2435
|
+
* @public
|
|
2436
|
+
*/
|
|
2437
|
+
export interface SessionEnvFileSystem {
|
|
2438
|
+
/** Entry names directly under `path` (`readdirSync`). */
|
|
2439
|
+
readonly readDirectory: (path: string) => ReadonlyArray<string>;
|
|
2440
|
+
/** The UTF-8 text of the file at `path` (`readFileSync(path, "utf8")`). */
|
|
2441
|
+
readonly readFile: (path: string) => string;
|
|
2442
|
+
/** Modification time of the file at `path`, in epoch milliseconds (`statSync(path).mtimeMs`). */
|
|
2443
|
+
readonly mtimeMs: (path: string) => number;
|
|
2444
|
+
}
|
|
2421
2445
|
/**
|
|
2422
2446
|
* Recover a {@link SessionContext} from the newest session-env hook file
|
|
2423
2447
|
* whose `VITEST_AGENT_PROJECT_DIR` matches `projectDir`.
|
|
@@ -2431,12 +2455,15 @@ export declare const parseSessionEnvExports: (content: string) => Record<string,
|
|
|
2431
2455
|
*
|
|
2432
2456
|
* @param opts - `projectDir` to match against; `homeDir` is the caller's
|
|
2433
2457
|
* home directory (ambient input — the MCP bin passes `os.homedir()`),
|
|
2434
|
-
* under which `.claude/session-env` is read
|
|
2458
|
+
* under which `.claude/session-env` is read; `fileSystem` replaces the
|
|
2459
|
+
* `node:fs` reads (tests pass an in-memory volume) and defaults to the
|
|
2460
|
+
* real disk
|
|
2435
2461
|
* @public
|
|
2436
2462
|
*/
|
|
2437
2463
|
export declare const recoverSessionContextFromSessionEnv: (opts: {
|
|
2438
2464
|
readonly projectDir: string;
|
|
2439
2465
|
readonly homeDir: string;
|
|
2466
|
+
readonly fileSystem?: SessionEnvFileSystem;
|
|
2440
2467
|
}) => SessionContext | null;
|
|
2441
2468
|
//#endregion
|
|
2442
2469
|
//#region src/project-dir.d.ts
|
|
@@ -2641,9 +2668,26 @@ export declare const resolveDataPath: (projectDir: string, options?: ResolveData
|
|
|
2641
2668
|
* Always returns a non-empty string — falls back to the cwd basename
|
|
2642
2669
|
* (or `"anonymous-project"`) so callers don't have to handle the
|
|
2643
2670
|
* empty case.
|
|
2671
|
+
*
|
|
2672
|
+
* Synchronous and reads the real disk through `node:fs`; Effect callers
|
|
2673
|
+
* that already hold a `FileSystem` use
|
|
2674
|
+
* {@link resolveProjectKeyFromCwdEffect}, which applies the same rules.
|
|
2644
2675
|
* @public
|
|
2645
2676
|
*/
|
|
2646
2677
|
export declare const resolveProjectKeyFromCwd: (cwd: string) => string;
|
|
2678
|
+
/**
|
|
2679
|
+
* {@link resolveProjectKeyFromCwd} read through the ambient
|
|
2680
|
+
* `FileSystem` service instead of `node:fs` — same upward walk to the
|
|
2681
|
+
* nearest `package.json`, same precedence, same basename fallback.
|
|
2682
|
+
*
|
|
2683
|
+
* Never fails: a filesystem error while probing or reading is treated like
|
|
2684
|
+
* a missing or malformed `package.json`, exactly as the synchronous form
|
|
2685
|
+
* does.
|
|
2686
|
+
*
|
|
2687
|
+
* @param cwd - Absolute directory to start the upward walk from.
|
|
2688
|
+
* @public
|
|
2689
|
+
*/
|
|
2690
|
+
export declare const resolveProjectKeyFromCwdEffect: (cwd: string) => Effect.Effect<string, never, FileSystem.FileSystem>;
|
|
2647
2691
|
//#endregion
|
|
2648
2692
|
//#region src/utils/resolve-workspace-key.d.ts
|
|
2649
2693
|
/**
|
package/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import { LoggerLive, resolveLogFile, resolveLogLevel } from "./layers/LoggerLive
|
|
|
22
22
|
import { OutputRenderer } from "./services/OutputRenderer.js";
|
|
23
23
|
import { OutputRendererLive } from "./layers/OutputRendererLive.js";
|
|
24
24
|
import { OutputPipelineLive } from "./layers/OutputPipelineLive.js";
|
|
25
|
-
import { APP_NAMESPACE, PathResolutionLive } from "./layers/PathResolutionLive.js";
|
|
25
|
+
import { APP_NAMESPACE, DATA_FALLBACK_DIR, PathResolutionLive } from "./layers/PathResolutionLive.js";
|
|
26
26
|
import { PerClientSessionMapReader, PerClientSessionMapWriter } from "./services/PerClientSessionMap.js";
|
|
27
27
|
import { PerClientSessionMapReaderLive, PerClientSessionMapWriterLive } from "./layers/PerClientSessionMapLive.js";
|
|
28
28
|
import { ProjectDiscovery } from "./services/ProjectDiscovery.js";
|
|
@@ -55,9 +55,9 @@ import { resolveProjectDir } from "./project-dir.js";
|
|
|
55
55
|
import { assembleHistoryRecord, assembleManifest } from "./sql/assemblers.js";
|
|
56
56
|
import { _resetMigrationCacheForTesting, ensureMigrated } from "./utils/ensure-migrated.js";
|
|
57
57
|
import { computeFailureSignature, normalizeAssertionShape } from "./utils/failure-signature.js";
|
|
58
|
-
import { resolveProjectKeyFromCwd } from "./utils/resolve-project-key-from-cwd.js";
|
|
58
|
+
import { resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect } from "./utils/resolve-project-key-from-cwd.js";
|
|
59
59
|
import { DATABASE_FILENAME, resolveDataPath } from "./utils/resolve-data-path.js";
|
|
60
60
|
import { resolveWorkspaceKey } from "./utils/resolve-workspace-key.js";
|
|
61
61
|
import { CURRENT_ENGINE_VERSION } from "./version.js";
|
|
62
62
|
|
|
63
|
-
export { APP_NAMESPACE, AgentContext, CURRENT_ENGINE_VERSION, ConfigLive, DATABASE_FILENAME, DATA_DB_FILENAME, DataReader, DataReaderLive, DataStore, DataStoreLive, DetailResolver, DetailResolverLive, DiscoveryRegistry, DiscoveryRegistryLive, EnvironmentDetector, EnvironmentDetectorLive, EnvironmentDetectorTest, ExecutorResolver, ExecutorResolverLive, FormatSelector, FormatSelectorLive, HistoryTracker, HistoryTrackerLive, HistoryTrackerTest, IDEMPOTENCY_ROOT_SENTINEL, INLINE_ATTACHMENT_BODY_CAP_BYTES, LoggerLive, NodePlatformLayer, OutputPipelineLive, OutputRenderer, OutputRendererLive, PROJECT_MIGRATIONS, PathResolutionLive, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PlatformLive, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityLive, ProjectIdentityTest, REGISTRY_DB_FILENAME, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, SESSIONS_DB_FILENAME, SidecarPlatformLive, VitestAgentConfigFile, _resetMigrationCacheForTesting, assembleHistoryRecord, assembleManifest, classifyEnvironment, computeFailureSignature, deriveIdempotencyKey, dispatchRecordTddArtifactEffect, endAgentEffect, ensureMigrated, formatTriageEffect, formatWrapupEffect, historyKey, makeSqliteStack, migration as migration0001, migration$1 as migration0002, normalizeAssertionShape, parseAndValidateTurnPayload, parseSessionEnvExports, recordRunWorkspaceChangesEffect, recordSessionEnd, recordSessionStart, recordTddArtifactByTaskIdEffect, recordTddArtifactEffect, recordTurnEffect, recoverSessionContextFromSessionEnv, registerAgentEffect, migration$2 as registryMigration0001, resolveDataPath, resolveHookPaths, resolveLogFile, resolveLogLevel, resolveProjectDir, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveSessionForRecording, resolveSessionMapPath, resolveWorkspaceKey, migration$3 as sessionMapMigration0001 };
|
|
63
|
+
export { APP_NAMESPACE, AgentContext, CURRENT_ENGINE_VERSION, ConfigLive, DATABASE_FILENAME, DATA_DB_FILENAME, DATA_FALLBACK_DIR, DataReader, DataReaderLive, DataStore, DataStoreLive, DetailResolver, DetailResolverLive, DiscoveryRegistry, DiscoveryRegistryLive, EnvironmentDetector, EnvironmentDetectorLive, EnvironmentDetectorTest, ExecutorResolver, ExecutorResolverLive, FormatSelector, FormatSelectorLive, HistoryTracker, HistoryTrackerLive, HistoryTrackerTest, IDEMPOTENCY_ROOT_SENTINEL, INLINE_ATTACHMENT_BODY_CAP_BYTES, LoggerLive, NodePlatformLayer, OutputPipelineLive, OutputRenderer, OutputRendererLive, PROJECT_MIGRATIONS, PathResolutionLive, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PlatformLive, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityLive, ProjectIdentityTest, REGISTRY_DB_FILENAME, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, SESSIONS_DB_FILENAME, SidecarPlatformLive, VitestAgentConfigFile, _resetMigrationCacheForTesting, assembleHistoryRecord, assembleManifest, classifyEnvironment, computeFailureSignature, deriveIdempotencyKey, dispatchRecordTddArtifactEffect, endAgentEffect, ensureMigrated, formatTriageEffect, formatWrapupEffect, historyKey, makeSqliteStack, migration as migration0001, migration$1 as migration0002, normalizeAssertionShape, parseAndValidateTurnPayload, parseSessionEnvExports, recordRunWorkspaceChangesEffect, recordSessionEnd, recordSessionStart, recordTddArtifactByTaskIdEffect, recordTddArtifactEffect, recordTurnEffect, recoverSessionContextFromSessionEnv, registerAgentEffect, migration$2 as registryMigration0001, resolveDataPath, resolveHookPaths, resolveLogFile, resolveLogLevel, resolveProjectDir, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect, resolveSessionForRecording, resolveSessionMapPath, resolveWorkspaceKey, migration$3 as sessionMapMigration0001 };
|
package/layers/DataReaderLive.js
CHANGED
|
@@ -606,14 +606,28 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
606
606
|
const runs = yield* sql`SELECT id FROM test_runs WHERE project = ${project} ORDER BY timestamp DESC LIMIT 1`;
|
|
607
607
|
return runs.length === 0 ? null : runs[0].id;
|
|
608
608
|
});
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
609
|
+
/**
|
|
610
|
+
* Load the attachments of every owner in `ownerIds` with a single query,
|
|
611
|
+
* grouped by owner id. Per-owner order follows `attachments.id`. An empty
|
|
612
|
+
* owner list issues no query.
|
|
613
|
+
*/
|
|
614
|
+
const attachmentsForMany = (column, ownerIds) => Effect.gen(function* () {
|
|
615
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
616
|
+
if (ownerIds.length === 0) return grouped;
|
|
617
|
+
const rows = column === "annotation_id" ? yield* sql`SELECT annotation_id AS owner_id, content_type, path, body, body_encoding, byte_size FROM attachments WHERE annotation_id IN ${sql.in(ownerIds)} ORDER BY id` : yield* sql`SELECT artifact_id AS owner_id, content_type, path, body, body_encoding, byte_size FROM attachments WHERE artifact_id IN ${sql.in(ownerIds)} ORDER BY id`;
|
|
618
|
+
for (const r of rows) {
|
|
619
|
+
const attachment = {
|
|
620
|
+
...r.content_type !== null && { contentType: r.content_type },
|
|
621
|
+
...r.path !== null && { path: r.path },
|
|
622
|
+
...r.body !== null && { body: r.body },
|
|
623
|
+
...r.body_encoding !== null && { bodyEncoding: r.body_encoding },
|
|
624
|
+
byteSize: r.byte_size
|
|
625
|
+
};
|
|
626
|
+
const list = grouped.get(r.owner_id);
|
|
627
|
+
if (list === void 0) grouped.set(r.owner_id, [attachment]);
|
|
628
|
+
else list.push(attachment);
|
|
629
|
+
}
|
|
630
|
+
return grouped;
|
|
617
631
|
});
|
|
618
632
|
const getAnnotationsForTest = (project, fullName, options) => Effect.gen(function* () {
|
|
619
633
|
yield* Effect.logDebug("getAnnotationsForTest").pipe(Effect.annotateLogs({
|
|
@@ -631,9 +645,10 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
631
645
|
WHERE tm.run_id = ${runId} AND tc.full_name = ${fullName}
|
|
632
646
|
AND (${modulePath} IS NULL OR tm.relative_module_id = ${modulePath})
|
|
633
647
|
ORDER BY ta.id`;
|
|
648
|
+
const attachmentsByOwner = yield* attachmentsForMany("annotation_id", rows.map((r) => r.id));
|
|
634
649
|
const out = [];
|
|
635
650
|
for (const r of rows) {
|
|
636
|
-
const attachments =
|
|
651
|
+
const attachments = attachmentsByOwner.get(r.id) ?? [];
|
|
637
652
|
out.push({
|
|
638
653
|
id: r.id,
|
|
639
654
|
type: r.type,
|
|
@@ -668,9 +683,10 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
668
683
|
WHERE tm.run_id = ${runId} AND tc.full_name = ${fullName}
|
|
669
684
|
AND (${modulePath} IS NULL OR tm.relative_module_id = ${modulePath})
|
|
670
685
|
ORDER BY ta.id`;
|
|
686
|
+
const attachmentsByOwner = yield* attachmentsForMany("artifact_id", rows.map((r) => r.id));
|
|
671
687
|
const out = [];
|
|
672
688
|
for (const r of rows) {
|
|
673
|
-
const attachments =
|
|
689
|
+
const attachments = attachmentsByOwner.get(r.id) ?? [];
|
|
674
690
|
out.push({
|
|
675
691
|
id: r.id,
|
|
676
692
|
type: r.type,
|
|
@@ -11,12 +11,24 @@ import { AppDirs, Xdg } from "@effected/xdg";
|
|
|
11
11
|
*/
|
|
12
12
|
const APP_NAMESPACE = "vitest-agent";
|
|
13
13
|
/**
|
|
14
|
+
* Where the XDG data root lands when `XDG_DATA_HOME` is unset:
|
|
15
|
+
* `$HOME/.local/share/vitest-agent` — the XDG spec default. `AppDirs` alone
|
|
16
|
+
* would fall back to `$HOME/.vitest-agent`. Shared by `PathResolutionLive`
|
|
17
|
+
* (reporter/MCP route) and `resolveHookPaths` (hook/sidecar route) so both
|
|
18
|
+
* routes land in the same directory.
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
const DATA_FALLBACK_DIR = `.local/share/${APP_NAMESPACE}`;
|
|
22
|
+
/**
|
|
14
23
|
* `AppDirs` over the ambient `Xdg` environment, bound to a `const` so the
|
|
15
24
|
* layer memoizes by reference (calling `AppDirs.layer(...)` inline at two
|
|
16
25
|
* provide sites would resolve two independent services). Requires
|
|
17
26
|
* `FileSystem` + `Path` at the edge.
|
|
18
27
|
*/
|
|
19
|
-
const AppDirsLive = AppDirs.layer({
|
|
28
|
+
const AppDirsLive = AppDirs.layer({
|
|
29
|
+
namespace: APP_NAMESPACE,
|
|
30
|
+
fallbackDir: DATA_FALLBACK_DIR
|
|
31
|
+
}).pipe(Layer.provide(Xdg.layer));
|
|
20
32
|
/**
|
|
21
33
|
* Composite layer providing every service `resolveDataPath` requires:
|
|
22
34
|
* `AppDirs` (XDG path resolution), `VitestAgentConfigFile` (TOML
|
|
@@ -43,4 +55,4 @@ const PathResolutionLive = (projectDir) => {
|
|
|
43
55
|
};
|
|
44
56
|
|
|
45
57
|
//#endregion
|
|
46
|
-
export { APP_NAMESPACE, PathResolutionLive };
|
|
58
|
+
export { APP_NAMESPACE, DATA_FALLBACK_DIR, PathResolutionLive };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared services, layers, SQLite data layer, migrations and platform resolution for the vitest-agent CLI and MCP server.",
|
|
6
6
|
"keywords": [
|
|
@@ -42,14 +42,14 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@effect/platform-node": "4.0.0-rc.118",
|
|
44
44
|
"@effect/sql-sqlite-node": "4.0.0-rc.118",
|
|
45
|
-
"@effected/config-file": "^0.13.
|
|
45
|
+
"@effected/config-file": "^0.13.1",
|
|
46
46
|
"@effected/engine": "^0.2.0",
|
|
47
47
|
"@effected/glob": "^0.9.0",
|
|
48
48
|
"@effected/jsonc": "^0.14.0",
|
|
49
49
|
"@effected/toml": "^0.10.0",
|
|
50
|
-
"@effected/walker": "^0.
|
|
51
|
-
"@effected/workspaces": "^0.30.
|
|
52
|
-
"@effected/xdg": "^0.8.
|
|
50
|
+
"@effected/walker": "^0.14.0",
|
|
51
|
+
"@effected/workspaces": "^0.30.2",
|
|
52
|
+
"@effected/xdg": "^0.8.1",
|
|
53
53
|
"@effected/yaml": "^0.18.0",
|
|
54
54
|
"@vitest-agent/sdk": "5.1.3",
|
|
55
55
|
"effect": "4.0.0-rc.118",
|
package/programs/hook-paths.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { APP_NAMESPACE } from "../layers/PathResolutionLive.js";
|
|
1
|
+
import { APP_NAMESPACE, DATA_FALLBACK_DIR } from "../layers/PathResolutionLive.js";
|
|
2
2
|
import { ProjectIdentityNotResolvableError } from "@vitest-agent/sdk";
|
|
3
3
|
import { ConfigProvider, Effect, FileSystem, Layer, Path } from "effect";
|
|
4
4
|
import { AppDirs, Xdg } from "@effected/xdg";
|
|
@@ -22,12 +22,6 @@ const SESSIONS_DB_FILENAME = "sessions.db";
|
|
|
22
22
|
* @public
|
|
23
23
|
*/
|
|
24
24
|
const REGISTRY_DB_FILENAME = "registry.db";
|
|
25
|
-
/**
|
|
26
|
-
* Where the XDG data root lands when `XDG_DATA_HOME` is unset:
|
|
27
|
-
* `$HOME/.local/share/vitest-agent` — the XDG spec default the sidecar has
|
|
28
|
-
* always used. (`AppDirs` alone would fall back to `$HOME/.vitest-agent`.)
|
|
29
|
-
*/
|
|
30
|
-
const HOOK_DATA_FALLBACK_DIR = `.local/share/${APP_NAMESPACE}`;
|
|
31
25
|
/** Directory under the home dir that holds the per-client `sessions.db` fallback. */
|
|
32
26
|
const SESSION_MAP_HOME_DIR = `.${APP_NAMESPACE}`;
|
|
33
27
|
const nonEmpty = (value) => value !== void 0 && value.length > 0 ? value : void 0;
|
|
@@ -48,7 +42,7 @@ const hookAppDirs = (env) => {
|
|
|
48
42
|
const XdgLive = Xdg.layer.pipe(Layer.provide(ConfigProvider.layer(provider)));
|
|
49
43
|
return AppDirs.layer({
|
|
50
44
|
namespace: APP_NAMESPACE,
|
|
51
|
-
fallbackDir:
|
|
45
|
+
fallbackDir: DATA_FALLBACK_DIR
|
|
52
46
|
}).pipe(Layer.provide(XdgLive));
|
|
53
47
|
};
|
|
54
48
|
/**
|
package/programs/session-env.js
CHANGED
|
@@ -32,6 +32,12 @@ const parseSessionEnvExports = (content) => {
|
|
|
32
32
|
}
|
|
33
33
|
return out;
|
|
34
34
|
};
|
|
35
|
+
/** The default {@link SessionEnvFileSystem}: the real disk through `node:fs`. */
|
|
36
|
+
const nodeSessionEnvFileSystem = {
|
|
37
|
+
readDirectory: (path) => readdirSync(path),
|
|
38
|
+
readFile: (path) => readFileSync(path, "utf8"),
|
|
39
|
+
mtimeMs: (path) => statSync(path).mtimeMs
|
|
40
|
+
};
|
|
35
41
|
/**
|
|
36
42
|
* Recover a {@link SessionContext} from the newest session-env hook file
|
|
37
43
|
* whose `VITEST_AGENT_PROJECT_DIR` matches `projectDir`.
|
|
@@ -45,15 +51,18 @@ const parseSessionEnvExports = (content) => {
|
|
|
45
51
|
*
|
|
46
52
|
* @param opts - `projectDir` to match against; `homeDir` is the caller's
|
|
47
53
|
* home directory (ambient input — the MCP bin passes `os.homedir()`),
|
|
48
|
-
* under which `.claude/session-env` is read
|
|
54
|
+
* under which `.claude/session-env` is read; `fileSystem` replaces the
|
|
55
|
+
* `node:fs` reads (tests pass an in-memory volume) and defaults to the
|
|
56
|
+
* real disk
|
|
49
57
|
* @public
|
|
50
58
|
*/
|
|
51
59
|
const recoverSessionContextFromSessionEnv = (opts) => {
|
|
60
|
+
const fs = opts.fileSystem ?? nodeSessionEnvFileSystem;
|
|
52
61
|
const root = join(opts.homeDir, ".claude", "session-env");
|
|
53
62
|
const wantDir = resolve(opts.projectDir);
|
|
54
63
|
let entries;
|
|
55
64
|
try {
|
|
56
|
-
entries =
|
|
65
|
+
entries = fs.readDirectory(root);
|
|
57
66
|
} catch {
|
|
58
67
|
return null;
|
|
59
68
|
}
|
|
@@ -61,15 +70,15 @@ const recoverSessionContextFromSessionEnv = (opts) => {
|
|
|
61
70
|
for (const entry of entries) {
|
|
62
71
|
const file = join(root, entry, "vitest-agent-hook.sh");
|
|
63
72
|
try {
|
|
64
|
-
const
|
|
65
|
-
const env = parseSessionEnvExports(
|
|
73
|
+
const mtimeMs = fs.mtimeMs(file);
|
|
74
|
+
const env = parseSessionEnvExports(fs.readFile(file));
|
|
66
75
|
const chatId = env.VITEST_AGENT_CHAT_ID;
|
|
67
76
|
const conversationId = env.VITEST_AGENT_CONVERSATION_ID;
|
|
68
77
|
const mainAgentId = env.VITEST_AGENT_MAIN_AGENT_ID ?? env.VITEST_AGENT_AGENT_ID;
|
|
69
78
|
const fileProjectDir = env.VITEST_AGENT_PROJECT_DIR;
|
|
70
79
|
if (chatId === void 0 || chatId.length === 0 || conversationId === void 0 || conversationId.length === 0 || mainAgentId === void 0 || mainAgentId.length === 0 || fileProjectDir === void 0 || resolve(fileProjectDir) !== wantDir) continue;
|
|
71
|
-
if (best === null ||
|
|
72
|
-
mtimeMs
|
|
80
|
+
if (best === null || mtimeMs > best.mtimeMs) best = {
|
|
81
|
+
mtimeMs,
|
|
73
82
|
ctx: {
|
|
74
83
|
chatId,
|
|
75
84
|
conversationId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VitestAgentConfigFile } from "../services/Config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveProjectKeyFromCwdEffect } from "./resolve-project-key-from-cwd.js";
|
|
3
3
|
import { VitestAgentConfig, normalizeWorkspaceKey } from "@vitest-agent/sdk";
|
|
4
4
|
import { Effect, FileSystem } from "effect";
|
|
5
5
|
import { AppDirs } from "@effected/xdg";
|
|
@@ -54,7 +54,7 @@ const resolveDataPath = (projectDir, options = {}) => Effect.gen(function* () {
|
|
|
54
54
|
return join(loaded.cacheDir, DATABASE_FILENAME);
|
|
55
55
|
}
|
|
56
56
|
const dataRoot = yield* (yield* AppDirs).ensureData;
|
|
57
|
-
const key = loaded.projectKey ? normalizeWorkspaceKey(loaded.projectKey) :
|
|
57
|
+
const key = loaded.projectKey ? normalizeWorkspaceKey(loaded.projectKey) : yield* resolveProjectKeyFromCwdEffect(projectDir);
|
|
58
58
|
const dir = join(dataRoot, key);
|
|
59
59
|
yield* fs.makeDirectory(dir, { recursive: true });
|
|
60
60
|
return join(dir, DATABASE_FILENAME);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { gitUrlToProjectKey, normalizeWorkspaceKey } from "@vitest-agent/sdk";
|
|
2
|
+
import { Effect, FileSystem } from "effect";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
5
|
|
|
@@ -24,6 +25,27 @@ const readRepositoryUrl = (parsed) => {
|
|
|
24
25
|
return null;
|
|
25
26
|
};
|
|
26
27
|
/**
|
|
28
|
+
* The key a `package.json` body names: the canonical `host__path` form of
|
|
29
|
+
* `repository.url` when present and canonicalizable, else the normalized
|
|
30
|
+
* `name`, else `null` (no usable field, or malformed JSON).
|
|
31
|
+
*/
|
|
32
|
+
const keyFromPackageJson = (content) => {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(content);
|
|
35
|
+
const repoUrl = readRepositoryUrl(parsed);
|
|
36
|
+
if (repoUrl !== null) {
|
|
37
|
+
const key = gitUrlToProjectKey(repoUrl);
|
|
38
|
+
if (key !== null) return key;
|
|
39
|
+
}
|
|
40
|
+
if (typeof parsed.name === "string" && parsed.name.length > 0) return normalizeWorkspaceKey(parsed.name);
|
|
41
|
+
} catch {}
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
44
|
+
const basenameKey = (cwd) => {
|
|
45
|
+
const basename = cwd.split("/").filter((segment) => segment.length > 0).pop() ?? "anonymous-project";
|
|
46
|
+
return normalizeWorkspaceKey(basename);
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
27
49
|
* Compute the project key for a workspace by reading `package.json`
|
|
28
50
|
* fields directly. Returns the canonical `host__path` form when a git
|
|
29
51
|
* remote URL is present, otherwise the normalized package name.
|
|
@@ -31,22 +53,50 @@ const readRepositoryUrl = (parsed) => {
|
|
|
31
53
|
* Always returns a non-empty string — falls back to the cwd basename
|
|
32
54
|
* (or `"anonymous-project"`) so callers don't have to handle the
|
|
33
55
|
* empty case.
|
|
56
|
+
*
|
|
57
|
+
* Synchronous and reads the real disk through `node:fs`; Effect callers
|
|
58
|
+
* that already hold a `FileSystem` use
|
|
59
|
+
* {@link resolveProjectKeyFromCwdEffect}, which applies the same rules.
|
|
34
60
|
* @public
|
|
35
61
|
*/
|
|
36
62
|
const resolveProjectKeyFromCwd = (cwd) => {
|
|
37
63
|
const pkgPath = findNearestPackageJson(cwd);
|
|
38
|
-
if (pkgPath !== null)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const basename = cwd.split("/").filter((segment) => segment.length > 0).pop() ?? "anonymous-project";
|
|
48
|
-
return normalizeWorkspaceKey(basename);
|
|
64
|
+
if (pkgPath !== null) {
|
|
65
|
+
let content = null;
|
|
66
|
+
try {
|
|
67
|
+
content = readFileSync(pkgPath, "utf-8");
|
|
68
|
+
} catch {}
|
|
69
|
+
const key = content === null ? null : keyFromPackageJson(content);
|
|
70
|
+
if (key !== null) return key;
|
|
71
|
+
}
|
|
72
|
+
return basenameKey(cwd);
|
|
49
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* {@link resolveProjectKeyFromCwd} read through the ambient
|
|
76
|
+
* `FileSystem` service instead of `node:fs` — same upward walk to the
|
|
77
|
+
* nearest `package.json`, same precedence, same basename fallback.
|
|
78
|
+
*
|
|
79
|
+
* Never fails: a filesystem error while probing or reading is treated like
|
|
80
|
+
* a missing or malformed `package.json`, exactly as the synchronous form
|
|
81
|
+
* does.
|
|
82
|
+
*
|
|
83
|
+
* @param cwd - Absolute directory to start the upward walk from.
|
|
84
|
+
* @public
|
|
85
|
+
*/
|
|
86
|
+
const resolveProjectKeyFromCwdEffect = (cwd) => Effect.gen(function* () {
|
|
87
|
+
const fs = yield* FileSystem.FileSystem;
|
|
88
|
+
let dir = cwd;
|
|
89
|
+
while (true) {
|
|
90
|
+
const candidate = join(dir, PACKAGE_JSON);
|
|
91
|
+
if (yield* fs.exists(candidate).pipe(Effect.orElseSucceed(() => false))) {
|
|
92
|
+
const content = yield* fs.readFileString(candidate).pipe(Effect.orElseSucceed(() => null));
|
|
93
|
+
return (content === null ? null : keyFromPackageJson(content)) ?? basenameKey(cwd);
|
|
94
|
+
}
|
|
95
|
+
const parent = dirname(dir);
|
|
96
|
+
if (parent === dir) return basenameKey(cwd);
|
|
97
|
+
dir = parent;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
50
100
|
|
|
51
101
|
//#endregion
|
|
52
|
-
export { resolveProjectKeyFromCwd };
|
|
102
|
+
export { resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect };
|
package/version.js
CHANGED