opencode-dotenv 0.2.0 → 0.3.2

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 +27 -160
  2. package/dist/index.js +146 -43
  3. package/package.json +20 -2
package/README.md CHANGED
@@ -2,28 +2,9 @@
2
2
 
3
3
  OpenCode plugin to load `.env` files at startup.
4
4
 
5
- ## Features
5
+ ## Setup
6
6
 
7
- - Load multiple `.env` files in order via config file
8
- - Load `.env` from current working directory (optional)
9
- - Override existing environment variables (later files override earlier ones)
10
- - Configurable logging to `/tmp/opencode-dotenv.log` (enabled by default)
11
- - Prevents double loading with load guard
12
- - JSONC config file format (supports comments and trailing commas)
13
- - **Requires Bun runtime**
14
-
15
- ## Installation
16
-
17
- Add to your `opencode.jsonc`:
18
-
19
- ```jsonc
20
- {
21
- "$schema": "https://opencode.ai/config.json",
22
- "plugin": ["file:./plugins/opencode-dotenv"]
23
- }
24
- ```
25
-
26
- After publishing to npm, you can use:
7
+ Add to `~/.config/opencode/opencode.jsonc`:
27
8
 
28
9
  ```jsonc
29
10
  {
@@ -31,165 +12,51 @@ After publishing to npm, you can use:
31
12
  }
32
13
  ```
33
14
 
34
- ## Configuration
35
-
36
- Create `opencode-dotenv.jsonc` in one of these locations:
37
-
38
- 1. `~/.config/opencode/opencode-dotenv.jsonc` (recommended, global config)
39
- 2. `./opencode-dotenv.jsonc` in current working directory (project-specific)
40
-
41
- **Note:** Config files are loaded in the order above; the first found file is used.
42
-
43
- ### Config Schema
44
-
45
- Config file uses **JSONC format** (JSON with Comments), which supports:
46
- - `//` single-line comments
47
- - `/* */` multi-line comments
48
- - Trailing commas
49
- - Trailing spaces
50
-
51
- ```jsonc
52
- {
53
- "files": [
54
- "~/.config/opencode/.env",
55
- "~/a/.env"
56
- ],
57
- "load_cwd_env": true,
58
- "logging": {
59
- "enabled": true
60
- }
61
- }
62
- ```
63
-
64
- **Fields:**
65
- - `files` (array, optional): List of `.env` file paths to load in order. Later files override earlier ones.
66
- - `load_cwd_env` (boolean, optional): Whether to load `.env` from the directory where OpenCode is opened. Defaults to `true`.
67
- - `logging.enabled` (boolean, optional): Enable/disable logging to `/tmp/opencode-dotenv.log`. Defaults to `true`.
68
-
69
- **Notes:**
70
- - Use `~` for home directory (automatically expanded)
71
- - Paths are expanded before loading
72
- - If no config file exists, only loads `./.env` from cwd (if present)
73
- - Logging writes to `/tmp/opencode-dotenv.log` for debugging
74
-
75
- ### Load Order
76
-
77
- 1. Files listed in `config.files` array (in order, later files override earlier ones)
78
- 2. `.env` from current working directory (if `load_cwd_env: true`)
15
+ ## Config
79
16
 
80
- This ensures project-specific env vars have the highest precedence.
81
-
82
- ## Usage Examples
83
-
84
- ### Load global and project-specific .env files
85
-
86
- Config (`~/.config/opencode/opencode-dotenv.jsonc`):
17
+ Create `~/.config/opencode/opencode-dotenv.jsonc`:
87
18
 
88
19
  ```jsonc
89
20
  {
90
- "files": [
91
- "~/.config/opencode/.env"
92
- ],
21
+ "files": ["~/.config/opencode/.env"],
93
22
  "load_cwd_env": true,
94
- "logging": {
95
- "enabled": true
96
- }
97
- }
98
- ```
99
-
100
- Result:
101
- 1. Loads `~/.config/opencode/.env`
102
- 2. Loads `./.env` from cwd (overrides any conflicts)
103
- 3. Logs all activity to `/tmp/opencode-dotenv.log`
104
-
105
- ### Load multiple global files without cwd .env
106
-
107
- Config (`~/.config/opencode/opencode-dotenv.jsonc`):
108
-
109
- ```jsonc
110
- {
111
- "files": [
112
- "~/.config/opencode/.env",
113
- "~/a/.env"
114
- ],
115
- "load_cwd_env": false,
116
- "logging": {
117
- "enabled": false
118
- }
23
+ "prefix": "", // or "MYAPP_"
24
+ "logging": { "enabled": true }
119
25
  }
120
26
  ```
121
27
 
122
- Result:
123
- 1. Loads `~/.config/opencode/.env`
124
- 2. Loads `~/a/.env` (overrides conflicts from first file)
125
- 3. Skips cwd `.env`
126
- 4. No logging output
127
-
128
- ### Example .env files
28
+ | Option | Default | Description |
29
+ |--------|---------|-------------|
30
+ | `files` | `[]` | `.env` files to load (later overrides earlier) |
31
+ | `load_cwd_env` | `true` | Load `.env` from cwd |
32
+ | `prefix` | `""` | Prefix for all variable names |
33
+ | `logging.enabled` | `true` | Log to `/tmp/opencode-dotenv.log` |
129
34
 
130
- `~/.config/opencode/.env`:
35
+ ## .env Format
131
36
 
132
37
  ```bash
133
- # OpenCode Dotenv Configuration
134
- OPENCODE_API_KEY=your_api_key_here
135
- OPENCODE_DEBUG=true
136
- OPENCODE_MAX_TOKENS=100000
38
+ KEY=value
39
+ export EXPORTED=value
40
+ QUOTED="with spaces"
41
+ SINGLE='literal'
42
+ MULTILINE=first\
43
+ second
44
+ INLINE=value # comment stripped
45
+ ESCAPES="line1\nline2\ttab"
137
46
  ```
138
47
 
139
- `./.env` (project-specific):
48
+ ## Security
140
49
 
141
- ```bash
142
- # Project-specific overrides
143
- OPENCODE_DEBUG=false
144
- PROJECT_API_KEY=project_specific_key
145
- ```
50
+ - Paths restricted to `$HOME` or cwd
51
+ - Path traversal rejected
52
+ - Keys validated: `^[a-zA-Z_][a-zA-Z0-9_]*$`
146
53
 
147
- Result: `OPENCODE_DEBUG` will be `false` (from cwd), `OPENCODE_API_KEY` from global, `PROJECT_API_KEY` from cwd.
148
-
149
- ### Logging
150
-
151
- View plugin activity logs:
54
+ ## Debug
152
55
 
153
56
  ```bash
154
57
  tail -f /tmp/opencode-dotenv.log
155
58
  ```
156
59
 
157
- Disable logging in config:
158
-
159
- ```jsonc
160
- {
161
- "files": ["~/.config/opencode/.env"],
162
- "logging": {
163
- "enabled": false
164
- }
165
- }
166
- ```
167
-
168
- ## Development
169
-
170
- ### Plugin structure
171
-
172
- ```
173
- opencode-dotenv/
174
- ├── package.json
175
- ├── src/
176
- │ └── index.ts
177
- └── dist/
178
- └── index.js (built)
179
- ```
180
-
181
- ### Build
182
-
183
- ```bash
184
- bun run build
185
- ```
186
-
187
- ### Publish
188
-
189
- ```bash
190
- npm publish
191
- ```
192
-
193
60
  ## License
194
61
 
195
62
  MIT
package/dist/index.js CHANGED
@@ -1,7 +1,4 @@
1
1
  // @bun
2
- // src/index.ts
3
- import { homedir } from "os";
4
-
5
2
  // node_modules/jsonc-parser/lib/esm/impl/scanner.js
6
3
  function createScanner(text, ignoreTrivia = false) {
7
4
  const len = text.length;
@@ -807,114 +804,220 @@ var ParseErrorCode;
807
804
  })(ParseErrorCode || (ParseErrorCode = {}));
808
805
 
809
806
  // src/index.ts
807
+ import { resolve, normalize } from "path";
808
+ import { homedir as osHomedir } from "os";
810
809
  var LOG_FILE = "/tmp/opencode-dotenv.log";
811
810
  var LOAD_GUARD = "__opencodeDotenvLoaded";
811
+ var VALID_ENV_KEY = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
812
+ function getHomeDir() {
813
+ return process.env.HOME ?? osHomedir();
814
+ }
812
815
  function parseDotenv(content) {
813
816
  const result = {};
814
- for (const line of content.split(`
815
- `)) {
816
- const trimmed = line.trim();
817
- if (!trimmed || trimmed.startsWith("#"))
817
+ const lines = content.split(`
818
+ `);
819
+ let i = 0;
820
+ while (i < lines.length) {
821
+ let line = lines[i].trim();
822
+ i++;
823
+ if (!line || line.startsWith("#"))
818
824
  continue;
819
- const match = trimmed.match(/^export\s+([^=]+)=(.*)$/);
820
- const key = match ? match[1] : trimmed.split("=")[0];
821
- const value = match ? match[2] : trimmed.substring(key.length + 1);
822
- if (key) {
823
- let parsedValue = value.trim();
824
- if (parsedValue.startsWith('"') && parsedValue.endsWith('"') || parsedValue.startsWith("'") && parsedValue.endsWith("'")) {
825
- parsedValue = parsedValue.slice(1, -1);
826
- }
827
- result[key.trim()] = parsedValue;
825
+ while (line.endsWith("\\") && i < lines.length) {
826
+ line = line.slice(0, -1) + lines[i];
827
+ i++;
828
+ }
829
+ const exportMatch = line.match(/^export\s+(.*)$/);
830
+ if (exportMatch) {
831
+ line = exportMatch[1];
832
+ }
833
+ const eqIndex = line.indexOf("=");
834
+ if (eqIndex === -1)
835
+ continue;
836
+ const key = line.substring(0, eqIndex).trim();
837
+ const value = parseValue(line.substring(eqIndex + 1));
838
+ if (key && isValidEnvKey(key)) {
839
+ result[key] = value;
828
840
  }
829
841
  }
830
842
  return result;
831
843
  }
832
- function expandPath(path) {
833
- return path.replace(/^~/, homedir());
844
+ function parseValue(raw) {
845
+ let value = raw.trim();
846
+ if (value.startsWith('"')) {
847
+ const endQuote = findClosingQuote(value, '"');
848
+ if (endQuote !== -1) {
849
+ value = value.substring(1, endQuote);
850
+ return value.replace(/\\n/g, `
851
+ `).replace(/\\r/g, "\r").replace(/\\t/g, "\t").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
852
+ }
853
+ return value;
854
+ }
855
+ if (value.startsWith("'")) {
856
+ const endQuote = findClosingQuote(value, "'");
857
+ if (endQuote !== -1) {
858
+ return value.substring(1, endQuote);
859
+ }
860
+ return value;
861
+ }
862
+ const inlineCommentIndex = value.indexOf(" #");
863
+ if (inlineCommentIndex !== -1) {
864
+ value = value.substring(0, inlineCommentIndex);
865
+ }
866
+ return value.trim();
867
+ }
868
+ function findClosingQuote(str, quote) {
869
+ let i = 1;
870
+ while (i < str.length) {
871
+ if (str[i] === "\\" && i + 1 < str.length) {
872
+ i += 2;
873
+ continue;
874
+ }
875
+ if (str[i] === quote) {
876
+ return i;
877
+ }
878
+ i++;
879
+ }
880
+ return -1;
881
+ }
882
+ function isValidEnvKey(key) {
883
+ return VALID_ENV_KEY.test(key);
884
+ }
885
+ function expandPath(rawPath) {
886
+ if (typeof rawPath !== "string") {
887
+ return null;
888
+ }
889
+ const home = getHomeDir();
890
+ const expanded = rawPath.replace(/^~/, home);
891
+ const resolved = resolve(expanded);
892
+ const normalized = normalize(resolved);
893
+ const cwd = process.cwd();
894
+ const isWithinAllowedDirectory = normalized.startsWith(home) || normalized.startsWith(cwd);
895
+ return isWithinAllowedDirectory ? normalized : null;
834
896
  }
835
897
  var loggingEnabled = true;
898
+ var logBuffer = [];
899
+ var flushScheduled = false;
836
900
  function logToFile(message) {
837
901
  if (!loggingEnabled)
838
902
  return;
903
+ const timestamp = new Date().toISOString();
904
+ logBuffer.push(`[${timestamp}] ${message}`);
905
+ if (!flushScheduled) {
906
+ flushScheduled = true;
907
+ queueMicrotask(flushLogs);
908
+ }
909
+ }
910
+ async function flushLogs() {
911
+ if (logBuffer.length === 0) {
912
+ flushScheduled = false;
913
+ return;
914
+ }
915
+ const messages = logBuffer.join(`
916
+ `) + `
917
+ `;
918
+ logBuffer = [];
919
+ flushScheduled = false;
839
920
  try {
840
- const timestamp = new Date().toISOString();
841
- Bun.appendFileSync(LOG_FILE, `[${timestamp}] ${message}
842
- `);
843
- } catch (e) {}
921
+ const file = Bun.file(LOG_FILE);
922
+ const existing = await file.exists() ? await file.text() : "";
923
+ await Bun.write(LOG_FILE, existing + messages);
924
+ } catch {
925
+ loggingEnabled = false;
926
+ }
844
927
  }
845
928
  async function loadConfig() {
846
- const configPaths = [
847
- `${homedir()}/.config/opencode/opencode-dotenv.jsonc`,
848
- `${process.cwd()}/opencode-dotenv.jsonc`
849
- ];
850
- for (const configPath of configPaths) {
851
- try {
852
- const file = Bun.file(configPath);
853
- if (!await file.exists())
854
- continue;
929
+ const configPath = `${getHomeDir()}/.config/opencode/opencode-dotenv.jsonc`;
930
+ try {
931
+ const file = Bun.file(configPath);
932
+ if (await file.exists()) {
855
933
  const content = await file.text();
856
- const config = parse2(content, [], { allowTrailingComma: true });
934
+ const config = parse2(content, [], {
935
+ allowTrailingComma: true
936
+ });
857
937
  loggingEnabled = config.logging?.enabled !== false;
858
938
  return config;
859
- } catch (e) {
860
- logToFile(`Failed to load config: ${e}`);
861
939
  }
940
+ } catch (e) {
941
+ logToFile(`Failed to load config: ${e}`);
862
942
  }
863
943
  return { files: [], load_cwd_env: true };
864
944
  }
865
- async function loadDotenvFile(filePath) {
945
+ async function loadDotenvFile(filePath, prefix) {
946
+ const skipped = [];
866
947
  try {
867
948
  const file = Bun.file(filePath);
868
949
  if (!await file.exists()) {
869
950
  logToFile(`File not found: ${filePath}`);
870
- return { count: 0, success: false };
951
+ return { count: 0, success: false, skipped };
871
952
  }
872
953
  const content = await file.text();
873
954
  const envVars = parseDotenv(content);
955
+ let count = 0;
874
956
  for (const [key, value] of Object.entries(envVars)) {
875
- process.env[key] = value;
957
+ if (!isValidEnvKey(key)) {
958
+ skipped.push(key);
959
+ continue;
960
+ }
961
+ const envKey = prefix ? `${prefix}${key}` : key;
962
+ process.env[envKey] = value;
963
+ count++;
876
964
  }
877
- return { count: Object.keys(envVars).length, success: true };
965
+ return { count, success: true, skipped };
878
966
  } catch (error) {
879
967
  logToFile(`Failed to load ${filePath}: ${error}`);
880
- return { count: 0, success: false };
968
+ return { count: 0, success: false, skipped };
881
969
  }
882
970
  }
883
- var DotEnvPlugin = async (ctx) => {
971
+ var DotEnvPlugin = async () => {
884
972
  if (globalThis[LOAD_GUARD]) {
885
973
  return {};
886
974
  }
887
975
  globalThis[LOAD_GUARD] = true;
888
976
  logToFile("Plugin started");
889
977
  const config = await loadConfig();
890
- logToFile(`Config loaded: ${config.files.length} files, load_cwd_env=${config.load_cwd_env}, logging=${loggingEnabled}`);
978
+ logToFile(`Config loaded: ${config.files.length} files, load_cwd_env=${config.load_cwd_env}, prefix=${config.prefix ?? "(none)"}, logging=${loggingEnabled}`);
891
979
  let totalFiles = 0;
892
980
  let totalVars = 0;
893
981
  for (const rawPath of config.files) {
894
982
  const filePath = expandPath(rawPath);
983
+ if (!filePath) {
984
+ logToFile(`SECURITY: Rejected path outside allowed directories: ${rawPath}`);
985
+ continue;
986
+ }
895
987
  logToFile(`Loading: ${filePath}`);
896
- const result = await loadDotenvFile(filePath);
988
+ const result = await loadDotenvFile(filePath, config.prefix);
897
989
  if (result.success) {
898
990
  totalFiles++;
899
991
  totalVars += result.count;
900
992
  logToFile(`Loaded ${result.count} vars`);
993
+ if (result.skipped.length > 0) {
994
+ logToFile(`Skipped invalid keys: ${result.skipped.join(", ")}`);
995
+ }
901
996
  }
902
997
  }
903
998
  if (config.load_cwd_env !== false) {
904
999
  const cwdEnvPath = `${process.cwd()}/.env`;
905
1000
  logToFile(`Loading cwd: ${cwdEnvPath}`);
906
- const result = await loadDotenvFile(cwdEnvPath);
1001
+ const result = await loadDotenvFile(cwdEnvPath, config.prefix);
907
1002
  if (result.success) {
908
1003
  totalFiles++;
909
1004
  totalVars += result.count;
910
1005
  logToFile(`Loaded ${result.count} vars from cwd`);
1006
+ if (result.skipped.length > 0) {
1007
+ logToFile(`Skipped invalid keys: ${result.skipped.join(", ")}`);
1008
+ }
911
1009
  }
912
1010
  }
913
1011
  logToFile(`Plugin finished: ${totalFiles} files, ${totalVars} vars`);
1012
+ await flushLogs();
914
1013
  return {};
915
1014
  };
916
1015
  var src_default = DotEnvPlugin;
917
1016
  export {
1017
+ parseValue,
1018
+ parseDotenv,
1019
+ isValidEnvKey,
1020
+ expandPath,
918
1021
  src_default as default,
919
1022
  DotEnvPlugin
920
1023
  };
package/package.json CHANGED
@@ -1,12 +1,28 @@
1
1
  {
2
2
  "name": "opencode-dotenv",
3
- "version": "0.2.0",
3
+ "version": "0.3.2",
4
4
  "description": "OpenCode plugin to load .env files at startup",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
7
7
  "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/assagman/opencode-dotenv.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/assagman/opencode-dotenv/issues"
14
+ },
15
+ "keywords": [
16
+ "opencode",
17
+ "plugin",
18
+ "dotenv",
19
+ "env",
20
+ "environment",
21
+ "variables"
22
+ ],
8
23
  "scripts": {
9
24
  "build": "bun build src/index.ts --outdir dist --target bun",
25
+ "test": "bun test",
10
26
  "prepublishOnly": "bun run build"
11
27
  },
12
28
  "files": [
@@ -20,5 +36,7 @@
20
36
  "dependencies": {
21
37
  "jsonc-parser": "^3.3.1"
22
38
  },
23
- "devDependencies": {}
39
+ "devDependencies": {
40
+ "@types/bun": "^1.2.4"
41
+ }
24
42
  }