nim-sync 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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,199 @@
1
+ # OpenCode NVIDIA NIM Sync Plugin
2
+
3
+ A global OpenCode plugin that automatically synchronizes NVIDIA NIM models with your OpenCode configuration on startup.
4
+
5
+ ## Features
6
+
7
+ - **Automatic Sync**: On OpenCode startup, fetches the latest NVIDIA model catalog
8
+ - **Config Management**: Updates `provider.nim.models` in your OpenCode config
9
+ - **TTL Cache**: Only refreshes models if last refresh was >24 hours ago
10
+ - **Manual Refresh**: `/nim-refresh` command for force updates
11
+ - **Atomic Operations**: Safe file writes with backups and locking
12
+ - **Error Handling**: Graceful fallback when offline or missing API key
13
+
14
+ ## Installation
15
+
16
+ Install the published plugin from npm through your OpenCode config.
17
+ npm plugins are installed automatically using Bun at startup, so end users do not need to run `npm install`, build the plugin locally, or copy files into the plugins directory.
18
+ If you are working from this repository before the package is published, use the local testing flow farther below instead.
19
+
20
+ Add `nim-sync` to the `plugin` array in your global or project OpenCode config:
21
+
22
+ ```json
23
+ {
24
+ "$schema": "https://opencode.ai/config.json",
25
+ "plugin": ["nim-sync"]
26
+ }
27
+ ```
28
+
29
+ After adding the plugin, restart OpenCode.
30
+ Ensure you have an NVIDIA API key either:
31
+ - Set `NVIDIA_API_KEY` environment variable
32
+ - Run `/connect` in OpenCode to add NVIDIA credentials
33
+
34
+ On startup, the plugin refreshes the NVIDIA model catalog in the background and registers `/nim-refresh` for manual updates.
35
+
36
+ ## Configuration
37
+
38
+ The plugin manages this subtree in your OpenCode config:
39
+
40
+ ```json
41
+ {
42
+ "provider": {
43
+ "nim": {
44
+ "npm": "@ai-sdk/openai-compatible",
45
+ "name": "NVIDIA NIM",
46
+ "options": {
47
+ "baseURL": "https://integrate.api.nvidia.com/v1"
48
+ },
49
+ "models": {
50
+ "meta/llama-3.1-70b-instruct": {
51
+ "name": "Meta Llama 3.1 70B Instruct"
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## User Ownership
60
+
61
+ The plugin ONLY manages:
62
+ - `provider.nim.npm`
63
+ - `provider.nim.name`
64
+ - `provider.nim.options.baseURL`
65
+ - `provider.nim.models`
66
+
67
+ You retain control over:
68
+ - Top-level `model` selection
69
+ - `small_model` setting
70
+ - Per-model option overrides
71
+ - Any unrelated providers
72
+
73
+ ## Development
74
+
75
+ ```bash
76
+ # Install dependencies
77
+ npm install
78
+
79
+ # Run tests
80
+ npm test -- --run
81
+
82
+ # Run tests with coverage
83
+ npm run test:coverage -- --run
84
+
85
+ # Build the bundled plugin artifact
86
+ npm run build
87
+
88
+ # Type check
89
+ npm run typecheck
90
+
91
+ # Lint
92
+ npm run lint
93
+ ```
94
+
95
+ ### Local Testing
96
+
97
+ If you are testing the plugin before it is published to npm, you can still use the bundled artifact in `dist/nim-sync.mjs` with OpenCode's local plugin directory.
98
+
99
+ ## Release Automation
100
+
101
+ This repository includes [`.github/workflows/publish.yml`](.github/workflows/publish.yml), which publishes to npm whenever you push a `v*` tag.
102
+ The workflow verifies that the tag matches `package.json`, then runs `npm ci`, tests, lint, typecheck, build, and `npm publish`.
103
+
104
+ ### One-Time Setup
105
+
106
+ Because `nim-sync` is not published yet, the first release must be done manually once so the package exists on npm.
107
+
108
+ 1. Log into npm on your release machine:
109
+
110
+ ```bash
111
+ npm adduser
112
+ ```
113
+
114
+ 2. Publish the first version from this repository root:
115
+
116
+ ```bash
117
+ npm publish
118
+ ```
119
+
120
+ 3. Open the npm package settings for `nim-sync` and add a `Trusted Publisher` for GitHub Actions:
121
+ - Owner/user: `EthanBerlant`
122
+ - Repository: `nim-sync`
123
+ - Workflow filename: `publish.yml`
124
+
125
+ After that one-time setup, future releases can be fully automated from Git tags.
126
+
127
+ If you cloned the repository before the rename, update your local remote:
128
+
129
+ ```bash
130
+ git remote set-url origin https://github.com/EthanBerlant/nim-sync.git
131
+ ```
132
+
133
+ ### Releasing a New Version
134
+
135
+ The easiest flow is:
136
+
137
+ ```bash
138
+ npm version patch
139
+ git push origin HEAD --follow-tags
140
+ ```
141
+
142
+ Use `npm version minor` or `npm version major` when appropriate.
143
+
144
+ If you prefer to create the tag manually instead of using `npm version`, this works too:
145
+
146
+ ```bash
147
+ npm version --no-git-tag-version 1.0.1
148
+ git add package.json package-lock.json
149
+ git commit -m "Release 1.0.1"
150
+ git tag v1.0.1
151
+ git push origin HEAD
152
+ git push origin v1.0.1
153
+ ```
154
+
155
+ ### What the Workflow Does
156
+
157
+ On every pushed `v*` tag, GitHub Actions:
158
+ - Verifies the pushed tag matches `package.json`
159
+ - Installs dependencies with `npm ci`
160
+ - Runs the test suite
161
+ - Runs lint and typecheck
162
+ - Builds the package
163
+ - Publishes the package to npm using GitHub OIDC trusted publishing
164
+
165
+ ## Test-Driven Development
166
+
167
+ This project follows strict TDD principles:
168
+
169
+ 1. **Tests First**: All functionality has failing tests written first
170
+ 2. **80%+ Coverage**: Unit, integration, and user journey tests
171
+ 3. **User Journeys**: Tests based on actual user scenarios
172
+ 4. **Red-Green-Refactor**: Standard TDD workflow
173
+
174
+ ## Architecture
175
+
176
+ ### File Structure
177
+ ```
178
+ src/
179
+ ├── index.ts # Plugin entry point
180
+ ├── plugin/nim-sync.ts # Main plugin implementation
181
+ ├── lib/file-utils.ts # File operations with JSONC support
182
+ ├── types/index.ts # TypeScript definitions
183
+ └── __tests__/ # Test suites
184
+
185
+ scripts/
186
+ ├── clean.mjs # Cross-platform dist cleanup
187
+ └── bundle.mjs # Standalone plugin bundling
188
+ ```
189
+
190
+ ### Key Components
191
+ - **Plugin Initialization**: Runs async refresh on OpenCode startup
192
+ - **Credential Resolution**: Checks `/connect` auth or env var
193
+ - **NVIDIA API Client**: Fetches `/v1/models` endpoint
194
+ - **Config Management**: Atomically updates OpenCode config
195
+ - **Cache System**: 24-hour TTL to avoid excessive API calls
196
+
197
+ ## License
198
+
199
+ MIT
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from './types/index.js';
2
+ declare const _default: Plugin;
3
+ export default _default;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;wBAGd,MAAM;AAAtC,wBAAsC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { syncNIMModels } from './plugin/nim-sync.js';
2
+ export default syncNIMModels;
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEpD,eAAe,aAAuB,CAAA"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Options for atomic file write operations.
3
+ */
4
+ export interface AtomicWriteOptions {
5
+ /** Whether to create a backup before overwriting */
6
+ backup?: boolean;
7
+ /** Whether to create backup directory if it doesn't exist */
8
+ createBackupDir?: boolean;
9
+ }
10
+ /**
11
+ * Timeout for NVIDIA API requests in milliseconds.
12
+ */
13
+ export declare const API_TIMEOUT_MS = 30000;
14
+ /**
15
+ * Time-to-live for cached model data in milliseconds (24 hours).
16
+ */
17
+ export declare const CACHE_TTL_MS: number;
18
+ /**
19
+ * Threshold for considering a lock stale in milliseconds (5 minutes).
20
+ */
21
+ export declare const LOCK_STALE_THRESHOLD_MS: number;
22
+ /**
23
+ * Interval between lock acquisition retry attempts in milliseconds.
24
+ */
25
+ export declare const LOCK_RETRY_INTERVAL_MS = 100;
26
+ /**
27
+ * Minimum interval between manual refresh operations in milliseconds (60 seconds).
28
+ */
29
+ export declare const MIN_MANUAL_REFRESH_INTERVAL_MS = 60000;
30
+ /**
31
+ * Maximum number of backup files to retain.
32
+ */
33
+ export declare const MAX_BACKUPS = 5;
34
+ /**
35
+ * Reads and parses a JSONC (JSON with Comments) file.
36
+ *
37
+ * @param filePath - Path to the JSONC file
38
+ * @param validate - Optional validation function to ensure type safety
39
+ * @returns Parsed content of type T, or empty object if file doesn't exist
40
+ * @throws Error if file read fails (except ENOENT) or if validation fails
41
+ */
42
+ export declare function readJSONC<T = unknown>(filePath: string, validate?: (data: unknown) => data is T): Promise<T>;
43
+ /**
44
+ * Writes data to a JSONC file with atomic operations.
45
+ *
46
+ * @param filePath - Path to write the file
47
+ * @param data - Data to serialize as JSON
48
+ * @param options - Optional backup and directory creation settings
49
+ */
50
+ export declare function writeJSONC<T = unknown>(filePath: string, data: T, options?: AtomicWriteOptions): Promise<void>;
51
+ /**
52
+ * Updates a specific path within a JSONC file while preserving unrelated
53
+ * comments and formatting.
54
+ *
55
+ * @param filePath - Path to the JSONC file
56
+ * @param jsonPath - JSON path to update
57
+ * @param data - Value to write at the given path
58
+ * @param options - Optional backup and directory creation settings
59
+ */
60
+ export declare function updateJSONCPath<T = unknown>(filePath: string, jsonPath: Array<string | number>, data: T, options?: AtomicWriteOptions): Promise<void>;
61
+ /**
62
+ * Atomically writes content to a file using temp file + rename pattern.
63
+ * Optionally creates backups before overwriting and cleans up old backups.
64
+ *
65
+ * @param filePath - Path to write the file
66
+ * @param content - String content to write
67
+ * @param options - Optional backup and directory creation settings
68
+ */
69
+ export declare function atomicWrite(filePath: string, content: string, options?: AtomicWriteOptions): Promise<void>;
70
+ /**
71
+ * Ensures a directory exists, creating it recursively if needed.
72
+ *
73
+ * @param dirPath - Path to the directory
74
+ */
75
+ export declare function ensureDir(dirPath: string): Promise<void>;
76
+ /**
77
+ * Returns the platform-specific config directory path.
78
+ *
79
+ * @returns Config directory path
80
+ */
81
+ export declare function getConfigDir(): string;
82
+ /**
83
+ * Returns the platform-specific cache directory path.
84
+ *
85
+ * @returns Cache directory path
86
+ */
87
+ export declare function getCacheDir(): string;
88
+ /**
89
+ * Returns the platform-specific data directory path.
90
+ *
91
+ * @returns Data directory path
92
+ */
93
+ export declare function getDataDir(): string;
94
+ /**
95
+ * Acquires an exclusive lock for coordinating file operations.
96
+ * Automatically cleans up stale locks from crashed processes.
97
+ *
98
+ * @param lockName - Name of the lock
99
+ * @param timeoutMs - Maximum time to wait for lock acquisition (default: 5000ms)
100
+ * @returns Function to release the lock
101
+ * @throws Error if lock cannot be acquired within timeout
102
+ */
103
+ export declare function acquireLock(lockName: string, timeoutMs?: number): Promise<() => Promise<void>>;
104
+ //# sourceMappingURL=file-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/lib/file-utils.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,oDAAoD;IACpD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,6DAA6D;IAC7D,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,QAAS,CAAA;AAEpC;;GAEG;AACH,eAAO,MAAM,YAAY,QAAsB,CAAA;AAE/C;;GAEG;AACH,eAAO,MAAM,uBAAuB,QAAgB,CAAA;AAEpD;;GAEG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAA;AAEzC;;GAEG;AACH,eAAO,MAAM,8BAA8B,QAAS,CAAA;AAEpD;;GAEG;AACH,eAAO,MAAM,WAAW,IAAI,CAAA;AAE5B;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAAC,CAAC,GAAG,OAAO,EACzC,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,IAAI,CAAC,GACtC,OAAO,CAAC,CAAC,CAAC,CAwBZ;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,CAAC,GAAG,OAAO,EAC1C,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAC/C,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,EAChC,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAsBf;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAsCf;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9D;AA+BD;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AA8CD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAuDlG"}
@@ -0,0 +1,312 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { applyEdits, modify as modifyJSONC, parse as parseJSONC } from 'jsonc-parser/lib/esm/main.js';
4
+ /**
5
+ * Timeout for NVIDIA API requests in milliseconds.
6
+ */
7
+ export const API_TIMEOUT_MS = 30_000;
8
+ /**
9
+ * Time-to-live for cached model data in milliseconds (24 hours).
10
+ */
11
+ export const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
12
+ /**
13
+ * Threshold for considering a lock stale in milliseconds (5 minutes).
14
+ */
15
+ export const LOCK_STALE_THRESHOLD_MS = 5 * 60 * 1000;
16
+ /**
17
+ * Interval between lock acquisition retry attempts in milliseconds.
18
+ */
19
+ export const LOCK_RETRY_INTERVAL_MS = 100;
20
+ /**
21
+ * Minimum interval between manual refresh operations in milliseconds (60 seconds).
22
+ */
23
+ export const MIN_MANUAL_REFRESH_INTERVAL_MS = 60_000;
24
+ /**
25
+ * Maximum number of backup files to retain.
26
+ */
27
+ export const MAX_BACKUPS = 5;
28
+ /**
29
+ * Reads and parses a JSONC (JSON with Comments) file.
30
+ *
31
+ * @param filePath - Path to the JSONC file
32
+ * @param validate - Optional validation function to ensure type safety
33
+ * @returns Parsed content of type T, or empty object if file doesn't exist
34
+ * @throws Error if file read fails (except ENOENT) or if validation fails
35
+ */
36
+ export async function readJSONC(filePath, validate) {
37
+ try {
38
+ const content = await fs.readFile(filePath, 'utf-8');
39
+ const errors = [];
40
+ const result = parseJSONC(content, errors);
41
+ if (errors.length > 0) {
42
+ const errorDetails = errors
43
+ .map(e => `Parse error code ${e.error} at offset ${e.offset}`)
44
+ .join('; ');
45
+ throw new Error(`JSONC parse errors in ${filePath}: ${errorDetails}`);
46
+ }
47
+ if (validate && !validate(result)) {
48
+ throw new Error(`Invalid data structure in ${filePath}`);
49
+ }
50
+ return result;
51
+ }
52
+ catch (error) {
53
+ if (error.code === 'ENOENT') {
54
+ return {};
55
+ }
56
+ throw error;
57
+ }
58
+ }
59
+ /**
60
+ * Writes data to a JSONC file with atomic operations.
61
+ *
62
+ * @param filePath - Path to write the file
63
+ * @param data - Data to serialize as JSON
64
+ * @param options - Optional backup and directory creation settings
65
+ */
66
+ export async function writeJSONC(filePath, data, options) {
67
+ const content = JSON.stringify(data, null, 2);
68
+ await atomicWrite(filePath, content, options);
69
+ }
70
+ /**
71
+ * Updates a specific path within a JSONC file while preserving unrelated
72
+ * comments and formatting.
73
+ *
74
+ * @param filePath - Path to the JSONC file
75
+ * @param jsonPath - JSON path to update
76
+ * @param data - Value to write at the given path
77
+ * @param options - Optional backup and directory creation settings
78
+ */
79
+ export async function updateJSONCPath(filePath, jsonPath, data, options) {
80
+ let existingContent = '';
81
+ try {
82
+ existingContent = await fs.readFile(filePath, 'utf-8');
83
+ }
84
+ catch (error) {
85
+ if (error.code !== 'ENOENT') {
86
+ throw error;
87
+ }
88
+ }
89
+ const eol = existingContent.includes('\r\n') ? '\r\n' : '\n';
90
+ const edits = modifyJSONC(existingContent, jsonPath, data, {
91
+ formattingOptions: {
92
+ insertSpaces: true,
93
+ tabSize: 2,
94
+ eol
95
+ }
96
+ });
97
+ const updatedContent = applyEdits(existingContent, edits);
98
+ await atomicWrite(filePath, updatedContent, options);
99
+ }
100
+ /**
101
+ * Atomically writes content to a file using temp file + rename pattern.
102
+ * Optionally creates backups before overwriting and cleans up old backups.
103
+ *
104
+ * @param filePath - Path to write the file
105
+ * @param content - String content to write
106
+ * @param options - Optional backup and directory creation settings
107
+ */
108
+ export async function atomicWrite(filePath, content, options = {}) {
109
+ const dir = path.dirname(filePath);
110
+ const tempPath = `${filePath}.${Date.now()}.tmp`;
111
+ try {
112
+ await fs.mkdir(dir, { recursive: true });
113
+ if (options.backup) {
114
+ try {
115
+ await fs.access(filePath);
116
+ const backupDir = path.join(dir, 'backups');
117
+ if (options.createBackupDir) {
118
+ await fs.mkdir(backupDir, { recursive: true });
119
+ }
120
+ const backupPath = path.join(backupDir, `${path.basename(filePath)}.${Date.now()}.bak`);
121
+ await fs.copyFile(filePath, backupPath);
122
+ // Clean up old backups after creating new one
123
+ await cleanupOldBackups(backupDir, path.basename(filePath));
124
+ }
125
+ catch (error) {
126
+ if (error.code !== 'ENOENT') {
127
+ throw new Error(`Failed to create backup: ${error instanceof Error ? error.message : 'Unknown error'}`);
128
+ }
129
+ }
130
+ }
131
+ await fs.writeFile(tempPath, content, 'utf-8');
132
+ await fs.rename(tempPath, filePath);
133
+ }
134
+ catch (error) {
135
+ try {
136
+ await fs.unlink(tempPath);
137
+ }
138
+ catch {
139
+ // Ignore temp file cleanup failures
140
+ }
141
+ throw error;
142
+ }
143
+ }
144
+ /**
145
+ * Ensures a directory exists, creating it recursively if needed.
146
+ *
147
+ * @param dirPath - Path to the directory
148
+ */
149
+ export async function ensureDir(dirPath) {
150
+ try {
151
+ await fs.mkdir(dirPath, { recursive: true });
152
+ }
153
+ catch (error) {
154
+ if (error.code !== 'EEXIST') {
155
+ throw error;
156
+ }
157
+ }
158
+ }
159
+ /**
160
+ * Returns platform-specific paths for config, data, and cache directories.
161
+ * Handles Windows, Linux, and macOS path conventions.
162
+ *
163
+ * @returns Platform-specific directory paths
164
+ * @example
165
+ * ```typescript
166
+ * const paths = getPlatformPaths()
167
+ * // Windows: { config: 'C:\\Users\\username\\AppData\\Roaming\\opencode', ... }
168
+ * // Linux: { config: '/home/username/.config/opencode', ... }
169
+ * ```
170
+ */
171
+ function getPlatformPaths() {
172
+ const home = process.env.HOME || process.env.USERPROFILE || '';
173
+ const isWindows = process.platform === 'win32';
174
+ return {
175
+ config: isWindows
176
+ ? path.join(home, 'AppData', 'Roaming', 'opencode')
177
+ : path.join(home, '.config', 'opencode'),
178
+ data: isWindows
179
+ ? path.join(home, 'AppData', 'Roaming', 'opencode')
180
+ : path.join(home, '.local', 'share', 'opencode'),
181
+ cache: isWindows
182
+ ? path.join(home, 'AppData', 'Local', 'opencode', 'cache')
183
+ : path.join(home, '.cache', 'opencode')
184
+ };
185
+ }
186
+ /**
187
+ * Returns the platform-specific config directory path.
188
+ *
189
+ * @returns Config directory path
190
+ */
191
+ export function getConfigDir() {
192
+ return getPlatformPaths().config;
193
+ }
194
+ /**
195
+ * Returns the platform-specific cache directory path.
196
+ *
197
+ * @returns Cache directory path
198
+ */
199
+ export function getCacheDir() {
200
+ return getPlatformPaths().cache;
201
+ }
202
+ /**
203
+ * Returns the platform-specific data directory path.
204
+ *
205
+ * @returns Data directory path
206
+ */
207
+ export function getDataDir() {
208
+ return getPlatformPaths().data;
209
+ }
210
+ /**
211
+ * Checks if a process with the given PID is currently running.
212
+ * Uses a non-destructive signal (0) to check process existence.
213
+ *
214
+ * @param pid - Process ID to check
215
+ * @returns true if the process exists, false otherwise
216
+ */
217
+ function isProcessRunning(pid) {
218
+ try {
219
+ // Signal 0 doesn't kill the process, just checks if it exists
220
+ process.kill(pid, 0);
221
+ return true;
222
+ }
223
+ catch {
224
+ return false;
225
+ }
226
+ }
227
+ /**
228
+ * Cleans up old backup files, keeping only the most recent MAX_BACKUPS.
229
+ *
230
+ * @param backupDir - Directory containing backup files
231
+ * @param baseName - Base name of the file being backed up
232
+ */
233
+ async function cleanupOldBackups(backupDir, baseName) {
234
+ try {
235
+ const files = await fs.readdir(backupDir);
236
+ const backups = files
237
+ .filter(f => f.startsWith(baseName) && f.endsWith('.bak'))
238
+ .map(f => ({
239
+ name: f,
240
+ // Extract timestamp from filename like "file.json.1234567890.bak"
241
+ timestamp: parseInt(f.split('.').slice(-2, -1)[0]) || 0
242
+ }))
243
+ .sort((a, b) => b.timestamp - a.timestamp); // Sort newest first
244
+ // Keep only the most recent backups
245
+ for (const oldBackup of backups.slice(MAX_BACKUPS)) {
246
+ await fs.unlink(path.join(backupDir, oldBackup.name));
247
+ }
248
+ }
249
+ catch {
250
+ // Ignore cleanup failures - don't want to fail the main operation
251
+ }
252
+ }
253
+ /**
254
+ * Acquires an exclusive lock for coordinating file operations.
255
+ * Automatically cleans up stale locks from crashed processes.
256
+ *
257
+ * @param lockName - Name of the lock
258
+ * @param timeoutMs - Maximum time to wait for lock acquisition (default: 5000ms)
259
+ * @returns Function to release the lock
260
+ * @throws Error if lock cannot be acquired within timeout
261
+ */
262
+ export async function acquireLock(lockName, timeoutMs = 5000) {
263
+ const lockDir = getCacheDir();
264
+ const lockPath = path.join(lockDir, `${lockName}.lock`);
265
+ await ensureDir(lockDir);
266
+ try {
267
+ const lockContent = await fs.readFile(lockPath, 'utf-8');
268
+ const metadata = JSON.parse(lockContent);
269
+ const staleThreshold = Date.now() - LOCK_STALE_THRESHOLD_MS;
270
+ // Check if timestamp is stale OR if the holding process is no longer running
271
+ const isStale = metadata.timestamp < staleThreshold;
272
+ const processExists = metadata.pid ? isProcessRunning(metadata.pid) : true;
273
+ // Note: TOCTOU race possible between check and delete, but mitigated by
274
+ // atomic 'wx' flag in subsequent fs.open() call which provides ultimate protection
275
+ if (isStale || !processExists) {
276
+ await fs.unlink(lockPath);
277
+ }
278
+ }
279
+ catch (error) {
280
+ if (error.code !== 'ENOENT') {
281
+ console.error('Failed to clean up stale lock');
282
+ }
283
+ }
284
+ const startTime = Date.now();
285
+ while (Date.now() - startTime < timeoutMs) {
286
+ try {
287
+ const fd = await fs.open(lockPath, 'wx');
288
+ const metadata = {
289
+ pid: process.pid,
290
+ timestamp: Date.now()
291
+ };
292
+ await fd.writeFile(JSON.stringify(metadata));
293
+ await fd.close();
294
+ return async () => {
295
+ try {
296
+ await fs.unlink(lockPath);
297
+ }
298
+ catch {
299
+ // Ignore unlock failures
300
+ }
301
+ };
302
+ }
303
+ catch (error) {
304
+ if (error.code !== 'EEXIST') {
305
+ throw error;
306
+ }
307
+ await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
308
+ }
309
+ }
310
+ throw new Error(`Failed to acquire lock "${lockName}" after ${timeoutMs}ms`);
311
+ }
312
+ //# sourceMappingURL=file-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-utils.js","sourceRoot":"","sources":["../../src/lib/file-utils.ts"],"names":[],"mappings":"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"}