opencode-dotenv 0.4.1 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +20 -7
  2. package/dist/index.js +221 -66
  3. package/package.json +28 -6
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  OpenCode plugin to load `.env` files at startup.
4
4
 
5
5
  ## Features
6
-
6
+
7
7
  - Load multiple `.env` files in order via config file
8
8
  - Load `.env` from current working directory (optional)
9
9
  - Override existing environment variables (later files override earlier ones)
@@ -11,7 +11,19 @@ OpenCode plugin to load `.env` files at startup.
11
11
  - Prevents double loading with load guard
12
12
  - JSONC config file format (supports comments and trailing commas)
13
13
  - **Requires Bun runtime**
14
+
15
+ ## Limitations
16
+
17
+ **Important:** This plugin loads AFTER OpenCode configuration is already parsed. Therefore:
18
+
19
+ 1. **Cannot modify existing OpenCode config** - Variables loaded by this plugin cannot be referenced in `opencode.jsonc` using `{env:VAR}` syntax. OpenCode reads its config before plugins initialize.
20
+
21
+ 2. **Only affects subsequent operations** - Loaded environment variables are only available to new chat sessions, tool calls, and operations that occur AFTER plugin initialization.
14
22
 
23
+ 3. **No config reload capability** - Changing `.env` files while OpenCode is running will NOT trigger config re-parsing. To apply changes, restart OpenCode.
24
+
25
+ **Recommended approach:** Set environment variables in your shell profile (`.zshrc`, `.bashrc`) before starting OpenCode.
26
+
15
27
  ## Installation
16
28
 
17
29
  Add to your `opencode.jsonc`:
@@ -126,18 +138,19 @@ Result:
126
138
  4. No logging output
127
139
 
128
140
  ### Example .env files
129
-
141
+
130
142
  `~/.config/opencode/.env`:
131
-
143
+
132
144
  ```bash
133
145
  # OpenCode Dotenv Configuration
134
- OPENCODE_API_KEY=your_api_key_here
135
- OPENCODE_DEBUG=true
136
- OPENCODE_MAX_TOKENS=100000
146
+ OPENCODE_API_KEY=your_api_key_here
147
+ OPENCODE_DEBUG=true
148
+ OPENCODE_MAX_TOKENS=100000
137
149
  ```
138
150
 
139
- `./.env` (project-specific):
151
+ **Note:** This plugin cannot inject these variables into OpenCode's configuration loading process. To use `OPENCODE_API_KEY` in `opencode.jsonc`, set it in your shell profile (`~/.zshrc`, `~/.bashrc`) before starting OpenCode.
140
152
 
153
+ `./.env` (project-specific):
141
154
  ```bash
142
155
  # Project-specific overrides
143
156
  OPENCODE_DEBUG=false
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // @bun
2
- // src/index.ts
2
+ // src/plugin.ts
3
3
  import { homedir } from "os";
4
+ import { appendFile, mkdir } from "fs/promises";
4
5
 
5
6
  // node_modules/jsonc-parser/lib/esm/impl/scanner.js
6
7
  function createScanner(text, ignoreTrivia = false) {
@@ -806,9 +807,165 @@ var ParseErrorCode;
806
807
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
807
808
  })(ParseErrorCode || (ParseErrorCode = {}));
808
809
 
809
- // src/index.ts
810
- var LOG_FILE = "/tmp/opencode-dotenv.log";
810
+ // src/profiler/profiler.ts
811
+ function percentile(sorted, p) {
812
+ if (sorted.length === 0)
813
+ return 0;
814
+ const index = Math.ceil(p / 100 * sorted.length) - 1;
815
+ return sorted[Math.max(0, index)];
816
+ }
817
+ function calculateStats(measurements) {
818
+ if (measurements.length === 0)
819
+ return null;
820
+ const sorted = [...measurements].sort((a, b) => a - b);
821
+ const total = sorted.reduce((sum, v) => sum + v, 0);
822
+ return {
823
+ count: sorted.length,
824
+ min: sorted[0],
825
+ max: sorted[sorted.length - 1],
826
+ avg: total / sorted.length,
827
+ p50: percentile(sorted, 50),
828
+ p95: percentile(sorted, 95),
829
+ p99: percentile(sorted, 99),
830
+ total
831
+ };
832
+ }
833
+
834
+ class Profiler {
835
+ marks = new Map;
836
+ measures = new Map;
837
+ fileMetrics = [];
838
+ initStartTime = null;
839
+ initEndTime = null;
840
+ initState = "idle";
841
+ configLoadTime = null;
842
+ configPath = null;
843
+ totalVars = 0;
844
+ startTime = performance.now();
845
+ mark(name) {
846
+ this.marks.set(name, performance.now());
847
+ }
848
+ measure(name, startMark) {
849
+ const start = this.marks.get(startMark);
850
+ if (start === undefined) {
851
+ return -1;
852
+ }
853
+ const duration = performance.now() - start;
854
+ const existing = this.measures.get(name) || [];
855
+ existing.push(duration);
856
+ this.measures.set(name, existing);
857
+ return duration;
858
+ }
859
+ record(name, duration) {
860
+ const existing = this.measures.get(name) || [];
861
+ existing.push(duration);
862
+ this.measures.set(name, existing);
863
+ }
864
+ getStats(name) {
865
+ const measurements = this.measures.get(name);
866
+ if (!measurements)
867
+ return null;
868
+ return calculateStats(measurements);
869
+ }
870
+ initStart() {
871
+ this.initStartTime = performance.now();
872
+ this.initState = "loading";
873
+ }
874
+ initComplete(state) {
875
+ this.initEndTime = performance.now();
876
+ this.initState = state;
877
+ }
878
+ recordConfigLoad(duration, path) {
879
+ this.configLoadTime = duration;
880
+ this.configPath = path;
881
+ }
882
+ recordFileLoad(path, duration, varCount, success, error) {
883
+ this.fileMetrics.push({
884
+ path,
885
+ duration,
886
+ varCount,
887
+ success,
888
+ error
889
+ });
890
+ if (success) {
891
+ this.totalVars += varCount;
892
+ this.record("file.load", duration);
893
+ }
894
+ }
895
+ getInitState() {
896
+ return this.initState;
897
+ }
898
+ getInitDuration() {
899
+ if (this.initStartTime === null)
900
+ return null;
901
+ if (this.initEndTime !== null) {
902
+ return this.initEndTime - this.initStartTime;
903
+ }
904
+ return performance.now() - this.initStartTime;
905
+ }
906
+ getTotalVars() {
907
+ return this.totalVars;
908
+ }
909
+ getFileMetrics() {
910
+ return [...this.fileMetrics];
911
+ }
912
+ export() {
913
+ return {
914
+ timestamp: new Date().toISOString(),
915
+ uptime: performance.now() - this.startTime,
916
+ initialization: {
917
+ startTime: this.initStartTime || 0,
918
+ endTime: this.initEndTime,
919
+ duration: this.getInitDuration(),
920
+ state: this.initState
921
+ },
922
+ config: {
923
+ loadTime: this.configLoadTime,
924
+ path: this.configPath
925
+ },
926
+ files: {
927
+ loadTimes: this.getStats("file.load"),
928
+ details: this.fileMetrics,
929
+ totalVars: this.totalVars
930
+ }
931
+ };
932
+ }
933
+ reset() {
934
+ this.marks.clear();
935
+ this.measures.clear();
936
+ this.fileMetrics = [];
937
+ this.initStartTime = null;
938
+ this.initEndTime = null;
939
+ this.initState = "idle";
940
+ this.configLoadTime = null;
941
+ this.configPath = null;
942
+ this.totalVars = 0;
943
+ }
944
+ startTimer(name) {
945
+ const start = performance.now();
946
+ return () => {
947
+ const duration = performance.now() - start;
948
+ this.record(name, duration);
949
+ return duration;
950
+ };
951
+ }
952
+ }
953
+ var globalProfiler = new Profiler;
954
+ // src/plugin.ts
955
+ var CONFIG_NAME = "dotenv.jsonc";
956
+ var LOG_DIR = `${homedir()}/.local/share/opencode`;
957
+ var LOG_FILE = `${LOG_DIR}/dotenv.log`;
811
958
  var LOAD_GUARD = "__opencodeDotenvLoaded";
959
+ var isTestEnv = !!process.env.BUN_TEST;
960
+ var loggingEnabled = false;
961
+ function log(message) {
962
+ if (!loggingEnabled || isTestEnv)
963
+ return;
964
+ const timestamp = new Date().toISOString();
965
+ const line = `[${timestamp}] ${message}
966
+ `;
967
+ mkdir(LOG_DIR, { recursive: true }).then(() => appendFile(LOG_FILE, line)).catch(() => {});
968
+ }
812
969
  function parseDotenv(content) {
813
970
  const result = {};
814
971
  if (typeof content !== "string")
@@ -820,114 +977,112 @@ function parseDotenv(content) {
820
977
  continue;
821
978
  const match = trimmed.match(/^export\s+([^=]+)=(.*)$/);
822
979
  const key = match ? match[1] : trimmed.split("=")[0];
823
- let value = match ? match[2] : trimmed.substring(key.length + 1);
824
- if (key) {
825
- let parsedValue = value.trim();
826
- if (!parsedValue.startsWith('"') && !parsedValue.startsWith("'")) {
827
- const inlineCommentIndex = parsedValue.indexOf(" #");
828
- if (inlineCommentIndex !== -1) {
829
- parsedValue = parsedValue.substring(0, inlineCommentIndex).trim();
830
- }
831
- }
832
- if (parsedValue.startsWith('"') && parsedValue.endsWith('"') || parsedValue.startsWith("'") && parsedValue.endsWith("'")) {
833
- parsedValue = parsedValue.slice(1, -1);
980
+ if (!key)
981
+ continue;
982
+ const value = match ? match[2] : trimmed.substring(key.length + 1);
983
+ if (value === undefined)
984
+ continue;
985
+ let parsedValue = value.trim();
986
+ if (!parsedValue.startsWith('"') && !parsedValue.startsWith("'")) {
987
+ const inlineCommentIndex = parsedValue.indexOf(" #");
988
+ if (inlineCommentIndex !== -1) {
989
+ parsedValue = parsedValue.substring(0, inlineCommentIndex).trim();
834
990
  }
835
- result[key.trim()] = parsedValue;
836
991
  }
992
+ if (parsedValue.startsWith('"') && parsedValue.endsWith('"') || parsedValue.startsWith("'") && parsedValue.endsWith("'")) {
993
+ parsedValue = parsedValue.slice(1, -1);
994
+ }
995
+ result[key.trim()] = parsedValue;
837
996
  }
838
997
  return result;
839
998
  }
840
999
  function expandPath(path) {
841
1000
  return path.replace(/^~/, homedir());
842
1001
  }
843
- var loggingEnabled = true;
844
- function logToFile(message) {
845
- if (!loggingEnabled)
846
- return;
847
- try {
848
- const timestamp = new Date().toISOString();
849
- Bun.appendFileSync(LOG_FILE, `[${timestamp}] ${message}
850
- `);
851
- } catch (e) {}
852
- }
853
1002
  async function loadConfig() {
1003
+ const timer = globalProfiler.startTimer("config.load");
854
1004
  const configPaths = [
855
- `${homedir()}/.config/opencode/opencode-dotenv.jsonc`,
856
- `${process.cwd()}/opencode-dotenv.jsonc`
1005
+ `${process.cwd()}/${CONFIG_NAME}`,
1006
+ `${homedir()}/.config/opencode/${CONFIG_NAME}`
857
1007
  ];
858
1008
  for (const configPath of configPaths) {
859
1009
  try {
860
1010
  const file = Bun.file(configPath);
861
- if (!await file.exists())
862
- continue;
863
1011
  const content = await file.text();
864
- const config = parse2(content, [], { allowTrailingComma: true });
865
- loggingEnabled = config.logging?.enabled !== false;
866
- return config;
867
- } catch (e) {
868
- logToFile(`Failed to load config: ${e}`);
869
- }
1012
+ if (!content || typeof content !== "string") {
1013
+ continue;
1014
+ }
1015
+ const config = parse2(content, [], {
1016
+ allowTrailingComma: true
1017
+ });
1018
+ if (config && typeof config === "object" && Array.isArray(config.files)) {
1019
+ loggingEnabled = config.logging?.enabled === true;
1020
+ const duration = timer();
1021
+ globalProfiler.recordConfigLoad(duration, configPath);
1022
+ return { config, path: configPath };
1023
+ }
1024
+ } catch {}
870
1025
  }
871
- return { files: [], load_cwd_env: true };
1026
+ timer();
1027
+ return { config: { files: [], load_cwd_env: true }, path: null };
872
1028
  }
873
1029
  async function loadDotenvFile(filePath) {
1030
+ const start = performance.now();
874
1031
  try {
875
1032
  const file = Bun.file(filePath);
876
- if (!await file.exists()) {
877
- logToFile(`File not found: ${filePath}`);
878
- return { count: 0, success: false };
879
- }
880
1033
  const content = await file.text();
881
- if (typeof content !== "string") {
882
- logToFile(`Invalid content type from ${filePath}: ${typeof content}`);
1034
+ if (typeof content !== "string" || !content) {
1035
+ const duration2 = performance.now() - start;
1036
+ globalProfiler.recordFileLoad(filePath, duration2, 0, false, "Empty or invalid content");
1037
+ log(`Invalid or empty content from ${filePath}`);
883
1038
  return { count: 0, success: false };
884
1039
  }
885
1040
  const envVars = parseDotenv(content);
1041
+ const count = Object.keys(envVars).length;
886
1042
  for (const [key, value] of Object.entries(envVars)) {
887
1043
  process.env[key] = value;
888
1044
  }
889
- return { count: Object.keys(envVars).length, success: true };
1045
+ const duration = performance.now() - start;
1046
+ globalProfiler.recordFileLoad(filePath, duration, count, true);
1047
+ return { count, success: true };
890
1048
  } catch (error) {
891
- logToFile(`Failed to load ${filePath}: ${error}`);
1049
+ const duration = performance.now() - start;
1050
+ const errorMsg = error instanceof Error ? error.message : String(error);
1051
+ globalProfiler.recordFileLoad(filePath, duration, 0, false, errorMsg);
1052
+ log(`Failed to load ${filePath}: ${errorMsg}`);
892
1053
  return { count: 0, success: false };
893
1054
  }
894
1055
  }
895
- var DotEnvPlugin = async (ctx) => {
1056
+ var DotEnvPlugin = async (_ctx) => {
896
1057
  if (globalThis[LOAD_GUARD]) {
897
1058
  return {};
898
1059
  }
899
1060
  globalThis[LOAD_GUARD] = true;
900
- logToFile("Plugin started");
901
- const config = await loadConfig();
902
- logToFile(`Config loaded: ${config.files.length} files, load_cwd_env=${config.load_cwd_env}, logging=${loggingEnabled}`);
1061
+ globalProfiler.initStart();
1062
+ log("Plugin started");
1063
+ const { config, path: configPath } = await loadConfig();
1064
+ log(`Config loaded from ${configPath || "default"}: ${config.files.length} files, load_cwd_env=${config.load_cwd_env}, logging=${loggingEnabled}`);
1065
+ const filesToLoad = [...config.files.map(expandPath)];
1066
+ if (config.load_cwd_env !== false) {
1067
+ filesToLoad.push(`${process.cwd()}/.env`);
1068
+ }
903
1069
  let totalFiles = 0;
904
1070
  let totalVars = 0;
905
- for (const rawPath of config.files) {
906
- const filePath = expandPath(rawPath);
907
- logToFile(`Loading: ${filePath}`);
1071
+ for (const filePath of filesToLoad) {
1072
+ log(`Loading: ${filePath}`);
908
1073
  const result = await loadDotenvFile(filePath);
909
1074
  if (result.success) {
910
1075
  totalFiles++;
911
1076
  totalVars += result.count;
912
- logToFile(`Loaded ${result.count} vars`);
913
- }
914
- }
915
- if (config.load_cwd_env !== false) {
916
- const cwdEnvPath = `${process.cwd()}/.env`;
917
- logToFile(`Loading cwd: ${cwdEnvPath}`);
918
- const result = await loadDotenvFile(cwdEnvPath);
919
- if (result.success) {
920
- totalFiles++;
921
- totalVars += result.count;
922
- logToFile(`Loaded ${result.count} vars from cwd`);
1077
+ log(`Loaded ${result.count} vars from ${filePath}`);
923
1078
  }
924
1079
  }
925
- logToFile(`Plugin finished: ${totalFiles} files, ${totalVars} vars`);
1080
+ globalProfiler.initComplete("ready");
1081
+ const initDuration = globalProfiler.getInitDuration();
1082
+ log(`Plugin finished: ${totalFiles} files, ${totalVars} vars in ${initDuration?.toFixed(2)}ms`);
926
1083
  return {};
927
1084
  };
928
- var src_default = DotEnvPlugin;
929
1085
  export {
930
- parseDotenv,
931
- src_default as default,
1086
+ DotEnvPlugin as default,
932
1087
  DotEnvPlugin
933
1088
  };
package/package.json CHANGED
@@ -1,24 +1,46 @@
1
1
  {
2
2
  "name": "opencode-dotenv",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "OpenCode plugin to load .env files at startup",
5
- "main": "./dist/index.js",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
6
7
  "type": "module",
7
- "license": "MIT",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
8
14
  "scripts": {
9
15
  "build": "bun build src/index.ts --outdir dist --target bun",
16
+ "dev": "bun --hot src/index.ts",
17
+ "test": "bun test",
18
+ "test:coverage": "bun test --coverage",
19
+ "typecheck": "tsc --noEmit",
20
+ "bench": "bun run bench/init.bench.ts",
10
21
  "prepublishOnly": "bun run build"
11
22
  },
12
23
  "files": [
13
- "dist/",
24
+ "dist",
14
25
  "README.md",
15
26
  "LICENSE"
16
27
  ],
28
+ "keywords": [
29
+ "opencode",
30
+ "plugin",
31
+ "dotenv",
32
+ "env",
33
+ "environment"
34
+ ],
35
+ "license": "MIT",
36
+ "devDependencies": {
37
+ "@types/bun": "latest",
38
+ "typescript": "^5.9.3"
39
+ },
17
40
  "peerDependencies": {
18
41
  "@opencode-ai/plugin": "*"
19
42
  },
20
43
  "dependencies": {
21
44
  "jsonc-parser": "^3.3.1"
22
- },
23
- "devDependencies": {}
45
+ }
24
46
  }