@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.
Files changed (51) hide show
  1. package/AGENTS.md +98 -0
  2. package/README.md +80 -0
  3. package/dist/src/cli.d.ts +3 -0
  4. package/dist/src/cli.d.ts.map +1 -0
  5. package/dist/src/cli.js +7 -0
  6. package/dist/src/cli.js.map +1 -0
  7. package/dist/src/config/index.d.ts +13 -0
  8. package/dist/src/config/index.d.ts.map +1 -0
  9. package/dist/src/config/index.js +95 -0
  10. package/dist/src/config/index.js.map +1 -0
  11. package/dist/src/config/types.d.ts +29 -0
  12. package/dist/src/config/types.d.ts.map +1 -0
  13. package/dist/src/config/types.js +2 -0
  14. package/dist/src/config/types.js.map +1 -0
  15. package/dist/src/index.d.ts +2 -0
  16. package/dist/src/index.d.ts.map +1 -0
  17. package/dist/src/index.js +24 -0
  18. package/dist/src/index.js.map +1 -0
  19. package/dist/src/provider/templates.d.ts +5 -0
  20. package/dist/src/provider/templates.d.ts.map +1 -0
  21. package/dist/src/provider/templates.js +74 -0
  22. package/dist/src/provider/templates.js.map +1 -0
  23. package/dist/src/tui/index.d.ts +12 -0
  24. package/dist/src/tui/index.d.ts.map +1 -0
  25. package/dist/src/tui/index.js +136 -0
  26. package/dist/src/tui/index.js.map +1 -0
  27. package/dist/src/validation/index.d.ts +6 -0
  28. package/dist/src/validation/index.d.ts.map +1 -0
  29. package/dist/src/validation/index.js +24 -0
  30. package/dist/src/validation/index.js.map +1 -0
  31. package/dist/tests/config.test.d.ts +2 -0
  32. package/dist/tests/config.test.d.ts.map +1 -0
  33. package/dist/tests/config.test.js +104 -0
  34. package/dist/tests/config.test.js.map +1 -0
  35. package/dist/tests/validation.test.d.ts +2 -0
  36. package/dist/tests/validation.test.d.ts.map +1 -0
  37. package/dist/tests/validation.test.js +37 -0
  38. package/dist/tests/validation.test.js.map +1 -0
  39. package/docs/plans/2026-02-12-opencode-oh-design.md +90 -0
  40. package/package.json +38 -0
  41. package/src/cli.ts +8 -0
  42. package/src/config/index.ts +106 -0
  43. package/src/config/types.ts +29 -0
  44. package/src/index.ts +26 -0
  45. package/src/provider/templates.ts +76 -0
  46. package/src/tui/index.ts +159 -0
  47. package/src/validation/index.ts +30 -0
  48. package/tests/config.test.ts +117 -0
  49. package/tests/validation.test.ts +44 -0
  50. package/tsconfig.json +20 -0
  51. package/vitest.config.ts +19 -0
@@ -0,0 +1,159 @@
1
+ import inquirer from 'inquirer';
2
+ import type { ProviderInput } from '../config/types.js';
3
+ import { getAllProviderTemplates } from '../provider/templates.js';
4
+ import { validateApiKey } from '../validation/index.js';
5
+ import type { ValidationResult } from '../validation/index.js';
6
+
7
+ type Language = 'en' | 'zh';
8
+
9
+ const MESSAGES: Record<Language, Record<string, string>> = {
10
+ en: {
11
+ welcome: 'Welcome to opencode-oh - OpenCode Helper Tool',
12
+ selectProvider: 'Select a provider to configure:',
13
+ providerBaseURL: 'Provider Base URL:',
14
+ enterApiKey: 'Enter API key:',
15
+ enterModelName: 'Enter model name:',
16
+ validating: 'Validating API key...',
17
+ validationSuccess: 'API key validated successfully',
18
+ validationFailed: 'API key validation failed',
19
+ retry: 'Would you like to retry?',
20
+ skipValidation: 'Skip validation and proceed?',
21
+ configUpdated: 'Configuration updated successfully',
22
+ backupCreated: 'Backup created at:',
23
+ errorOccurred: 'An error occurred:',
24
+ noProviders: 'No providers available'
25
+ },
26
+ zh: {
27
+ welcome: '欢迎使用 opencode-oh - OpenCode 辅助工具',
28
+ selectProvider: '选择要配置的提供商:',
29
+ providerBaseURL: '提供商 Base URL:',
30
+ enterApiKey: '输入 API key:',
31
+ enterModelName: '输入模型名称:',
32
+ validating: '正在验证 API key...',
33
+ validationSuccess: 'API key 验证成功',
34
+ validationFailed: 'API key 验证失败',
35
+ retry: '是否重试?',
36
+ skipValidation: '跳过验证并继续?',
37
+ configUpdated: '配置更新成功',
38
+ backupCreated: '备份已创建于:',
39
+ errorOccurred: '发生错误:',
40
+ noProviders: '没有可用的提供商'
41
+ }
42
+ };
43
+
44
+ export class TUIManager {
45
+ private language: Language;
46
+ private messages: Record<string, string>;
47
+
48
+ constructor() {
49
+ const locale = process.env.LANG || 'en_US';
50
+ this.language = locale.startsWith('zh') ? 'zh' : 'en';
51
+ this.messages = MESSAGES[this.language];
52
+ }
53
+
54
+ async run(): Promise<ProviderInput | null> {
55
+ console.log(`\n${this.messages.welcome}\n`);
56
+
57
+ const providers = getAllProviderTemplates();
58
+ if (providers.length === 0) {
59
+ console.error(this.messages.noProviders);
60
+ return null;
61
+ }
62
+
63
+ const { providerId } = await inquirer.prompt([
64
+ {
65
+ type: 'list',
66
+ name: 'providerId',
67
+ message: this.messages.selectProvider,
68
+ choices: providers.map(p => ({
69
+ name: p.displayName,
70
+ value: p.id
71
+ }))
72
+ }
73
+ ]);
74
+
75
+ const selectedProvider = providers.find(p => p.id === providerId);
76
+ if (selectedProvider) {
77
+ console.log(`\n${this.messages.providerBaseURL} ${selectedProvider.baseURL}\n`);
78
+ }
79
+
80
+ const { apiKey } = await inquirer.prompt([
81
+ {
82
+ type: 'password',
83
+ name: 'apiKey',
84
+ message: this.messages.enterApiKey,
85
+ validate: (input: string) => input.length > 0 || 'API key is required'
86
+ }
87
+ ]);
88
+
89
+ const { modelName } = await inquirer.prompt([
90
+ {
91
+ type: 'input',
92
+ name: 'modelName',
93
+ message: this.messages.enterModelName,
94
+ validate: (input: string) => input.length > 0 || 'Model name is required'
95
+ }
96
+ ]);
97
+
98
+ const input: ProviderInput = { providerId, apiKey, modelName };
99
+
100
+ return await this.validateWithRetry(input);
101
+ }
102
+
103
+ private async validateWithRetry(input: ProviderInput): Promise<ProviderInput | null> {
104
+ console.log(`\n${this.messages.validating}`);
105
+
106
+ const result: ValidationResult = await validateApiKey(input.providerId, input.apiKey, input.modelName);
107
+
108
+ if (result.valid) {
109
+ console.log(`✓ ${this.messages.validationSuccess}\n`);
110
+ return input;
111
+ }
112
+
113
+ console.log(`✗ ${this.messages.validationFailed}`);
114
+ if (result.error) {
115
+ console.log(` ${result.error}`);
116
+ }
117
+
118
+ const { action } = await inquirer.prompt([
119
+ {
120
+ type: 'list',
121
+ name: 'action',
122
+ message: this.messages.retry,
123
+ choices: [
124
+ { name: 'Retry', value: 'retry' },
125
+ { name: 'Skip', value: 'skip' },
126
+ { name: 'Cancel', value: 'cancel' }
127
+ ]
128
+ }
129
+ ]);
130
+
131
+ if (action === 'retry') {
132
+ const { apiKey } = await inquirer.prompt([
133
+ {
134
+ type: 'password',
135
+ name: 'apiKey',
136
+ message: this.messages.enterApiKey
137
+ }
138
+ ]);
139
+ input.apiKey = apiKey;
140
+ return this.validateWithRetry(input);
141
+ } else if (action === 'skip') {
142
+ return input;
143
+ }
144
+
145
+ return null;
146
+ }
147
+
148
+ showMessage(key: string): void {
149
+ console.log(this.messages[key]);
150
+ }
151
+
152
+ showError(message: string): void {
153
+ console.error(`${this.messages.errorOccurred} ${message}`);
154
+ }
155
+
156
+ showBackup(path: string): void {
157
+ console.log(`${this.messages.backupCreated} ${path}`);
158
+ }
159
+ }
@@ -0,0 +1,30 @@
1
+ import { getProviderTemplate } from '../provider/templates.js';
2
+
3
+ export interface ValidationResult {
4
+ valid: boolean;
5
+ error?: string;
6
+ }
7
+
8
+ export async function validateApiKey(providerId: string, apiKey: string, model?: string): Promise<ValidationResult> {
9
+ const template = getProviderTemplate(providerId);
10
+ if (!template) {
11
+ return { valid: false, error: `Unknown provider: ${providerId}` };
12
+ }
13
+
14
+ if (!template.supportsApiKeyValidation || !template.validateApiKey) {
15
+ return { valid: true };
16
+ }
17
+
18
+ try {
19
+ const valid = await template.validateApiKey(apiKey, model || '');
20
+ if (!valid) {
21
+ return { valid: false, error: 'API key validation failed' };
22
+ }
23
+ return { valid: true };
24
+ } catch (error) {
25
+ return {
26
+ valid: false,
27
+ error: `Validation error: ${error instanceof Error ? error.message : String(error)}`
28
+ };
29
+ }
30
+ }
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { ConfigManager } from '../src/config/index.js';
3
+ import type { ProviderInput } from '../src/config/types.js';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
7
+
8
+ describe('ConfigManager', () => {
9
+ let configManager: ConfigManager;
10
+ let testConfigPath: string;
11
+ let testConfigDir: string;
12
+
13
+ beforeEach(() => {
14
+ testConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-oh-test-'));
15
+ testConfigPath = path.join(testConfigDir, 'opencode.json');
16
+ configManager = new ConfigManager();
17
+ (configManager as any).configPath = testConfigPath;
18
+ });
19
+
20
+ afterEach(() => {
21
+ if (fs.existsSync(testConfigDir)) {
22
+ fs.rmSync(testConfigDir, { recursive: true, force: true });
23
+ }
24
+ });
25
+
26
+ it('should return null when config does not exist', () => {
27
+ const config = configManager.readConfig();
28
+ expect(config).toBeNull();
29
+ });
30
+
31
+ it('should read existing config', () => {
32
+ const testConfig = {
33
+ $schema: 'https://opencode.ai/config.json',
34
+ provider: {}
35
+ };
36
+ fs.writeFileSync(testConfigPath, JSON.stringify(testConfig));
37
+ const config = configManager.readConfig();
38
+ expect(config).toEqual(testConfig);
39
+ });
40
+
41
+ it('should write config', () => {
42
+ const testConfig = {
43
+ $schema: 'https://opencode.ai/config.json',
44
+ provider: {}
45
+ };
46
+ configManager.writeConfig(testConfig);
47
+ expect(fs.existsSync(testConfigPath)).toBe(true);
48
+ const read = JSON.parse(fs.readFileSync(testConfigPath, 'utf-8'));
49
+ expect(read).toEqual(testConfig);
50
+ });
51
+
52
+ it('should backup existing config', () => {
53
+ const testConfig = {
54
+ $schema: 'https://opencode.ai/config.json',
55
+ provider: {}
56
+ };
57
+ fs.writeFileSync(testConfigPath, JSON.stringify(testConfig));
58
+ const backupPath = configManager.backupConfig();
59
+ expect(backupPath).toBeTruthy();
60
+ expect(fs.existsSync(backupPath as string)).toBe(true);
61
+ });
62
+
63
+ it('should return null when backing up non-existent config', () => {
64
+ const backupPath = configManager.backupConfig();
65
+ expect(backupPath).toBeNull();
66
+ });
67
+
68
+ it('should add provider to config', () => {
69
+ const input: ProviderInput = {
70
+ providerId: 'volcengine',
71
+ apiKey: 'test-key',
72
+ modelName: 'test-model'
73
+ };
74
+ configManager.addProvider(input);
75
+ const config = configManager.readConfig();
76
+ expect(config).not.toBeNull();
77
+ expect(config?.provider.volcengine).toBeDefined();
78
+ expect(config?.provider.volcengine.options.apiKey).toBe('test-key');
79
+ });
80
+
81
+ it('should add provider to config when provider property is missing', () => {
82
+ const testConfig = {
83
+ $schema: 'https://opencode.ai/config.json'
84
+ };
85
+ fs.writeFileSync(testConfigPath, JSON.stringify(testConfig));
86
+
87
+ const input: ProviderInput = {
88
+ providerId: 'volcengine',
89
+ apiKey: 'test-key',
90
+ modelName: 'test-model'
91
+ };
92
+ configManager.addProvider(input);
93
+ const config = configManager.readConfig();
94
+ expect(config).not.toBeNull();
95
+ expect(config?.provider.volcengine).toBeDefined();
96
+ expect(config?.provider.volcengine.options.apiKey).toBe('test-key');
97
+ });
98
+
99
+ it('should add provider to config when provider is null', () => {
100
+ const testConfig = {
101
+ $schema: 'https://opencode.ai/config.json',
102
+ provider: null
103
+ };
104
+ fs.writeFileSync(testConfigPath, JSON.stringify(testConfig));
105
+
106
+ const input: ProviderInput = {
107
+ providerId: 'volcengine',
108
+ apiKey: 'test-key',
109
+ modelName: 'test-model'
110
+ };
111
+ configManager.addProvider(input);
112
+ const config = configManager.readConfig();
113
+ expect(config).not.toBeNull();
114
+ expect(config?.provider.volcengine).toBeDefined();
115
+ expect(config?.provider.volcengine.options.apiKey).toBe('test-key');
116
+ });
117
+ });
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { validateApiKey } from '../src/validation/index.js';
3
+
4
+ describe('validateApiKey', () => {
5
+ beforeEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+
9
+ it('should validate volcengine API key successfully', async () => {
10
+ const mockFetch = vi.fn().mockResolvedValue({
11
+ ok: true
12
+ });
13
+ global.fetch = mockFetch;
14
+
15
+ const result = await validateApiKey('volcengine', 'valid-key');
16
+ expect(result.valid).toBe(true);
17
+ });
18
+
19
+ it('should return error for invalid volcengine API key', async () => {
20
+ const mockFetch = vi.fn().mockResolvedValue({
21
+ ok: false
22
+ });
23
+ global.fetch = mockFetch;
24
+
25
+ const result = await validateApiKey('volcengine', 'invalid-key');
26
+ expect(result.valid).toBe(false);
27
+ expect(result.error).toBeDefined();
28
+ });
29
+
30
+ it('should handle network errors', async () => {
31
+ const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
32
+ global.fetch = mockFetch;
33
+
34
+ const result = await validateApiKey('volcengine', 'test-key');
35
+ expect(result.valid).toBe(false);
36
+ expect(result.error).toBe('API key validation failed');
37
+ });
38
+
39
+ it('should return error for unknown provider', async () => {
40
+ const result = await validateApiKey('unknown', 'test-key');
41
+ expect(result.valid).toBe(false);
42
+ expect(result.error).toContain('Unknown provider');
43
+ });
44
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "outDir": "./dist",
7
+ "rootDir": ".",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "types": ["node", "vitest/globals"]
17
+ },
18
+ "include": ["src/**/*", "tests/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ coverage: {
8
+ provider: 'v8',
9
+ reporter: ['text', 'json', 'html'],
10
+ exclude: [
11
+ 'node_modules/',
12
+ 'dist/',
13
+ 'tests/',
14
+ '**/*.test.ts',
15
+ '**/*.config.ts'
16
+ ]
17
+ }
18
+ }
19
+ });