opencode-morphllm 0.0.6 → 0.0.7

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.
@@ -0,0 +1,404 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+ import {
5
+ getOpenCodeConfigDir,
6
+ getOpenCodeConfigPaths,
7
+ isDevBuild,
8
+ detectExistingConfigDir,
9
+ TAURI_APP_IDENTIFIER,
10
+ TAURI_APP_IDENTIFIER_DEV,
11
+ } from './opencode-config-dir';
12
+
13
+ describe('opencode-config-dir', () => {
14
+ let originalPlatform: NodeJS.Platform;
15
+ let originalEnv: Record<string, string | undefined>;
16
+
17
+ beforeEach(() => {
18
+ originalPlatform = process.platform;
19
+ originalEnv = {
20
+ APPDATA: process.env.APPDATA,
21
+ XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
22
+ XDG_DATA_HOME: process.env.XDG_DATA_HOME,
23
+ OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
24
+ };
25
+ });
26
+
27
+ afterEach(() => {
28
+ Object.defineProperty(process, 'platform', { value: originalPlatform });
29
+ for (const [key, value] of Object.entries(originalEnv)) {
30
+ if (value !== undefined) {
31
+ process.env[key] = value;
32
+ } else {
33
+ delete process.env[key];
34
+ }
35
+ }
36
+ });
37
+
38
+ describe('OPENCODE_CONFIG_DIR environment variable', () => {
39
+ test('returns OPENCODE_CONFIG_DIR when env var is set', () => {
40
+ // #given OPENCODE_CONFIG_DIR is set to a custom path
41
+ process.env.OPENCODE_CONFIG_DIR = '/custom/opencode/path';
42
+ Object.defineProperty(process, 'platform', { value: 'linux' });
43
+
44
+ // #when getOpenCodeConfigDir is called with binary="opencode"
45
+ const result = getOpenCodeConfigDir({
46
+ binary: 'opencode',
47
+ version: '1.0.200',
48
+ });
49
+
50
+ // #then returns the custom path
51
+ expect(result).toBe('/custom/opencode/path');
52
+ });
53
+
54
+ test('falls back to default when env var is not set', () => {
55
+ // #given OPENCODE_CONFIG_DIR is not set, platform is Linux
56
+ delete process.env.OPENCODE_CONFIG_DIR;
57
+ delete process.env.XDG_CONFIG_HOME;
58
+ Object.defineProperty(process, 'platform', { value: 'linux' });
59
+
60
+ // #when getOpenCodeConfigDir is called with binary="opencode"
61
+ const result = getOpenCodeConfigDir({
62
+ binary: 'opencode',
63
+ version: '1.0.200',
64
+ });
65
+
66
+ // #then returns default ~/.config/opencode
67
+ expect(result).toBe(join(homedir(), '.config', 'opencode'));
68
+ });
69
+
70
+ test('falls back to default when env var is empty string', () => {
71
+ // #given OPENCODE_CONFIG_DIR is set to empty string
72
+ process.env.OPENCODE_CONFIG_DIR = '';
73
+ delete process.env.XDG_CONFIG_HOME;
74
+ Object.defineProperty(process, 'platform', { value: 'linux' });
75
+
76
+ // #when getOpenCodeConfigDir is called with binary="opencode"
77
+ const result = getOpenCodeConfigDir({
78
+ binary: 'opencode',
79
+ version: '1.0.200',
80
+ });
81
+
82
+ // #then returns default ~/.config/opencode
83
+ expect(result).toBe(join(homedir(), '.config', 'opencode'));
84
+ });
85
+
86
+ test('falls back to default when env var is whitespace only', () => {
87
+ // #given OPENCODE_CONFIG_DIR is set to whitespace only
88
+ process.env.OPENCODE_CONFIG_DIR = ' ';
89
+ delete process.env.XDG_CONFIG_HOME;
90
+ Object.defineProperty(process, 'platform', { value: 'linux' });
91
+
92
+ // #when getOpenCodeConfigDir is called with binary="opencode"
93
+ const result = getOpenCodeConfigDir({
94
+ binary: 'opencode',
95
+ version: '1.0.200',
96
+ });
97
+
98
+ // #then returns default ~/.config/opencode
99
+ expect(result).toBe(join(homedir(), '.config', 'opencode'));
100
+ });
101
+
102
+ test('resolves relative path to absolute path', () => {
103
+ // #given OPENCODE_CONFIG_DIR is set to a relative path
104
+ process.env.OPENCODE_CONFIG_DIR = './my-opencode-config';
105
+ Object.defineProperty(process, 'platform', { value: 'linux' });
106
+
107
+ // #when getOpenCodeConfigDir is called with binary="opencode"
108
+ const result = getOpenCodeConfigDir({
109
+ binary: 'opencode',
110
+ version: '1.0.200',
111
+ });
112
+
113
+ // #then returns resolved absolute path
114
+ expect(result).toBe(resolve('./my-opencode-config'));
115
+ });
116
+
117
+ test('OPENCODE_CONFIG_DIR takes priority over XDG_CONFIG_HOME', () => {
118
+ // #given both OPENCODE_CONFIG_DIR and XDG_CONFIG_HOME are set
119
+ process.env.OPENCODE_CONFIG_DIR = '/custom/opencode/path';
120
+ process.env.XDG_CONFIG_HOME = '/xdg/config';
121
+ Object.defineProperty(process, 'platform', { value: 'linux' });
122
+
123
+ // #when getOpenCodeConfigDir is called with binary="opencode"
124
+ const result = getOpenCodeConfigDir({
125
+ binary: 'opencode',
126
+ version: '1.0.200',
127
+ });
128
+
129
+ // #then OPENCODE_CONFIG_DIR takes priority
130
+ expect(result).toBe('/custom/opencode/path');
131
+ });
132
+ });
133
+
134
+ describe('isDevBuild', () => {
135
+ test('returns false for null version', () => {
136
+ expect(isDevBuild(null)).toBe(false);
137
+ });
138
+
139
+ test('returns false for undefined version', () => {
140
+ expect(isDevBuild(undefined)).toBe(false);
141
+ });
142
+
143
+ test('returns false for production version', () => {
144
+ expect(isDevBuild('1.0.200')).toBe(false);
145
+ expect(isDevBuild('2.1.0')).toBe(false);
146
+ });
147
+
148
+ test('returns true for version containing -dev', () => {
149
+ expect(isDevBuild('1.0.0-dev')).toBe(true);
150
+ expect(isDevBuild('1.0.0-dev.123')).toBe(true);
151
+ });
152
+
153
+ test('returns true for version containing .dev', () => {
154
+ expect(isDevBuild('1.0.0.dev')).toBe(true);
155
+ expect(isDevBuild('1.0.0.dev.456')).toBe(true);
156
+ });
157
+ });
158
+
159
+ describe('getOpenCodeConfigDir', () => {
160
+ describe('for opencode CLI binary', () => {
161
+ test('returns ~/.config/opencode on Linux', () => {
162
+ // #given opencode CLI binary detected, platform is Linux
163
+ Object.defineProperty(process, 'platform', { value: 'linux' });
164
+ delete process.env.XDG_CONFIG_HOME;
165
+ delete process.env.OPENCODE_CONFIG_DIR;
166
+
167
+ // #when getOpenCodeConfigDir is called with binary="opencode"
168
+ const result = getOpenCodeConfigDir({
169
+ binary: 'opencode',
170
+ version: '1.0.200',
171
+ });
172
+
173
+ // #then returns ~/.config/opencode
174
+ expect(result).toBe(join(homedir(), '.config', 'opencode'));
175
+ });
176
+
177
+ test('returns $XDG_CONFIG_HOME/opencode on Linux when XDG_CONFIG_HOME is set', () => {
178
+ // #given opencode CLI binary detected, platform is Linux with XDG_CONFIG_HOME set
179
+ Object.defineProperty(process, 'platform', { value: 'linux' });
180
+ process.env.XDG_CONFIG_HOME = '/custom/config';
181
+ delete process.env.OPENCODE_CONFIG_DIR;
182
+
183
+ // #when getOpenCodeConfigDir is called with binary="opencode"
184
+ const result = getOpenCodeConfigDir({
185
+ binary: 'opencode',
186
+ version: '1.0.200',
187
+ });
188
+
189
+ // #then returns $XDG_CONFIG_HOME/opencode
190
+ expect(result).toBe('/custom/config/opencode');
191
+ });
192
+
193
+ test('returns ~/.config/opencode on macOS', () => {
194
+ // #given opencode CLI binary detected, platform is macOS
195
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
196
+ delete process.env.XDG_CONFIG_HOME;
197
+ delete process.env.OPENCODE_CONFIG_DIR;
198
+
199
+ // #when getOpenCodeConfigDir is called with binary="opencode"
200
+ const result = getOpenCodeConfigDir({
201
+ binary: 'opencode',
202
+ version: '1.0.200',
203
+ });
204
+
205
+ // #then returns ~/.config/opencode
206
+ expect(result).toBe(join(homedir(), '.config', 'opencode'));
207
+ });
208
+
209
+ test('returns ~/.config/opencode on Windows by default', () => {
210
+ // #given opencode CLI binary detected, platform is Windows
211
+ Object.defineProperty(process, 'platform', { value: 'win32' });
212
+ delete process.env.APPDATA;
213
+ delete process.env.OPENCODE_CONFIG_DIR;
214
+
215
+ // #when getOpenCodeConfigDir is called with binary="opencode"
216
+ const result = getOpenCodeConfigDir({
217
+ binary: 'opencode',
218
+ version: '1.0.200',
219
+ checkExisting: false,
220
+ });
221
+
222
+ // #then returns ~/.config/opencode (cross-platform default)
223
+ expect(result).toBe(join(homedir(), '.config', 'opencode'));
224
+ });
225
+ });
226
+
227
+ describe('for opencode-desktop Tauri binary', () => {
228
+ test('returns ~/.config/ai.opencode.desktop on Linux', () => {
229
+ // #given opencode-desktop binary detected, platform is Linux
230
+ Object.defineProperty(process, 'platform', { value: 'linux' });
231
+ delete process.env.XDG_CONFIG_HOME;
232
+
233
+ // #when getOpenCodeConfigDir is called with binary="opencode-desktop"
234
+ const result = getOpenCodeConfigDir({
235
+ binary: 'opencode-desktop',
236
+ version: '1.0.200',
237
+ checkExisting: false,
238
+ });
239
+
240
+ // #then returns ~/.config/ai.opencode.desktop
241
+ expect(result).toBe(join(homedir(), '.config', TAURI_APP_IDENTIFIER));
242
+ });
243
+
244
+ test('returns ~/Library/Application Support/ai.opencode.desktop on macOS', () => {
245
+ // #given opencode-desktop binary detected, platform is macOS
246
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
247
+
248
+ // #when getOpenCodeConfigDir is called with binary="opencode-desktop"
249
+ const result = getOpenCodeConfigDir({
250
+ binary: 'opencode-desktop',
251
+ version: '1.0.200',
252
+ checkExisting: false,
253
+ });
254
+
255
+ // #then returns ~/Library/Application Support/ai.opencode.desktop
256
+ expect(result).toBe(
257
+ join(
258
+ homedir(),
259
+ 'Library',
260
+ 'Application Support',
261
+ TAURI_APP_IDENTIFIER
262
+ )
263
+ );
264
+ });
265
+
266
+ test('returns %APPDATA%/ai.opencode.desktop on Windows', () => {
267
+ // #given opencode-desktop binary detected, platform is Windows
268
+ Object.defineProperty(process, 'platform', { value: 'win32' });
269
+ process.env.APPDATA = 'C:\\Users\\TestUser\\AppData\\Roaming';
270
+
271
+ // #when getOpenCodeConfigDir is called with binary="opencode-desktop"
272
+ const result = getOpenCodeConfigDir({
273
+ binary: 'opencode-desktop',
274
+ version: '1.0.200',
275
+ checkExisting: false,
276
+ });
277
+
278
+ // #then returns %APPDATA%/ai.opencode.desktop
279
+ expect(result).toBe(
280
+ join('C:\\Users\\TestUser\\AppData\\Roaming', TAURI_APP_IDENTIFIER)
281
+ );
282
+ });
283
+ });
284
+
285
+ describe('dev build detection', () => {
286
+ test('returns ai.opencode.desktop.dev path when dev version detected', () => {
287
+ // #given opencode-desktop dev version
288
+ Object.defineProperty(process, 'platform', { value: 'linux' });
289
+ delete process.env.XDG_CONFIG_HOME;
290
+
291
+ // #when getOpenCodeConfigDir is called with dev version
292
+ const result = getOpenCodeConfigDir({
293
+ binary: 'opencode-desktop',
294
+ version: '1.0.0-dev.123',
295
+ checkExisting: false,
296
+ });
297
+
298
+ // #then returns path with ai.opencode.desktop.dev
299
+ expect(result).toBe(
300
+ join(homedir(), '.config', TAURI_APP_IDENTIFIER_DEV)
301
+ );
302
+ });
303
+
304
+ test('returns ai.opencode.desktop.dev on macOS for dev build', () => {
305
+ // #given opencode-desktop dev version on macOS
306
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
307
+
308
+ // #when getOpenCodeConfigDir is called with dev version
309
+ const result = getOpenCodeConfigDir({
310
+ binary: 'opencode-desktop',
311
+ version: '1.0.0-dev',
312
+ checkExisting: false,
313
+ });
314
+
315
+ // #then returns path with ai.opencode.desktop.dev
316
+ expect(result).toBe(
317
+ join(
318
+ homedir(),
319
+ 'Library',
320
+ 'Application Support',
321
+ TAURI_APP_IDENTIFIER_DEV
322
+ )
323
+ );
324
+ });
325
+ });
326
+ });
327
+
328
+ describe('getOpenCodeConfigPaths', () => {
329
+ test('returns all config paths for CLI binary', () => {
330
+ // #given opencode CLI binary on Linux
331
+ Object.defineProperty(process, 'platform', { value: 'linux' });
332
+ delete process.env.XDG_CONFIG_HOME;
333
+ delete process.env.OPENCODE_CONFIG_DIR;
334
+
335
+ // #when getOpenCodeConfigPaths is called
336
+ const paths = getOpenCodeConfigPaths({
337
+ binary: 'opencode',
338
+ version: '1.0.200',
339
+ });
340
+
341
+ // #then returns all expected paths
342
+ const expectedDir = join(homedir(), '.config', 'opencode');
343
+ expect(paths.configDir).toBe(expectedDir);
344
+ expect(paths.configJson).toBe(join(expectedDir, 'opencode.json'));
345
+ expect(paths.configJsonc).toBe(join(expectedDir, 'opencode.jsonc'));
346
+ expect(paths.packageJson).toBe(join(expectedDir, 'package.json'));
347
+ expect(paths.omoConfig).toBe(join(expectedDir, 'oh-my-opencode.json'));
348
+ });
349
+
350
+ test('returns all config paths for desktop binary', () => {
351
+ // #given opencode-desktop binary on macOS
352
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
353
+
354
+ // #when getOpenCodeConfigPaths is called
355
+ const paths = getOpenCodeConfigPaths({
356
+ binary: 'opencode-desktop',
357
+ version: '1.0.200',
358
+ checkExisting: false,
359
+ });
360
+
361
+ // #then returns all expected paths
362
+ const expectedDir = join(
363
+ homedir(),
364
+ 'Library',
365
+ 'Application Support',
366
+ TAURI_APP_IDENTIFIER
367
+ );
368
+ expect(paths.configDir).toBe(expectedDir);
369
+ expect(paths.configJson).toBe(join(expectedDir, 'opencode.json'));
370
+ expect(paths.configJsonc).toBe(join(expectedDir, 'opencode.jsonc'));
371
+ expect(paths.packageJson).toBe(join(expectedDir, 'package.json'));
372
+ expect(paths.omoConfig).toBe(join(expectedDir, 'oh-my-opencode.json'));
373
+ });
374
+ });
375
+
376
+ describe('detectExistingConfigDir', () => {
377
+ test('returns null when no config exists', () => {
378
+ // #given no config files exist
379
+ Object.defineProperty(process, 'platform', { value: 'linux' });
380
+ delete process.env.XDG_CONFIG_HOME;
381
+ delete process.env.OPENCODE_CONFIG_DIR;
382
+
383
+ // #when detectExistingConfigDir is called
384
+ const result = detectExistingConfigDir('opencode', '1.0.200');
385
+
386
+ // #then result is either null or a valid string path
387
+ expect(result === null || typeof result === 'string').toBe(true);
388
+ });
389
+
390
+ test('includes OPENCODE_CONFIG_DIR in search locations when set', () => {
391
+ // #given OPENCODE_CONFIG_DIR is set to a custom path
392
+ process.env.OPENCODE_CONFIG_DIR = '/custom/opencode/path';
393
+ Object.defineProperty(process, 'platform', { value: 'linux' });
394
+ delete process.env.XDG_CONFIG_HOME;
395
+
396
+ // #when detectExistingConfigDir is called
397
+ const result = detectExistingConfigDir('opencode', '1.0.200');
398
+
399
+ // #then result is either null (no config file exists) or a valid string path
400
+ // The important thing is that the function doesn't throw
401
+ expect(result === null || typeof result === 'string').toBe(true);
402
+ });
403
+ });
404
+ });
@@ -2,12 +2,30 @@ import { existsSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, resolve } from 'node:path';
4
4
 
5
- interface OpenCodeConfigDirOptions {
6
- binary: 'opencode' | 'opencode-desktop';
5
+ export type OpenCodeBinaryType = 'opencode' | 'opencode-desktop';
6
+
7
+ export interface OpenCodeConfigDirOptions {
8
+ binary: OpenCodeBinaryType;
7
9
  version?: string | null;
8
10
  checkExisting?: boolean;
9
11
  }
10
12
 
13
+ export interface OpenCodeConfigPaths {
14
+ configDir: string;
15
+ configJson: string;
16
+ configJsonc: string;
17
+ packageJson: string;
18
+ omoConfig: string;
19
+ }
20
+
21
+ export const TAURI_APP_IDENTIFIER = 'ai.opencode.desktop';
22
+ export const TAURI_APP_IDENTIFIER_DEV = 'ai.opencode.desktop.dev';
23
+
24
+ export function isDevBuild(version: string | null | undefined): boolean {
25
+ if (!version) return false;
26
+ return version.includes('-dev') || version.includes('.dev');
27
+ }
28
+
11
29
  function getTauriConfigDir(identifier: string): string {
12
30
  const platform = process.platform;
13
31
 
@@ -63,15 +81,76 @@ function getCliConfigDir(): string {
63
81
  export function getOpenCodeConfigDir(
64
82
  options: OpenCodeConfigDirOptions
65
83
  ): string {
66
- if (options.binary === 'opencode-desktop') {
67
- const version = options.version;
68
- const isDev =
69
- !!version && (version.includes('-dev') || version.includes('.dev'));
70
- const identifier = isDev
71
- ? 'ai.opencode.desktop.dev'
72
- : 'ai.opencode.desktop';
73
- return getTauriConfigDir(identifier);
84
+ const { binary, version, checkExisting = true } = options;
85
+
86
+ if (binary === 'opencode') {
87
+ return getCliConfigDir();
88
+ }
89
+
90
+ const identifier = isDevBuild(version)
91
+ ? TAURI_APP_IDENTIFIER_DEV
92
+ : TAURI_APP_IDENTIFIER;
93
+ const tauriDir = getTauriConfigDir(identifier);
94
+
95
+ if (checkExisting) {
96
+ const legacyDir = getCliConfigDir();
97
+ const legacyConfig = join(legacyDir, 'opencode.json');
98
+ const legacyConfigC = join(legacyDir, 'opencode.jsonc');
99
+
100
+ if (existsSync(legacyConfig) || existsSync(legacyConfigC)) {
101
+ return legacyDir;
102
+ }
103
+ }
104
+
105
+ return tauriDir;
106
+ }
107
+
108
+ export function getOpenCodeConfigPaths(
109
+ options: OpenCodeConfigDirOptions
110
+ ): OpenCodeConfigPaths {
111
+ const configDir = getOpenCodeConfigDir(options);
112
+
113
+ return {
114
+ configDir,
115
+ configJson: join(configDir, 'opencode.json'),
116
+ configJsonc: join(configDir, 'opencode.jsonc'),
117
+ packageJson: join(configDir, 'package.json'),
118
+ omoConfig: join(configDir, 'oh-my-opencode.json'),
119
+ };
120
+ }
121
+
122
+ export function detectExistingConfigDir(
123
+ binary: OpenCodeBinaryType,
124
+ version?: string | null
125
+ ): string | null {
126
+ const locations: string[] = [];
127
+
128
+ const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
129
+ if (envConfigDir) {
130
+ locations.push(resolve(envConfigDir));
131
+ }
132
+
133
+ if (binary === 'opencode-desktop') {
134
+ const identifier = isDevBuild(version)
135
+ ? TAURI_APP_IDENTIFIER_DEV
136
+ : TAURI_APP_IDENTIFIER;
137
+ locations.push(getTauriConfigDir(identifier));
138
+
139
+ if (isDevBuild(version)) {
140
+ locations.push(getTauriConfigDir(TAURI_APP_IDENTIFIER));
141
+ }
142
+ }
143
+
144
+ locations.push(getCliConfigDir());
145
+
146
+ for (const dir of locations) {
147
+ const configJson = join(dir, 'opencode.json');
148
+ const configJsonc = join(dir, 'opencode.jsonc');
149
+
150
+ if (existsSync(configJson) || existsSync(configJsonc)) {
151
+ return dir;
152
+ }
74
153
  }
75
154
 
76
- return getCliConfigDir();
155
+ return null;
77
156
  }