@openharmonyinsight/opencode-oh 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.
- package/AGENTS.md +98 -0
- package/README.md +80 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +7 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config/index.d.ts +13 -0
- package/dist/src/config/index.d.ts.map +1 -0
- package/dist/src/config/index.js +95 -0
- package/dist/src/config/index.js.map +1 -0
- package/dist/src/config/types.d.ts +29 -0
- package/dist/src/config/types.d.ts.map +1 -0
- package/dist/src/config/types.js +2 -0
- package/dist/src/config/types.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/provider/templates.d.ts +5 -0
- package/dist/src/provider/templates.d.ts.map +1 -0
- package/dist/src/provider/templates.js +74 -0
- package/dist/src/provider/templates.js.map +1 -0
- package/dist/src/tui/index.d.ts +12 -0
- package/dist/src/tui/index.d.ts.map +1 -0
- package/dist/src/tui/index.js +136 -0
- package/dist/src/tui/index.js.map +1 -0
- package/dist/src/validation/index.d.ts +6 -0
- package/dist/src/validation/index.d.ts.map +1 -0
- package/dist/src/validation/index.js +24 -0
- package/dist/src/validation/index.js.map +1 -0
- package/dist/tests/config.test.d.ts +2 -0
- package/dist/tests/config.test.d.ts.map +1 -0
- package/dist/tests/config.test.js +104 -0
- package/dist/tests/config.test.js.map +1 -0
- package/dist/tests/validation.test.d.ts +2 -0
- package/dist/tests/validation.test.d.ts.map +1 -0
- package/dist/tests/validation.test.js +37 -0
- package/dist/tests/validation.test.js.map +1 -0
- package/docs/plans/2026-02-12-opencode-oh-design.md +90 -0
- package/package.json +38 -0
- package/src/cli.ts +8 -0
- package/src/config/index.ts +106 -0
- package/src/config/types.ts +29 -0
- package/src/index.ts +26 -0
- package/src/provider/templates.ts +76 -0
- package/src/tui/index.ts +159 -0
- package/src/validation/index.ts +30 -0
- package/tests/config.test.ts +117 -0
- package/tests/validation.test.ts +44 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/validation/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsBlH"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getProviderTemplate } from '../provider/templates.js';
|
|
2
|
+
export async function validateApiKey(providerId, apiKey, model) {
|
|
3
|
+
const template = getProviderTemplate(providerId);
|
|
4
|
+
if (!template) {
|
|
5
|
+
return { valid: false, error: `Unknown provider: ${providerId}` };
|
|
6
|
+
}
|
|
7
|
+
if (!template.supportsApiKeyValidation || !template.validateApiKey) {
|
|
8
|
+
return { valid: true };
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const valid = await template.validateApiKey(apiKey, model || '');
|
|
12
|
+
if (!valid) {
|
|
13
|
+
return { valid: false, error: 'API key validation failed' };
|
|
14
|
+
}
|
|
15
|
+
return { valid: true };
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
return {
|
|
19
|
+
valid: false,
|
|
20
|
+
error: `Validation error: ${error instanceof Error ? error.message : String(error)}`
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/validation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAO/D,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,UAAkB,EAAE,MAAc,EAAE,KAAc;IACrF,MAAM,QAAQ,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;IACjD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,qBAAqB,UAAU,EAAE,EAAE,CAAC;IACpE,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,wBAAwB,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,CAAC;QACnE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACzB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,cAAc,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QACjE,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC;QAC9D,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACzB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,KAAK,EAAE,qBAAqB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;SACrF,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../../tests/config.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { ConfigManager } from '../src/config/index.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
describe('ConfigManager', () => {
|
|
7
|
+
let configManager;
|
|
8
|
+
let testConfigPath;
|
|
9
|
+
let testConfigDir;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
testConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-oh-test-'));
|
|
12
|
+
testConfigPath = path.join(testConfigDir, 'opencode.json');
|
|
13
|
+
configManager = new ConfigManager();
|
|
14
|
+
configManager.configPath = testConfigPath;
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (fs.existsSync(testConfigDir)) {
|
|
18
|
+
fs.rmSync(testConfigDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
it('should return null when config does not exist', () => {
|
|
22
|
+
const config = configManager.readConfig();
|
|
23
|
+
expect(config).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
it('should read existing config', () => {
|
|
26
|
+
const testConfig = {
|
|
27
|
+
$schema: 'https://opencode.ai/config.json',
|
|
28
|
+
provider: {}
|
|
29
|
+
};
|
|
30
|
+
fs.writeFileSync(testConfigPath, JSON.stringify(testConfig));
|
|
31
|
+
const config = configManager.readConfig();
|
|
32
|
+
expect(config).toEqual(testConfig);
|
|
33
|
+
});
|
|
34
|
+
it('should write config', () => {
|
|
35
|
+
const testConfig = {
|
|
36
|
+
$schema: 'https://opencode.ai/config.json',
|
|
37
|
+
provider: {}
|
|
38
|
+
};
|
|
39
|
+
configManager.writeConfig(testConfig);
|
|
40
|
+
expect(fs.existsSync(testConfigPath)).toBe(true);
|
|
41
|
+
const read = JSON.parse(fs.readFileSync(testConfigPath, 'utf-8'));
|
|
42
|
+
expect(read).toEqual(testConfig);
|
|
43
|
+
});
|
|
44
|
+
it('should backup existing config', () => {
|
|
45
|
+
const testConfig = {
|
|
46
|
+
$schema: 'https://opencode.ai/config.json',
|
|
47
|
+
provider: {}
|
|
48
|
+
};
|
|
49
|
+
fs.writeFileSync(testConfigPath, JSON.stringify(testConfig));
|
|
50
|
+
const backupPath = configManager.backupConfig();
|
|
51
|
+
expect(backupPath).toBeTruthy();
|
|
52
|
+
expect(fs.existsSync(backupPath)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
it('should return null when backing up non-existent config', () => {
|
|
55
|
+
const backupPath = configManager.backupConfig();
|
|
56
|
+
expect(backupPath).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
it('should add provider to config', () => {
|
|
59
|
+
const input = {
|
|
60
|
+
providerId: 'volcengine',
|
|
61
|
+
apiKey: 'test-key',
|
|
62
|
+
modelName: 'test-model'
|
|
63
|
+
};
|
|
64
|
+
configManager.addProvider(input);
|
|
65
|
+
const config = configManager.readConfig();
|
|
66
|
+
expect(config).not.toBeNull();
|
|
67
|
+
expect(config?.provider.volcengine).toBeDefined();
|
|
68
|
+
expect(config?.provider.volcengine.options.apiKey).toBe('test-key');
|
|
69
|
+
});
|
|
70
|
+
it('should add provider to config when provider property is missing', () => {
|
|
71
|
+
const testConfig = {
|
|
72
|
+
$schema: 'https://opencode.ai/config.json'
|
|
73
|
+
};
|
|
74
|
+
fs.writeFileSync(testConfigPath, JSON.stringify(testConfig));
|
|
75
|
+
const input = {
|
|
76
|
+
providerId: 'volcengine',
|
|
77
|
+
apiKey: 'test-key',
|
|
78
|
+
modelName: 'test-model'
|
|
79
|
+
};
|
|
80
|
+
configManager.addProvider(input);
|
|
81
|
+
const config = configManager.readConfig();
|
|
82
|
+
expect(config).not.toBeNull();
|
|
83
|
+
expect(config?.provider.volcengine).toBeDefined();
|
|
84
|
+
expect(config?.provider.volcengine.options.apiKey).toBe('test-key');
|
|
85
|
+
});
|
|
86
|
+
it('should add provider to config when provider is null', () => {
|
|
87
|
+
const testConfig = {
|
|
88
|
+
$schema: 'https://opencode.ai/config.json',
|
|
89
|
+
provider: null
|
|
90
|
+
};
|
|
91
|
+
fs.writeFileSync(testConfigPath, JSON.stringify(testConfig));
|
|
92
|
+
const input = {
|
|
93
|
+
providerId: 'volcengine',
|
|
94
|
+
apiKey: 'test-key',
|
|
95
|
+
modelName: 'test-model'
|
|
96
|
+
};
|
|
97
|
+
configManager.addProvider(input);
|
|
98
|
+
const config = configManager.readConfig();
|
|
99
|
+
expect(config).not.toBeNull();
|
|
100
|
+
expect(config?.provider.volcengine).toBeDefined();
|
|
101
|
+
expect(config?.provider.volcengine.options.apiKey).toBe('test-key');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
//# sourceMappingURL=config.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../tests/config.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.test.d.ts","sourceRoot":"","sources":["../../tests/validation.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { validateApiKey } from '../src/validation/index.js';
|
|
3
|
+
describe('validateApiKey', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
});
|
|
7
|
+
it('should validate volcengine API key successfully', async () => {
|
|
8
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
9
|
+
ok: true
|
|
10
|
+
});
|
|
11
|
+
global.fetch = mockFetch;
|
|
12
|
+
const result = await validateApiKey('volcengine', 'valid-key');
|
|
13
|
+
expect(result.valid).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it('should return error for invalid volcengine API key', async () => {
|
|
16
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
17
|
+
ok: false
|
|
18
|
+
});
|
|
19
|
+
global.fetch = mockFetch;
|
|
20
|
+
const result = await validateApiKey('volcengine', 'invalid-key');
|
|
21
|
+
expect(result.valid).toBe(false);
|
|
22
|
+
expect(result.error).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
it('should handle network errors', async () => {
|
|
25
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
26
|
+
global.fetch = mockFetch;
|
|
27
|
+
const result = await validateApiKey('volcengine', 'test-key');
|
|
28
|
+
expect(result.valid).toBe(false);
|
|
29
|
+
expect(result.error).toBe('API key validation failed');
|
|
30
|
+
});
|
|
31
|
+
it('should return error for unknown provider', async () => {
|
|
32
|
+
const result = await validateApiKey('unknown', 'test-key');
|
|
33
|
+
expect(result.valid).toBe(false);
|
|
34
|
+
expect(result.error).toContain('Unknown provider');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
//# sourceMappingURL=validation.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.test.js","sourceRoot":"","sources":["../../tests/validation.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAE5D,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,UAAU,CAAC,GAAG,EAAE;QACd,EAAE,CAAC,eAAe,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC;YAC1C,EAAE,EAAE,IAAI;SACT,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,GAAG,SAAS,CAAC;QAEzB,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;QAC/D,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC;YAC1C,EAAE,EAAE,KAAK;SACV,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,GAAG,SAAS,CAAC;QAEzB,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;QACjE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,KAAK,IAAI,EAAE;QAC5C,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;QACxE,MAAM,CAAC,KAAK,GAAG,SAAS,CAAC;QAEzB,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QAC9D,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QAC3D,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# opencode-oh Design Document
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-02-12
|
|
4
|
+
**Topic:** opencode-oh - OpenHarmony OpenCode Helper Tool
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
opencode-oh is a CLI helper tool for configuring opencode with third-party model providers. It features an interactive TUI interface and supports API key validation, configuration backup, and multi-language support.
|
|
9
|
+
|
|
10
|
+
## Architecture
|
|
11
|
+
|
|
12
|
+
### Core Components
|
|
13
|
+
|
|
14
|
+
**Configuration Management Module**
|
|
15
|
+
- Reads, parses, backs up, and writes `~/.config/opencode/opencode.json`
|
|
16
|
+
- Validates configuration integrity using JSON Schema
|
|
17
|
+
|
|
18
|
+
**Provider Module**
|
|
19
|
+
- Defines configuration templates for providers (e.g., Volcengine)
|
|
20
|
+
- Implements provider-specific validation logic
|
|
21
|
+
- Extensible for additional providers
|
|
22
|
+
|
|
23
|
+
**TUI Interaction Module**
|
|
24
|
+
- Interactive interface using inquirer or prompts
|
|
25
|
+
- Supports bilingual (Chinese/English) prompts and error messages
|
|
26
|
+
- Guides users through the configuration process
|
|
27
|
+
|
|
28
|
+
**Validation Module**
|
|
29
|
+
- Validates API keys by calling Provider APIs
|
|
30
|
+
- Ensures configuration usability before committing changes
|
|
31
|
+
|
|
32
|
+
**Main Program Flow**
|
|
33
|
+
1. Start → Detect configuration file
|
|
34
|
+
2. TUI select provider → Guide user input
|
|
35
|
+
3. Validate API → Backup config
|
|
36
|
+
4. Update configuration
|
|
37
|
+
|
|
38
|
+
## Data Structures
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
interface ProviderConfig {
|
|
42
|
+
npm: string; // @ai-sdk/openai-compatible
|
|
43
|
+
name: string;
|
|
44
|
+
options: {
|
|
45
|
+
baseURL: string;
|
|
46
|
+
apiKey: string;
|
|
47
|
+
};
|
|
48
|
+
models: Record<string, { name: string }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface OpenCodeConfig {
|
|
52
|
+
$schema: string;
|
|
53
|
+
provider: Record<string, ProviderConfig>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ProviderTemplate {
|
|
57
|
+
id: string;
|
|
58
|
+
displayName: string;
|
|
59
|
+
npm: string;
|
|
60
|
+
baseURL: string;
|
|
61
|
+
supportsApiKeyValidation: boolean;
|
|
62
|
+
validateApiKey?: (apiKey: string, model: string) => Promise<boolean>;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Provider templates predefine common providers like Volcengine, with support for dynamic extension. TUI-collected input maps to ProviderConfig and merges into existing configuration.
|
|
67
|
+
|
|
68
|
+
## Error Handling
|
|
69
|
+
|
|
70
|
+
- **Config file missing or invalid**: Prompt user, ask to create default config
|
|
71
|
+
- **API validation failure**: Offer retry or skip option, log error
|
|
72
|
+
- **File write failure**: Restore backup, prompt about permissions
|
|
73
|
+
- **Network errors**: Friendly timeout message, suggest network check
|
|
74
|
+
|
|
75
|
+
## Testing Strategy
|
|
76
|
+
|
|
77
|
+
- **Unit tests**: Config read/write, template mapping, validation logic
|
|
78
|
+
- **Integration tests**: Simulated TUI input, verify complete configuration flow
|
|
79
|
+
- **E2E tests**: Execute actual configuration in temp directory, verify generated opencode.json
|
|
80
|
+
- **Mock API tests**: Use nock to simulate Provider API responses
|
|
81
|
+
|
|
82
|
+
**Framework**: Vitest
|
|
83
|
+
**Coverage target**: 80%+
|
|
84
|
+
|
|
85
|
+
## Implementation Notes
|
|
86
|
+
|
|
87
|
+
- Language: Node.js with TypeScript
|
|
88
|
+
- TUI Framework: Interactive prompts with bilingual support
|
|
89
|
+
- Configuration path: `~/.config/opencode/opencode.json`
|
|
90
|
+
- Backup strategy: Automatic `.bak` file before modifications
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openharmonyinsight/opencode-oh",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenHarmony OpenCode Helper Tool - Configure opencode with third-party model providers",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"opencode-oh": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"test": "vitest",
|
|
12
|
+
"test:coverage": "vitest --coverage",
|
|
13
|
+
"lint": "eslint src --ext .ts",
|
|
14
|
+
"dev": "tsx src/cli.ts"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"opencode",
|
|
18
|
+
"openharmony",
|
|
19
|
+
"cli",
|
|
20
|
+
"tui"
|
|
21
|
+
],
|
|
22
|
+
"author": "raymond.wangxing@huawei.com",
|
|
23
|
+
"homepage": "https://openharmonyinsight.cn",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"inquirer": "^9.3.7"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/inquirer": "^9.0.8",
|
|
31
|
+
"@types/node": "^22.10.5",
|
|
32
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
33
|
+
"eslint": "^9.17.0",
|
|
34
|
+
"tsx": "^4.19.2",
|
|
35
|
+
"typescript": "^5.7.3",
|
|
36
|
+
"vitest": "^4.0.18"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import type { OpenCodeConfig, ProviderConfig, ProviderInput } from './types.js';
|
|
5
|
+
import { getProviderTemplate } from '../provider/templates.js';
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'opencode.json');
|
|
9
|
+
const DEFAULT_SCHEMA = 'https://opencode.ai/config.json';
|
|
10
|
+
|
|
11
|
+
export class ConfigManager {
|
|
12
|
+
private configPath: string;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
this.configPath = CONFIG_FILE;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getConfigPath(): string {
|
|
19
|
+
return this.configPath;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
readConfig(): OpenCodeConfig | null {
|
|
23
|
+
try {
|
|
24
|
+
if (!fs.existsSync(this.configPath)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const content = fs.readFileSync(this.configPath, 'utf-8');
|
|
28
|
+
return JSON.parse(content);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`Failed to read config: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
writeConfig(config: OpenCodeConfig): void {
|
|
35
|
+
try {
|
|
36
|
+
const configDir = path.dirname(this.configPath);
|
|
37
|
+
if (!fs.existsSync(configDir)) {
|
|
38
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new Error(`Failed to write config: ${error instanceof Error ? error.message : String(error)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
backupConfig(): string | null {
|
|
47
|
+
try {
|
|
48
|
+
if (!fs.existsSync(this.configPath)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const backupPath = `${this.configPath}.bak`;
|
|
52
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
53
|
+
const timestampedBackupPath = `${this.configPath}.${timestamp}.bak`;
|
|
54
|
+
|
|
55
|
+
fs.copyFileSync(this.configPath, timestampedBackupPath);
|
|
56
|
+
return timestampedBackupPath;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(`Failed to backup config: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
restoreBackup(backupPath: string): void {
|
|
63
|
+
try {
|
|
64
|
+
fs.copyFileSync(backupPath, this.configPath);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(`Failed to restore backup: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
addProvider(input: ProviderInput): void {
|
|
71
|
+
const config = this.readConfig() || this.getDefaultConfig();
|
|
72
|
+
|
|
73
|
+
if (!config.provider || typeof config.provider !== 'object') {
|
|
74
|
+
config.provider = {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const template = getProviderTemplate(input.providerId);
|
|
78
|
+
if (!template) {
|
|
79
|
+
throw new Error(`Unknown provider: ${input.providerId}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const providerConfig: ProviderConfig = {
|
|
83
|
+
npm: template.npm,
|
|
84
|
+
name: template.id,
|
|
85
|
+
options: {
|
|
86
|
+
baseURL: template.baseURL,
|
|
87
|
+
apiKey: input.apiKey
|
|
88
|
+
},
|
|
89
|
+
models: {
|
|
90
|
+
[input.modelName]: {
|
|
91
|
+
name: input.modelName
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
config.provider[input.providerId] = providerConfig;
|
|
97
|
+
this.writeConfig(config);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getDefaultConfig(): OpenCodeConfig {
|
|
101
|
+
return {
|
|
102
|
+
$schema: DEFAULT_SCHEMA,
|
|
103
|
+
provider: {}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface ProviderConfig {
|
|
2
|
+
npm: string;
|
|
3
|
+
name: string;
|
|
4
|
+
options: {
|
|
5
|
+
baseURL: string;
|
|
6
|
+
apiKey: string;
|
|
7
|
+
};
|
|
8
|
+
models: Record<string, { name: string }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface OpenCodeConfig {
|
|
12
|
+
$schema: string;
|
|
13
|
+
provider: Record<string, ProviderConfig>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProviderTemplate {
|
|
17
|
+
id: string;
|
|
18
|
+
displayName: string;
|
|
19
|
+
npm: string;
|
|
20
|
+
baseURL: string;
|
|
21
|
+
supportsApiKeyValidation: boolean;
|
|
22
|
+
validateApiKey?: (apiKey: string, model: string) => Promise<boolean>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ProviderInput {
|
|
26
|
+
providerId: string;
|
|
27
|
+
apiKey: string;
|
|
28
|
+
modelName: string;
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ConfigManager } from './config/index.js';
|
|
2
|
+
import { TUIManager } from './tui/index.js';
|
|
3
|
+
|
|
4
|
+
export async function run(): Promise<void> {
|
|
5
|
+
const configManager = new ConfigManager();
|
|
6
|
+
const tuiManager = new TUIManager();
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const input = await tuiManager.run();
|
|
10
|
+
if (!input) {
|
|
11
|
+
console.log('Cancelled');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const backupPath = configManager.backupConfig();
|
|
16
|
+
if (backupPath) {
|
|
17
|
+
tuiManager.showBackup(backupPath);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
configManager.addProvider(input);
|
|
21
|
+
tuiManager.showMessage('configUpdated');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
tuiManager.showError(error instanceof Error ? error.message : String(error));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ProviderTemplate } from '../config/types.js';
|
|
2
|
+
|
|
3
|
+
export const PROVIDER_TEMPLATES: Record<string, ProviderTemplate> = {
|
|
4
|
+
volcengine: {
|
|
5
|
+
id: 'volcengine',
|
|
6
|
+
displayName: '火山引擎',
|
|
7
|
+
npm: '@ai-sdk/openai-compatible',
|
|
8
|
+
baseURL: 'https://ark.cn-beijing.volces.com/api/v3',
|
|
9
|
+
supportsApiKeyValidation: true,
|
|
10
|
+
validateApiKey: async (apiKey: string, model: string) => {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch('https://ark.cn-beijing.volces.com/api/v3/models', {
|
|
13
|
+
method: 'GET',
|
|
14
|
+
headers: {
|
|
15
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
16
|
+
'Content-Type': 'application/json'
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return response.ok;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
'volcengine-codingplan': {
|
|
27
|
+
id: 'volcengine-codingplan',
|
|
28
|
+
displayName: '火山引擎CodingPlan',
|
|
29
|
+
npm: '@ai-sdk/openai-compatible',
|
|
30
|
+
baseURL: 'https://ark.cn-beijing.volces.com/api/coding/v3',
|
|
31
|
+
supportsApiKeyValidation: true,
|
|
32
|
+
validateApiKey: async (apiKey: string, model: string) => {
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch('https://ark.cn-beijing.volces.com/api/coding/v3/models', {
|
|
35
|
+
method: 'GET',
|
|
36
|
+
headers: {
|
|
37
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
38
|
+
'Content-Type': 'application/json'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return response.ok;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
'ZhiPu-CodingPlan': {
|
|
49
|
+
id: 'ZhiPu-CodingPlan',
|
|
50
|
+
displayName: '智谱CodingPlan',
|
|
51
|
+
npm: '@ai-sdk/openai-compatible',
|
|
52
|
+
baseURL: 'https://open.bigmodel.cn/api/paas/v4',
|
|
53
|
+
supportsApiKeyValidation: true,
|
|
54
|
+
validateApiKey: async (apiKey: string) => {
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch('https://open.bigmodel.cn/api/paas/v4/models', {
|
|
57
|
+
method: 'GET',
|
|
58
|
+
headers: {
|
|
59
|
+
'Authorization': `Bearer ${apiKey}`
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return response.ok;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function getProviderTemplate(id: string): ProviderTemplate | undefined {
|
|
71
|
+
return PROVIDER_TEMPLATES[id];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getAllProviderTemplates(): ProviderTemplate[] {
|
|
75
|
+
return Object.values(PROVIDER_TEMPLATES);
|
|
76
|
+
}
|