opencode-synced 0.5.0 → 0.6.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.
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { deepMerge, parseJsonc, pathExists, stripOverrides, writeJsonFile } from './config.js';
3
+ import { deepMerge, hasOwn, parseJsonc, pathExists, stripOverrides, writeJsonFile, } from './config.js';
4
4
  import { extractMcpSecrets, hasOverrides, mergeOverrides, stripOverrideKeys, } from './mcp-secrets.js';
5
5
  import { normalizePath } from './paths.js';
6
6
  export async function syncRepoToLocal(plan, overrides) {
@@ -214,7 +214,7 @@ function isDeepEqual(left, right) {
214
214
  if (leftKeys.length !== rightKeys.length)
215
215
  return false;
216
216
  for (const key of leftKeys) {
217
- if (!Object.hasOwn(right, key))
217
+ if (!hasOwn(right, key))
218
218
  return false;
219
219
  if (!isDeepEqual(left[key], right[key])) {
220
220
  return false;
@@ -35,3 +35,5 @@ export declare function writeJsonFile(filePath: string, data: unknown, options?:
35
35
  jsonc: boolean;
36
36
  mode?: number;
37
37
  }): Promise<void>;
38
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
39
+ export declare function hasOwn(target: Record<string, unknown>, key: string): boolean;
@@ -106,31 +106,14 @@ export function stripOverrides(localConfig, overrides, baseConfig) {
106
106
  return result;
107
107
  }
108
108
  export function parseJsonc(content) {
109
- const stripped = stripJsonComments(content);
110
- return JSON.parse(stripped);
111
- }
112
- export async function writeJsonFile(filePath, data, options = { jsonc: false }) {
113
- const json = JSON.stringify(data, null, 2);
114
- const content = options.jsonc ? `// Generated by opencode-synced\n${json}\n` : `${json}\n`;
115
- await fs.writeFile(filePath, content, 'utf8');
116
- if (options.mode !== undefined) {
117
- await fs.chmod(filePath, options.mode);
118
- }
119
- }
120
- function isPlainObject(value) {
121
- if (!value || typeof value !== 'object')
122
- return false;
123
- return Object.getPrototypeOf(value) === Object.prototype;
124
- }
125
- function stripJsonComments(input) {
126
109
  let output = '';
127
110
  let inString = false;
128
111
  let inSingleLine = false;
129
112
  let inMultiLine = false;
130
113
  let escapeNext = false;
131
- for (let i = 0; i < input.length; i += 1) {
132
- const current = input[i];
133
- const next = input[i + 1];
114
+ for (let i = 0; i < content.length; i += 1) {
115
+ const current = content[i];
116
+ const next = content[i + 1];
134
117
  if (inSingleLine) {
135
118
  if (current === '\n') {
136
119
  inSingleLine = false;
@@ -160,7 +143,7 @@ function stripJsonComments(input) {
160
143
  }
161
144
  continue;
162
145
  }
163
- if (current === '"' && !inString) {
146
+ if (current === '"') {
164
147
  inString = true;
165
148
  output += current;
166
149
  continue;
@@ -175,7 +158,33 @@ function stripJsonComments(input) {
175
158
  i += 1;
176
159
  continue;
177
160
  }
161
+ if (current === ',') {
162
+ let nextIndex = i + 1;
163
+ while (nextIndex < content.length && /\s/.test(content[nextIndex])) {
164
+ nextIndex += 1;
165
+ }
166
+ const nextChar = content[nextIndex];
167
+ if (nextChar === '}' || nextChar === ']') {
168
+ continue;
169
+ }
170
+ }
178
171
  output += current;
179
172
  }
180
- return output;
173
+ return JSON.parse(output);
174
+ }
175
+ export async function writeJsonFile(filePath, data, options = { jsonc: false }) {
176
+ const json = JSON.stringify(data, null, 2);
177
+ const content = options.jsonc ? `// Generated by opencode-synced\n${json}\n` : `${json}\n`;
178
+ await fs.writeFile(filePath, content, 'utf8');
179
+ if (options.mode !== undefined) {
180
+ await fs.chmod(filePath, options.mode);
181
+ }
182
+ }
183
+ export function isPlainObject(value) {
184
+ if (!value || typeof value !== 'object')
185
+ return false;
186
+ return Object.getPrototypeOf(value) === Object.prototype;
187
+ }
188
+ export function hasOwn(target, key) {
189
+ return Object.hasOwn(target, key);
181
190
  }
@@ -1,4 +1,4 @@
1
- import { deepMerge } from './config.js';
1
+ import { deepMerge, hasOwn, isPlainObject } from './config.js';
2
2
  const ENV_PLACEHOLDER_PATTERN = /\{env:[^}]+\}/i;
3
3
  export function extractMcpSecrets(config) {
4
4
  const sanitizedConfig = cloneConfig(config);
@@ -88,15 +88,7 @@ function setNestedValue(target, path, value) {
88
88
  function getPlainObject(value) {
89
89
  return isPlainObject(value) ? value : null;
90
90
  }
91
- function isPlainObject(value) {
92
- if (!value || typeof value !== 'object')
93
- return false;
94
- return Object.getPrototypeOf(value) === Object.prototype;
95
- }
96
91
  function cloneConfig(config) {
97
- if (typeof structuredClone === 'function') {
98
- return structuredClone(config);
99
- }
100
92
  return JSON.parse(JSON.stringify(config));
101
93
  }
102
94
  export function mergeOverrides(base, extra) {
@@ -108,7 +100,7 @@ export function stripOverrideKeys(base, toRemove) {
108
100
  }
109
101
  const result = { ...base };
110
102
  for (const [key, removeValue] of Object.entries(toRemove)) {
111
- if (!Object.hasOwn(result, key))
103
+ if (!hasOwn(result, key))
112
104
  continue;
113
105
  const currentValue = result[key];
114
106
  if (isPlainObject(removeValue) && isPlainObject(currentValue)) {
@@ -10,7 +10,16 @@ export function createSyncService(ctx) {
10
10
  const log = createLogger(ctx.client);
11
11
  return {
12
12
  startupSync: async () => {
13
- const config = await loadSyncConfig(locations);
13
+ let config = null;
14
+ try {
15
+ config = await loadSyncConfig(locations);
16
+ }
17
+ catch (error) {
18
+ const message = `Failed to load opencode-synced config: ${formatError(error)}`;
19
+ log.error(message, { path: locations.syncConfigPath });
20
+ await showToast(ctx.client, `Failed to load opencode-synced config. Check ${locations.syncConfigPath} for JSON errors.`, 'error');
21
+ return;
22
+ }
14
23
  if (!config) {
15
24
  await showToast(ctx.client, 'Configure opencode-synced with /sync-init or link to an existing repo with /sync-link', 'info');
16
25
  return;
@@ -23,9 +23,14 @@ function log(client, level, message, extra) {
23
23
  });
24
24
  }
25
25
  export async function showToast(client, message, variant) {
26
- await client.tui.showToast({
27
- body: { title: 'opencode-synced plugin', message, variant },
28
- });
26
+ try {
27
+ await client.tui.showToast({
28
+ body: { title: 'opencode-synced plugin', message, variant },
29
+ });
30
+ }
31
+ catch {
32
+ // Ignore toast failures (e.g. headless mode or early startup).
33
+ }
29
34
  }
30
35
  export function unwrapData(response) {
31
36
  if (!response || typeof response !== 'object')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-synced",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Sync global OpenCode config across machines via GitHub.",
5
5
  "author": {
6
6
  "name": "Ian Hildebrand"