@gengjiawen/os-init 1.14.1 → 1.15.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.15.0](https://github.com/gengjiawen/os-init/compare/v1.14.1...v1.15.0) (2026-03-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * add glm and kimi models to opencode template ([597379d](https://github.com/gengjiawen/os-init/commit/597379db82169f068b4732e71ccba4631e58c951))
9
+ * add opencode setup and update agents installer ([b62d83d](https://github.com/gengjiawen/os-init/commit/b62d83d925cb11bd2fff1465a3936c17706b1267))
10
+
3
11
  ## [1.14.1](https://github.com/gengjiawen/os-init/compare/v1.14.0...v1.14.1) (2026-02-28)
4
12
 
5
13
 
package/README.md CHANGED
@@ -98,6 +98,17 @@ Configures Codex CLI with your API key. This command will:
98
98
  - Write `~/.codex/auth.json`
99
99
  - Install global tool: `@openai/codex`
100
100
 
101
+ ### Configure OpenCode CLI
102
+
103
+ ```bash
104
+ pnpx @gengjiawen/os-init set-opencode <API_KEY>
105
+ ```
106
+
107
+ Configures OpenCode CLI with your API key. This command will:
108
+
109
+ - Write `~/.config/opencode/opencode.json`
110
+ - Install global tool: `opencode-ai` (provides `opencode` command)
111
+
101
112
  ### Configure Raycast AI
102
113
 
103
114
  ```bash
package/bin/bin.js CHANGED
@@ -8,6 +8,8 @@ const {
8
8
  installCodexDeps,
9
9
  writeGeminiConfig,
10
10
  installGeminiDeps,
11
+ writeOpencodeConfig,
12
+ installOpencodeDeps,
11
13
  writeAllAgentsConfig,
12
14
  installAllAgentsDeps,
13
15
  writeRaycastConfig,
@@ -91,10 +93,33 @@ program
91
93
  )
92
94
  })
93
95
 
96
+ program
97
+ .command('set-opencode')
98
+ .description('setup OpenCode CLI config and auth')
99
+ .argument('<apiKey>', 'API key to set for OpenCode')
100
+ .action(async (apiKey) => {
101
+ if (!apiKey || String(apiKey).trim().length === 0) {
102
+ console.error('Missing required argument: <apiKey>')
103
+ program.help({ error: true })
104
+ return
105
+ }
106
+ try {
107
+ const { configPath } = writeOpencodeConfig(apiKey)
108
+ console.log(`OpenCode config written to: ${configPath}`)
109
+ await installOpencodeDeps()
110
+ } catch (err) {
111
+ console.error('Failed to setup OpenCode:', err.message)
112
+ process.exit(1)
113
+ }
114
+ console.log(
115
+ 'OpenCode is ready. use `opencode` in terminal to start building'
116
+ )
117
+ })
118
+
94
119
  program
95
120
  .command('set-agents')
96
121
  .description(
97
- 'setup Claude Code and Codex at once (use --full to include Gemini CLI)'
122
+ 'setup Claude Code, Codex, and OpenCode at once (use --full to include Gemini CLI)'
98
123
  )
99
124
  .argument('<apiKey>', 'API key to set for agents')
100
125
  .option('--full', 'Also setup Gemini CLI')
@@ -110,8 +135,8 @@ program
110
135
  try {
111
136
  console.log(
112
137
  full
113
- ? 'Setting up Claude Code + Codex + Gemini CLI...\n'
114
- : 'Setting up Claude Code + Codex...\n'
138
+ ? 'Setting up Claude Code + Codex + OpenCode + Gemini CLI...\n'
139
+ : 'Setting up Claude Code + Codex + OpenCode...\n'
115
140
  )
116
141
 
117
142
  const result = writeAllAgentsConfig(apiKey, { full })
@@ -126,6 +151,9 @@ program
126
151
  console.log(` Config written to: ${result.codex.configPath}`)
127
152
  console.log(` Auth written to: ${result.codex.authPath}`)
128
153
 
154
+ console.log('\nOpenCode:')
155
+ console.log(` Config written to: ${result.opencode.configPath}`)
156
+
129
157
  if (result.gemini) {
130
158
  console.log('\nGemini CLI:')
131
159
  console.log(` Env written to: ${result.gemini.envPath}`)
@@ -142,6 +170,7 @@ program
142
170
  console.log('\nSetup complete!')
143
171
  console.log(' - Use `claude` for Claude Code')
144
172
  console.log(' - Use `codex` for Codex')
173
+ console.log(' - Use `opencode` for OpenCode')
145
174
  if (full) console.log(' - Use `gemini` for Gemini CLI')
146
175
  })
147
176
 
@@ -10,6 +10,9 @@ export interface AllAgentsResult {
10
10
  configPath: string;
11
11
  authPath: string;
12
12
  };
13
+ opencode: {
14
+ configPath: string;
15
+ };
13
16
  gemini?: {
14
17
  envPath: string;
15
18
  settingsPath: string;
@@ -5,12 +5,15 @@ exports.installAllAgentsDeps = installAllAgentsDeps;
5
5
  const claude_code_1 = require("./claude-code");
6
6
  const codex_1 = require("./codex");
7
7
  const gemini_cli_1 = require("./gemini-cli");
8
+ const opencode_1 = require("./opencode");
8
9
  function writeAllAgentsConfig(apiKey, options = {}) {
9
10
  const claudeResult = (0, claude_code_1.writeClaudeConfig)(apiKey);
10
11
  const codexResult = (0, codex_1.writeCodexConfig)(apiKey);
12
+ const opencodeResult = (0, opencode_1.writeOpencodeConfig)(apiKey);
11
13
  const result = {
12
14
  claude: claudeResult,
13
15
  codex: codexResult,
16
+ opencode: opencodeResult,
14
17
  };
15
18
  if (options.full) {
16
19
  result.gemini = (0, gemini_cli_1.writeGeminiConfig)(apiKey);
@@ -18,7 +21,7 @@ function writeAllAgentsConfig(apiKey, options = {}) {
18
21
  return result;
19
22
  }
20
23
  async function installAllAgentsDeps(options = {}) {
21
- const installers = [(0, claude_code_1.installDeps)(), (0, codex_1.installCodexDeps)()];
24
+ const installers = [(0, claude_code_1.installDeps)(), (0, codex_1.installCodexDeps)(), (0, opencode_1.installOpencodeDeps)()];
22
25
  if (options.full)
23
26
  installers.push((0, gemini_cli_1.installGeminiDeps)());
24
27
  await Promise.all(installers);
@@ -90,7 +90,10 @@ async function installDeps() {
90
90
  const usePnpm = await (0, utils_1.commandExists)('pnpm');
91
91
  if (usePnpm) {
92
92
  console.log('pnpm detected. Installing dependencies with pnpm...');
93
- await (0, execa_1.execa)('pnpm', ['add', '-g', ...packages], { stdio: 'inherit' });
93
+ await (0, execa_1.execa)('pnpm', ['add', '-g', ...packages], {
94
+ stdio: 'inherit',
95
+ env: utils_1.PNPM_INSTALL_ENV,
96
+ });
94
97
  }
95
98
  else {
96
99
  console.log('pnpm not found. Falling back to npm...');
package/build/codex.js CHANGED
@@ -36,7 +36,10 @@ async function installCodexDeps() {
36
36
  const usePnpm = await (0, utils_1.commandExists)('pnpm');
37
37
  if (usePnpm) {
38
38
  console.log('pnpm detected. Installing Codex dependency with pnpm...');
39
- await (0, execa_1.execa)('pnpm', ['add', '-g', ...packages], { stdio: 'inherit' });
39
+ await (0, execa_1.execa)('pnpm', ['add', '-g', ...packages], {
40
+ stdio: 'inherit',
41
+ env: utils_1.PNPM_INSTALL_ENV,
42
+ });
40
43
  }
41
44
  else {
42
45
  console.log('pnpm not found. Falling back to npm...');
@@ -39,7 +39,10 @@ async function installGeminiDeps() {
39
39
  const geminiPackage = '@google/gemini-cli';
40
40
  if (usePnpm) {
41
41
  console.log('pnpm detected. Installing Gemini CLI with pnpm...');
42
- await (0, execa_1.execa)('pnpm', ['add', '-g', geminiPackage], { stdio: 'inherit' });
42
+ await (0, execa_1.execa)('pnpm', ['add', '-g', geminiPackage], {
43
+ stdio: 'inherit',
44
+ env: utils_1.PNPM_INSTALL_ENV,
45
+ });
43
46
  }
44
47
  else {
45
48
  console.log('pnpm not found. Falling back to npm...');
package/build/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare function writeRaycastConfig(apiKey: string): {
4
4
  export { writeClaudeConfig, installDeps } from './claude-code';
5
5
  export { writeCodexConfig, installCodexDeps } from './codex';
6
6
  export { writeGeminiConfig, installGeminiDeps } from './gemini-cli';
7
+ export { writeOpencodeConfig, installOpencodeDeps } from './opencode';
7
8
  export { writeAllAgentsConfig, installAllAgentsDeps } from './all-agents';
8
9
  export { setupDevEnvironment } from './dev-setup';
9
10
  export { setupAndroidEnvironment } from './android-setup';
package/build/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setupAndroidEnvironment = exports.setupDevEnvironment = exports.installAllAgentsDeps = exports.writeAllAgentsConfig = exports.installGeminiDeps = exports.writeGeminiConfig = exports.installCodexDeps = exports.writeCodexConfig = exports.installDeps = exports.writeClaudeConfig = void 0;
3
+ exports.setupAndroidEnvironment = exports.setupDevEnvironment = exports.installAllAgentsDeps = exports.writeAllAgentsConfig = exports.installOpencodeDeps = exports.writeOpencodeConfig = exports.installGeminiDeps = exports.writeGeminiConfig = exports.installCodexDeps = exports.writeCodexConfig = exports.installDeps = exports.writeClaudeConfig = void 0;
4
4
  exports.writeRaycastConfig = writeRaycastConfig;
5
5
  const fs = require("fs");
6
6
  const path = require("path");
@@ -47,6 +47,9 @@ Object.defineProperty(exports, "installCodexDeps", { enumerable: true, get: func
47
47
  var gemini_cli_1 = require("./gemini-cli");
48
48
  Object.defineProperty(exports, "writeGeminiConfig", { enumerable: true, get: function () { return gemini_cli_1.writeGeminiConfig; } });
49
49
  Object.defineProperty(exports, "installGeminiDeps", { enumerable: true, get: function () { return gemini_cli_1.installGeminiDeps; } });
50
+ var opencode_1 = require("./opencode");
51
+ Object.defineProperty(exports, "writeOpencodeConfig", { enumerable: true, get: function () { return opencode_1.writeOpencodeConfig; } });
52
+ Object.defineProperty(exports, "installOpencodeDeps", { enumerable: true, get: function () { return opencode_1.installOpencodeDeps; } });
50
53
  var all_agents_1 = require("./all-agents");
51
54
  Object.defineProperty(exports, "writeAllAgentsConfig", { enumerable: true, get: function () { return all_agents_1.writeAllAgentsConfig; } });
52
55
  Object.defineProperty(exports, "installAllAgentsDeps", { enumerable: true, get: function () { return all_agents_1.installAllAgentsDeps; } });
@@ -0,0 +1,4 @@
1
+ export declare function writeOpencodeConfig(apiKey: string): {
2
+ configPath: string;
3
+ };
4
+ export declare function installOpencodeDeps(): Promise<void>;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writeOpencodeConfig = writeOpencodeConfig;
4
+ exports.installOpencodeDeps = installOpencodeDeps;
5
+ const fs = require("fs");
6
+ const os = require("os");
7
+ const path = require("path");
8
+ const execa_1 = require("execa");
9
+ const utils_1 = require("./utils");
10
+ const OPENCODE_PROVIDER_ID = 'MyCustomProvider';
11
+ const OPENCODE_MODEL_ID = 'code';
12
+ const OPENCODE_GLM_MODEL_ID = 'glm';
13
+ const OPENCODE_KIMI_MODEL_ID = 'kimi';
14
+ const OPENCODE_BASE_URL = 'https://ai.gengjiawen.com/api/openai/v1';
15
+ function getOpencodeConfigDir() {
16
+ return path.join(os.homedir(), '.config', 'opencode');
17
+ }
18
+ function writeOpencodeConfig(apiKey) {
19
+ const configDir = getOpencodeConfigDir();
20
+ (0, utils_1.ensureDir)(configDir);
21
+ const configContent = {
22
+ $schema: 'https://opencode.ai/config.json',
23
+ provider: {
24
+ [OPENCODE_PROVIDER_ID]: {
25
+ npm: '@ai-sdk/openai-compatible',
26
+ name: 'JWProvider',
27
+ options: {
28
+ baseURL: OPENCODE_BASE_URL,
29
+ apiKey,
30
+ },
31
+ models: {
32
+ [OPENCODE_MODEL_ID]: {
33
+ name: OPENCODE_MODEL_ID,
34
+ },
35
+ [OPENCODE_GLM_MODEL_ID]: {
36
+ name: OPENCODE_GLM_MODEL_ID,
37
+ },
38
+ [OPENCODE_KIMI_MODEL_ID]: {
39
+ name: OPENCODE_KIMI_MODEL_ID,
40
+ },
41
+ },
42
+ },
43
+ },
44
+ model: `${OPENCODE_PROVIDER_ID}/${OPENCODE_MODEL_ID}`,
45
+ small_model: `${OPENCODE_PROVIDER_ID}/${OPENCODE_MODEL_ID}`,
46
+ };
47
+ const configPath = path.join(configDir, 'opencode.json');
48
+ fs.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
49
+ return { configPath };
50
+ }
51
+ async function installOpencodeDeps() {
52
+ const packages = ['opencode-ai'];
53
+ const usePnpm = await (0, utils_1.commandExists)('pnpm');
54
+ if (usePnpm) {
55
+ console.log('pnpm detected. Installing OpenCode dependency with pnpm...');
56
+ await (0, execa_1.execa)('pnpm', ['add', '-g', ...packages], {
57
+ stdio: 'inherit',
58
+ env: utils_1.PNPM_INSTALL_ENV,
59
+ });
60
+ }
61
+ else {
62
+ console.log('pnpm not found. Falling back to npm...');
63
+ await (0, execa_1.execa)('npm', ['install', '-g', ...packages], { stdio: 'inherit' });
64
+ }
65
+ console.log('OpenCode dependency installed successfully.');
66
+ }
package/build/utils.d.ts CHANGED
@@ -1,2 +1,5 @@
1
+ export declare const PNPM_INSTALL_ENV: {
2
+ PNPM_CONFIG_ENABLE_PRE_POST_SCRIPTS: string;
3
+ };
1
4
  export declare function ensureDir(dirPath: string): void;
2
5
  export declare function commandExists(command: string): Promise<boolean>;
package/build/utils.js CHANGED
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PNPM_INSTALL_ENV = void 0;
3
4
  exports.ensureDir = ensureDir;
4
5
  exports.commandExists = commandExists;
5
6
  const fs = require("fs");
6
7
  const execa_1 = require("execa");
8
+ exports.PNPM_INSTALL_ENV = {
9
+ PNPM_CONFIG_ENABLE_PRE_POST_SCRIPTS: 'true',
10
+ };
7
11
  function ensureDir(dirPath) {
8
12
  fs.mkdirSync(dirPath, { recursive: true });
9
13
  }
@@ -0,0 +1,50 @@
1
+ import * as fs from 'fs'
2
+ import * as os from 'os'
3
+ import * as path from 'path'
4
+
5
+ jest.mock('execa', () => ({
6
+ execa: jest.fn(),
7
+ }))
8
+
9
+ import { writeAllAgentsConfig } from './all-agents'
10
+
11
+ describe('writeAllAgentsConfig', () => {
12
+ let tempHome: string
13
+ let homedirSpy: jest.SpiedFunction<typeof os.homedir>
14
+ let originalAppData: string | undefined
15
+
16
+ beforeEach(() => {
17
+ tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'os-init-all-agents-'))
18
+ homedirSpy = jest.spyOn(os, 'homedir').mockReturnValue(tempHome)
19
+ originalAppData = process.env.APPDATA
20
+ process.env.APPDATA = path.join(tempHome, 'AppData', 'Roaming')
21
+ })
22
+
23
+ afterEach(() => {
24
+ homedirSpy.mockRestore()
25
+ if (originalAppData === undefined) {
26
+ delete process.env.APPDATA
27
+ } else {
28
+ process.env.APPDATA = originalAppData
29
+ }
30
+ fs.rmSync(tempHome, { recursive: true, force: true })
31
+ })
32
+
33
+ test('writes Claude, Codex and OpenCode config by default', () => {
34
+ const result = writeAllAgentsConfig('test-api-key')
35
+
36
+ expect(fs.existsSync(result.claude.settingsPath)).toBe(true)
37
+ expect(fs.existsSync(result.codex.configPath)).toBe(true)
38
+ expect(fs.existsSync(result.codex.authPath)).toBe(true)
39
+ expect(fs.existsSync(result.opencode.configPath)).toBe(true)
40
+ expect(result.gemini).toBeUndefined()
41
+ })
42
+
43
+ test('includes Gemini config when full option is enabled', () => {
44
+ const result = writeAllAgentsConfig('test-api-key', { full: true })
45
+
46
+ expect(result.gemini).toBeDefined()
47
+ expect(fs.existsSync(result.gemini!.envPath)).toBe(true)
48
+ expect(fs.existsSync(result.gemini!.settingsPath)).toBe(true)
49
+ })
50
+ })
@@ -1,6 +1,7 @@
1
1
  import { writeClaudeConfig, installDeps } from './claude-code'
2
2
  import { writeCodexConfig, installCodexDeps } from './codex'
3
3
  import { writeGeminiConfig, installGeminiDeps } from './gemini-cli'
4
+ import { writeOpencodeConfig, installOpencodeDeps } from './opencode'
4
5
 
5
6
  export interface AllAgentsOptions {
6
7
  /** When true, also include Gemini CLI in the combined setup. */
@@ -10,20 +11,23 @@ export interface AllAgentsOptions {
10
11
  export interface AllAgentsResult {
11
12
  claude: { settingsPath: string; vscodeSettingsPath: string }
12
13
  codex: { configPath: string; authPath: string }
14
+ opencode: { configPath: string }
13
15
  gemini?: { envPath: string; settingsPath: string }
14
16
  }
15
17
 
16
- /** Write configuration for the combined setup (Claude Code + Codex, optional Gemini CLI). */
18
+ /** Write configuration for the combined setup (Claude Code + Codex + OpenCode, optional Gemini CLI). */
17
19
  export function writeAllAgentsConfig(
18
20
  apiKey: string,
19
21
  options: AllAgentsOptions = {}
20
22
  ): AllAgentsResult {
21
23
  const claudeResult = writeClaudeConfig(apiKey)
22
24
  const codexResult = writeCodexConfig(apiKey)
25
+ const opencodeResult = writeOpencodeConfig(apiKey)
23
26
 
24
27
  const result: AllAgentsResult = {
25
28
  claude: claudeResult,
26
29
  codex: codexResult,
30
+ opencode: opencodeResult,
27
31
  }
28
32
 
29
33
  if (options.full) {
@@ -33,11 +37,11 @@ export function writeAllAgentsConfig(
33
37
  return result
34
38
  }
35
39
 
36
- /** Install dependencies for the combined setup (Claude Code + Codex, optional Gemini CLI). */
40
+ /** Install dependencies for the combined setup (Claude Code + Codex + OpenCode, optional Gemini CLI). */
37
41
  export async function installAllAgentsDeps(
38
42
  options: AllAgentsOptions = {}
39
43
  ): Promise<void> {
40
- const installers = [installDeps(), installCodexDeps()]
44
+ const installers = [installDeps(), installCodexDeps(), installOpencodeDeps()]
41
45
  if (options.full) installers.push(installGeminiDeps())
42
46
  await Promise.all(installers)
43
47
  }
@@ -3,7 +3,7 @@ import * as path from 'path'
3
3
  import * as os from 'os'
4
4
  import { execa } from 'execa'
5
5
  import { ParseError, applyEdits, modify, parse } from 'jsonc-parser'
6
- import { ensureDir, commandExists } from './utils'
6
+ import { ensureDir, commandExists, PNPM_INSTALL_ENV } from './utils'
7
7
 
8
8
  const CLAUDE_BASE_URL = 'https://ai.gengjiawen.com/api/claude/'
9
9
 
@@ -139,7 +139,10 @@ export async function installDeps(): Promise<void> {
139
139
 
140
140
  if (usePnpm) {
141
141
  console.log('pnpm detected. Installing dependencies with pnpm...')
142
- await execa('pnpm', ['add', '-g', ...packages], { stdio: 'inherit' })
142
+ await execa('pnpm', ['add', '-g', ...packages], {
143
+ stdio: 'inherit',
144
+ env: PNPM_INSTALL_ENV,
145
+ })
143
146
  } else {
144
147
  console.log('pnpm not found. Falling back to npm...')
145
148
  await execa('npm', ['install', '-g', ...packages], { stdio: 'inherit' })
package/libs/codex.ts CHANGED
@@ -2,7 +2,7 @@ import * as fs from 'fs'
2
2
  import * as path from 'path'
3
3
  import * as os from 'os'
4
4
  import { execa } from 'execa'
5
- import { ensureDir, commandExists } from './utils'
5
+ import { ensureDir, commandExists, PNPM_INSTALL_ENV } from './utils'
6
6
 
7
7
  /** Return Codex configuration directory path */
8
8
  function getCodexConfigDir(): string {
@@ -47,7 +47,10 @@ export async function installCodexDeps(): Promise<void> {
47
47
 
48
48
  if (usePnpm) {
49
49
  console.log('pnpm detected. Installing Codex dependency with pnpm...')
50
- await execa('pnpm', ['add', '-g', ...packages], { stdio: 'inherit' })
50
+ await execa('pnpm', ['add', '-g', ...packages], {
51
+ stdio: 'inherit',
52
+ env: PNPM_INSTALL_ENV,
53
+ })
51
54
  } else {
52
55
  console.log('pnpm not found. Falling back to npm...')
53
56
  await execa('npm', ['install', '-g', ...packages], { stdio: 'inherit' })
@@ -2,7 +2,7 @@ import * as fs from 'fs'
2
2
  import * as path from 'path'
3
3
  import * as os from 'os'
4
4
  import { execa } from 'execa'
5
- import { ensureDir, commandExists } from './utils'
5
+ import { ensureDir, commandExists, PNPM_INSTALL_ENV } from './utils'
6
6
 
7
7
  /** Return Gemini CLI configuration directory path */
8
8
  function getGeminiConfigDir(): string {
@@ -57,7 +57,10 @@ export async function installGeminiDeps(): Promise<void> {
57
57
 
58
58
  if (usePnpm) {
59
59
  console.log('pnpm detected. Installing Gemini CLI with pnpm...')
60
- await execa('pnpm', ['add', '-g', geminiPackage], { stdio: 'inherit' })
60
+ await execa('pnpm', ['add', '-g', geminiPackage], {
61
+ stdio: 'inherit',
62
+ env: PNPM_INSTALL_ENV,
63
+ })
61
64
  } else {
62
65
  console.log('pnpm not found. Falling back to npm...')
63
66
  await execa('npm', ['install', '-g', geminiPackage], { stdio: 'inherit' })
package/libs/index.ts CHANGED
@@ -55,6 +55,9 @@ export { writeCodexConfig, installCodexDeps } from './codex'
55
55
  // Re-export gemini-cli functionality
56
56
  export { writeGeminiConfig, installGeminiDeps } from './gemini-cli'
57
57
 
58
+ // Re-export opencode functionality
59
+ export { writeOpencodeConfig, installOpencodeDeps } from './opencode'
60
+
58
61
  // Re-export all-agents functionality
59
62
  export { writeAllAgentsConfig, installAllAgentsDeps } from './all-agents'
60
63
 
@@ -0,0 +1,55 @@
1
+ import * as fs from 'fs'
2
+ import * as os from 'os'
3
+ import * as path from 'path'
4
+
5
+ jest.mock('execa', () => ({
6
+ execa: jest.fn(),
7
+ }))
8
+
9
+ import { writeOpencodeConfig } from './opencode'
10
+
11
+ describe('writeOpencodeConfig', () => {
12
+ let tempHome: string
13
+ let homedirSpy: jest.SpiedFunction<typeof os.homedir>
14
+
15
+ beforeEach(() => {
16
+ tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'os-init-opencode-'))
17
+ homedirSpy = jest.spyOn(os, 'homedir').mockReturnValue(tempHome)
18
+ })
19
+
20
+ afterEach(() => {
21
+ homedirSpy.mockRestore()
22
+ fs.rmSync(tempHome, { recursive: true, force: true })
23
+ })
24
+
25
+ test('writes OpenCode config file', () => {
26
+ const result = writeOpencodeConfig('test-api-key')
27
+
28
+ expect(result.configPath).toBe(
29
+ path.join(tempHome, '.config', 'opencode', 'opencode.json')
30
+ )
31
+
32
+ const config = JSON.parse(fs.readFileSync(result.configPath, 'utf8'))
33
+
34
+ expect(config.$schema).toBe('https://opencode.ai/config.json')
35
+ expect(config.provider.MyCustomProvider.npm).toBe(
36
+ '@ai-sdk/openai-compatible'
37
+ )
38
+ expect(config.provider.MyCustomProvider.name).toBe('JWProvider')
39
+ expect(config.provider.MyCustomProvider.options.baseURL).toBe(
40
+ 'https://ai.gengjiawen.com/api/openai/v1'
41
+ )
42
+ expect(config.provider.MyCustomProvider.options.apiKey).toBe('test-api-key')
43
+ expect(config.provider.MyCustomProvider.models.code).toEqual({
44
+ name: 'code',
45
+ })
46
+ expect(config.provider.MyCustomProvider.models.glm).toEqual({
47
+ name: 'glm',
48
+ })
49
+ expect(config.provider.MyCustomProvider.models.kimi).toEqual({
50
+ name: 'kimi',
51
+ })
52
+ expect(config.model).toBe('MyCustomProvider/code')
53
+ expect(config.small_model).toBe('MyCustomProvider/code')
54
+ })
55
+ })
@@ -0,0 +1,72 @@
1
+ import * as fs from 'fs'
2
+ import * as os from 'os'
3
+ import * as path from 'path'
4
+ import { execa } from 'execa'
5
+ import { commandExists, ensureDir, PNPM_INSTALL_ENV } from './utils'
6
+
7
+ const OPENCODE_PROVIDER_ID = 'MyCustomProvider'
8
+ const OPENCODE_MODEL_ID = 'code'
9
+ const OPENCODE_GLM_MODEL_ID = 'glm'
10
+ const OPENCODE_KIMI_MODEL_ID = 'kimi'
11
+ const OPENCODE_BASE_URL = 'https://ai.gengjiawen.com/api/openai/v1'
12
+
13
+ /** Return OpenCode configuration directory path */
14
+ function getOpencodeConfigDir(): string {
15
+ return path.join(os.homedir(), '.config', 'opencode')
16
+ }
17
+
18
+ /** Write OpenCode config file */
19
+ export function writeOpencodeConfig(apiKey: string): { configPath: string } {
20
+ const configDir = getOpencodeConfigDir()
21
+ ensureDir(configDir)
22
+
23
+ const configContent = {
24
+ $schema: 'https://opencode.ai/config.json',
25
+ provider: {
26
+ [OPENCODE_PROVIDER_ID]: {
27
+ npm: '@ai-sdk/openai-compatible',
28
+ name: 'JWProvider',
29
+ options: {
30
+ baseURL: OPENCODE_BASE_URL,
31
+ apiKey,
32
+ },
33
+ models: {
34
+ [OPENCODE_MODEL_ID]: {
35
+ name: OPENCODE_MODEL_ID,
36
+ },
37
+ [OPENCODE_GLM_MODEL_ID]: {
38
+ name: OPENCODE_GLM_MODEL_ID,
39
+ },
40
+ [OPENCODE_KIMI_MODEL_ID]: {
41
+ name: OPENCODE_KIMI_MODEL_ID,
42
+ },
43
+ },
44
+ },
45
+ },
46
+ model: `${OPENCODE_PROVIDER_ID}/${OPENCODE_MODEL_ID}`,
47
+ small_model: `${OPENCODE_PROVIDER_ID}/${OPENCODE_MODEL_ID}`,
48
+ }
49
+
50
+ const configPath = path.join(configDir, 'opencode.json')
51
+ fs.writeFileSync(configPath, JSON.stringify(configContent, null, 2))
52
+
53
+ return { configPath }
54
+ }
55
+
56
+ /** Install OpenCode dependency */
57
+ export async function installOpencodeDeps(): Promise<void> {
58
+ const packages = ['opencode-ai']
59
+ const usePnpm = await commandExists('pnpm')
60
+
61
+ if (usePnpm) {
62
+ console.log('pnpm detected. Installing OpenCode dependency with pnpm...')
63
+ await execa('pnpm', ['add', '-g', ...packages], {
64
+ stdio: 'inherit',
65
+ env: PNPM_INSTALL_ENV,
66
+ })
67
+ } else {
68
+ console.log('pnpm not found. Falling back to npm...')
69
+ await execa('npm', ['install', '-g', ...packages], { stdio: 'inherit' })
70
+ }
71
+ console.log('OpenCode dependency installed successfully.')
72
+ }
package/libs/utils.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import * as fs from 'fs'
2
2
  import { execa } from 'execa'
3
3
 
4
+ export const PNPM_INSTALL_ENV = {
5
+ PNPM_CONFIG_ENABLE_PRE_POST_SCRIPTS: 'true',
6
+ }
7
+
4
8
  /** Ensure directory exists */
5
9
  export function ensureDir(dirPath: string): void {
6
10
  fs.mkdirSync(dirPath, { recursive: true })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gengjiawen/os-init",
3
3
  "private": false,
4
- "version": "1.14.1",
4
+ "version": "1.15.0",
5
5
  "description": "",
6
6
  "main": "index.js",
7
7
  "bin": {