opencode-synced 0.3.0 → 0.4.1

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.
@@ -0,0 +1,453 @@
1
+ import { syncLocalToRepo, syncRepoToLocal } from './apply.js';
2
+ import { generateCommitMessage } from './commit.js';
3
+ import { loadOverrides, loadState, loadSyncConfig, normalizeSyncConfig, writeState, writeSyncConfig, } from './config.js';
4
+ import { SyncCommandError, SyncConfigMissingError } from './errors.js';
5
+ import { buildSyncPlan, resolveRepoRoot, resolveSyncLocations } from './paths.js';
6
+ import { commitAll, ensureRepoCloned, ensureRepoPrivate, fetchAndFastForward, findSyncRepo, getAuthenticatedUser, getRepoStatus, hasLocalChanges, isRepoCloned, pushBranch, repoExists, resolveRepoBranch, resolveRepoIdentifier, } from './repo.js';
7
+ import { createLogger, extractTextFromResponse, resolveSmallModel, showToast, unwrapData, } from './utils.js';
8
+ export function createSyncService(ctx) {
9
+ const locations = resolveSyncLocations();
10
+ const log = createLogger(ctx.client);
11
+ return {
12
+ startupSync: async () => {
13
+ const config = await loadSyncConfig(locations);
14
+ if (!config) {
15
+ await showToast(ctx.client, 'Configure opencode-synced with /sync-init.', 'info');
16
+ return;
17
+ }
18
+ try {
19
+ await runStartup(ctx, locations, config, log);
20
+ }
21
+ catch (error) {
22
+ log.error('Startup sync failed', { error: formatError(error) });
23
+ await showToast(ctx.client, formatError(error), 'error');
24
+ }
25
+ },
26
+ status: async () => {
27
+ const config = await loadSyncConfig(locations);
28
+ if (!config) {
29
+ return 'opencode-synced is not configured. Run /sync-init to set it up.';
30
+ }
31
+ const repoRoot = resolveRepoRoot(config, locations);
32
+ const state = await loadState(locations);
33
+ let repoStatus = [];
34
+ let branch = resolveRepoBranch(config);
35
+ const cloned = await isRepoCloned(repoRoot);
36
+ if (!cloned) {
37
+ repoStatus = ['Repo not cloned'];
38
+ }
39
+ else {
40
+ try {
41
+ const status = await getRepoStatus(ctx.$, repoRoot);
42
+ repoStatus = status.changes;
43
+ branch = status.branch;
44
+ }
45
+ catch {
46
+ repoStatus = ['Repo status unavailable'];
47
+ }
48
+ }
49
+ const repoIdentifier = resolveRepoIdentifier(config);
50
+ const includeSecrets = config.includeSecrets ? 'enabled' : 'disabled';
51
+ const includeSessions = config.includeSessions ? 'enabled' : 'disabled';
52
+ const includePromptStash = config.includePromptStash ? 'enabled' : 'disabled';
53
+ const lastPull = state.lastPull ?? 'never';
54
+ const lastPush = state.lastPush ?? 'never';
55
+ let changesLabel = 'clean';
56
+ if (!cloned) {
57
+ changesLabel = 'not cloned';
58
+ }
59
+ else if (repoStatus.length > 0) {
60
+ if (repoStatus[0] === 'Repo status unavailable') {
61
+ changesLabel = 'unknown';
62
+ }
63
+ else {
64
+ changesLabel = `${repoStatus.length} pending`;
65
+ }
66
+ }
67
+ const statusLines = [
68
+ `Repo: ${repoIdentifier}`,
69
+ `Branch: ${branch}`,
70
+ `Secrets: ${includeSecrets}`,
71
+ `Sessions: ${includeSessions}`,
72
+ `Prompt stash: ${includePromptStash}`,
73
+ `Last pull: ${lastPull}`,
74
+ `Last push: ${lastPush}`,
75
+ `Working tree: ${changesLabel}`,
76
+ ];
77
+ return statusLines.join('\n');
78
+ },
79
+ init: async (options) => {
80
+ const config = await buildConfigFromInit(ctx.$, options);
81
+ const repoIdentifier = resolveRepoIdentifier(config);
82
+ const isPrivate = options.private ?? true;
83
+ const exists = await repoExists(ctx.$, repoIdentifier);
84
+ let created = false;
85
+ if (!exists) {
86
+ await createRepo(ctx.$, config, isPrivate);
87
+ created = true;
88
+ }
89
+ await writeSyncConfig(locations, config);
90
+ const repoRoot = resolveRepoRoot(config, locations);
91
+ await ensureRepoCloned(ctx.$, config, repoRoot);
92
+ await ensureSecretsPolicy(ctx, config);
93
+ if (created) {
94
+ const overrides = await loadOverrides(locations);
95
+ const plan = buildSyncPlan(config, locations, repoRoot);
96
+ await syncLocalToRepo(plan, overrides);
97
+ const dirty = await hasLocalChanges(ctx.$, repoRoot);
98
+ if (dirty) {
99
+ const branch = resolveRepoBranch(config);
100
+ await commitAll(ctx.$, repoRoot, 'Initial sync from opencode-synced');
101
+ await pushBranch(ctx.$, repoRoot, branch);
102
+ await writeState(locations, { lastPush: new Date().toISOString() });
103
+ }
104
+ }
105
+ const lines = [
106
+ 'opencode-synced configured.',
107
+ `Repo: ${repoIdentifier}${created ? ' (created)' : ''}`,
108
+ `Branch: ${resolveRepoBranch(config)}`,
109
+ `Local repo: ${repoRoot}`,
110
+ ];
111
+ return lines.join('\n');
112
+ },
113
+ link: async (options) => {
114
+ const found = await findSyncRepo(ctx.$, options.repo);
115
+ if (!found) {
116
+ const searchedFor = options.repo
117
+ ? `"${options.repo}"`
118
+ : 'common sync repo names (my-opencode-config, opencode-config, etc.)';
119
+ const lines = [
120
+ `Could not find an existing sync repo. Searched for: ${searchedFor}`,
121
+ '',
122
+ 'To link to an existing repo, run:',
123
+ ' /sync-link <repo-name>',
124
+ '',
125
+ 'To create a new sync repo, run:',
126
+ ' /sync-init',
127
+ ];
128
+ return lines.join('\n');
129
+ }
130
+ const config = normalizeSyncConfig({
131
+ repo: { owner: found.owner, name: found.name },
132
+ includeSecrets: false,
133
+ includeSessions: false,
134
+ includePromptStash: false,
135
+ extraSecretPaths: [],
136
+ });
137
+ await writeSyncConfig(locations, config);
138
+ const repoRoot = resolveRepoRoot(config, locations);
139
+ await ensureRepoCloned(ctx.$, config, repoRoot);
140
+ const branch = await resolveBranch(ctx, config, repoRoot);
141
+ await fetchAndFastForward(ctx.$, repoRoot, branch);
142
+ const overrides = await loadOverrides(locations);
143
+ const plan = buildSyncPlan(config, locations, repoRoot);
144
+ await syncRepoToLocal(plan, overrides);
145
+ await writeState(locations, {
146
+ lastPull: new Date().toISOString(),
147
+ lastRemoteUpdate: new Date().toISOString(),
148
+ });
149
+ const lines = [
150
+ `Linked to existing sync repo: ${found.owner}/${found.name}`,
151
+ '',
152
+ 'Your local OpenCode config has been OVERWRITTEN with the synced config.',
153
+ 'Your local overrides file was preserved and applied on top.',
154
+ '',
155
+ 'Restart OpenCode to apply the new settings.',
156
+ '',
157
+ found.isPrivate
158
+ ? 'To enable secrets sync, run: /sync-enable-secrets'
159
+ : 'Note: Repo is public. Secrets sync is disabled.',
160
+ ];
161
+ await showToast(ctx.client, 'Config synced. Restart OpenCode to apply.', 'info');
162
+ return lines.join('\n');
163
+ },
164
+ pull: async () => {
165
+ const config = await getConfigOrThrow(locations);
166
+ const repoRoot = resolveRepoRoot(config, locations);
167
+ await ensureRepoCloned(ctx.$, config, repoRoot);
168
+ await ensureSecretsPolicy(ctx, config);
169
+ const branch = await resolveBranch(ctx, config, repoRoot);
170
+ const dirty = await hasLocalChanges(ctx.$, repoRoot);
171
+ if (dirty) {
172
+ throw new SyncCommandError(`Local sync repo has uncommitted changes. Resolve in ${repoRoot} before pulling.`);
173
+ }
174
+ const update = await fetchAndFastForward(ctx.$, repoRoot, branch);
175
+ if (!update.updated) {
176
+ return 'Already up to date.';
177
+ }
178
+ const overrides = await loadOverrides(locations);
179
+ const plan = buildSyncPlan(config, locations, repoRoot);
180
+ await syncRepoToLocal(plan, overrides);
181
+ await writeState(locations, {
182
+ lastPull: new Date().toISOString(),
183
+ lastRemoteUpdate: new Date().toISOString(),
184
+ });
185
+ await showToast(ctx.client, 'Config updated. Restart OpenCode to apply.', 'info');
186
+ return 'Remote config applied. Restart OpenCode to use new settings.';
187
+ },
188
+ push: async () => {
189
+ const config = await getConfigOrThrow(locations);
190
+ const repoRoot = resolveRepoRoot(config, locations);
191
+ await ensureRepoCloned(ctx.$, config, repoRoot);
192
+ await ensureSecretsPolicy(ctx, config);
193
+ const branch = await resolveBranch(ctx, config, repoRoot);
194
+ const preDirty = await hasLocalChanges(ctx.$, repoRoot);
195
+ if (preDirty) {
196
+ throw new SyncCommandError(`Local sync repo has uncommitted changes. Resolve in ${repoRoot} before pushing.`);
197
+ }
198
+ const overrides = await loadOverrides(locations);
199
+ const plan = buildSyncPlan(config, locations, repoRoot);
200
+ await syncLocalToRepo(plan, overrides);
201
+ const dirty = await hasLocalChanges(ctx.$, repoRoot);
202
+ if (!dirty) {
203
+ return 'No local changes to push.';
204
+ }
205
+ const message = await generateCommitMessage({ client: ctx.client, $: ctx.$ }, repoRoot);
206
+ await commitAll(ctx.$, repoRoot, message);
207
+ await pushBranch(ctx.$, repoRoot, branch);
208
+ await writeState(locations, {
209
+ lastPush: new Date().toISOString(),
210
+ });
211
+ return `Pushed changes: ${message}`;
212
+ },
213
+ enableSecrets: async (extraSecretPaths) => {
214
+ const config = await getConfigOrThrow(locations);
215
+ config.includeSecrets = true;
216
+ if (extraSecretPaths) {
217
+ config.extraSecretPaths = extraSecretPaths;
218
+ }
219
+ await ensureRepoPrivate(ctx.$, config);
220
+ await writeSyncConfig(locations, config);
221
+ return 'Secrets sync enabled for this repo.';
222
+ },
223
+ resolve: async () => {
224
+ const config = await getConfigOrThrow(locations);
225
+ const repoRoot = resolveRepoRoot(config, locations);
226
+ await ensureRepoCloned(ctx.$, config, repoRoot);
227
+ const dirty = await hasLocalChanges(ctx.$, repoRoot);
228
+ if (!dirty) {
229
+ return 'No uncommitted changes to resolve.';
230
+ }
231
+ const status = await getRepoStatus(ctx.$, repoRoot);
232
+ const decision = await analyzeAndDecideResolution({ client: ctx.client, $: ctx.$ }, repoRoot, status.changes);
233
+ if (decision.action === 'commit') {
234
+ const message = decision.message ?? 'Sync: Auto-resolved uncommitted changes';
235
+ await commitAll(ctx.$, repoRoot, message);
236
+ return `Resolved by committing changes: ${message}`;
237
+ }
238
+ if (decision.action === 'reset') {
239
+ try {
240
+ await ctx.$ `git -C ${repoRoot} reset --hard HEAD`.quiet();
241
+ await ctx.$ `git -C ${repoRoot} clean -fd`.quiet();
242
+ return 'Resolved by discarding all uncommitted changes.';
243
+ }
244
+ catch (error) {
245
+ throw new SyncCommandError(`Failed to reset changes: ${formatError(error)}`);
246
+ }
247
+ }
248
+ return `Unable to automatically resolve. Please manually resolve in: ${repoRoot}`;
249
+ },
250
+ };
251
+ }
252
+ async function runStartup(ctx, locations, config, log) {
253
+ const repoRoot = resolveRepoRoot(config, locations);
254
+ log.debug('Starting sync', { repoRoot });
255
+ await ensureRepoCloned(ctx.$, config, repoRoot);
256
+ await ensureSecretsPolicy(ctx, config);
257
+ const branch = await resolveBranch(ctx, config, repoRoot);
258
+ log.debug('Resolved branch', { branch });
259
+ const dirty = await hasLocalChanges(ctx.$, repoRoot);
260
+ if (dirty) {
261
+ log.warn('Uncommitted changes detected', { repoRoot });
262
+ await showToast(ctx.client, `Uncommitted changes detected. Run /sync-resolve to auto-fix, or manually resolve in: ${repoRoot}`, 'warning');
263
+ return;
264
+ }
265
+ const update = await fetchAndFastForward(ctx.$, repoRoot, branch);
266
+ if (update.updated) {
267
+ log.info('Pulled remote changes', { branch });
268
+ const overrides = await loadOverrides(locations);
269
+ const plan = buildSyncPlan(config, locations, repoRoot);
270
+ await syncRepoToLocal(plan, overrides);
271
+ await writeState(locations, {
272
+ lastPull: new Date().toISOString(),
273
+ lastRemoteUpdate: new Date().toISOString(),
274
+ });
275
+ await showToast(ctx.client, 'Config updated. Restart OpenCode to apply.', 'info');
276
+ return;
277
+ }
278
+ const overrides = await loadOverrides(locations);
279
+ const plan = buildSyncPlan(config, locations, repoRoot);
280
+ await syncLocalToRepo(plan, overrides);
281
+ const changes = await hasLocalChanges(ctx.$, repoRoot);
282
+ if (!changes) {
283
+ log.debug('No local changes to push');
284
+ return;
285
+ }
286
+ const message = await generateCommitMessage({ client: ctx.client, $: ctx.$ }, repoRoot);
287
+ log.info('Pushing local changes', { message });
288
+ await commitAll(ctx.$, repoRoot, message);
289
+ await pushBranch(ctx.$, repoRoot, branch);
290
+ await writeState(locations, {
291
+ lastPush: new Date().toISOString(),
292
+ });
293
+ }
294
+ async function getConfigOrThrow(locations) {
295
+ const config = await loadSyncConfig(locations);
296
+ if (!config) {
297
+ throw new SyncConfigMissingError('Missing opencode-synced config. Run /sync-init to set it up.');
298
+ }
299
+ return config;
300
+ }
301
+ async function ensureSecretsPolicy(ctx, config) {
302
+ if (!config.includeSecrets)
303
+ return;
304
+ await ensureRepoPrivate(ctx.$, config);
305
+ }
306
+ async function resolveBranch(ctx, config, repoRoot) {
307
+ try {
308
+ const status = await getRepoStatus(ctx.$, repoRoot);
309
+ return resolveRepoBranch(config, status.branch);
310
+ }
311
+ catch {
312
+ return resolveRepoBranch(config);
313
+ }
314
+ }
315
+ const DEFAULT_REPO_NAME = 'my-opencode-config';
316
+ async function buildConfigFromInit($, options) {
317
+ const repo = await resolveRepoFromInit($, options);
318
+ return normalizeSyncConfig({
319
+ repo,
320
+ includeSecrets: options.includeSecrets ?? false,
321
+ includeSessions: options.includeSessions ?? false,
322
+ includePromptStash: options.includePromptStash ?? false,
323
+ extraSecretPaths: options.extraSecretPaths ?? [],
324
+ localRepoPath: options.localRepoPath,
325
+ });
326
+ }
327
+ async function resolveRepoFromInit($, options) {
328
+ if (options.url) {
329
+ return { url: options.url, branch: options.branch };
330
+ }
331
+ if (options.owner && options.name) {
332
+ return { owner: options.owner, name: options.name, branch: options.branch };
333
+ }
334
+ if (options.repo) {
335
+ if (options.repo.includes('://') || options.repo.endsWith('.git')) {
336
+ return { url: options.repo, branch: options.branch };
337
+ }
338
+ if (options.repo.includes('/')) {
339
+ const [owner, name] = options.repo.split('/');
340
+ if (owner && name) {
341
+ return { owner, name, branch: options.branch };
342
+ }
343
+ }
344
+ const owner = await getAuthenticatedUser($);
345
+ return { owner, name: options.repo, branch: options.branch };
346
+ }
347
+ // Default: auto-detect owner, use default repo name
348
+ const owner = await getAuthenticatedUser($);
349
+ const name = DEFAULT_REPO_NAME;
350
+ return { owner, name, branch: options.branch };
351
+ }
352
+ async function createRepo($, config, isPrivate) {
353
+ const owner = config.repo?.owner;
354
+ const name = config.repo?.name;
355
+ if (!owner || !name) {
356
+ throw new SyncCommandError('Repo creation requires owner/name.');
357
+ }
358
+ const visibility = isPrivate ? '--private' : '--public';
359
+ try {
360
+ await $ `gh repo create ${owner}/${name} ${visibility} --confirm`.quiet();
361
+ }
362
+ catch (error) {
363
+ throw new SyncCommandError(`Failed to create repo: ${formatError(error)}`);
364
+ }
365
+ }
366
+ function formatError(error) {
367
+ if (error instanceof Error)
368
+ return error.message;
369
+ return String(error);
370
+ }
371
+ async function analyzeAndDecideResolution(ctx, repoRoot, changes) {
372
+ try {
373
+ const diff = await ctx.$ `git -C ${repoRoot} diff HEAD`.quiet().text();
374
+ const statusOutput = changes.join('\n');
375
+ const prompt = [
376
+ 'You are analyzing uncommitted changes in an opencode-synced repository.',
377
+ 'Decide whether to commit these changes or discard them.',
378
+ '',
379
+ 'IMPORTANT: Only choose "commit" if the changes appear to be legitimate config updates.',
380
+ 'Choose "discard" if the changes look like temporary files, cache, or corruption.',
381
+ '',
382
+ 'Respond with ONLY a JSON object in this exact format:',
383
+ '{"action": "commit", "message": "your commit message here"}',
384
+ 'OR',
385
+ '{"action": "discard", "reason": "explanation why discarding"}',
386
+ '',
387
+ 'Status:',
388
+ statusOutput,
389
+ '',
390
+ 'Diff preview (first 2000 chars):',
391
+ diff.slice(0, 2000),
392
+ ].join('\n');
393
+ const model = await resolveSmallModel(ctx.client);
394
+ if (!model) {
395
+ return { action: 'manual', reason: 'No AI model available' };
396
+ }
397
+ let sessionId = null;
398
+ try {
399
+ const sessionResult = await ctx.client.session.create({
400
+ body: { title: 'sync-resolve' },
401
+ });
402
+ const session = unwrapData(sessionResult);
403
+ sessionId = session?.id ?? null;
404
+ if (!sessionId) {
405
+ return { action: 'manual', reason: 'Failed to create session' };
406
+ }
407
+ const response = await ctx.client.session.prompt({
408
+ path: { id: sessionId },
409
+ body: {
410
+ model,
411
+ parts: [{ type: 'text', text: prompt }],
412
+ },
413
+ });
414
+ const messageText = extractTextFromResponse(unwrapData(response) ?? response);
415
+ if (!messageText) {
416
+ return { action: 'manual', reason: 'No response from AI' };
417
+ }
418
+ const decision = parseResolutionDecision(messageText);
419
+ return decision;
420
+ }
421
+ finally {
422
+ if (sessionId) {
423
+ try {
424
+ await ctx.client.session.delete({ path: { id: sessionId } });
425
+ }
426
+ catch { }
427
+ }
428
+ }
429
+ }
430
+ catch (error) {
431
+ console.error('[ERROR] AI resolution analysis failed:', error);
432
+ return { action: 'manual', reason: `Error analyzing changes: ${formatError(error)}` };
433
+ }
434
+ }
435
+ function parseResolutionDecision(text) {
436
+ try {
437
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
438
+ if (!jsonMatch) {
439
+ return { action: 'manual', reason: 'Could not parse AI response' };
440
+ }
441
+ const parsed = JSON.parse(jsonMatch[0]);
442
+ if (parsed.action === 'commit' && parsed.message) {
443
+ return { action: 'commit', message: parsed.message };
444
+ }
445
+ if (parsed.action === 'discard') {
446
+ return { action: 'reset', reason: parsed.reason };
447
+ }
448
+ return { action: 'manual', reason: 'Unexpected AI response format' };
449
+ }
450
+ catch {
451
+ return { action: 'manual', reason: 'Failed to parse AI decision' };
452
+ }
453
+ }
@@ -0,0 +1,17 @@
1
+ import type { PluginInput } from '@opencode-ai/plugin';
2
+ type Client = PluginInput['client'];
3
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
4
+ export declare function createLogger(client: Client): {
5
+ debug: (message: string, extra?: Record<string, unknown>) => void;
6
+ info: (message: string, extra?: Record<string, unknown>) => void;
7
+ warn: (message: string, extra?: Record<string, unknown>) => void;
8
+ error: (message: string, extra?: Record<string, unknown>) => void;
9
+ };
10
+ export declare function showToast(client: Client, message: string, variant: 'info' | 'success' | 'warning' | 'error'): Promise<void>;
11
+ export declare function unwrapData<T>(response: unknown): T | null;
12
+ export declare function extractTextFromResponse(response: unknown): string | null;
13
+ export declare function resolveSmallModel(client: Client): Promise<{
14
+ providerID: string;
15
+ modelID: string;
16
+ } | null>;
17
+ export {};
@@ -0,0 +1,70 @@
1
+ const SERVICE_NAME = 'opencode-synced';
2
+ export function createLogger(client) {
3
+ return {
4
+ debug: (message, extra) => log(client, 'debug', message, extra),
5
+ info: (message, extra) => log(client, 'info', message, extra),
6
+ warn: (message, extra) => log(client, 'warn', message, extra),
7
+ error: (message, extra) => log(client, 'error', message, extra),
8
+ };
9
+ }
10
+ function log(client, level, message, extra) {
11
+ client.app
12
+ .log({
13
+ body: {
14
+ service: SERVICE_NAME,
15
+ level,
16
+ message,
17
+ extra,
18
+ },
19
+ })
20
+ .catch((err) => {
21
+ const errorMsg = err instanceof Error ? err.message : String(err);
22
+ showToast(client, `Logging failed: ${errorMsg}`, 'error');
23
+ });
24
+ }
25
+ export async function showToast(client, message, variant) {
26
+ await client.tui.showToast({
27
+ body: { title: 'opencode-synced plugin', message, variant },
28
+ });
29
+ }
30
+ export function unwrapData(response) {
31
+ if (!response || typeof response !== 'object')
32
+ return null;
33
+ const maybeError = response.error;
34
+ if (maybeError)
35
+ return null;
36
+ if ('data' in response) {
37
+ const data = response.data;
38
+ if (data !== undefined)
39
+ return data;
40
+ return null;
41
+ }
42
+ return response;
43
+ }
44
+ export function extractTextFromResponse(response) {
45
+ if (!response || typeof response !== 'object')
46
+ return null;
47
+ const parts = response.parts ??
48
+ response.info?.parts ??
49
+ [];
50
+ const textPart = parts.find((part) => part.type === 'text' && part.text);
51
+ return textPart?.text?.trim() ?? null;
52
+ }
53
+ export async function resolveSmallModel(client) {
54
+ try {
55
+ const response = await client.config.get();
56
+ const config = unwrapData(response);
57
+ if (!config)
58
+ return null;
59
+ const modelValue = config.small_model ?? config.model;
60
+ if (!modelValue)
61
+ return null;
62
+ const [providerID, modelID] = modelValue.split('/', 2);
63
+ if (!providerID || !modelID)
64
+ return null;
65
+ return { providerID, modelID };
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-synced",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Sync global OpenCode config across machines via GitHub.",
5
5
  "author": {
6
6
  "name": "Ian Hildebrand"
@@ -37,7 +37,7 @@
37
37
  "vitest": "^3.2.4"
38
38
  },
39
39
  "scripts": {
40
- "build": "rm -rf dist && bun build ./src/index.ts --outdir dist --target bun && tsc -p tsconfig.build.json && cp -r src/command dist/command",
40
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && cp -r src/command dist/command",
41
41
  "test": "vitest run",
42
42
  "test:watch": "vitest",
43
43
  "lint": "biome lint .",