opencode-dotenv 0.1.0 → 0.3.1

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 +143 -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,217 @@ 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
+ const home = getHomeDir();
887
+ const expanded = rawPath.replace(/^~/, home);
888
+ const resolved = resolve(expanded);
889
+ const normalized = normalize(resolved);
890
+ const cwd = process.cwd();
891
+ const isWithinAllowedDirectory = normalized.startsWith(home) || normalized.startsWith(cwd);
892
+ return isWithinAllowedDirectory ? normalized : null;
834
893
  }
835
894
  var loggingEnabled = true;
895
+ var logBuffer = [];
896
+ var flushScheduled = false;
836
897
  function logToFile(message) {
837
898
  if (!loggingEnabled)
838
899
  return;
900
+ const timestamp = new Date().toISOString();
901
+ logBuffer.push(`[${timestamp}] ${message}`);
902
+ if (!flushScheduled) {
903
+ flushScheduled = true;
904
+ queueMicrotask(flushLogs);
905
+ }
906
+ }
907
+ async function flushLogs() {
908
+ if (logBuffer.length === 0) {
909
+ flushScheduled = false;
910
+ return;
911
+ }
912
+ const messages = logBuffer.join(`
913
+ `) + `
914
+ `;
915
+ logBuffer = [];
916
+ flushScheduled = false;
839
917
  try {
840
- const timestamp = new Date().toISOString();
841
- Bun.appendFileSync(LOG_FILE, `[${timestamp}] ${message}
842
- `);
843
- } catch (e) {}
918
+ const file = Bun.file(LOG_FILE);
919
+ const existing = await file.exists() ? await file.text() : "";
920
+ await Bun.write(LOG_FILE, existing + messages);
921
+ } catch {
922
+ loggingEnabled = false;
923
+ }
844
924
  }
845
925
  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;
926
+ const configPath = `${getHomeDir()}/.config/opencode/opencode-dotenv.jsonc`;
927
+ try {
928
+ const file = Bun.file(configPath);
929
+ if (await file.exists()) {
855
930
  const content = await file.text();
856
- const config = parse2(content, [], { allowTrailingComma: true });
931
+ const config = parse2(content, [], {
932
+ allowTrailingComma: true
933
+ });
857
934
  loggingEnabled = config.logging?.enabled !== false;
858
935
  return config;
859
- } catch (e) {
860
- logToFile(`Failed to load config: ${e}`);
861
936
  }
937
+ } catch (e) {
938
+ logToFile(`Failed to load config: ${e}`);
862
939
  }
863
940
  return { files: [], load_cwd_env: true };
864
941
  }
865
- async function loadDotenvFile(filePath) {
942
+ async function loadDotenvFile(filePath, prefix) {
943
+ const skipped = [];
866
944
  try {
867
945
  const file = Bun.file(filePath);
868
946
  if (!await file.exists()) {
869
947
  logToFile(`File not found: ${filePath}`);
870
- return { count: 0, success: false };
948
+ return { count: 0, success: false, skipped };
871
949
  }
872
950
  const content = await file.text();
873
951
  const envVars = parseDotenv(content);
952
+ let count = 0;
874
953
  for (const [key, value] of Object.entries(envVars)) {
875
- process.env[key] = value;
954
+ if (!isValidEnvKey(key)) {
955
+ skipped.push(key);
956
+ continue;
957
+ }
958
+ const envKey = prefix ? `${prefix}${key}` : key;
959
+ process.env[envKey] = value;
960
+ count++;
876
961
  }
877
- return { count: Object.keys(envVars).length, success: true };
962
+ return { count, success: true, skipped };
878
963
  } catch (error) {
879
964
  logToFile(`Failed to load ${filePath}: ${error}`);
880
- return { count: 0, success: false };
965
+ return { count: 0, success: false, skipped };
881
966
  }
882
967
  }
883
- var DotEnvPlugin = async (ctx) => {
968
+ var DotEnvPlugin = async () => {
884
969
  if (globalThis[LOAD_GUARD]) {
885
970
  return {};
886
971
  }
887
972
  globalThis[LOAD_GUARD] = true;
888
973
  logToFile("Plugin started");
889
974
  const config = await loadConfig();
890
- logToFile(`Config loaded: ${config.files.length} files, load_cwd_env=${config.load_cwd_env}, logging=${loggingEnabled}`);
975
+ logToFile(`Config loaded: ${config.files.length} files, load_cwd_env=${config.load_cwd_env}, prefix=${config.prefix ?? "(none)"}, logging=${loggingEnabled}`);
891
976
  let totalFiles = 0;
892
977
  let totalVars = 0;
893
978
  for (const rawPath of config.files) {
894
979
  const filePath = expandPath(rawPath);
980
+ if (!filePath) {
981
+ logToFile(`SECURITY: Rejected path outside allowed directories: ${rawPath}`);
982
+ continue;
983
+ }
895
984
  logToFile(`Loading: ${filePath}`);
896
- const result = await loadDotenvFile(filePath);
985
+ const result = await loadDotenvFile(filePath, config.prefix);
897
986
  if (result.success) {
898
987
  totalFiles++;
899
988
  totalVars += result.count;
900
989
  logToFile(`Loaded ${result.count} vars`);
990
+ if (result.skipped.length > 0) {
991
+ logToFile(`Skipped invalid keys: ${result.skipped.join(", ")}`);
992
+ }
901
993
  }
902
994
  }
903
995
  if (config.load_cwd_env !== false) {
904
996
  const cwdEnvPath = `${process.cwd()}/.env`;
905
997
  logToFile(`Loading cwd: ${cwdEnvPath}`);
906
- const result = await loadDotenvFile(cwdEnvPath);
998
+ const result = await loadDotenvFile(cwdEnvPath, config.prefix);
907
999
  if (result.success) {
908
1000
  totalFiles++;
909
1001
  totalVars += result.count;
910
1002
  logToFile(`Loaded ${result.count} vars from cwd`);
1003
+ if (result.skipped.length > 0) {
1004
+ logToFile(`Skipped invalid keys: ${result.skipped.join(", ")}`);
1005
+ }
911
1006
  }
912
1007
  }
913
1008
  logToFile(`Plugin finished: ${totalFiles} files, ${totalVars} vars`);
1009
+ await flushLogs();
914
1010
  return {};
915
1011
  };
916
1012
  var src_default = DotEnvPlugin;
917
1013
  export {
1014
+ parseValue,
1015
+ parseDotenv,
1016
+ isValidEnvKey,
1017
+ expandPath,
918
1018
  src_default as default,
919
1019
  DotEnvPlugin
920
1020
  };
package/package.json CHANGED
@@ -1,12 +1,28 @@
1
1
  {
2
2
  "name": "opencode-dotenv",
3
- "version": "0.1.0",
3
+ "version": "0.3.1",
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
  }