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 +21 -0
- package/README.md +90 -0
- package/dist/cache.d.ts +7 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +55 -0
- package/dist/cache.js.map +1 -0
- package/dist/direnv.d.ts +3 -0
- package/dist/direnv.d.ts.map +1 -0
- package/dist/direnv.js +41 -0
- package/dist/direnv.js.map +1 -0
- package/dist/filter.d.ts +2 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +57 -0
- package/dist/filter.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/nix.d.ts +3 -0
- package/dist/nix.d.ts.map +1 -0
- package/dist/nix.js +63 -0
- package/dist/nix.js.map +1 -0
- package/dist/resolve.d.ts +11 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +58 -0
- package/dist/resolve.js.map +1 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +37 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
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
|
package/dist/cache.d.ts
ADDED
|
@@ -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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"}
|
package/dist/direnv.d.ts
ADDED
|
@@ -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"}
|
package/dist/filter.d.ts
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
package/dist/nix.js.map
ADDED
|
@@ -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"}
|
package/dist/resolve.js
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|