@vitest-agent/engine 2.1.0 → 3.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/index.d.ts +141 -37
- package/index.js +7 -4
- package/layers/ConfigLive.js +31 -4
- package/layers/DataStoreLive.js +2 -1
- package/layers/EnvironmentDetectorLive.js +3 -6
- package/layers/LoggerLive.js +6 -14
- package/layers/ProjectDiscoveryLive.js +25 -23
- package/layers/ProjectIdentityLive.js +36 -36
- package/layers/RunContextLive.js +33 -47
- package/package.json +15 -13
- package/platform.js +9 -3
- package/programs/hook-paths.js +29 -20
- package/programs/platform-sidecar.js +28 -12
- package/programs/resolve-session-for-recording.js +4 -2
- package/programs/session-env.js +7 -33
- package/services/ProjectIdentity.js +34 -17
- package/stores.js +27 -18
- package/utils/ensure-migrated.js +33 -37
- package/utils/format-fatal-error.js +55 -0
- package/utils/host-metadata.js +101 -0
- package/utils/package-manifest.js +21 -0
- package/utils/resolve-data-path.js +21 -4
- package/utils/resolve-project-key-from-cwd.js +26 -35
- package/utils/resolve-workspace-key.js +8 -4
- package/version.js +1 -1
package/stores.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { Context
|
|
2
|
-
import { SqlClient } from "effect/sql/SqlClient";
|
|
1
|
+
import { Context } from "effect";
|
|
3
2
|
|
|
4
3
|
// src/stores.ts
|
|
5
4
|
/**
|
|
@@ -10,24 +9,34 @@ import { SqlClient } from "effect/sql/SqlClient";
|
|
|
10
9
|
*/
|
|
11
10
|
const LEDGER_OPTIONS = { adoptMigratorLedger: true };
|
|
12
11
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* and names line up with an adopted `effect_sql_migrations` ledger. A key that
|
|
17
|
-
* does not match is skipped, as effect/sql skips it.
|
|
12
|
+
* How long a connection waits on a locked database before `SQLITE_BUSY`:
|
|
13
|
+
* five seconds, the value the 2.x migrations set with `PRAGMA busy_timeout`.
|
|
14
|
+
* Passed as the driver's `busyTimeout`, which applies it on every open.
|
|
18
15
|
*
|
|
19
16
|
* @internal
|
|
20
17
|
*/
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
const BUSY_TIMEOUT = "5 seconds";
|
|
19
|
+
/**
|
|
20
|
+
* The per-connection settings every database applies as its connection
|
|
21
|
+
* opens, before any migration: foreign-key enforcement on. The 2.x
|
|
22
|
+
* migrations set `foreign_keys` / `busy_timeout` themselves, which a
|
|
23
|
+
* migration can only do for the one connection that runs it; the
|
|
24
|
+
* `node:sqlite` driver happens to default both today, and this pins them
|
|
25
|
+
* so a driver default change cannot silently drop them.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
const applyConnectionPragmas = (sql) => sql`PRAGMA foreign_keys = ON`;
|
|
30
|
+
/**
|
|
31
|
+
* The connection options every keyed store opens with: the driver's
|
|
32
|
+
* `busyTimeout` and {@link applyConnectionPragmas} as `onConnect`.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
const CONNECTION_OPTIONS = {
|
|
37
|
+
client: { busyTimeout: BUSY_TIMEOUT },
|
|
38
|
+
onConnect: applyConnectionPragmas
|
|
39
|
+
};
|
|
31
40
|
/**
|
|
32
41
|
* The global discovery registry (`<XDG data>/vitest-agent/registry.db`).
|
|
33
42
|
* @internal
|
|
@@ -39,4 +48,4 @@ var RegistryStore = class extends Context.Service()("vitest-agent/RegistryStore"
|
|
|
39
48
|
*/
|
|
40
49
|
var SessionMapStore = class extends Context.Service()("vitest-agent/SessionMapStore") {};
|
|
41
50
|
|
|
42
|
-
export { LEDGER_OPTIONS, RegistryStore, SessionMapStore,
|
|
51
|
+
export { BUSY_TIMEOUT, CONNECTION_OPTIONS, LEDGER_OPTIONS, RegistryStore, SessionMapStore, applyConnectionPragmas };
|
package/utils/ensure-migrated.js
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
import { LoggerLive } from "../layers/LoggerLive.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { PROJECT_MIGRATIONS } from "../migrations/index.js";
|
|
3
|
+
import { BUSY_TIMEOUT, LEDGER_OPTIONS, applyConnectionPragmas } from "../stores.js";
|
|
4
|
+
import { NodePlatformLayer } from "../platform.js";
|
|
5
|
+
import { Effect } from "effect";
|
|
6
|
+
import { Store } from "@effected/store";
|
|
5
7
|
|
|
6
8
|
// src/utils/ensure-migrated.ts
|
|
7
|
-
const GLOBAL_KEY = Symbol.for("vitest-agent/migration-promises");
|
|
8
|
-
const getCache = () => {
|
|
9
|
-
const g = globalThis;
|
|
10
|
-
let cache = g[GLOBAL_KEY];
|
|
11
|
-
if (!cache) {
|
|
12
|
-
cache = /* @__PURE__ */ new Map();
|
|
13
|
-
g[GLOBAL_KEY] = cache;
|
|
14
|
-
}
|
|
15
|
-
return cache;
|
|
16
|
-
};
|
|
17
9
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
* The project migrations as `Store` runs them, built once so every call
|
|
11
|
+
* hands `Store.migrateOnce` the same list.
|
|
12
|
+
*/
|
|
13
|
+
const PROJECT_STORE_MIGRATIONS = Store.fromMigratorRecord(PROJECT_MIGRATIONS);
|
|
14
|
+
/**
|
|
15
|
+
* Ensure the SQLite database at `dbPath` is migrated to the head of the
|
|
16
|
+
* project migration set.
|
|
17
|
+
*
|
|
18
|
+
* Runs through `@effected/store`'s `Store.migrateOnce`: at most one
|
|
19
|
+
* successful migration run per database per process, shared by every
|
|
20
|
+
* concurrent caller (and by every copy of the kit loaded in the process).
|
|
21
|
+
* The run is keyed on the real path of the file's directory, so a relative,
|
|
22
|
+
* an absolute and a symlinked spelling of one database share it. A failed
|
|
23
|
+
* run (for example `SQLITE_BUSY`) is not remembered: the next call tries
|
|
24
|
+
* again. The connection opens with the same per-connection settings as
|
|
25
|
+
* `makeSqliteStack` (`busy_timeout`, `synchronous = NORMAL`,
|
|
26
|
+
* `foreign_keys = ON`).
|
|
27
|
+
*
|
|
28
|
+
* The parent directory must exist; `dbPath` must name a file (`":memory:"`
|
|
29
|
+
* is a wiring defect).
|
|
22
30
|
*
|
|
23
31
|
* @param dbPath - the SQLite file to migrate
|
|
24
32
|
* @param logLevel - the diagnostics level for the migrator's records; see `LoggerLive`
|
|
@@ -28,25 +36,13 @@ const getCache = () => {
|
|
|
28
36
|
* @public
|
|
29
37
|
*/
|
|
30
38
|
function ensureMigrated(dbPath, logLevel, logFile, env) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}).pipe(Effect.provide(
|
|
38
|
-
const promise = Effect.runPromise(program);
|
|
39
|
-
cache.set(dbPath, promise);
|
|
40
|
-
promise.catch(() => {});
|
|
41
|
-
return promise;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Reset the migration cache. Test-only.
|
|
45
|
-
*
|
|
46
|
-
* @internal
|
|
47
|
-
*/
|
|
48
|
-
function _resetMigrationCacheForTesting() {
|
|
49
|
-
getCache().clear();
|
|
39
|
+
return Effect.runPromise(Store.migrateOnce({
|
|
40
|
+
filename: dbPath,
|
|
41
|
+
migrations: PROJECT_STORE_MIGRATIONS,
|
|
42
|
+
...LEDGER_OPTIONS,
|
|
43
|
+
client: { busyTimeout: BUSY_TIMEOUT },
|
|
44
|
+
onConnect: (sql) => Effect.andThen(sql`PRAGMA synchronous = NORMAL`, applyConnectionPragmas(sql))
|
|
45
|
+
}).pipe(Effect.provide(NodePlatformLayer), Effect.provide(LoggerLive(logLevel, logFile, env))));
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
export {
|
|
48
|
+
export { ensureMigrated };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Cause } from "effect";
|
|
2
|
+
import { CliFailure, Render } from "@effected/cli";
|
|
3
|
+
|
|
4
|
+
// src/utils/format-fatal-error.ts
|
|
5
|
+
const ISSUE_URL = "https://github.com/spencerbeggs/vitest-agent/issues";
|
|
6
|
+
/**
|
|
7
|
+
* Plain text for a person reading stderr: no escapes of any kind, Unicode
|
|
8
|
+
* glyphs, unbounded width (a terminal wraps a long line itself).
|
|
9
|
+
*/
|
|
10
|
+
const CONTEXT = Render.contextOf({ audience: "human" });
|
|
11
|
+
/**
|
|
12
|
+
* Format an unexpected failure for stderr, with the issue URL appended.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Under Effect v4, `Effect.runPromise` / `Effect.runSync` reject with
|
|
16
|
+
* `Cause.squash` of the failure: the error value itself, with no
|
|
17
|
+
* FiberFailure wrapper and no `Cause` attached. So a `Cause` is used as it
|
|
18
|
+
* is (one from `Effect.runPromiseExit` keeps every reason and the span
|
|
19
|
+
* trail), and anything else is rendered as a defect, `Cause.die(err)`: an
|
|
20
|
+
* unexpected throw is a bug to report, and a defect is the shape whose
|
|
21
|
+
* document carries the stack. `@effected/cli`'s `CliFailure.toDoc` keeps
|
|
22
|
+
* the program's own frames, hides `node_modules` and runtime frames,
|
|
23
|
+
* follows an `Error.cause` chain, and renders a non-`Error` value as its
|
|
24
|
+
* text.
|
|
25
|
+
*
|
|
26
|
+
* Total: `CliFailure.toDoc` guards the `stack`, `message`, `cause` and
|
|
27
|
+
* `issue` reads itself, but wrapping a `Proxy` whose traps throw can still
|
|
28
|
+
* throw, so any throw while formatting falls back to a fixed marker naming
|
|
29
|
+
* the value's constructor.
|
|
30
|
+
*
|
|
31
|
+
* @param err - the thrown value, rejection reason, or `Cause`
|
|
32
|
+
* @returns the formatted failure followed by the issue-report line
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
const formatFatalError = (err) => {
|
|
36
|
+
let detail;
|
|
37
|
+
try {
|
|
38
|
+
const cause = Cause.isCause(err) ? err : Cause.die(err);
|
|
39
|
+
detail = Render.plain(CliFailure.toDoc(cause), CONTEXT);
|
|
40
|
+
} catch {
|
|
41
|
+
detail = `<unserializable ${constructorName(err)}>`;
|
|
42
|
+
}
|
|
43
|
+
return `${detail}\nPlease report at ${ISSUE_URL}`;
|
|
44
|
+
};
|
|
45
|
+
/** The value's constructor name, guarded: an own `constructor` getter or a `Proxy` trap can throw too. */
|
|
46
|
+
const constructorName = (err) => {
|
|
47
|
+
try {
|
|
48
|
+
const name = err?.constructor?.name;
|
|
49
|
+
return typeof name === "string" && name !== "" ? name : "error";
|
|
50
|
+
} catch {
|
|
51
|
+
return "error";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export { formatFatalError };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
2
|
+
import { RuntimeEnv } from "@effected/env";
|
|
3
|
+
|
|
4
|
+
// src/utils/host-metadata.ts
|
|
5
|
+
/**
|
|
6
|
+
* The `ci_provider` string persisted for each CI whose job coordinate is
|
|
7
|
+
* recorded. These are this project's own spellings (`github`, `circle`),
|
|
8
|
+
* stored since 2.0; they are mapped from `CiName` rather than replaced by it
|
|
9
|
+
* so existing rows keep matching.
|
|
10
|
+
*/
|
|
11
|
+
const CI_ROWS = {
|
|
12
|
+
"github-actions": {
|
|
13
|
+
provider: "github",
|
|
14
|
+
variable: "GITHUB_RUN_ID",
|
|
15
|
+
coordinate: "runId"
|
|
16
|
+
},
|
|
17
|
+
buildkite: {
|
|
18
|
+
provider: "buildkite",
|
|
19
|
+
variable: "BUILDKITE_JOB_ID",
|
|
20
|
+
coordinate: "jobId"
|
|
21
|
+
},
|
|
22
|
+
circleci: {
|
|
23
|
+
provider: "circle",
|
|
24
|
+
variable: "CIRCLE_BUILD_NUM",
|
|
25
|
+
coordinate: "jobId"
|
|
26
|
+
},
|
|
27
|
+
gitlab: {
|
|
28
|
+
provider: "gitlab",
|
|
29
|
+
variable: "CI_JOB_ID",
|
|
30
|
+
coordinate: "jobId"
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const NONE = {
|
|
34
|
+
source: null,
|
|
35
|
+
value: null,
|
|
36
|
+
metadata: null
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The host identity of an environment record, from `@effected/env`'s
|
|
40
|
+
* `RuntimeEnv` snapshot (`session`, `ci` / `ciRun`, `termProgram`).
|
|
41
|
+
*
|
|
42
|
+
* Priority: the terminal session (`TMUX_PANE`, `WT_SESSION`, `WEZTERM_PANE`,
|
|
43
|
+
* `KITTY_WINDOW_ID`, `TERM_SESSION_ID`), then the CI job (GitHub Actions run,
|
|
44
|
+
* Buildkite, CircleCI, GitLab job). The first match is the source; when
|
|
45
|
+
* nothing matches, all three fields are `null`. Every metadata object
|
|
46
|
+
* carries `term_program` / `term_program_version` when they are set, then
|
|
47
|
+
* the source's own keys (`tmux_socket` for a tmux pane, `ci`, `ci_provider`
|
|
48
|
+
* and `github_run_attempt` for CI), in that key order.
|
|
49
|
+
*
|
|
50
|
+
* GNU screen (`STY`) is not a source: the snapshot is taken with `STY`
|
|
51
|
+
* removed, so a screen session falls through to the next source exactly as
|
|
52
|
+
* it did before the kit, and no `STY` row is ever written. The
|
|
53
|
+
* `VSCODE_INJECTION` + parent-pid sources need `process.ppid` and are the
|
|
54
|
+
* caller's.
|
|
55
|
+
*
|
|
56
|
+
* @param env - the environment record to read (the front end passes `process.env`)
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
const probeHostMetadataFromEnv = (env) => {
|
|
60
|
+
const snapshot = RuntimeEnv.fromRecord({
|
|
61
|
+
...env,
|
|
62
|
+
STY: void 0
|
|
63
|
+
});
|
|
64
|
+
const decoration = (extra) => ({
|
|
65
|
+
...Option.match(snapshot.termProgram.name, {
|
|
66
|
+
onNone: () => ({}),
|
|
67
|
+
onSome: (name) => ({ term_program: name })
|
|
68
|
+
}),
|
|
69
|
+
...Option.match(snapshot.termProgram.version, {
|
|
70
|
+
onNone: () => ({}),
|
|
71
|
+
onSome: (version) => ({ term_program_version: version })
|
|
72
|
+
}),
|
|
73
|
+
...extra
|
|
74
|
+
});
|
|
75
|
+
if (Option.isSome(snapshot.session)) {
|
|
76
|
+
const session = snapshot.session.value;
|
|
77
|
+
const socket = session.source === "tmux" ? Option.filter(session.multiplexer, (multiplexer) => multiplexer.name === "tmux") : Option.none();
|
|
78
|
+
return {
|
|
79
|
+
source: session.variable,
|
|
80
|
+
value: session.id,
|
|
81
|
+
metadata: decoration(Option.isSome(socket) ? { tmux_socket: socket.value.value } : {})
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (Option.isNone(snapshot.ci) || Option.isNone(snapshot.ciRun)) return NONE;
|
|
85
|
+
const row = CI_ROWS[snapshot.ci.value];
|
|
86
|
+
if (row === void 0) return NONE;
|
|
87
|
+
const run = snapshot.ciRun.value;
|
|
88
|
+
const value = run[row.coordinate];
|
|
89
|
+
if (Option.isNone(value)) return NONE;
|
|
90
|
+
return {
|
|
91
|
+
source: row.variable,
|
|
92
|
+
value: value.value,
|
|
93
|
+
metadata: decoration({
|
|
94
|
+
ci: true,
|
|
95
|
+
ci_provider: row.provider,
|
|
96
|
+
...row.provider === "github" && Option.isSome(run.attempt) ? { github_run_attempt: run.attempt.value } : {}
|
|
97
|
+
})
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export { probeHostMetadataFromEnv };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
import { Repository } from "@effected/package-json";
|
|
3
|
+
|
|
4
|
+
// src/utils/package-manifest.ts
|
|
5
|
+
const decodeRepository = Schema.decodeUnknownOption(Repository.FromValue);
|
|
6
|
+
/**
|
|
7
|
+
* The trimmed `repository` URL a manifest names, from the string form or the
|
|
8
|
+
* object form's `url`, or `null` when there is none (absent, blank, an object
|
|
9
|
+
* with no string `url`, or a value of neither form).
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
const repositoryUrlOf = (repository) => repository === void 0 ? null : Option.match(decodeRepository(repository), {
|
|
14
|
+
onNone: () => null,
|
|
15
|
+
onSome: (repo) => {
|
|
16
|
+
const url = repo.url.trim();
|
|
17
|
+
return url.length > 0 ? url : null;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export { repositoryUrlOf };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { VitestAgentConfigFile } from "../services/Config.js";
|
|
2
2
|
import { resolveProjectKeyFromCwdEffect } from "./resolve-project-key-from-cwd.js";
|
|
3
|
-
import {
|
|
4
|
-
import { Effect, FileSystem } from "effect";
|
|
3
|
+
import { ProjectIdentitySourceError, VitestAgentConfig } from "@vitest-agent/sdk";
|
|
4
|
+
import { Effect, FileSystem, Result } from "effect";
|
|
5
5
|
import { AppDirs } from "@effected/xdg";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
|
|
@@ -12,6 +12,20 @@ import { join } from "node:path";
|
|
|
12
12
|
*/
|
|
13
13
|
const DATABASE_FILENAME = "data.db";
|
|
14
14
|
/**
|
|
15
|
+
* The data directory name for a `projectKey` from `vitest-agent.config.toml`,
|
|
16
|
+
* or a `ProjectIdentitySourceError` (source `toml`) when it maps to `.` or
|
|
17
|
+
* `..` — the same refusal `ProjectIdentityLive` gives the same key.
|
|
18
|
+
*/
|
|
19
|
+
const configuredKey = (projectKey, projectDir) => Result.match(AppDirs.segment(projectKey), {
|
|
20
|
+
onSuccess: Effect.succeed,
|
|
21
|
+
onFailure: (cause) => Effect.fail(new ProjectIdentitySourceError({
|
|
22
|
+
source: "toml",
|
|
23
|
+
path: projectDir,
|
|
24
|
+
detail: `projectKey ${JSON.stringify(projectKey)} is not a usable directory name (${cause.message})`,
|
|
25
|
+
cause
|
|
26
|
+
}))
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
15
29
|
* Resolve the absolute path to the SQLite database for `projectDir`.
|
|
16
30
|
*
|
|
17
31
|
* The directory containing the database is ensured to exist before the
|
|
@@ -23,7 +37,10 @@ const DATABASE_FILENAME = "data.db";
|
|
|
23
37
|
*
|
|
24
38
|
* 1. `options.cacheDir` — programmatic override.
|
|
25
39
|
* 2. `cacheDir` from `vitest-agent.config.toml`.
|
|
26
|
-
* 3. `<XDG data>/<normalized projectKey from config>/data.db`.
|
|
40
|
+
* 3. `<XDG data>/<normalized projectKey from config>/data.db`. A
|
|
41
|
+
* `projectKey` that normalizes to `.` or `..` fails with
|
|
42
|
+
* `ProjectIdentitySourceError` (source `toml`) rather than writing
|
|
43
|
+
* outside the data root.
|
|
27
44
|
* 4. `<XDG data>/<projectKey resolved from cwd's package.json>/data.db`.
|
|
28
45
|
* Source 4 prefers `repository.url` (canonicalized to `host__path`),
|
|
29
46
|
* falling back to the normalized `name`. This matches the sidecar
|
|
@@ -54,7 +71,7 @@ const resolveDataPath = (projectDir, options = {}) => Effect.gen(function* () {
|
|
|
54
71
|
return join(loaded.cacheDir, DATABASE_FILENAME);
|
|
55
72
|
}
|
|
56
73
|
const dataRoot = yield* (yield* AppDirs).ensureData;
|
|
57
|
-
const key = loaded.projectKey ?
|
|
74
|
+
const key = loaded.projectKey ? yield* configuredKey(loaded.projectKey, projectDir) : yield* resolveProjectKeyFromCwdEffect(projectDir);
|
|
58
75
|
const dir = join(dataRoot, key);
|
|
59
76
|
yield* fs.makeDirectory(dir, { recursive: true });
|
|
60
77
|
return join(dir, DATABASE_FILENAME);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { repositoryUrlOf } from "./package-manifest.js";
|
|
2
|
+
import { gitUrlToProjectKey } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, FileSystem, Option, Path, Result } from "effect";
|
|
4
|
+
import { AppDirs } from "@effected/xdg";
|
|
3
5
|
import { dirname, join } from "node:path";
|
|
6
|
+
import { Walker } from "@effected/walker";
|
|
7
|
+
import { LenientManifest } from "@effected/package-json";
|
|
4
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
9
|
|
|
6
10
|
// src/utils/resolve-project-key-from-cwd.ts
|
|
@@ -15,35 +19,27 @@ const findNearestPackageJson = (startDir) => {
|
|
|
15
19
|
dir = parent;
|
|
16
20
|
}
|
|
17
21
|
};
|
|
18
|
-
const readRepositoryUrl = (parsed) => {
|
|
19
|
-
const repo = parsed.repository;
|
|
20
|
-
if (typeof repo === "string" && repo.trim().length > 0) return repo.trim();
|
|
21
|
-
if (repo !== null && typeof repo === "object" && "url" in repo && typeof repo.url === "string") {
|
|
22
|
-
const url = repo.url.trim();
|
|
23
|
-
return url.length > 0 ? url : null;
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
};
|
|
27
22
|
/**
|
|
28
23
|
* The key a `package.json` body names: the canonical `host__path` form of
|
|
29
24
|
* `repository.url` when present and canonicalizable, else the normalized
|
|
30
|
-
* `name`, else `null` (no usable field, or
|
|
25
|
+
* `name`, else `null` (no usable field, malformed JSON, or JSON that is not
|
|
26
|
+
* an object).
|
|
31
27
|
*/
|
|
32
28
|
const keyFromPackageJson = (content) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
const parsed = LenientManifest.parseResult(content);
|
|
30
|
+
if (Result.isFailure(parsed)) return null;
|
|
31
|
+
const manifest = parsed.success;
|
|
32
|
+
const repoUrl = repositoryUrlOf(manifest.repository);
|
|
33
|
+
if (repoUrl !== null) {
|
|
34
|
+
const key = gitUrlToProjectKey(repoUrl);
|
|
35
|
+
if (key !== null) return key;
|
|
36
|
+
}
|
|
37
|
+
if (manifest.name !== void 0) return Result.getOrNull(AppDirs.segment(manifest.name));
|
|
42
38
|
return null;
|
|
43
39
|
};
|
|
44
40
|
const basenameKey = (cwd) => {
|
|
45
41
|
const basename = cwd.split("/").filter((segment) => segment.length > 0).pop() ?? "anonymous-project";
|
|
46
|
-
return
|
|
42
|
+
return Result.getOrElse(AppDirs.segment(basename), () => "anonymous-project");
|
|
47
43
|
};
|
|
48
44
|
/**
|
|
49
45
|
* Compute the project key for a workspace by reading `package.json`
|
|
@@ -74,7 +70,8 @@ const resolveProjectKeyFromCwd = (cwd) => {
|
|
|
74
70
|
/**
|
|
75
71
|
* {@link resolveProjectKeyFromCwd} read through the ambient
|
|
76
72
|
* `FileSystem` service instead of `node:fs` — same upward walk to the
|
|
77
|
-
* nearest `package.json
|
|
73
|
+
* nearest `package.json` (`@effected/walker`'s `Walker.ascend` +
|
|
74
|
+
* `Walker.findUpward`), same precedence, same basename fallback.
|
|
78
75
|
*
|
|
79
76
|
* Never fails: a filesystem error while probing or reading is treated like
|
|
80
77
|
* a missing or malformed `package.json`, exactly as the synchronous form
|
|
@@ -85,17 +82,11 @@ const resolveProjectKeyFromCwd = (cwd) => {
|
|
|
85
82
|
*/
|
|
86
83
|
const resolveProjectKeyFromCwdEffect = (cwd) => Effect.gen(function* () {
|
|
87
84
|
const fs = yield* FileSystem.FileSystem;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
const parent = dirname(dir);
|
|
96
|
-
if (parent === dir) return basenameKey(cwd);
|
|
97
|
-
dir = parent;
|
|
98
|
-
}
|
|
99
|
-
});
|
|
85
|
+
const dirs = yield* Walker.ascend(cwd);
|
|
86
|
+
const found = yield* Walker.findUpward(dirs, (dir) => [join(dir, PACKAGE_JSON)]);
|
|
87
|
+
if (Option.isNone(found)) return basenameKey(cwd);
|
|
88
|
+
const content = yield* fs.readFileString(found.value).pipe(Effect.orElseSucceed(() => null));
|
|
89
|
+
return (content === null ? null : keyFromPackageJson(content)) ?? basenameKey(cwd);
|
|
90
|
+
}).pipe(Effect.provide(Path.layer));
|
|
100
91
|
|
|
101
92
|
export { resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Effect } from "effect";
|
|
1
|
+
import { Effect, Result } from "effect";
|
|
3
2
|
import { WorkspaceDiscovery, WorkspaceRootNotFoundError } from "@effected/workspaces";
|
|
3
|
+
import { AppDirs } from "@effected/xdg";
|
|
4
4
|
|
|
5
5
|
// src/utils/resolve-workspace-key.ts
|
|
6
6
|
/**
|
|
@@ -8,7 +8,8 @@ import { WorkspaceDiscovery, WorkspaceRootNotFoundError } from "@effected/worksp
|
|
|
8
8
|
* `projectDir`.
|
|
9
9
|
*
|
|
10
10
|
* The key is the root `package.json`'s `name`, normalized via
|
|
11
|
-
* `
|
|
11
|
+
* `@effected/xdg`'s `AppDirs.segment` (a name that maps to `.` or `..` fails
|
|
12
|
+
* with `AppDirsSegmentError`). This becomes the directory segment under
|
|
12
13
|
* `$XDG_DATA_HOME/vitest-agent/` where the SQLite database lives.
|
|
13
14
|
*
|
|
14
15
|
* Fails with `WorkspaceRootNotFoundError` when no root workspace is
|
|
@@ -34,7 +35,10 @@ const resolveWorkspaceKey = (projectDir) => Effect.gen(function* () {
|
|
|
34
35
|
searchPath: projectDir,
|
|
35
36
|
markers: []
|
|
36
37
|
}));
|
|
37
|
-
return
|
|
38
|
+
return yield* Result.match(AppDirs.segment(root.name), {
|
|
39
|
+
onSuccess: Effect.succeed,
|
|
40
|
+
onFailure: Effect.fail
|
|
41
|
+
});
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
export { resolveWorkspaceKey };
|
package/version.js
CHANGED