opencode-workspace-env 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenCode Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # opencode-workspace-env
2
+
3
+ Per-workspace environment injection for OpenCode.
4
+
5
+ `opencode-workspace-env` is an OpenCode plugin that injects per-workspace env vars into every shell execution via the `shell.env` hook. It lets one OpenCode server work across multiple repos, each with its own nix or direnv environment.
6
+
7
+ ## What This Does
8
+
9
+ OpenCode agents often need different toolchains per workspace — different Node, Python, or nix environments. This plugin resolves the nearest env source from the current working directory, loads env vars, and injects them into the shell command that is about to run.
10
+
11
+ Two env source paths are supported:
12
+
13
+ 1. **`.envrc`** (primary) — uses `direnv export json`
14
+ 2. **`flake.nix`** (fallback) — uses `nix print-dev-env --json` directly, no `.envrc` or direnv needed
15
+
16
+ ```text
17
+ Agent runs shell command
18
+ → plugin resolves env source from cwd (bounded by git root)
19
+ → .envrc found? → direnv export json
20
+ → no .envrc, flake.nix found? → nix print-dev-env --json
21
+ → result cached by source file + flake.lock fingerprint
22
+ → shell.env injects output.env
23
+ → command runs with workspace-specific PATH and env vars
24
+ ```
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ npm install opencode-workspace-env
30
+ ```
31
+
32
+ Add it to `opencode.json`:
33
+
34
+ ```json
35
+ {
36
+ "plugin": ["opencode-workspace-env"]
37
+ }
38
+ ```
39
+
40
+ Prerequisites:
41
+ - **direnv** must be installed globally on the host (for `.envrc` path)
42
+ - **nix** must be installed (for `flake.nix` fallback path — only needed if you have repos without `.envrc`)
43
+
44
+ ## Usage
45
+
46
+ ### With `.envrc` (recommended)
47
+
48
+ ```bash
49
+ # in repo root
50
+ printf 'use flake\n' > .envrc
51
+ direnv allow
52
+ ```
53
+
54
+ ### With `flake.nix` only (no `.envrc` needed)
55
+
56
+ ```bash
57
+ # just have a flake.nix with devShells.default — plugin detects it automatically
58
+ git add flake.nix
59
+ ```
60
+
61
+ With the plugin enabled, any OpenCode shell command run inside that repo gets the exported environment for that workspace. Resolution stops at the git root, so a parent directory outside the repo is never used.
62
+
63
+ ## Architecture
64
+
65
+ ```text
66
+ src/
67
+ ├── index.ts # Plugin entry. shell.env hook, dispatches envrc vs flake
68
+ ├── resolve.ts # cwd → ResolvedEnvSource (.envrc first, flake.nix fallback)
69
+ ├── direnv.ts # `direnv export json` → ExportEnvResult
70
+ ├── nix.ts # `nix print-dev-env --json` → NixEnvResult
71
+ ├── filter.ts # Shared env key filter (DIRENV_*, NIX_BUILD_*, nix internals)
72
+ └── cache.ts # In-memory cache keyed by source path + SHA-256 fingerprint
73
+ ```
74
+
75
+ - Writes only to `output.env`, never `process.env`
76
+ - Caches successful exports only — failed results are retried on next call
77
+ - Fails silent when no env source found or tooling unavailable
78
+ - Invalidates cache when source file or `flake.lock` changes
79
+ - Filters nix build internals (`stdenv`, `builder`, `phases`, etc.) from both direnv and nix outputs
80
+
81
+ ## Limitations
82
+
83
+ - Cache fingerprint tracks source file + `flake.lock`, not files sourced by `.envrc`
84
+ - `flake.nix` must be `git add`ed for nix to see it
85
+ - `direnv` must be globally installed, not inside the devShell
86
+ - `nix print-dev-env` can be slow on first eval (~10s+), cached after
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1,7 @@
1
+ export declare class EnvCache {
2
+ private storage;
3
+ get(sourcePath: string, fingerprint: string): Record<string, string> | undefined;
4
+ set(sourcePath: string, fingerprint: string, env: Record<string, string>): void;
5
+ computeFingerprint(sourcePath: string): Promise<string>;
6
+ }
7
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AA+BA,qBAAa,QAAQ;IACnB,OAAO,CAAC,OAAO,CAAiB;IAEhC,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS;IAKhF,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAkBzE,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAQ9D"}
package/dist/cache.js ADDED
@@ -0,0 +1,55 @@
1
+ import { dirname, join } from "node:path";
2
+ // Module-level shared storage (singleton pattern - shared across all EnvCache instances)
3
+ const sharedStorage = new Map();
4
+ // Track latest fingerprint per sourcePath for eviction
5
+ const latestFingerprints = new Map();
6
+ const MAX_ENTRIES = 50;
7
+ function buildCacheKey(sourcePath, fingerprint) {
8
+ return `${sourcePath}::${fingerprint}`;
9
+ }
10
+ function cloneEnv(env) {
11
+ return { ...env };
12
+ }
13
+ async function readTextIfPresent(filePath) {
14
+ try {
15
+ return await Bun.file(filePath).text();
16
+ }
17
+ catch {
18
+ return "";
19
+ }
20
+ }
21
+ async function sha256Hex(content) {
22
+ const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content));
23
+ return Array.from(new Uint8Array(hashBuffer))
24
+ .map((byte) => byte.toString(16).padStart(2, "0"))
25
+ .join("");
26
+ }
27
+ export class EnvCache {
28
+ storage = sharedStorage;
29
+ get(sourcePath, fingerprint) {
30
+ const cachedEnv = this.storage.get(buildCacheKey(sourcePath, fingerprint));
31
+ return cachedEnv ? cloneEnv(cachedEnv) : undefined;
32
+ }
33
+ set(sourcePath, fingerprint, env) {
34
+ // Evict old fingerprint entry if it exists and differs from new one
35
+ const previousFingerprint = latestFingerprints.get(sourcePath);
36
+ if (previousFingerprint && previousFingerprint !== fingerprint) {
37
+ this.storage.delete(buildCacheKey(sourcePath, previousFingerprint));
38
+ }
39
+ // Update tracking and store new entry
40
+ latestFingerprints.set(sourcePath, fingerprint);
41
+ this.storage.set(buildCacheKey(sourcePath, fingerprint), cloneEnv(env));
42
+ if (latestFingerprints.size > MAX_ENTRIES) {
43
+ const oldestSourcePath = latestFingerprints.keys().next().value;
44
+ const oldestFingerprint = latestFingerprints.get(oldestSourcePath);
45
+ this.storage.delete(buildCacheKey(oldestSourcePath, oldestFingerprint));
46
+ latestFingerprints.delete(oldestSourcePath);
47
+ }
48
+ }
49
+ async computeFingerprint(sourcePath) {
50
+ const flakeLockPath = join(dirname(sourcePath), "flake.lock");
51
+ const content = (await readTextIfPresent(sourcePath)) + (await readTextIfPresent(flakeLockPath));
52
+ return sha256Hex(content);
53
+ }
54
+ }
55
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { type EnvExportResult } from "./types.js";
2
+ export declare function exportEnv(envrcPath: string): Promise<EnvExportResult>;
3
+ //# sourceMappingURL=direnv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"direnv.d.ts","sourceRoot":"","sources":["../src/direnv.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,eAAe,EAAiB,MAAM,YAAY,CAAC;AAqBjE,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAwB3E"}
package/dist/direnv.js ADDED
@@ -0,0 +1,41 @@
1
+ import { dirname } from "node:path";
2
+ import { isExcludedEnvKey } from "./filter.js";
3
+ import { safeParseJson } from "./types.js";
4
+ function collectExportedEnv(parsed) {
5
+ const env = {};
6
+ for (const [key, value] of Object.entries(parsed)) {
7
+ if (value === null) {
8
+ continue;
9
+ }
10
+ if (typeof value !== "string") {
11
+ continue;
12
+ }
13
+ if (isExcludedEnvKey(key)) {
14
+ continue;
15
+ }
16
+ env[key] = value;
17
+ }
18
+ return env;
19
+ }
20
+ export async function exportEnv(envrcPath) {
21
+ try {
22
+ const cwd = dirname(envrcPath);
23
+ const result = Bun.spawnSync(["direnv", "export", "json"], { cwd });
24
+ if (!result.success) {
25
+ return { ok: false, reason: "direnv command failed" };
26
+ }
27
+ const raw = result.stdout.toString().trim();
28
+ if (!raw) {
29
+ return { env: {}, ok: true };
30
+ }
31
+ const parsed = safeParseJson(raw);
32
+ if (!parsed) {
33
+ return { ok: false, reason: "direnv output is not valid JSON" };
34
+ }
35
+ return { env: collectExportedEnv(parsed), ok: true };
36
+ }
37
+ catch (error) {
38
+ return { ok: false, reason: `direnv export failed: ${String(error)}` };
39
+ }
40
+ }
41
+ //# sourceMappingURL=direnv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"direnv.js","sourceRoot":"","sources":["../src/direnv.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAwB,aAAa,EAAE,MAAM,YAAY,CAAC;AAEjE,SAAS,kBAAkB,CAAC,MAA+B;IACzD,MAAM,GAAG,GAA2B,EAAE,CAAC;IAEvC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,SAAS;QACX,CAAC;QACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,SAAS;QACX,CAAC;QACD,IAAI,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1B,SAAS;QACX,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACnB,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,SAAiB;IAC/C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;QAE/B,MAAM,MAAM,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QAEpE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAC;QACxD,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;QAC5C,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QAC/B,CAAC;QAED,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iCAAiC,EAAE,CAAC;QAClE,CAAC;QAED,OAAO,EAAE,GAAG,EAAE,kBAAkB,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACvD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,yBAAyB,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;IACzE,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function isExcludedEnvKey(key: string): boolean;
2
+ //# sourceMappingURL=filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../src/filter.ts"],"names":[],"mappings":"AAoDA,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAKrD"}
package/dist/filter.js ADDED
@@ -0,0 +1,57 @@
1
+ /** Prefixes that indicate nix build-system internals */
2
+ const EXCLUDED_PREFIXES = ["DIRENV_", "NIX_BUILD_", "__"];
3
+ /** Known nix build-system variables that should not leak into shell env */
4
+ const EXCLUDED_VARS = new Set([
5
+ "name",
6
+ "system",
7
+ "builder",
8
+ "out",
9
+ "src",
10
+ "outputs",
11
+ "phases",
12
+ "prePhases",
13
+ "preConfigurePhases",
14
+ "preBuildPhases",
15
+ "preInstallPhases",
16
+ "preFixupPhases",
17
+ "preDistPhases",
18
+ "postPhases",
19
+ "buildPhase",
20
+ "configurePhase",
21
+ "installPhase",
22
+ "fixupPhase",
23
+ "distPhase",
24
+ "unpackPhase",
25
+ "patchPhase",
26
+ "checkPhase",
27
+ "buildInputs",
28
+ "nativeBuildInputs",
29
+ "propagatedBuildInputs",
30
+ "propagatedNativeBuildInputs",
31
+ "depsBuildBuild",
32
+ "depsBuildBuildPropagated",
33
+ "depsBuildHost",
34
+ "depsBuildHostPropagated",
35
+ "depsBuildTarget",
36
+ "depsBuildTargetPropagated",
37
+ "depsHostHost",
38
+ "depsHostHostPropagated",
39
+ "depsTargetTarget",
40
+ "depsTargetTargetPropagated",
41
+ "stdenv",
42
+ "strictDeps",
43
+ "shell",
44
+ "dontAddDisableDepTrack",
45
+ "initialPath",
46
+ "cmakeFlags",
47
+ "mesonFlags",
48
+ "DETERMINISTIC_BUILD",
49
+ "SOURCE_DATE_EPOCH",
50
+ ]);
51
+ export function isExcludedEnvKey(key) {
52
+ if (EXCLUDED_VARS.has(key)) {
53
+ return true;
54
+ }
55
+ return EXCLUDED_PREFIXES.some((prefix) => key.startsWith(prefix));
56
+ }
57
+ //# sourceMappingURL=filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.js","sourceRoot":"","sources":["../src/filter.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,MAAM,iBAAiB,GAAG,CAAC,SAAS,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;AAE1D,2EAA2E;AAC3E,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IAC5B,MAAM;IACN,QAAQ;IACR,SAAS;IACT,KAAK;IACL,KAAK;IACL,SAAS;IACT,QAAQ;IACR,WAAW;IACX,oBAAoB;IACpB,gBAAgB;IAChB,kBAAkB;IAClB,gBAAgB;IAChB,eAAe;IACf,YAAY;IACZ,YAAY;IACZ,gBAAgB;IAChB,cAAc;IACd,YAAY;IACZ,WAAW;IACX,aAAa;IACb,YAAY;IACZ,YAAY;IACZ,aAAa;IACb,mBAAmB;IACnB,uBAAuB;IACvB,6BAA6B;IAC7B,gBAAgB;IAChB,0BAA0B;IAC1B,eAAe;IACf,yBAAyB;IACzB,iBAAiB;IACjB,2BAA2B;IAC3B,cAAc;IACd,wBAAwB;IACxB,kBAAkB;IAClB,4BAA4B;IAC5B,QAAQ;IACR,YAAY;IACZ,OAAO;IACP,wBAAwB;IACxB,aAAa;IACb,YAAY;IACZ,YAAY;IACZ,qBAAqB;IACrB,mBAAmB;CACpB,CAAC,CAAC;AAEH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AACpE,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export declare const name = "opencode-workspace-env";
3
+ export declare const version = "0.1.0";
4
+ export declare const description = "OpenCode plugin for per-workspace env injection via shell.env hook";
5
+ declare const plugin: Plugin;
6
+ export default plugin;
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAOtE,eAAO,MAAM,IAAI,2BAA2B,CAAC;AAC7C,eAAO,MAAM,OAAO,UAAU,CAAC;AAC/B,eAAO,MAAM,WAAW,uEAAuE,CAAC;AAIhG,QAAA,MAAM,MAAM,EAAE,MAMb,CAAC;AA4CF,eAAe,MAAM,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,49 @@
1
+ import { EnvCache } from "./cache.js";
2
+ import { exportEnv } from "./direnv.js";
3
+ import { exportNixEnv } from "./nix.js";
4
+ import { resolveEnvSource } from "./resolve.js";
5
+ export const name = "opencode-workspace-env";
6
+ export const version = "0.1.0";
7
+ export const description = "OpenCode plugin for per-workspace env injection via shell.env hook";
8
+ const cache = new EnvCache();
9
+ const plugin = async (_input) => {
10
+ return {
11
+ "shell.env": async (hookInput, output) => {
12
+ output.env = await loadWorkspaceEnv(hookInput.cwd);
13
+ },
14
+ };
15
+ };
16
+ async function loadWorkspaceEnv(cwd) {
17
+ try {
18
+ const resolved = await resolveEnvSource(cwd);
19
+ if (!resolved) {
20
+ return emptyEnv();
21
+ }
22
+ if (resolved.type === "envrc") {
23
+ return await readCachedEnv(resolved.envrcPath, () => exportEnv(resolved.envrcPath));
24
+ }
25
+ return await readCachedEnv(resolved.flakeNixPath, () => exportNixEnv(resolved.flakeNixPath));
26
+ }
27
+ catch {
28
+ /* fail-silent per design, reason lost intentionally at top level */
29
+ return emptyEnv();
30
+ }
31
+ }
32
+ async function readCachedEnv(sourcePath, exporter) {
33
+ const fingerprint = await cache.computeFingerprint(sourcePath);
34
+ const cached = cache.get(sourcePath, fingerprint);
35
+ if (cached) {
36
+ return cached;
37
+ }
38
+ const exported = await exporter();
39
+ if (!exported.ok) {
40
+ return emptyEnv();
41
+ }
42
+ cache.set(sourcePath, fingerprint, exported.env);
43
+ return exported.env;
44
+ }
45
+ function emptyEnv() {
46
+ return {};
47
+ }
48
+ export default plugin;
49
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGhD,MAAM,CAAC,MAAM,IAAI,GAAG,wBAAwB,CAAC;AAC7C,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAC/B,MAAM,CAAC,MAAM,WAAW,GAAG,oEAAoE,CAAC;AAEhG,MAAM,KAAK,GAAG,IAAI,QAAQ,EAAE,CAAC;AAE7B,MAAM,MAAM,GAAW,KAAK,EAAE,MAAmB,EAAkB,EAAE;IACnE,OAAO;QACL,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE;YACvC,MAAM,CAAC,GAAG,GAAG,MAAM,gBAAgB,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACrD,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEF,KAAK,UAAU,gBAAgB,CAAC,GAAW;IACzC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAC7C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,QAAQ,EAAE,CAAC;QACpB,CAAC;QAED,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC9B,OAAO,MAAM,aAAa,CAAC,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;QACtF,CAAC;QAED,OAAO,MAAM,aAAa,CAAC,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/F,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;QACpE,OAAO,QAAQ,EAAE,CAAC;IACpB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,UAAkB,EAClB,QAAwC;IAExC,MAAM,WAAW,GAAG,MAAM,KAAK,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IAElD,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,QAAQ,EAAE,CAAC;IAClC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,OAAO,QAAQ,EAAE,CAAC;IACpB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;IACjD,OAAO,QAAQ,CAAC,GAAG,CAAC;AACtB,CAAC;AAED,SAAS,QAAQ;IACf,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,eAAe,MAAM,CAAC"}
package/dist/nix.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { type EnvExportResult } from "./types.js";
2
+ export declare function exportNixEnv(flakeNixPath: string): Promise<EnvExportResult>;
3
+ //# sourceMappingURL=nix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nix.d.ts","sourceRoot":"","sources":["../src/nix.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,eAAe,EAAiB,MAAM,YAAY,CAAC;AA4DjE,wBAAsB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAwBjF"}
package/dist/nix.js ADDED
@@ -0,0 +1,63 @@
1
+ import { dirname } from "node:path";
2
+ import { isExcludedEnvKey } from "./filter.js";
3
+ import { safeParseJson } from "./types.js";
4
+ function isNixPrintDevEnvOutput(v) {
5
+ if (typeof v !== "object" || v === null || Array.isArray(v)) {
6
+ return false;
7
+ }
8
+ if (!("variables" in v)) {
9
+ return true;
10
+ }
11
+ const { variables } = v;
12
+ return (variables === undefined ||
13
+ (typeof variables === "object" && variables !== null && !Array.isArray(variables)));
14
+ }
15
+ function parseNixOutput(raw) {
16
+ const parsed = safeParseJson(raw);
17
+ if (!parsed || !isNixPrintDevEnvOutput(parsed)) {
18
+ return undefined;
19
+ }
20
+ return parsed;
21
+ }
22
+ function collectNixEnv(output) {
23
+ const env = {};
24
+ const variables = output.variables;
25
+ if (!variables || typeof variables !== "object") {
26
+ return env;
27
+ }
28
+ for (const [key, variable] of Object.entries(variables)) {
29
+ if (isExcludedEnvKey(key)) {
30
+ continue;
31
+ }
32
+ if (variable.type !== "exported") {
33
+ continue;
34
+ }
35
+ if (typeof variable.value !== "string") {
36
+ continue;
37
+ }
38
+ env[key] = variable.value;
39
+ }
40
+ return env;
41
+ }
42
+ export async function exportNixEnv(flakeNixPath) {
43
+ try {
44
+ const cwd = dirname(flakeNixPath);
45
+ const result = Bun.spawnSync(["nix", "print-dev-env", "--json"], { cwd });
46
+ if (!result.success) {
47
+ return { ok: false, reason: "nix command failed" };
48
+ }
49
+ const raw = result.stdout.toString().trim();
50
+ if (!raw) {
51
+ return { env: {}, ok: true };
52
+ }
53
+ const parsed = parseNixOutput(raw);
54
+ if (!parsed) {
55
+ return { ok: false, reason: "nix output is not valid JSON" };
56
+ }
57
+ return { env: collectNixEnv(parsed), ok: true };
58
+ }
59
+ catch (error) {
60
+ return { ok: false, reason: `nix print-dev-env failed: ${String(error)}` };
61
+ }
62
+ }
63
+ //# sourceMappingURL=nix.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nix.js","sourceRoot":"","sources":["../src/nix.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export type ResolvedEnvSource = {
2
+ envrcPath: string;
3
+ gitRoot: string;
4
+ type: "envrc";
5
+ } | {
6
+ flakeNixPath: string;
7
+ gitRoot: string;
8
+ type: "flake";
9
+ };
10
+ export declare function resolveEnvSource(cwd: string): Promise<ResolvedEnvSource | null>;
11
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,iBAAiB,GACzB;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GACrD;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7D,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAmBrF"}
@@ -0,0 +1,58 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ export async function resolveEnvSource(cwd) {
4
+ const absoluteCwd = normalizeCwd(cwd);
5
+ const gitRoot = await getGitRoot(absoluteCwd);
6
+ if (!gitRoot) {
7
+ return null;
8
+ }
9
+ const envrcPath = findNearestFile(absoluteCwd, gitRoot, ".envrc");
10
+ if (envrcPath) {
11
+ return { envrcPath, gitRoot, type: "envrc" };
12
+ }
13
+ const flakeNixPath = findNearestFile(absoluteCwd, gitRoot, "flake.nix");
14
+ if (flakeNixPath) {
15
+ return { flakeNixPath, gitRoot, type: "flake" };
16
+ }
17
+ return null;
18
+ }
19
+ function normalizeCwd(cwd) {
20
+ try {
21
+ return Bun.fileURLToPath(new URL(cwd, `file://${process.cwd()}/`));
22
+ }
23
+ catch {
24
+ return resolve(cwd);
25
+ }
26
+ }
27
+ function findNearestFile(startDir, gitRoot, filename) {
28
+ let currentDir = startDir;
29
+ while (true) {
30
+ const filePath = join(currentDir, filename);
31
+ if (existsSync(filePath)) {
32
+ return filePath;
33
+ }
34
+ if (currentDir === gitRoot) {
35
+ return null;
36
+ }
37
+ const parentDir = dirname(currentDir);
38
+ if (parentDir === currentDir) {
39
+ return null;
40
+ }
41
+ currentDir = parentDir;
42
+ }
43
+ }
44
+ async function getGitRoot(cwd) {
45
+ try {
46
+ const result = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
47
+ cwd,
48
+ });
49
+ if (result.success) {
50
+ return result.stdout.toString().trim();
51
+ }
52
+ return null;
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMnD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAW;IAChD,MAAM,WAAW,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAEtC,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,WAAW,CAAC,CAAC;IAC9C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,SAAS,GAAG,eAAe,CAAC,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;IAClE,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC/C,CAAC;IAED,MAAM,YAAY,GAAG,eAAe,CAAC,WAAW,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;IACxE,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAClD,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,YAAY,CAAC,GAAW;IAC/B,IAAI,CAAC;QACH,OAAO,GAAG,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,GAAG,EAAE,UAAU,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,QAAgB,EAAE,OAAe,EAAE,QAAgB;IAC1E,IAAI,UAAU,GAAG,QAAQ,CAAC;IAE1B,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAC5C,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzB,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,IAAI,UAAU,KAAK,OAAO,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;QACtC,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC;QACd,CAAC;QAED,UAAU,GAAG,SAAS,CAAC;IACzB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,GAAW;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,iBAAiB,CAAC,EAAE;YACpE,GAAG;SACJ,CAAC,CAAC;QAEH,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;QACzC,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared types and JSON utilities for environment export operations.
3
+ */
4
+ /**
5
+ * Result type for environment export operations.
6
+ * Discriminated union: ok=true returns env, ok=false indicates failure.
7
+ */
8
+ export type EnvExportResult = {
9
+ env: Record<string, string>;
10
+ ok: true;
11
+ } | {
12
+ ok: false;
13
+ reason?: string;
14
+ };
15
+ /**
16
+ * Type guard to check if a value is a plain object (not null, not array).
17
+ * Replaces the repeated check: `typeof x !== 'object' || x === null || Array.isArray(x)`
18
+ */
19
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
20
+ /**
21
+ * Safely parse a JSON string that may have non-JSON prefix content.
22
+ * Extracts JSON starting from the first '{' character, parses it, and
23
+ * validates the result is a plain object.
24
+ *
25
+ * @param raw - String that may contain JSON with optional prefix
26
+ * @returns Parsed object or undefined if parsing fails or result isn't an object
27
+ */
28
+ export declare function safeParseJson(raw: string): Record<string, unknown> | undefined;
29
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GACzC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE9E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAoB9E"}
package/dist/types.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared types and JSON utilities for environment export operations.
3
+ */
4
+ /**
5
+ * Type guard to check if a value is a plain object (not null, not array).
6
+ * Replaces the repeated check: `typeof x !== 'object' || x === null || Array.isArray(x)`
7
+ */
8
+ export function isPlainObject(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ /**
12
+ * Safely parse a JSON string that may have non-JSON prefix content.
13
+ * Extracts JSON starting from the first '{' character, parses it, and
14
+ * validates the result is a plain object.
15
+ *
16
+ * @param raw - String that may contain JSON with optional prefix
17
+ * @returns Parsed object or undefined if parsing fails or result isn't an object
18
+ */
19
+ export function safeParseJson(raw) {
20
+ const jsonStart = raw.indexOf("{");
21
+ if (jsonStart === -1) {
22
+ return undefined;
23
+ }
24
+ const jsonPayload = raw.slice(jsonStart);
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(jsonPayload);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ if (!isPlainObject(parsed)) {
33
+ return undefined;
34
+ }
35
+ return parsed;
36
+ }
37
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,SAAS,KAAK,CAAC,CAAC,EAAE,CAAC;QACrB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,WAAW,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAEzC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "opencode-workspace-env",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin for per-workspace env injection via shell.env hook",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "test": "bun test",
10
+ "build": "tsc",
11
+ "typecheck": "tsgo --noEmit",
12
+ "lint": "oxlint",
13
+ "check": "bun test && bun run typecheck",
14
+ "format": "biome format --write .",
15
+ "prepublishOnly": "bun run build"
16
+ },
17
+ "keywords": [
18
+ "opencode",
19
+ "plugin",
20
+ "workspace",
21
+ "environment",
22
+ "direnv",
23
+ "nix"
24
+ ],
25
+ "author": "OpenCode Team",
26
+ "files": [
27
+ "dist",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "license": "MIT",
32
+ "repository": "github:yimsk/opencode-workspace-env",
33
+ "devDependencies": {
34
+ "@nkzw/oxlint-config": "^1.0.1",
35
+ "@types/bun": "latest",
36
+ "@types/node": "^24.5.2",
37
+ "@typescript/native-preview": "^7.0.0-dev.20260310.1",
38
+ "oxlint": "^1.56.0",
39
+ "typescript": "^5.4.0"
40
+ },
41
+ "peerDependencies": {
42
+ "@opencode-ai/plugin": "*"
43
+ }
44
+ }