Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/opencode-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,22 @@ jobs:
}
EOF

SYNC_REPO="$XDG_DATA_HOME/opencode-synced/repo"
mkdir -p "$SYNC_REPO"
git -C "$SYNC_REPO" init -q

cat > "$XDG_CONFIG_HOME/opencode/opencode-synced.jsonc" <<EOF
{
"repo": {
"owner": "smoke",
"name": "opencode-config",
},
"localRepoPath": "$SYNC_REPO",
"includeSecrets": false,
"extraSecretPaths": [],
}
EOF

rm -rf "$XDG_CACHE_HOME/opencode/node_modules"

- name: Launch opencode (smoke)
Expand Down
1 change: 1 addition & 0 deletions opencode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
8 changes: 2 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"files": ["dist"],
"dependencies": {
"@opencode-ai/plugin": "1.0.85"
},
Expand All @@ -51,8 +49,6 @@
"prepare": "husky"
},
"lint-staged": {
"*.{js,ts,json}": [
"biome check --write --no-errors-on-unmatched"
]
"*.{js,ts,json}": ["biome check --write --no-errors-on-unmatched"]
}
}
30 changes: 29 additions & 1 deletion src/sync/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { describe, expect, it } from 'vitest';

import { canCommitMcpSecrets, deepMerge, normalizeSyncConfig, stripOverrides } from './config.js';
import {
canCommitMcpSecrets,
deepMerge,
normalizeSyncConfig,
parseJsonc,
stripOverrides,
} from './config.js';

describe('deepMerge', () => {
it('merges nested objects and replaces arrays', () => {
Expand Down Expand Up @@ -70,3 +76,25 @@ describe('canCommitMcpSecrets', () => {
expect(canCommitMcpSecrets({ includeSecrets: true, includeMcpSecrets: true })).toBe(true);
});
});

describe('parseJsonc', () => {
it('parses JSONC with comments and trailing commas', () => {
const input = `{
// comment
"repo": {
"owner": "me",
"name": "opencode-config",
},
"includeSecrets": false,
"extraSecretPaths": [
"foo",
],
}`;

expect(parseJsonc(input)).toEqual({
repo: { owner: 'me', name: 'opencode-config' },
includeSecrets: false,
extraSecretPaths: ['foo'],
});
});
});
70 changes: 38 additions & 32 deletions src/sync/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,42 +161,15 @@ export function stripOverrides(
}

export function parseJsonc<T>(content: string): T {
const stripped = stripJsonComments(content);
return JSON.parse(stripped) as T;
}

export async function writeJsonFile(
filePath: string,
data: unknown,
options: { jsonc: boolean; mode?: number } = { jsonc: false }
): Promise<void> {
const json = JSON.stringify(data, null, 2);
const content = options.jsonc ? `// Generated by opencode-synced\n${json}\n` : `${json}\n`;
await fs.writeFile(filePath, content, 'utf8');
if (options.mode !== undefined) {
await fs.chmod(filePath, options.mode);
}
}

export function isPlainObject(value: unknown): value is Record<string, unknown> {
if (!value || typeof value !== 'object') return false;
return Object.getPrototypeOf(value) === Object.prototype;
}

export function hasOwn(target: Record<string, unknown>, key: string): boolean {
return Object.hasOwn(target, key);
}

function stripJsonComments(input: string): string {
let output = '';
let inString = false;
let inSingleLine = false;
let inMultiLine = false;
let escapeNext = false;

for (let i = 0; i < input.length; i += 1) {
const current = input[i];
const next = input[i + 1];
for (let i = 0; i < content.length; i += 1) {
const current = content[i];
const next = content[i + 1];

if (inSingleLine) {
if (current === '\n') {
Expand Down Expand Up @@ -230,7 +203,7 @@ function stripJsonComments(input: string): string {
continue;
}

if (current === '"' && !inString) {
if (current === '"') {
inString = true;
output += current;
continue;
Expand All @@ -248,8 +221,41 @@ function stripJsonComments(input: string): string {
continue;
}

if (current === ',') {
let nextIndex = i + 1;
while (nextIndex < content.length && /\s/.test(content[nextIndex])) {
nextIndex += 1;
}
const nextChar = content[nextIndex];
if (nextChar === '}' || nextChar === ']') {
continue;
}
}

output += current;
}

return output;
return JSON.parse(output) as T;
}

export async function writeJsonFile(
filePath: string,
data: unknown,
options: { jsonc: boolean; mode?: number } = { jsonc: false }
): Promise<void> {
const json = JSON.stringify(data, null, 2);
const content = options.jsonc ? `// Generated by opencode-synced\n${json}\n` : `${json}\n`;
await fs.writeFile(filePath, content, 'utf8');
if (options.mode !== undefined) {
await fs.chmod(filePath, options.mode);
}
}

export function isPlainObject(value: unknown): value is Record<string, unknown> {
if (!value || typeof value !== 'object') return false;
return Object.getPrototypeOf(value) === Object.prototype;
}

export function hasOwn(target: Record<string, unknown>, key: string): boolean {
return Object.hasOwn(target, key);
}
14 changes: 13 additions & 1 deletion src/sync/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,19 @@ export function createSyncService(ctx: SyncServiceContext): SyncService {

return {
startupSync: async () => {
const config = await loadSyncConfig(locations);
let config: ReturnType<typeof normalizeSyncConfig> | null = null;
try {
config = await loadSyncConfig(locations);
} catch (error) {
const message = `Failed to load opencode-synced config: ${formatError(error)}`;
log.error(message, { path: locations.syncConfigPath });
await showToast(
ctx.client,
`Failed to load opencode-synced config. Check ${locations.syncConfigPath} for JSON errors.`,
'error'
);
return;
}
if (!config) {
await showToast(
ctx.client,
Expand Down
10 changes: 7 additions & 3 deletions src/sync/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,13 @@ export async function showToast(
message: string,
variant: 'info' | 'success' | 'warning' | 'error'
): Promise<void> {
await client.tui.showToast({
body: { title: 'opencode-synced plugin', message, variant },
});
try {
await client.tui.showToast({
body: { title: 'opencode-synced plugin', message, variant },
});
} catch {
// Ignore toast failures (e.g. headless mode or early startup).
}
}

export function unwrapData<T>(response: unknown): T | null {
Expand Down
Loading