opencode-dotenv 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/dist/index.js +920 -0
  4. package/package.json +24 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sercan Sagman
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,195 @@
1
+ # opencode-dotenv
2
+
3
+ OpenCode plugin to load `.env` files at startup.
4
+
5
+ ## Features
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:
27
+
28
+ ```jsonc
29
+ {
30
+ "plugin": ["opencode-dotenv"]
31
+ }
32
+ ```
33
+
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`)
79
+
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`):
87
+
88
+ ```jsonc
89
+ {
90
+ "files": [
91
+ "~/.config/opencode/.env"
92
+ ],
93
+ "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
+ }
119
+ }
120
+ ```
121
+
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
129
+
130
+ `~/.config/opencode/.env`:
131
+
132
+ ```bash
133
+ # OpenCode Dotenv Configuration
134
+ OPENCODE_API_KEY=your_api_key_here
135
+ OPENCODE_DEBUG=true
136
+ OPENCODE_MAX_TOKENS=100000
137
+ ```
138
+
139
+ `./.env` (project-specific):
140
+
141
+ ```bash
142
+ # Project-specific overrides
143
+ OPENCODE_DEBUG=false
144
+ PROJECT_API_KEY=project_specific_key
145
+ ```
146
+
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:
152
+
153
+ ```bash
154
+ tail -f /tmp/opencode-dotenv.log
155
+ ```
156
+
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
+ ## License
194
+
195
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,920 @@
1
+ // @bun
2
+ // src/index.ts
3
+ import { homedir } from "os";
4
+
5
+ // node_modules/jsonc-parser/lib/esm/impl/scanner.js
6
+ function createScanner(text, ignoreTrivia = false) {
7
+ const len = text.length;
8
+ let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0;
9
+ function scanHexDigits(count, exact) {
10
+ let digits = 0;
11
+ let value2 = 0;
12
+ while (digits < count || !exact) {
13
+ let ch = text.charCodeAt(pos);
14
+ if (ch >= 48 && ch <= 57) {
15
+ value2 = value2 * 16 + ch - 48;
16
+ } else if (ch >= 65 && ch <= 70) {
17
+ value2 = value2 * 16 + ch - 65 + 10;
18
+ } else if (ch >= 97 && ch <= 102) {
19
+ value2 = value2 * 16 + ch - 97 + 10;
20
+ } else {
21
+ break;
22
+ }
23
+ pos++;
24
+ digits++;
25
+ }
26
+ if (digits < count) {
27
+ value2 = -1;
28
+ }
29
+ return value2;
30
+ }
31
+ function setPosition(newPosition) {
32
+ pos = newPosition;
33
+ value = "";
34
+ tokenOffset = 0;
35
+ token = 16;
36
+ scanError = 0;
37
+ }
38
+ function scanNumber() {
39
+ let start = pos;
40
+ if (text.charCodeAt(pos) === 48) {
41
+ pos++;
42
+ } else {
43
+ pos++;
44
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
45
+ pos++;
46
+ }
47
+ }
48
+ if (pos < text.length && text.charCodeAt(pos) === 46) {
49
+ pos++;
50
+ if (pos < text.length && isDigit(text.charCodeAt(pos))) {
51
+ pos++;
52
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
53
+ pos++;
54
+ }
55
+ } else {
56
+ scanError = 3;
57
+ return text.substring(start, pos);
58
+ }
59
+ }
60
+ let end = pos;
61
+ if (pos < text.length && (text.charCodeAt(pos) === 69 || text.charCodeAt(pos) === 101)) {
62
+ pos++;
63
+ if (pos < text.length && text.charCodeAt(pos) === 43 || text.charCodeAt(pos) === 45) {
64
+ pos++;
65
+ }
66
+ if (pos < text.length && isDigit(text.charCodeAt(pos))) {
67
+ pos++;
68
+ while (pos < text.length && isDigit(text.charCodeAt(pos))) {
69
+ pos++;
70
+ }
71
+ end = pos;
72
+ } else {
73
+ scanError = 3;
74
+ }
75
+ }
76
+ return text.substring(start, end);
77
+ }
78
+ function scanString() {
79
+ let result = "", start = pos;
80
+ while (true) {
81
+ if (pos >= len) {
82
+ result += text.substring(start, pos);
83
+ scanError = 2;
84
+ break;
85
+ }
86
+ const ch = text.charCodeAt(pos);
87
+ if (ch === 34) {
88
+ result += text.substring(start, pos);
89
+ pos++;
90
+ break;
91
+ }
92
+ if (ch === 92) {
93
+ result += text.substring(start, pos);
94
+ pos++;
95
+ if (pos >= len) {
96
+ scanError = 2;
97
+ break;
98
+ }
99
+ const ch2 = text.charCodeAt(pos++);
100
+ switch (ch2) {
101
+ case 34:
102
+ result += '"';
103
+ break;
104
+ case 92:
105
+ result += "\\";
106
+ break;
107
+ case 47:
108
+ result += "/";
109
+ break;
110
+ case 98:
111
+ result += "\b";
112
+ break;
113
+ case 102:
114
+ result += "\f";
115
+ break;
116
+ case 110:
117
+ result += `
118
+ `;
119
+ break;
120
+ case 114:
121
+ result += "\r";
122
+ break;
123
+ case 116:
124
+ result += "\t";
125
+ break;
126
+ case 117:
127
+ const ch3 = scanHexDigits(4, true);
128
+ if (ch3 >= 0) {
129
+ result += String.fromCharCode(ch3);
130
+ } else {
131
+ scanError = 4;
132
+ }
133
+ break;
134
+ default:
135
+ scanError = 5;
136
+ }
137
+ start = pos;
138
+ continue;
139
+ }
140
+ if (ch >= 0 && ch <= 31) {
141
+ if (isLineBreak(ch)) {
142
+ result += text.substring(start, pos);
143
+ scanError = 2;
144
+ break;
145
+ } else {
146
+ scanError = 6;
147
+ }
148
+ }
149
+ pos++;
150
+ }
151
+ return result;
152
+ }
153
+ function scanNext() {
154
+ value = "";
155
+ scanError = 0;
156
+ tokenOffset = pos;
157
+ lineStartOffset = lineNumber;
158
+ prevTokenLineStartOffset = tokenLineStartOffset;
159
+ if (pos >= len) {
160
+ tokenOffset = len;
161
+ return token = 17;
162
+ }
163
+ let code = text.charCodeAt(pos);
164
+ if (isWhiteSpace(code)) {
165
+ do {
166
+ pos++;
167
+ value += String.fromCharCode(code);
168
+ code = text.charCodeAt(pos);
169
+ } while (isWhiteSpace(code));
170
+ return token = 15;
171
+ }
172
+ if (isLineBreak(code)) {
173
+ pos++;
174
+ value += String.fromCharCode(code);
175
+ if (code === 13 && text.charCodeAt(pos) === 10) {
176
+ pos++;
177
+ value += `
178
+ `;
179
+ }
180
+ lineNumber++;
181
+ tokenLineStartOffset = pos;
182
+ return token = 14;
183
+ }
184
+ switch (code) {
185
+ case 123:
186
+ pos++;
187
+ return token = 1;
188
+ case 125:
189
+ pos++;
190
+ return token = 2;
191
+ case 91:
192
+ pos++;
193
+ return token = 3;
194
+ case 93:
195
+ pos++;
196
+ return token = 4;
197
+ case 58:
198
+ pos++;
199
+ return token = 6;
200
+ case 44:
201
+ pos++;
202
+ return token = 5;
203
+ case 34:
204
+ pos++;
205
+ value = scanString();
206
+ return token = 10;
207
+ case 47:
208
+ const start = pos - 1;
209
+ if (text.charCodeAt(pos + 1) === 47) {
210
+ pos += 2;
211
+ while (pos < len) {
212
+ if (isLineBreak(text.charCodeAt(pos))) {
213
+ break;
214
+ }
215
+ pos++;
216
+ }
217
+ value = text.substring(start, pos);
218
+ return token = 12;
219
+ }
220
+ if (text.charCodeAt(pos + 1) === 42) {
221
+ pos += 2;
222
+ const safeLength = len - 1;
223
+ let commentClosed = false;
224
+ while (pos < safeLength) {
225
+ const ch = text.charCodeAt(pos);
226
+ if (ch === 42 && text.charCodeAt(pos + 1) === 47) {
227
+ pos += 2;
228
+ commentClosed = true;
229
+ break;
230
+ }
231
+ pos++;
232
+ if (isLineBreak(ch)) {
233
+ if (ch === 13 && text.charCodeAt(pos) === 10) {
234
+ pos++;
235
+ }
236
+ lineNumber++;
237
+ tokenLineStartOffset = pos;
238
+ }
239
+ }
240
+ if (!commentClosed) {
241
+ pos++;
242
+ scanError = 1;
243
+ }
244
+ value = text.substring(start, pos);
245
+ return token = 13;
246
+ }
247
+ value += String.fromCharCode(code);
248
+ pos++;
249
+ return token = 16;
250
+ case 45:
251
+ value += String.fromCharCode(code);
252
+ pos++;
253
+ if (pos === len || !isDigit(text.charCodeAt(pos))) {
254
+ return token = 16;
255
+ }
256
+ case 48:
257
+ case 49:
258
+ case 50:
259
+ case 51:
260
+ case 52:
261
+ case 53:
262
+ case 54:
263
+ case 55:
264
+ case 56:
265
+ case 57:
266
+ value += scanNumber();
267
+ return token = 11;
268
+ default:
269
+ while (pos < len && isUnknownContentCharacter(code)) {
270
+ pos++;
271
+ code = text.charCodeAt(pos);
272
+ }
273
+ if (tokenOffset !== pos) {
274
+ value = text.substring(tokenOffset, pos);
275
+ switch (value) {
276
+ case "true":
277
+ return token = 8;
278
+ case "false":
279
+ return token = 9;
280
+ case "null":
281
+ return token = 7;
282
+ }
283
+ return token = 16;
284
+ }
285
+ value += String.fromCharCode(code);
286
+ pos++;
287
+ return token = 16;
288
+ }
289
+ }
290
+ function isUnknownContentCharacter(code) {
291
+ if (isWhiteSpace(code) || isLineBreak(code)) {
292
+ return false;
293
+ }
294
+ switch (code) {
295
+ case 125:
296
+ case 93:
297
+ case 123:
298
+ case 91:
299
+ case 34:
300
+ case 58:
301
+ case 44:
302
+ case 47:
303
+ return false;
304
+ }
305
+ return true;
306
+ }
307
+ function scanNextNonTrivia() {
308
+ let result;
309
+ do {
310
+ result = scanNext();
311
+ } while (result >= 12 && result <= 15);
312
+ return result;
313
+ }
314
+ return {
315
+ setPosition,
316
+ getPosition: () => pos,
317
+ scan: ignoreTrivia ? scanNextNonTrivia : scanNext,
318
+ getToken: () => token,
319
+ getTokenValue: () => value,
320
+ getTokenOffset: () => tokenOffset,
321
+ getTokenLength: () => pos - tokenOffset,
322
+ getTokenStartLine: () => lineStartOffset,
323
+ getTokenStartCharacter: () => tokenOffset - prevTokenLineStartOffset,
324
+ getTokenError: () => scanError
325
+ };
326
+ }
327
+ function isWhiteSpace(ch) {
328
+ return ch === 32 || ch === 9;
329
+ }
330
+ function isLineBreak(ch) {
331
+ return ch === 10 || ch === 13;
332
+ }
333
+ function isDigit(ch) {
334
+ return ch >= 48 && ch <= 57;
335
+ }
336
+ var CharacterCodes;
337
+ (function(CharacterCodes2) {
338
+ CharacterCodes2[CharacterCodes2["lineFeed"] = 10] = "lineFeed";
339
+ CharacterCodes2[CharacterCodes2["carriageReturn"] = 13] = "carriageReturn";
340
+ CharacterCodes2[CharacterCodes2["space"] = 32] = "space";
341
+ CharacterCodes2[CharacterCodes2["_0"] = 48] = "_0";
342
+ CharacterCodes2[CharacterCodes2["_1"] = 49] = "_1";
343
+ CharacterCodes2[CharacterCodes2["_2"] = 50] = "_2";
344
+ CharacterCodes2[CharacterCodes2["_3"] = 51] = "_3";
345
+ CharacterCodes2[CharacterCodes2["_4"] = 52] = "_4";
346
+ CharacterCodes2[CharacterCodes2["_5"] = 53] = "_5";
347
+ CharacterCodes2[CharacterCodes2["_6"] = 54] = "_6";
348
+ CharacterCodes2[CharacterCodes2["_7"] = 55] = "_7";
349
+ CharacterCodes2[CharacterCodes2["_8"] = 56] = "_8";
350
+ CharacterCodes2[CharacterCodes2["_9"] = 57] = "_9";
351
+ CharacterCodes2[CharacterCodes2["a"] = 97] = "a";
352
+ CharacterCodes2[CharacterCodes2["b"] = 98] = "b";
353
+ CharacterCodes2[CharacterCodes2["c"] = 99] = "c";
354
+ CharacterCodes2[CharacterCodes2["d"] = 100] = "d";
355
+ CharacterCodes2[CharacterCodes2["e"] = 101] = "e";
356
+ CharacterCodes2[CharacterCodes2["f"] = 102] = "f";
357
+ CharacterCodes2[CharacterCodes2["g"] = 103] = "g";
358
+ CharacterCodes2[CharacterCodes2["h"] = 104] = "h";
359
+ CharacterCodes2[CharacterCodes2["i"] = 105] = "i";
360
+ CharacterCodes2[CharacterCodes2["j"] = 106] = "j";
361
+ CharacterCodes2[CharacterCodes2["k"] = 107] = "k";
362
+ CharacterCodes2[CharacterCodes2["l"] = 108] = "l";
363
+ CharacterCodes2[CharacterCodes2["m"] = 109] = "m";
364
+ CharacterCodes2[CharacterCodes2["n"] = 110] = "n";
365
+ CharacterCodes2[CharacterCodes2["o"] = 111] = "o";
366
+ CharacterCodes2[CharacterCodes2["p"] = 112] = "p";
367
+ CharacterCodes2[CharacterCodes2["q"] = 113] = "q";
368
+ CharacterCodes2[CharacterCodes2["r"] = 114] = "r";
369
+ CharacterCodes2[CharacterCodes2["s"] = 115] = "s";
370
+ CharacterCodes2[CharacterCodes2["t"] = 116] = "t";
371
+ CharacterCodes2[CharacterCodes2["u"] = 117] = "u";
372
+ CharacterCodes2[CharacterCodes2["v"] = 118] = "v";
373
+ CharacterCodes2[CharacterCodes2["w"] = 119] = "w";
374
+ CharacterCodes2[CharacterCodes2["x"] = 120] = "x";
375
+ CharacterCodes2[CharacterCodes2["y"] = 121] = "y";
376
+ CharacterCodes2[CharacterCodes2["z"] = 122] = "z";
377
+ CharacterCodes2[CharacterCodes2["A"] = 65] = "A";
378
+ CharacterCodes2[CharacterCodes2["B"] = 66] = "B";
379
+ CharacterCodes2[CharacterCodes2["C"] = 67] = "C";
380
+ CharacterCodes2[CharacterCodes2["D"] = 68] = "D";
381
+ CharacterCodes2[CharacterCodes2["E"] = 69] = "E";
382
+ CharacterCodes2[CharacterCodes2["F"] = 70] = "F";
383
+ CharacterCodes2[CharacterCodes2["G"] = 71] = "G";
384
+ CharacterCodes2[CharacterCodes2["H"] = 72] = "H";
385
+ CharacterCodes2[CharacterCodes2["I"] = 73] = "I";
386
+ CharacterCodes2[CharacterCodes2["J"] = 74] = "J";
387
+ CharacterCodes2[CharacterCodes2["K"] = 75] = "K";
388
+ CharacterCodes2[CharacterCodes2["L"] = 76] = "L";
389
+ CharacterCodes2[CharacterCodes2["M"] = 77] = "M";
390
+ CharacterCodes2[CharacterCodes2["N"] = 78] = "N";
391
+ CharacterCodes2[CharacterCodes2["O"] = 79] = "O";
392
+ CharacterCodes2[CharacterCodes2["P"] = 80] = "P";
393
+ CharacterCodes2[CharacterCodes2["Q"] = 81] = "Q";
394
+ CharacterCodes2[CharacterCodes2["R"] = 82] = "R";
395
+ CharacterCodes2[CharacterCodes2["S"] = 83] = "S";
396
+ CharacterCodes2[CharacterCodes2["T"] = 84] = "T";
397
+ CharacterCodes2[CharacterCodes2["U"] = 85] = "U";
398
+ CharacterCodes2[CharacterCodes2["V"] = 86] = "V";
399
+ CharacterCodes2[CharacterCodes2["W"] = 87] = "W";
400
+ CharacterCodes2[CharacterCodes2["X"] = 88] = "X";
401
+ CharacterCodes2[CharacterCodes2["Y"] = 89] = "Y";
402
+ CharacterCodes2[CharacterCodes2["Z"] = 90] = "Z";
403
+ CharacterCodes2[CharacterCodes2["asterisk"] = 42] = "asterisk";
404
+ CharacterCodes2[CharacterCodes2["backslash"] = 92] = "backslash";
405
+ CharacterCodes2[CharacterCodes2["closeBrace"] = 125] = "closeBrace";
406
+ CharacterCodes2[CharacterCodes2["closeBracket"] = 93] = "closeBracket";
407
+ CharacterCodes2[CharacterCodes2["colon"] = 58] = "colon";
408
+ CharacterCodes2[CharacterCodes2["comma"] = 44] = "comma";
409
+ CharacterCodes2[CharacterCodes2["dot"] = 46] = "dot";
410
+ CharacterCodes2[CharacterCodes2["doubleQuote"] = 34] = "doubleQuote";
411
+ CharacterCodes2[CharacterCodes2["minus"] = 45] = "minus";
412
+ CharacterCodes2[CharacterCodes2["openBrace"] = 123] = "openBrace";
413
+ CharacterCodes2[CharacterCodes2["openBracket"] = 91] = "openBracket";
414
+ CharacterCodes2[CharacterCodes2["plus"] = 43] = "plus";
415
+ CharacterCodes2[CharacterCodes2["slash"] = 47] = "slash";
416
+ CharacterCodes2[CharacterCodes2["formFeed"] = 12] = "formFeed";
417
+ CharacterCodes2[CharacterCodes2["tab"] = 9] = "tab";
418
+ })(CharacterCodes || (CharacterCodes = {}));
419
+
420
+ // node_modules/jsonc-parser/lib/esm/impl/string-intern.js
421
+ var cachedSpaces = new Array(20).fill(0).map((_, index) => {
422
+ return " ".repeat(index);
423
+ });
424
+ var maxCachedValues = 200;
425
+ var cachedBreakLinesWithSpaces = {
426
+ " ": {
427
+ "\n": new Array(maxCachedValues).fill(0).map((_, index) => {
428
+ return `
429
+ ` + " ".repeat(index);
430
+ }),
431
+ "\r": new Array(maxCachedValues).fill(0).map((_, index) => {
432
+ return "\r" + " ".repeat(index);
433
+ }),
434
+ "\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
435
+ return `\r
436
+ ` + " ".repeat(index);
437
+ })
438
+ },
439
+ "\t": {
440
+ "\n": new Array(maxCachedValues).fill(0).map((_, index) => {
441
+ return `
442
+ ` + "\t".repeat(index);
443
+ }),
444
+ "\r": new Array(maxCachedValues).fill(0).map((_, index) => {
445
+ return "\r" + "\t".repeat(index);
446
+ }),
447
+ "\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
448
+ return `\r
449
+ ` + "\t".repeat(index);
450
+ })
451
+ }
452
+ };
453
+
454
+ // node_modules/jsonc-parser/lib/esm/impl/parser.js
455
+ var ParseOptions;
456
+ (function(ParseOptions2) {
457
+ ParseOptions2.DEFAULT = {
458
+ allowTrailingComma: false
459
+ };
460
+ })(ParseOptions || (ParseOptions = {}));
461
+ function parse(text, errors = [], options = ParseOptions.DEFAULT) {
462
+ let currentProperty = null;
463
+ let currentParent = [];
464
+ const previousParents = [];
465
+ function onValue(value) {
466
+ if (Array.isArray(currentParent)) {
467
+ currentParent.push(value);
468
+ } else if (currentProperty !== null) {
469
+ currentParent[currentProperty] = value;
470
+ }
471
+ }
472
+ const visitor = {
473
+ onObjectBegin: () => {
474
+ const object = {};
475
+ onValue(object);
476
+ previousParents.push(currentParent);
477
+ currentParent = object;
478
+ currentProperty = null;
479
+ },
480
+ onObjectProperty: (name) => {
481
+ currentProperty = name;
482
+ },
483
+ onObjectEnd: () => {
484
+ currentParent = previousParents.pop();
485
+ },
486
+ onArrayBegin: () => {
487
+ const array = [];
488
+ onValue(array);
489
+ previousParents.push(currentParent);
490
+ currentParent = array;
491
+ currentProperty = null;
492
+ },
493
+ onArrayEnd: () => {
494
+ currentParent = previousParents.pop();
495
+ },
496
+ onLiteralValue: onValue,
497
+ onError: (error, offset, length) => {
498
+ errors.push({ error, offset, length });
499
+ }
500
+ };
501
+ visit(text, visitor, options);
502
+ return currentParent[0];
503
+ }
504
+ function visit(text, visitor, options = ParseOptions.DEFAULT) {
505
+ const _scanner = createScanner(text, false);
506
+ const _jsonPath = [];
507
+ let suppressedCallbacks = 0;
508
+ function toNoArgVisit(visitFunction) {
509
+ return visitFunction ? () => suppressedCallbacks === 0 && visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
510
+ }
511
+ function toOneArgVisit(visitFunction) {
512
+ return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
513
+ }
514
+ function toOneArgVisitWithPath(visitFunction) {
515
+ return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
516
+ }
517
+ function toBeginVisit(visitFunction) {
518
+ return visitFunction ? () => {
519
+ if (suppressedCallbacks > 0) {
520
+ suppressedCallbacks++;
521
+ } else {
522
+ let cbReturn = visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice());
523
+ if (cbReturn === false) {
524
+ suppressedCallbacks = 1;
525
+ }
526
+ }
527
+ } : () => true;
528
+ }
529
+ function toEndVisit(visitFunction) {
530
+ return visitFunction ? () => {
531
+ if (suppressedCallbacks > 0) {
532
+ suppressedCallbacks--;
533
+ }
534
+ if (suppressedCallbacks === 0) {
535
+ visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter());
536
+ }
537
+ } : () => true;
538
+ }
539
+ const onObjectBegin = toBeginVisit(visitor.onObjectBegin), onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty), onObjectEnd = toEndVisit(visitor.onObjectEnd), onArrayBegin = toBeginVisit(visitor.onArrayBegin), onArrayEnd = toEndVisit(visitor.onArrayEnd), onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue), onSeparator = toOneArgVisit(visitor.onSeparator), onComment = toNoArgVisit(visitor.onComment), onError = toOneArgVisit(visitor.onError);
540
+ const disallowComments = options && options.disallowComments;
541
+ const allowTrailingComma = options && options.allowTrailingComma;
542
+ function scanNext() {
543
+ while (true) {
544
+ const token = _scanner.scan();
545
+ switch (_scanner.getTokenError()) {
546
+ case 4:
547
+ handleError(14);
548
+ break;
549
+ case 5:
550
+ handleError(15);
551
+ break;
552
+ case 3:
553
+ handleError(13);
554
+ break;
555
+ case 1:
556
+ if (!disallowComments) {
557
+ handleError(11);
558
+ }
559
+ break;
560
+ case 2:
561
+ handleError(12);
562
+ break;
563
+ case 6:
564
+ handleError(16);
565
+ break;
566
+ }
567
+ switch (token) {
568
+ case 12:
569
+ case 13:
570
+ if (disallowComments) {
571
+ handleError(10);
572
+ } else {
573
+ onComment();
574
+ }
575
+ break;
576
+ case 16:
577
+ handleError(1);
578
+ break;
579
+ case 15:
580
+ case 14:
581
+ break;
582
+ default:
583
+ return token;
584
+ }
585
+ }
586
+ }
587
+ function handleError(error, skipUntilAfter = [], skipUntil = []) {
588
+ onError(error);
589
+ if (skipUntilAfter.length + skipUntil.length > 0) {
590
+ let token = _scanner.getToken();
591
+ while (token !== 17) {
592
+ if (skipUntilAfter.indexOf(token) !== -1) {
593
+ scanNext();
594
+ break;
595
+ } else if (skipUntil.indexOf(token) !== -1) {
596
+ break;
597
+ }
598
+ token = scanNext();
599
+ }
600
+ }
601
+ }
602
+ function parseString(isValue) {
603
+ const value = _scanner.getTokenValue();
604
+ if (isValue) {
605
+ onLiteralValue(value);
606
+ } else {
607
+ onObjectProperty(value);
608
+ _jsonPath.push(value);
609
+ }
610
+ scanNext();
611
+ return true;
612
+ }
613
+ function parseLiteral() {
614
+ switch (_scanner.getToken()) {
615
+ case 11:
616
+ const tokenValue = _scanner.getTokenValue();
617
+ let value = Number(tokenValue);
618
+ if (isNaN(value)) {
619
+ handleError(2);
620
+ value = 0;
621
+ }
622
+ onLiteralValue(value);
623
+ break;
624
+ case 7:
625
+ onLiteralValue(null);
626
+ break;
627
+ case 8:
628
+ onLiteralValue(true);
629
+ break;
630
+ case 9:
631
+ onLiteralValue(false);
632
+ break;
633
+ default:
634
+ return false;
635
+ }
636
+ scanNext();
637
+ return true;
638
+ }
639
+ function parseProperty() {
640
+ if (_scanner.getToken() !== 10) {
641
+ handleError(3, [], [2, 5]);
642
+ return false;
643
+ }
644
+ parseString(false);
645
+ if (_scanner.getToken() === 6) {
646
+ onSeparator(":");
647
+ scanNext();
648
+ if (!parseValue()) {
649
+ handleError(4, [], [2, 5]);
650
+ }
651
+ } else {
652
+ handleError(5, [], [2, 5]);
653
+ }
654
+ _jsonPath.pop();
655
+ return true;
656
+ }
657
+ function parseObject() {
658
+ onObjectBegin();
659
+ scanNext();
660
+ let needsComma = false;
661
+ while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
662
+ if (_scanner.getToken() === 5) {
663
+ if (!needsComma) {
664
+ handleError(4, [], []);
665
+ }
666
+ onSeparator(",");
667
+ scanNext();
668
+ if (_scanner.getToken() === 2 && allowTrailingComma) {
669
+ break;
670
+ }
671
+ } else if (needsComma) {
672
+ handleError(6, [], []);
673
+ }
674
+ if (!parseProperty()) {
675
+ handleError(4, [], [2, 5]);
676
+ }
677
+ needsComma = true;
678
+ }
679
+ onObjectEnd();
680
+ if (_scanner.getToken() !== 2) {
681
+ handleError(7, [2], []);
682
+ } else {
683
+ scanNext();
684
+ }
685
+ return true;
686
+ }
687
+ function parseArray() {
688
+ onArrayBegin();
689
+ scanNext();
690
+ let isFirstElement = true;
691
+ let needsComma = false;
692
+ while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
693
+ if (_scanner.getToken() === 5) {
694
+ if (!needsComma) {
695
+ handleError(4, [], []);
696
+ }
697
+ onSeparator(",");
698
+ scanNext();
699
+ if (_scanner.getToken() === 4 && allowTrailingComma) {
700
+ break;
701
+ }
702
+ } else if (needsComma) {
703
+ handleError(6, [], []);
704
+ }
705
+ if (isFirstElement) {
706
+ _jsonPath.push(0);
707
+ isFirstElement = false;
708
+ } else {
709
+ _jsonPath[_jsonPath.length - 1]++;
710
+ }
711
+ if (!parseValue()) {
712
+ handleError(4, [], [4, 5]);
713
+ }
714
+ needsComma = true;
715
+ }
716
+ onArrayEnd();
717
+ if (!isFirstElement) {
718
+ _jsonPath.pop();
719
+ }
720
+ if (_scanner.getToken() !== 4) {
721
+ handleError(8, [4], []);
722
+ } else {
723
+ scanNext();
724
+ }
725
+ return true;
726
+ }
727
+ function parseValue() {
728
+ switch (_scanner.getToken()) {
729
+ case 3:
730
+ return parseArray();
731
+ case 1:
732
+ return parseObject();
733
+ case 10:
734
+ return parseString(true);
735
+ default:
736
+ return parseLiteral();
737
+ }
738
+ }
739
+ scanNext();
740
+ if (_scanner.getToken() === 17) {
741
+ if (options.allowEmptyContent) {
742
+ return true;
743
+ }
744
+ handleError(4, [], []);
745
+ return false;
746
+ }
747
+ if (!parseValue()) {
748
+ handleError(4, [], []);
749
+ return false;
750
+ }
751
+ if (_scanner.getToken() !== 17) {
752
+ handleError(9, [], []);
753
+ }
754
+ return true;
755
+ }
756
+
757
+ // node_modules/jsonc-parser/lib/esm/main.js
758
+ var ScanError;
759
+ (function(ScanError2) {
760
+ ScanError2[ScanError2["None"] = 0] = "None";
761
+ ScanError2[ScanError2["UnexpectedEndOfComment"] = 1] = "UnexpectedEndOfComment";
762
+ ScanError2[ScanError2["UnexpectedEndOfString"] = 2] = "UnexpectedEndOfString";
763
+ ScanError2[ScanError2["UnexpectedEndOfNumber"] = 3] = "UnexpectedEndOfNumber";
764
+ ScanError2[ScanError2["InvalidUnicode"] = 4] = "InvalidUnicode";
765
+ ScanError2[ScanError2["InvalidEscapeCharacter"] = 5] = "InvalidEscapeCharacter";
766
+ ScanError2[ScanError2["InvalidCharacter"] = 6] = "InvalidCharacter";
767
+ })(ScanError || (ScanError = {}));
768
+ var SyntaxKind;
769
+ (function(SyntaxKind2) {
770
+ SyntaxKind2[SyntaxKind2["OpenBraceToken"] = 1] = "OpenBraceToken";
771
+ SyntaxKind2[SyntaxKind2["CloseBraceToken"] = 2] = "CloseBraceToken";
772
+ SyntaxKind2[SyntaxKind2["OpenBracketToken"] = 3] = "OpenBracketToken";
773
+ SyntaxKind2[SyntaxKind2["CloseBracketToken"] = 4] = "CloseBracketToken";
774
+ SyntaxKind2[SyntaxKind2["CommaToken"] = 5] = "CommaToken";
775
+ SyntaxKind2[SyntaxKind2["ColonToken"] = 6] = "ColonToken";
776
+ SyntaxKind2[SyntaxKind2["NullKeyword"] = 7] = "NullKeyword";
777
+ SyntaxKind2[SyntaxKind2["TrueKeyword"] = 8] = "TrueKeyword";
778
+ SyntaxKind2[SyntaxKind2["FalseKeyword"] = 9] = "FalseKeyword";
779
+ SyntaxKind2[SyntaxKind2["StringLiteral"] = 10] = "StringLiteral";
780
+ SyntaxKind2[SyntaxKind2["NumericLiteral"] = 11] = "NumericLiteral";
781
+ SyntaxKind2[SyntaxKind2["LineCommentTrivia"] = 12] = "LineCommentTrivia";
782
+ SyntaxKind2[SyntaxKind2["BlockCommentTrivia"] = 13] = "BlockCommentTrivia";
783
+ SyntaxKind2[SyntaxKind2["LineBreakTrivia"] = 14] = "LineBreakTrivia";
784
+ SyntaxKind2[SyntaxKind2["Trivia"] = 15] = "Trivia";
785
+ SyntaxKind2[SyntaxKind2["Unknown"] = 16] = "Unknown";
786
+ SyntaxKind2[SyntaxKind2["EOF"] = 17] = "EOF";
787
+ })(SyntaxKind || (SyntaxKind = {}));
788
+ var parse2 = parse;
789
+ var ParseErrorCode;
790
+ (function(ParseErrorCode2) {
791
+ ParseErrorCode2[ParseErrorCode2["InvalidSymbol"] = 1] = "InvalidSymbol";
792
+ ParseErrorCode2[ParseErrorCode2["InvalidNumberFormat"] = 2] = "InvalidNumberFormat";
793
+ ParseErrorCode2[ParseErrorCode2["PropertyNameExpected"] = 3] = "PropertyNameExpected";
794
+ ParseErrorCode2[ParseErrorCode2["ValueExpected"] = 4] = "ValueExpected";
795
+ ParseErrorCode2[ParseErrorCode2["ColonExpected"] = 5] = "ColonExpected";
796
+ ParseErrorCode2[ParseErrorCode2["CommaExpected"] = 6] = "CommaExpected";
797
+ ParseErrorCode2[ParseErrorCode2["CloseBraceExpected"] = 7] = "CloseBraceExpected";
798
+ ParseErrorCode2[ParseErrorCode2["CloseBracketExpected"] = 8] = "CloseBracketExpected";
799
+ ParseErrorCode2[ParseErrorCode2["EndOfFileExpected"] = 9] = "EndOfFileExpected";
800
+ ParseErrorCode2[ParseErrorCode2["InvalidCommentToken"] = 10] = "InvalidCommentToken";
801
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfComment"] = 11] = "UnexpectedEndOfComment";
802
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfString"] = 12] = "UnexpectedEndOfString";
803
+ ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfNumber"] = 13] = "UnexpectedEndOfNumber";
804
+ ParseErrorCode2[ParseErrorCode2["InvalidUnicode"] = 14] = "InvalidUnicode";
805
+ ParseErrorCode2[ParseErrorCode2["InvalidEscapeCharacter"] = 15] = "InvalidEscapeCharacter";
806
+ ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
807
+ })(ParseErrorCode || (ParseErrorCode = {}));
808
+
809
+ // src/index.ts
810
+ var LOG_FILE = "/tmp/opencode-dotenv.log";
811
+ var LOAD_GUARD = "__opencodeDotenvLoaded";
812
+ function parseDotenv(content) {
813
+ const result = {};
814
+ for (const line of content.split(`
815
+ `)) {
816
+ const trimmed = line.trim();
817
+ if (!trimmed || trimmed.startsWith("#"))
818
+ 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;
828
+ }
829
+ }
830
+ return result;
831
+ }
832
+ function expandPath(path) {
833
+ return path.replace(/^~/, homedir());
834
+ }
835
+ var loggingEnabled = true;
836
+ function logToFile(message) {
837
+ if (!loggingEnabled)
838
+ return;
839
+ try {
840
+ const timestamp = new Date().toISOString();
841
+ Bun.appendFileSync(LOG_FILE, `[${timestamp}] ${message}
842
+ `);
843
+ } catch (e) {}
844
+ }
845
+ 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;
855
+ const content = await file.text();
856
+ const config = parse2(content, [], { allowTrailingComma: true });
857
+ loggingEnabled = config.logging?.enabled !== false;
858
+ return config;
859
+ } catch (e) {
860
+ logToFile(`Failed to load config: ${e}`);
861
+ }
862
+ }
863
+ return { files: [], load_cwd_env: true };
864
+ }
865
+ async function loadDotenvFile(filePath) {
866
+ try {
867
+ const file = Bun.file(filePath);
868
+ if (!await file.exists()) {
869
+ logToFile(`File not found: ${filePath}`);
870
+ return { count: 0, success: false };
871
+ }
872
+ const content = await file.text();
873
+ const envVars = parseDotenv(content);
874
+ for (const [key, value] of Object.entries(envVars)) {
875
+ process.env[key] = value;
876
+ }
877
+ return { count: Object.keys(envVars).length, success: true };
878
+ } catch (error) {
879
+ logToFile(`Failed to load ${filePath}: ${error}`);
880
+ return { count: 0, success: false };
881
+ }
882
+ }
883
+ var DotEnvPlugin = async (ctx) => {
884
+ if (globalThis[LOAD_GUARD]) {
885
+ return {};
886
+ }
887
+ globalThis[LOAD_GUARD] = true;
888
+ logToFile("Plugin started");
889
+ const config = await loadConfig();
890
+ logToFile(`Config loaded: ${config.files.length} files, load_cwd_env=${config.load_cwd_env}, logging=${loggingEnabled}`);
891
+ let totalFiles = 0;
892
+ let totalVars = 0;
893
+ for (const rawPath of config.files) {
894
+ const filePath = expandPath(rawPath);
895
+ logToFile(`Loading: ${filePath}`);
896
+ const result = await loadDotenvFile(filePath);
897
+ if (result.success) {
898
+ totalFiles++;
899
+ totalVars += result.count;
900
+ logToFile(`Loaded ${result.count} vars`);
901
+ }
902
+ }
903
+ if (config.load_cwd_env !== false) {
904
+ const cwdEnvPath = `${process.cwd()}/.env`;
905
+ logToFile(`Loading cwd: ${cwdEnvPath}`);
906
+ const result = await loadDotenvFile(cwdEnvPath);
907
+ if (result.success) {
908
+ totalFiles++;
909
+ totalVars += result.count;
910
+ logToFile(`Loaded ${result.count} vars from cwd`);
911
+ }
912
+ }
913
+ logToFile(`Plugin finished: ${totalFiles} files, ${totalVars} vars`);
914
+ return {};
915
+ };
916
+ var src_default = DotEnvPlugin;
917
+ export {
918
+ src_default as default,
919
+ DotEnvPlugin
920
+ };
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "opencode-dotenv",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin to load .env files at startup",
5
+ "main": "./dist/index.js",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "scripts": {
9
+ "build": "bun build src/index.ts --outdir dist --target bun",
10
+ "prepublishOnly": "bun run build"
11
+ },
12
+ "files": [
13
+ "dist/",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "peerDependencies": {
18
+ "@opencode-ai/plugin": "*"
19
+ },
20
+ "dependencies": {
21
+ "jsonc-parser": "^3.3.1"
22
+ },
23
+ "devDependencies": {}
24
+ }