zedx 0.4.0 → 0.5.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # zedx
2
2
 
3
- Boilerplate generator for [Zed Editor](https://zed.dev/) extensions.
3
+ Scaffold [Zed Editor](https://zed.dev/) extensions and sync your settings across machines.
4
4
 
5
5
  ![screenshot](./assets/screenshot1.png)
6
6
 
@@ -30,6 +30,12 @@ zedx check
30
30
  zedx version patch # 1.2.3 → 1.2.4
31
31
  zedx version minor # 1.2.3 → 1.3.0
32
32
  zedx version major # 1.2.3 → 2.0.0
33
+
34
+ # Sync Zed settings and extensions via a GitHub repo
35
+ zedx sync init # Link a GitHub repo as the sync target (run once)
36
+ zedx sync # Sync local and remote config automatically
37
+ zedx sync install # Install an OS daemon to auto-sync when Zed config changes
38
+ zedx sync uninstall # Remove the OS daemon
33
39
  ```
34
40
 
35
41
  ### Supported extension types:
@@ -0,0 +1,2 @@
1
+ export declare function syncInstall(): Promise<void>;
2
+ export declare function syncUninstall(): Promise<void>;
package/dist/daemon.js ADDED
@@ -0,0 +1,183 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import { execSync } from 'child_process';
5
+ import * as p from '@clack/prompts';
6
+ import color from 'picocolors';
7
+ import { resolveZedPaths } from './zed-paths.js';
8
+ const LAUNCHD_LABEL = 'dev.zedx.sync';
9
+ const LAUNCHD_PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
10
+ const SYSTEMD_SERVICE_NAME = 'zedx-sync';
11
+ const SYSTEMD_UNIT_DIR = path.join(os.homedir(), '.config', 'systemd', 'user');
12
+ const SYSTEMD_SERVICE_PATH = path.join(SYSTEMD_UNIT_DIR, `${SYSTEMD_SERVICE_NAME}.service`);
13
+ const SYSTEMD_PATH_PATH = path.join(SYSTEMD_UNIT_DIR, `${SYSTEMD_SERVICE_NAME}.path`);
14
+ function resolveZedxBinary() {
15
+ try {
16
+ const bin = execSync('which zedx', { encoding: 'utf-8' }).trim();
17
+ if (bin)
18
+ return bin;
19
+ }
20
+ catch { /* fall through */ }
21
+ return `${process.execPath} ${process.argv[1]}`;
22
+ }
23
+ function unsupportedPlatform() {
24
+ p.log.error(color.red(`zedx sync install is only supported on macOS and Linux.`));
25
+ process.exit(1);
26
+ }
27
+ function buildPlist(zedxBin, watchPaths) {
28
+ const watchEntries = watchPaths
29
+ .map((wp) => ` <string>${wp}</string>`)
30
+ .join('\n');
31
+ return `<?xml version="1.0" encoding="UTF-8"?>
32
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
+ <plist version="1.0">
34
+ <dict>
35
+ <key>Label</key>
36
+ <string>${LAUNCHD_LABEL}</string>
37
+
38
+ <key>ProgramArguments</key>
39
+ <array>
40
+ <string>${zedxBin}</string>
41
+ <string>sync</string>
42
+ </array>
43
+
44
+ <key>RunAtLoad</key>
45
+ <true/>
46
+
47
+ <key>WatchPaths</key>
48
+ <array>
49
+ ${watchEntries}
50
+ </array>
51
+
52
+ <key>ThrottleInterval</key>
53
+ <integer>30</integer>
54
+
55
+ <key>StandardOutPath</key>
56
+ <string>${os.homedir()}/Library/Logs/zedx-sync.log</string>
57
+
58
+ <key>StandardErrorPath</key>
59
+ <string>${os.homedir()}/Library/Logs/zedx-sync.log</string>
60
+ </dict>
61
+ </plist>
62
+ `;
63
+ }
64
+ async function installMacos(zedxBin, watchPaths) {
65
+ const plist = buildPlist(zedxBin, watchPaths);
66
+ await fs.ensureDir(path.dirname(LAUNCHD_PLIST_PATH));
67
+ await fs.writeFile(LAUNCHD_PLIST_PATH, plist, 'utf-8');
68
+ try {
69
+ execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null`, { stdio: 'pipe' });
70
+ }
71
+ catch { /* not loaded yet */ }
72
+ execSync(`launchctl load "${LAUNCHD_PLIST_PATH}"`);
73
+ p.log.success(`Daemon installed: ${color.dim(LAUNCHD_PLIST_PATH)}`);
74
+ p.log.info(`Logs: ${color.dim(`${os.homedir()}/Library/Logs/zedx-sync.log`)}`);
75
+ p.log.info(`To check status: ${color.cyan(`launchctl list ${LAUNCHD_LABEL}`)}`);
76
+ }
77
+ async function uninstallMacos() {
78
+ if (!(await fs.pathExists(LAUNCHD_PLIST_PATH))) {
79
+ p.log.warn(color.yellow('No launchd agent found — nothing to uninstall.'));
80
+ return;
81
+ }
82
+ try {
83
+ execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}"`, { stdio: 'pipe' });
84
+ }
85
+ catch { /* already unloaded */ }
86
+ await fs.remove(LAUNCHD_PLIST_PATH);
87
+ p.log.success('Daemon uninstalled.');
88
+ }
89
+ function buildSystemdService(zedxBin) {
90
+ return `[Unit]
91
+ Description=zedx Zed config sync
92
+ After=network-online.target
93
+ Wants=network-online.target
94
+
95
+ [Service]
96
+ Type=oneshot
97
+ ExecStart=${zedxBin} sync
98
+ StandardOutput=journal
99
+ StandardError=journal
100
+
101
+ [Install]
102
+ WantedBy=default.target
103
+ `;
104
+ }
105
+ function buildSystemdPath(watchPaths) {
106
+ const pathChangedEntries = watchPaths
107
+ .map((wp) => `PathChanged=${wp}`)
108
+ .join('\n');
109
+ return `[Unit]
110
+ Description=Watch Zed config files for zedx sync
111
+
112
+ [Path]
113
+ ${pathChangedEntries}
114
+ Unit=${SYSTEMD_SERVICE_NAME}.service
115
+
116
+ [Install]
117
+ WantedBy=default.target
118
+ `;
119
+ }
120
+ async function installLinux(zedxBin, watchPaths) {
121
+ await fs.ensureDir(SYSTEMD_UNIT_DIR);
122
+ await fs.writeFile(SYSTEMD_SERVICE_PATH, buildSystemdService(zedxBin), 'utf-8');
123
+ await fs.writeFile(SYSTEMD_PATH_PATH, buildSystemdPath(watchPaths), 'utf-8');
124
+ execSync('systemctl --user daemon-reload');
125
+ execSync(`systemctl --user enable --now ${SYSTEMD_SERVICE_NAME}.path`);
126
+ p.log.success(`Service installed: ${color.dim(SYSTEMD_SERVICE_PATH)}`);
127
+ p.log.success(`Path unit installed: ${color.dim(SYSTEMD_PATH_PATH)}`);
128
+ p.log.info(`To check status: ${color.cyan(`systemctl --user status ${SYSTEMD_SERVICE_NAME}.path`)}`);
129
+ p.log.info(`Logs: ${color.cyan(`journalctl --user -u ${SYSTEMD_SERVICE_NAME}.service`)}`);
130
+ }
131
+ async function uninstallLinux() {
132
+ const serviceExists = await fs.pathExists(SYSTEMD_SERVICE_PATH);
133
+ const pathExists = await fs.pathExists(SYSTEMD_PATH_PATH);
134
+ if (!serviceExists && !pathExists) {
135
+ p.log.warn(color.yellow('No systemd units found — nothing to uninstall.'));
136
+ return;
137
+ }
138
+ try {
139
+ execSync(`systemctl --user disable --now ${SYSTEMD_SERVICE_NAME}.path`, { stdio: 'pipe' });
140
+ }
141
+ catch { /* already inactive */ }
142
+ if (serviceExists)
143
+ await fs.remove(SYSTEMD_SERVICE_PATH);
144
+ if (pathExists)
145
+ await fs.remove(SYSTEMD_PATH_PATH);
146
+ execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
147
+ p.log.success('Daemon uninstalled.');
148
+ }
149
+ export async function syncInstall() {
150
+ p.intro(color.bold('zedx sync install'));
151
+ const platform = process.platform;
152
+ if (platform !== 'darwin' && platform !== 'linux')
153
+ unsupportedPlatform();
154
+ const zedPaths = resolveZedPaths();
155
+ const watchPaths = [zedPaths.settings, zedPaths.extensions];
156
+ const zedxBin = resolveZedxBinary();
157
+ p.log.info(`Binary: ${color.dim(zedxBin)}`);
158
+ p.log.info(`Watching:`);
159
+ for (const wp of watchPaths) {
160
+ p.log.info(` ${color.dim(wp)}`);
161
+ }
162
+ if (platform === 'darwin') {
163
+ await installMacos(zedxBin, watchPaths);
164
+ }
165
+ else {
166
+ await installLinux(zedxBin, watchPaths);
167
+ }
168
+ p.outro(`${color.green('✓')} zedx sync will now run automatically whenever your Zed config changes.\n\n` +
169
+ ` Run ${color.cyan('zedx sync uninstall')} to remove the daemon at any time.`);
170
+ }
171
+ export async function syncUninstall() {
172
+ p.intro(color.bold('zedx sync uninstall'));
173
+ const platform = process.platform;
174
+ if (platform !== 'darwin' && platform !== 'linux')
175
+ unsupportedPlatform();
176
+ if (platform === 'darwin') {
177
+ await uninstallMacos();
178
+ }
179
+ else {
180
+ await uninstallLinux();
181
+ }
182
+ p.outro(`${color.green('✓')} Done.`);
183
+ }
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ import { promptUser, promptThemeDetails, promptLanguageDetails } from './prompts
8
8
  import { generateExtension } from './generator.js';
9
9
  import { runCheck } from './check.js';
10
10
  import { addTheme, addLanguage } from './add.js';
11
+ import { syncInit, runSync } from './sync.js';
12
+ import { syncInstall, syncUninstall } from './daemon.js';
11
13
  function bumpVersion(version, type) {
12
14
  const [major, minor, patch] = version.split('.').map(Number);
13
15
  switch (type) {
@@ -76,6 +78,30 @@ async function main() {
76
78
  .action(async (id) => {
77
79
  await addLanguage(getCallerDir(), id);
78
80
  });
81
+ const syncCmd = program
82
+ .command('sync')
83
+ .description('Sync Zed settings and extensions via a GitHub repo')
84
+ .action(async () => {
85
+ await runSync();
86
+ });
87
+ syncCmd
88
+ .command('init')
89
+ .description('Link a GitHub repo as the sync target')
90
+ .action(async () => {
91
+ await syncInit();
92
+ });
93
+ syncCmd
94
+ .command('install')
95
+ .description('Install the OS daemon to auto-sync when Zed config changes')
96
+ .action(async () => {
97
+ await syncInstall();
98
+ });
99
+ syncCmd
100
+ .command('uninstall')
101
+ .description('Remove the OS daemon')
102
+ .action(async () => {
103
+ await syncUninstall();
104
+ });
79
105
  if (process.argv.length <= 2) {
80
106
  const options = await promptUser();
81
107
  if (options.types.includes('theme')) {
package/dist/sync.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare function syncInit(): Promise<void>;
2
+ export declare function runSync(opts?: {
3
+ silent?: boolean;
4
+ }): Promise<void>;
package/dist/sync.js ADDED
@@ -0,0 +1,317 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import * as p from '@clack/prompts';
5
+ import color from 'picocolors';
6
+ import simpleGit from 'simple-git';
7
+ import { resolveZedPaths } from './zed-paths.js';
8
+ const ZEDX_CONFIG_DIR = path.join(os.homedir(), '.config', 'zedx');
9
+ const ZEDX_CONFIG_PATH = path.join(ZEDX_CONFIG_DIR, 'config.json');
10
+ async function readSyncConfig() {
11
+ if (!(await fs.pathExists(ZEDX_CONFIG_PATH))) {
12
+ return null;
13
+ }
14
+ return fs.readJson(ZEDX_CONFIG_PATH);
15
+ }
16
+ async function requireSyncConfig() {
17
+ const config = await readSyncConfig();
18
+ if (!config) {
19
+ p.log.error(color.red('No sync config found. Run ') + color.cyan('zedx sync init') + color.red(' first.'));
20
+ process.exit(1);
21
+ }
22
+ return config;
23
+ }
24
+ async function writeSyncConfig(config) {
25
+ await fs.ensureDir(ZEDX_CONFIG_DIR);
26
+ await fs.writeJson(ZEDX_CONFIG_PATH, config, { spaces: 4 });
27
+ }
28
+ // Temp dir helper
29
+ async function withTempDir(fn) {
30
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'zedx-sync-'));
31
+ try {
32
+ return await fn(tmp);
33
+ }
34
+ finally {
35
+ await fs.remove(tmp);
36
+ }
37
+ }
38
+ // Extension merge helper
39
+ async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPath, silent = false) {
40
+ // Backup existing settings
41
+ if (await fs.pathExists(localSettingsPath)) {
42
+ await fs.copy(localSettingsPath, localSettingsPath + '.bak', { overwrite: true });
43
+ if (!silent)
44
+ p.log.info(`Backed up settings to ${color.dim(localSettingsPath + '.bak')}`);
45
+ }
46
+ let settingsJson = await fs.readFile(repoSettings, 'utf-8');
47
+ // Merge auto_install_extensions from index.json into settings
48
+ if (await fs.pathExists(repoExtensions)) {
49
+ try {
50
+ const indexJson = (await fs.readJson(repoExtensions));
51
+ const extensionIds = Object.keys(indexJson.extensions ?? {}).filter((id) => !indexJson.extensions[id]?.dev);
52
+ if (extensionIds.length > 0) {
53
+ const stripped = settingsJson.replace(/\/\/[^\n]*/g, '');
54
+ let settingsObj = {};
55
+ try {
56
+ settingsObj = JSON.parse(stripped);
57
+ }
58
+ catch {
59
+ if (!silent)
60
+ p.log.warn(color.yellow('Could not parse settings.json — skipping extension merge.'));
61
+ }
62
+ const autoInstall = {};
63
+ for (const id of extensionIds) {
64
+ autoInstall[id] = true;
65
+ }
66
+ settingsObj['auto_install_extensions'] = autoInstall;
67
+ settingsJson = JSON.stringify(settingsObj, null, 4);
68
+ }
69
+ }
70
+ catch {
71
+ if (!silent)
72
+ p.log.warn(color.yellow('Could not parse extensions/index.json — skipping extension merge.'));
73
+ }
74
+ }
75
+ await fs.ensureDir(path.dirname(localSettingsPath));
76
+ await fs.writeFile(localSettingsPath, settingsJson, 'utf-8');
77
+ }
78
+ // zedx sync init
79
+ export async function syncInit() {
80
+ p.intro(color.bold('zedx sync init'));
81
+ const repo = await p.text({
82
+ message: 'GitHub repo URL (SSH or HTTPS)',
83
+ placeholder: 'https://github.com/you/zed-config.git',
84
+ validate: (v) => {
85
+ if (!v.trim())
86
+ return 'Repo URL is required';
87
+ if (!v.startsWith('https://') && !v.startsWith('git@')) {
88
+ return 'Must be a valid HTTPS or SSH git URL';
89
+ }
90
+ }
91
+ });
92
+ if (p.isCancel(repo)) {
93
+ p.cancel('Cancelled.');
94
+ process.exit(0);
95
+ }
96
+ const branch = await p.text({
97
+ message: 'Branch name',
98
+ placeholder: 'main',
99
+ defaultValue: 'main'
100
+ });
101
+ if (p.isCancel(branch)) {
102
+ p.cancel('Cancelled.');
103
+ process.exit(0);
104
+ }
105
+ const spinner = p.spinner();
106
+ spinner.start('Verifying repo is reachable...');
107
+ try {
108
+ const git = simpleGit();
109
+ await git.listRemote(['--heads', repo]);
110
+ spinner.stop('Repo verified.');
111
+ }
112
+ catch {
113
+ spinner.stop(color.yellow('Could not verify repo (may be empty or private — continuing anyway).'));
114
+ }
115
+ const config = {
116
+ syncRepo: repo.trim(),
117
+ branch: (branch || 'main').trim()
118
+ };
119
+ await writeSyncConfig(config);
120
+ p.outro(`${color.green('✓')} Sync config saved to ${color.cyan(ZEDX_CONFIG_PATH)}\n\n` +
121
+ ` Run ${color.cyan('zedx sync')} to sync your Zed config.`);
122
+ }
123
+ // zedx sync
124
+ export async function runSync(opts = {}) {
125
+ const { silent = false } = opts;
126
+ // In silent mode (daemon/watch), route all UI through plain console.log
127
+ // Interactive conflict prompts fall back to "local wins".
128
+ const log = {
129
+ info: (msg) => {
130
+ if (!silent)
131
+ p.log.info(msg);
132
+ },
133
+ warn: (msg) => {
134
+ if (!silent)
135
+ p.log.warn(msg);
136
+ else
137
+ console.error(`[zedx] warn: ${msg}`);
138
+ },
139
+ success: (msg) => {
140
+ if (!silent)
141
+ p.log.success(msg);
142
+ }
143
+ };
144
+ if (!silent)
145
+ p.intro(color.bold('zedx sync'));
146
+ const config = await requireSyncConfig();
147
+ const zedPaths = resolveZedPaths();
148
+ // Spinner shim: in silent mode just log to stderr so daemons can capture it
149
+ const spinner = silent
150
+ ? {
151
+ start: (m) => console.error(`[zedx] ${m}`),
152
+ stop: (m) => console.error(`[zedx] ${m}`)
153
+ }
154
+ : p.spinner();
155
+ await withTempDir(async (tmp) => {
156
+ // 1. Clone the remote repo
157
+ const git = simpleGit(tmp);
158
+ let remoteExists = true;
159
+ spinner.start(`Fetching ${config.syncRepo}...`);
160
+ try {
161
+ await git.clone(config.syncRepo, tmp, ['--depth', '1', '--branch', config.branch]);
162
+ spinner.stop('Remote fetched.');
163
+ }
164
+ catch {
165
+ remoteExists = false;
166
+ spinner.stop('Remote is empty or branch not found — will push fresh.');
167
+ await git.init();
168
+ await git.addRemote('origin', config.syncRepo);
169
+ }
170
+ // 2. Determine what changed for each file
171
+ const lastSync = config.lastSync ? new Date(config.lastSync) : null;
172
+ const files = [
173
+ {
174
+ repoPath: path.join(tmp, 'settings.json'),
175
+ localPath: zedPaths.settings,
176
+ label: 'settings.json'
177
+ },
178
+ {
179
+ repoPath: path.join(tmp, 'extensions', 'index.json'),
180
+ localPath: zedPaths.extensions,
181
+ label: 'extensions/index.json'
182
+ }
183
+ ];
184
+ let anyChanges = false;
185
+ for (const file of files) {
186
+ const localExists = await fs.pathExists(file.localPath);
187
+ const remoteFileExists = remoteExists && (await fs.pathExists(file.repoPath));
188
+ // Both missing — skip
189
+ if (!localExists && !remoteFileExists) {
190
+ log.warn(`${file.label}: not found locally or remotely — skipping.`);
191
+ continue;
192
+ }
193
+ // Remote doesn't have it yet — push local
194
+ if (localExists && !remoteFileExists) {
195
+ log.info(`${file.label}: ${color.green('pushing')} (not in remote yet)`);
196
+ await fs.ensureDir(path.dirname(file.repoPath));
197
+ await fs.copy(file.localPath, file.repoPath, { overwrite: true });
198
+ anyChanges = true;
199
+ continue;
200
+ }
201
+ // Local doesn't have it — pull remote
202
+ if (!localExists && remoteFileExists) {
203
+ log.info(`${file.label}: ${color.cyan('pulling')} (not found locally)`);
204
+ if (file.label === 'settings.json') {
205
+ await applyRemoteSettings(file.repoPath, path.join(tmp, 'extensions', 'index.json'), file.localPath, silent);
206
+ }
207
+ else {
208
+ await fs.ensureDir(path.dirname(file.localPath));
209
+ await fs.copy(file.repoPath, file.localPath, { overwrite: true });
210
+ }
211
+ continue;
212
+ }
213
+ // Both exist — compare content
214
+ const localContent = await fs.readFile(file.localPath, 'utf-8');
215
+ const remoteContent = await fs.readFile(file.repoPath, 'utf-8');
216
+ if (localContent === remoteContent) {
217
+ log.success(`${file.label}: ${color.dim('already in sync')}`);
218
+ continue;
219
+ }
220
+ // Detect which side changed since last sync via mtime
221
+ const localMtime = (await fs.stat(file.localPath)).mtime;
222
+ const remoteMtime = remoteFileExists ? (await fs.stat(file.repoPath)).mtime : new Date(0);
223
+ const localChanged = !lastSync || localMtime > lastSync;
224
+ const remoteChanged = !lastSync || remoteMtime > lastSync;
225
+ if (localChanged && !remoteChanged) {
226
+ // Only local changed → push
227
+ log.info(`${file.label}: ${color.green('pushing')} (local is newer)`);
228
+ await fs.ensureDir(path.dirname(file.repoPath));
229
+ await fs.copy(file.localPath, file.repoPath, { overwrite: true });
230
+ anyChanges = true;
231
+ }
232
+ else if (remoteChanged && !localChanged) {
233
+ // Only remote changed → pull
234
+ log.info(`${file.label}: ${color.cyan('pulling')} (remote is newer)`);
235
+ if (file.label === 'settings.json') {
236
+ await applyRemoteSettings(file.repoPath, path.join(tmp, 'extensions', 'index.json'), file.localPath, silent);
237
+ }
238
+ else {
239
+ await fs.ensureDir(path.dirname(file.localPath));
240
+ await fs.copy(file.repoPath, file.localPath, { overwrite: true });
241
+ }
242
+ }
243
+ else {
244
+ // Both changed
245
+ if (silent) {
246
+ // Daemon can't prompt — local wins, will be pushed
247
+ log.warn(`${file.label}: conflict detected in unattended mode — keeping local.`);
248
+ await fs.ensureDir(path.dirname(file.repoPath));
249
+ await fs.copy(file.localPath, file.repoPath, { overwrite: true });
250
+ anyChanges = true;
251
+ }
252
+ else {
253
+ p.log.warn(color.yellow(`${file.label}: both local and remote changed.`));
254
+ const choice = await p.select({
255
+ message: `Which version of ${color.bold(file.label)} should win?`,
256
+ options: [
257
+ {
258
+ value: 'local',
259
+ label: 'Keep local',
260
+ hint: `modified ${localMtime.toLocaleString()}`
261
+ },
262
+ {
263
+ value: 'remote',
264
+ label: 'Use remote',
265
+ hint: `modified ${remoteMtime.toLocaleString()}`
266
+ }
267
+ ]
268
+ });
269
+ if (p.isCancel(choice)) {
270
+ p.cancel('Cancelled.');
271
+ process.exit(0);
272
+ }
273
+ if (choice === 'local') {
274
+ p.log.info(`${file.label}: ${color.green('keeping local, will push')}`);
275
+ await fs.ensureDir(path.dirname(file.repoPath));
276
+ await fs.copy(file.localPath, file.repoPath, { overwrite: true });
277
+ anyChanges = true;
278
+ }
279
+ else {
280
+ p.log.info(`${file.label}: ${color.cyan('applying remote')}`);
281
+ if (file.label === 'settings.json') {
282
+ await applyRemoteSettings(file.repoPath, path.join(tmp, 'extensions', 'index.json'), file.localPath, silent);
283
+ }
284
+ else {
285
+ await fs.ensureDir(path.dirname(file.localPath));
286
+ await fs.copy(file.repoPath, file.localPath, { overwrite: true });
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+ // 3. Commit + push if any local files were written to the repo
293
+ if (anyChanges) {
294
+ spinner.start('Pushing changes to remote...');
295
+ await git.add(['settings.json', path.join('extensions', 'index.json')]);
296
+ const status = await git.status();
297
+ if (status.staged.length > 0) {
298
+ const timestamp = new Date().toISOString();
299
+ await git.commit(`sync: ${timestamp}`);
300
+ try {
301
+ await git.push('origin', config.branch, ['--set-upstream']);
302
+ }
303
+ catch {
304
+ await git.push('origin', config.branch);
305
+ }
306
+ spinner.stop('Pushed.');
307
+ }
308
+ else {
309
+ spinner.stop('Nothing staged to push.');
310
+ }
311
+ }
312
+ });
313
+ // 4. Save last sync timestamp
314
+ await writeSyncConfig({ ...config, lastSync: new Date().toISOString() });
315
+ if (!silent)
316
+ p.outro(`${color.green('✓')} Sync complete.`);
317
+ }
@@ -1,4 +1,12 @@
1
1
  export type ExtensionType = 'theme' | 'language';
2
+ export interface ZedPaths {
3
+ settings: string;
4
+ extensions: string;
5
+ }
6
+ export interface SyncConfig {
7
+ syncRepo: string;
8
+ branch: string;
9
+ }
2
10
  export type License = 'Apache-2.0' | 'BSD-2-Clause' | 'BSD-3-Clause' | 'GPL-3.0' | 'LGPL-3.0' | 'MIT' | 'Zlib';
3
11
  export interface ExtensionOptions {
4
12
  name: string;
@@ -0,0 +1,2 @@
1
+ import type { ZedPaths } from './types/index.js';
2
+ export declare function resolveZedPaths(): ZedPaths;
@@ -0,0 +1,30 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ export function resolveZedPaths() {
4
+ const home = os.homedir();
5
+ const platform = process.platform;
6
+ if (platform === 'darwin') {
7
+ return {
8
+ settings: path.join(home, '.config', 'zed', 'settings.json'),
9
+ extensions: path.join(home, 'Library', 'Application Support', 'Zed', 'extensions', 'index.json'),
10
+ };
11
+ }
12
+ if (platform === 'linux') {
13
+ const xdgData = process.env.FLATPAK_XDG_DATA_HOME ||
14
+ process.env.XDG_DATA_HOME ||
15
+ path.join(home, '.local', 'share');
16
+ return {
17
+ settings: path.join(home, '.config', 'zed', 'settings.json'),
18
+ extensions: path.join(xdgData, 'zed', 'extensions', 'index.json'),
19
+ };
20
+ }
21
+ if (platform === 'win32') {
22
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
23
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
24
+ return {
25
+ settings: path.join(appData, 'Zed', 'settings.json'),
26
+ extensions: path.join(localAppData, 'Zed', 'extensions', 'index.json'),
27
+ };
28
+ }
29
+ throw new Error(`Unsupported platform: ${platform}`);
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zedx",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Boilerplate generator for Zed Edittor extensions.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -47,7 +47,8 @@
47
47
  "commander": "^14.0.3",
48
48
  "ejs": "^4.0.1",
49
49
  "fs-extra": "^11.3.3",
50
- "picocolors": "^1.1.1"
50
+ "picocolors": "^1.1.1",
51
+ "simple-git": "^3.33.0"
51
52
  },
52
53
  "packageManager": "pnpm@10.30.0+sha512.2b5753de015d480eeb88f5b5b61e0051f05b4301808a82ec8b840c9d2adf7748eb352c83f5c1593ca703ff1017295bc3fdd3119abb9686efc96b9fcb18200937"
53
54
  }