zedx 0.9.0 → 0.11.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,10 @@
1
- # zedx
1
+ <div align="center">
2
2
 
3
- Scaffold [Zed Editor](https://zed.dev/) extensions and sync your settings across machines.
3
+ <img src="./assets/zedx-logo.png" width="300" alt="ZedX Logo" />
4
+
5
+ CLI toolkit for Scaffolding [Zed Editor](https://zed.dev/) extensions and syncing settings across machines.
6
+
7
+ </div>
4
8
 
5
9
  ![screenshot](./assets/screenshot1.png)
6
10
 
@@ -13,7 +17,9 @@ npm install -g zedx
13
17
  brew install tahayvr/tap/zedx
14
18
  ```
15
19
 
16
- ### Usage
20
+ ## Usage
21
+
22
+ ### Scaffolding an extension
17
23
 
18
24
  ```bash
19
25
  # Create a new extension
@@ -22,28 +28,84 @@ zedx create
22
28
  # Add a theme or language to an existing extension
23
29
  zedx add theme "Midnight Blue"
24
30
  zedx add language rust
31
+ ```
25
32
 
33
+ ### Supported extension types:
34
+
35
+ 1. **Themes** - Color schemes for the editor
36
+ 2. **Languages** - Syntax highlighting, indentation, and optional LSP support
37
+
38
+ You can choose to include theme, language, or both when creating an extension.
39
+
40
+ ### Validation
41
+
42
+ ```bash
26
43
  # Validate extension config and show what is missing or incomplete
27
44
  zedx check
45
+ ```
46
+
47
+ ### Sync
48
+
49
+ Sync your Zed config across machines using a private Git repo as the source of truth.
50
+
51
+ **1. Link a repo (one-time setup)**
52
+
53
+ ```bash
54
+ zedx sync init
55
+ ```
56
+
57
+ Prompts for a Git repo URL (SSH or HTTPS) and a branch name (defaults to `main`). The repo is saved to `~/.config/zedx/config.json`. No files are synced yet.
58
+
59
+ > [!NOTE]
60
+ > `settings.json` and `keymap.json` are tracked. Extension sync is handled via the `auto_install_extensions` field within `settings.json`, which Zed uses to automatically download and install extensions.
61
+
62
+ **2. Run a sync**
63
+
64
+ ```bash
65
+ zedx sync # Sync local ↔ remote, prompts when both sides changed
66
+ zedx sync --local # Always keep local on conflict (no prompt)
67
+ zedx sync --remote # Always use remote on conflict (no prompt)
68
+ ```
69
+
70
+ **3. Check sync state**
71
+
72
+ ```bash
73
+ zedx sync status
74
+ ```
75
+
76
+ **4. Auto-sync with an OS daemon**
77
+
78
+ ```bash
79
+ zedx sync install # Install and enable the daemon
80
+ zedx sync uninstall # Disable and remove the daemon
81
+ ```
82
+
83
+ Installs a file-watcher that triggers `zedx sync` automatically whenever config files are saved. Supported platforms:
84
+
85
+ | Platform | Mechanism | Logs |
86
+ | -------- | ------------------------------------------------------ | ---------------------------------------- |
87
+ | macOS | launchd (`~/Library/LaunchAgents/dev.zedx.sync.plist`) | `~/Library/Logs/zedx-sync.log` |
88
+ | Linux | systemd user units (`~/.config/systemd/user/`) | `journalctl --user -u zedx-sync.service` |
89
+
90
+ The daemon enforces a 30-second throttle on macOS to avoid rapid re-triggers. When a conflict is detected in daemon mode (no TTY), local always wins and a warning is logged.
91
+
92
+ ### Config
28
93
 
29
- # Bump extension version
94
+ ```bash
95
+ zedx config # Open interactive config menu
96
+ zedx config repo # Change your sync repo and branch directly
97
+ ```
98
+
99
+ ### Versioning
100
+
101
+ Bump the extension version:
102
+
103
+ ```bash
30
104
  zedx version patch # 1.2.3 → 1.2.4
31
105
  zedx version minor # 1.2.3 → 1.3.0
32
106
  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 (prompts on conflict)
37
- zedx sync --local # Sync, always keeping local on conflict
38
- zedx sync --remote # Sync, always using remote on conflict
39
- zedx sync status # Show sync state between local config and the remote repo
40
- zedx sync install # Install an OS daemon to auto-sync when Zed config changes
41
- zedx sync uninstall # Remove the OS daemon
42
107
  ```
43
108
 
44
- ### Supported extension types:
45
-
46
- 1. **Themes** - Color schemes for the editor
47
- 2. **Languages** - Syntax highlighting, indentation, and optional LSP support
109
+ ### License
48
110
 
49
- You can choose to include theme, language, or both when creating an extension.
111
+ License is Apache-2.0. See [LICENSE](./LICENSE) for details.
package/dist/check.js CHANGED
@@ -42,7 +42,7 @@ export async function runCheck(callerDir) {
42
42
  extIssues.push({
43
43
  file: 'extension.toml',
44
44
  message: 'repository still uses the default placeholder URL',
45
- hint: 'Set it to your actual GitHub repository URL',
45
+ hint: 'Set it to your actual Git repository URL',
46
46
  });
47
47
  }
48
48
  // Detect language entries by looking for uncommented [grammars.*] sections
@@ -0,0 +1,4 @@
1
+ export declare function configRepo(): Promise<void>;
2
+ type ConfigOption = 'repo';
3
+ export declare function runConfig(direct?: ConfigOption): Promise<void>;
4
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,95 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import * as p from '@clack/prompts';
4
+ import fs from 'fs-extra';
5
+ import color from 'picocolors';
6
+ import simpleGit from 'simple-git';
7
+ const ZEDX_CONFIG_DIR = path.join(os.homedir(), '.config', 'zedx');
8
+ const ZEDX_CONFIG_PATH = path.join(ZEDX_CONFIG_DIR, 'config.json');
9
+ async function readConfig() {
10
+ if (!(await fs.pathExists(ZEDX_CONFIG_PATH)))
11
+ return null;
12
+ return fs.readJson(ZEDX_CONFIG_PATH);
13
+ }
14
+ async function writeConfig(config) {
15
+ await fs.ensureDir(ZEDX_CONFIG_DIR);
16
+ await fs.writeJson(ZEDX_CONFIG_PATH, config, { spaces: 4 });
17
+ }
18
+ // zedx config repo
19
+ export async function configRepo() {
20
+ console.log('');
21
+ p.intro(`${color.bgBlue(color.bold(' zedx config repo '))} ${color.blue('Change your sync repo and branch…')}`);
22
+ const existing = await readConfig();
23
+ const repo = await p.text({
24
+ message: 'Git repo URL (SSH or HTTPS)',
25
+ placeholder: 'https://github.com/you/zed-config.git',
26
+ initialValue: existing?.syncRepo ?? '',
27
+ validate: v => {
28
+ if (!v.trim())
29
+ return 'Repo URL is required';
30
+ if (!v.startsWith('https://') && !v.startsWith('git@')) {
31
+ return 'Must be a valid HTTPS or SSH git URL';
32
+ }
33
+ },
34
+ });
35
+ if (p.isCancel(repo)) {
36
+ p.cancel('Cancelled.');
37
+ process.exit(0);
38
+ }
39
+ const branch = await p.text({
40
+ message: 'Branch name',
41
+ placeholder: 'main',
42
+ initialValue: existing?.branch ?? 'main',
43
+ defaultValue: 'main',
44
+ });
45
+ if (p.isCancel(branch)) {
46
+ p.cancel('Cancelled.');
47
+ process.exit(0);
48
+ }
49
+ const spinner = p.spinner();
50
+ spinner.start('Verifying repo is reachable...');
51
+ try {
52
+ const git = simpleGit();
53
+ await git.listRemote(['--heads', repo]);
54
+ spinner.stop('Repo verified.');
55
+ }
56
+ catch {
57
+ spinner.stop(color.red('Could not reach repo. Check the URL and your access.'));
58
+ process.exit(1);
59
+ }
60
+ const updated = {
61
+ ...existing,
62
+ syncRepo: repo.trim(),
63
+ branch: (branch || 'main').trim(),
64
+ };
65
+ await writeConfig(updated);
66
+ p.outro(`${color.green('✓')} Sync repo updated.\n\n` +
67
+ ` Repo: ${color.cyan(updated.syncRepo)}\n` +
68
+ ` Branch: ${color.cyan(updated.branch)}`);
69
+ }
70
+ // zedx config (interactive menu)
71
+ export async function runConfig(direct) {
72
+ if (direct === 'repo') {
73
+ await configRepo();
74
+ return;
75
+ }
76
+ console.log('');
77
+ p.intro(`${color.bgBlue(color.bold(' zedx config '))} ${color.blue('Zedx settings')}`);
78
+ const option = await p.select({
79
+ message: 'What do you want to configure?',
80
+ options: [
81
+ {
82
+ value: 'repo',
83
+ label: 'Sync repo',
84
+ hint: 'Change your git repo and branch for zedx sync',
85
+ },
86
+ ],
87
+ });
88
+ if (p.isCancel(option)) {
89
+ p.cancel('Cancelled.');
90
+ process.exit(0);
91
+ }
92
+ if (option === 'repo') {
93
+ await configRepo();
94
+ }
95
+ }
package/dist/daemon.js CHANGED
@@ -157,7 +157,7 @@ export async function syncInstall() {
157
157
  if (platform !== 'darwin' && platform !== 'linux')
158
158
  unsupportedPlatform();
159
159
  const zedPaths = resolveZedPaths();
160
- const watchPaths = [zedPaths.settings, zedPaths.extensions];
160
+ const watchPaths = [zedPaths.settings, zedPaths.keymap];
161
161
  const zedxBin = resolveZedxBinary();
162
162
  p.log.info(`Binary: ${color.dim(zedxBin)}`);
163
163
  p.log.info(`Watching:`);
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import fs from 'fs-extra';
7
7
  import color from 'picocolors';
8
8
  import { addTheme, addLanguage } from './add.js';
9
9
  import { runCheck } from './check.js';
10
+ import { runConfig, configRepo } from './config.js';
10
11
  import { syncInstall, syncUninstall } from './daemon.js';
11
12
  import { generateExtension } from './generator.js';
12
13
  import { installDevExtension } from './install.js';
@@ -59,7 +60,17 @@ function printWelcome() {
59
60
  `.trim();
60
61
  console.log('\n' + color.cyan(color.bold(ascii)) + '\n');
61
62
  console.log(color.bold(' The CLI toolkit for Zed Editor') + '\n');
62
- const commands = [
63
+ const syncCommands = [
64
+ ['zedx sync', 'Sync Zed settings via a git repo'],
65
+ ['zedx sync init', 'Link a git repo as the sync target'],
66
+ ['zedx sync select', 'Choose which files to sync interactively'],
67
+ ['zedx sync status', 'Show sync state between local and remote'],
68
+ ['zedx sync install', 'Install the OS daemon for auto-sync'],
69
+ ['zedx sync uninstall', 'Remove the auto-sync daemon'],
70
+ ['zedx config', 'Configure zedx settings'],
71
+ ['zedx config repo', 'Change your sync repo and branch'],
72
+ ];
73
+ const extensionCommands = [
63
74
  ['zedx create', 'Scaffold a new Zed extension'],
64
75
  ['zedx add theme <name>', 'Add a theme to an existing extension'],
65
76
  ['zedx add language <id>', 'Add a language to an existing extension'],
@@ -67,18 +78,17 @@ function printWelcome() {
67
78
  ['zedx check', 'Validate your extension config'],
68
79
  ['zedx install', 'Install as a Zed dev extension'],
69
80
  ['zedx version <major|minor|patch>', 'Bump extension version'],
70
- ['zedx sync', 'Sync Zed settings via a git repo'],
71
- ['zedx sync select', 'Choose which files to sync interactively'],
72
- ['zedx sync init', 'Link a git repo as the sync target'],
73
- ['zedx sync status', 'Show sync state between local and remote'],
74
- ['zedx sync install', 'Install the OS daemon for auto-sync'],
75
- ['zedx sync uninstall', 'Remove the auto-sync daemon'],
76
81
  ];
77
- console.log(color.dim(' Commands:\n'));
78
- for (const [cmd, desc] of commands) {
79
- console.log(` ${color.cyan(cmd.padEnd(38))}${color.dim(desc)}`);
82
+ const pad = 38;
83
+ console.log(` ${color.bold('Sync')}\n`);
84
+ for (const [cmd, desc] of syncCommands) {
85
+ console.log(` ${color.cyan(cmd.padEnd(pad))}${color.dim(desc)}`);
86
+ }
87
+ console.log(`\n ${color.bold('Extensions')}\n`);
88
+ for (const [cmd, desc] of extensionCommands) {
89
+ console.log(` ${color.cyan(cmd.padEnd(pad))}${color.dim(desc)}`);
80
90
  }
81
- console.log(`\n ${color.dim('Zed Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions'))}\n`);
91
+ console.log(`\n ${color.dim('Zedx Repo:')} ${color.underline(color.blue('https://github.com/tahayvr/zedx'))}\n`);
82
92
  }
83
93
  async function runCreate() {
84
94
  const options = await promptUser();
@@ -159,7 +169,7 @@ async function main() {
159
169
  });
160
170
  const syncCmd = program
161
171
  .command('sync')
162
- .description('Sync Zed settings and extensions via a GitHub repo')
172
+ .description('Sync Zed settings and extensions via a Git repo')
163
173
  .option('--local', 'On conflict, always keep the local version')
164
174
  .option('--remote', 'On conflict, always use the remote version')
165
175
  .action(async (opts) => {
@@ -176,7 +186,7 @@ async function main() {
176
186
  });
177
187
  syncCmd
178
188
  .command('init')
179
- .description('Link a GitHub repo as the sync target')
189
+ .description('Link a Git repo as the sync target')
180
190
  .action(async () => {
181
191
  await syncInit();
182
192
  });
@@ -204,6 +214,18 @@ async function main() {
204
214
  .action(async () => {
205
215
  await syncUninstall();
206
216
  });
217
+ const configCmd = program
218
+ .command('config')
219
+ .description('Configure zedx settings')
220
+ .action(async () => {
221
+ await runConfig();
222
+ });
223
+ configCmd
224
+ .command('repo')
225
+ .description('Change your sync repo and branch')
226
+ .action(async () => {
227
+ await configRepo();
228
+ });
207
229
  const argv = process.argv.filter(arg => arg !== '--');
208
230
  if (argv.length <= 2) {
209
231
  printWelcome();
package/dist/sync.js CHANGED
@@ -37,76 +37,66 @@ async function withTempDir(fn) {
37
37
  await fs.remove(tmp);
38
38
  }
39
39
  }
40
- // Prepare settings.json for pushing to the repo by stripping auto_install_extensions.
41
- // That field is derived from extensions/index.json on pull, so storing it in the
42
- // remote would create stale/conflicting data across machines.
43
- async function prepareSettingsForPush(localSettingsPath, repoSettingsPath) {
44
- const raw = await fs.readFile(localSettingsPath, 'utf-8');
45
- const stripped = raw.replace(/\/\/[^\n]*/g, '');
46
- let settingsObj = {};
47
- try {
48
- settingsObj = JSON.parse(stripped);
49
- }
50
- catch {
51
- // If we can't parse it (e.g. complex comments), push as-is
52
- await fs.ensureDir(path.dirname(repoSettingsPath));
53
- await fs.copy(localSettingsPath, repoSettingsPath, { overwrite: true });
40
+ // Before every push, reconcile auto_install_extensions in settings.json against
41
+ // the live extensions/index.json so that:
42
+ // - newly installed extensions (present in index, missing from the list) are added as true
43
+ // - uninstalled extensions (absent from index, set to true in the list) are removed
44
+ // - entries explicitly set to false by the user are always preserved (user intent)
45
+ async function reconcileAutoInstallExtensions(localSettingsPath, localExtensionsIndexPath, silent = false) {
46
+ if (!(await fs.pathExists(localExtensionsIndexPath)))
54
47
  return;
55
- }
56
- delete settingsObj['auto_install_extensions'];
57
- await fs.ensureDir(path.dirname(repoSettingsPath));
58
- await fs.writeFile(repoSettingsPath, JSON.stringify(settingsObj, null, 4), 'utf-8');
59
- }
60
- // Extension merge helper
61
- async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPath, silent = false) {
62
- // Backup existing settings
48
+ let settingsObj = {};
63
49
  if (await fs.pathExists(localSettingsPath)) {
64
- await fs.copy(localSettingsPath, localSettingsPath + '.bak', { overwrite: true });
65
- if (!silent)
66
- p.log.info(`Backed up settings to ${color.dim(localSettingsPath + '.bak')}`);
67
- }
68
- let settingsJson = await fs.readFile(repoSettings, 'utf-8');
69
- // Merge auto_install_extensions from index.json into settings
70
- if (await fs.pathExists(repoExtensions)) {
71
50
  try {
72
- const indexJson = (await fs.readJson(repoExtensions));
73
- const extensionIds = Object.keys(indexJson.extensions ?? {}).filter(id => !indexJson.extensions[id]?.dev);
74
- if (extensionIds.length > 0) {
75
- const stripped = settingsJson.replace(/\/\/[^\n]*/g, '');
76
- let settingsObj = {};
77
- try {
78
- settingsObj = JSON.parse(stripped);
79
- }
80
- catch {
81
- if (!silent)
82
- p.log.warn(color.yellow('Could not parse settings.json — skipping extension merge.'));
83
- }
84
- // Preserve any existing entries (e.g. false entries for "never install"),
85
- // then add true for every extension recorded in index.json.
86
- const existing = typeof settingsObj['auto_install_extensions'] === 'object' &&
87
- settingsObj['auto_install_extensions'] !== null
88
- ? settingsObj['auto_install_extensions']
89
- : {};
90
- const autoInstall = { ...existing };
91
- for (const id of extensionIds) {
92
- // Only set to true if there is no explicit user preference already
93
- if (!(id in autoInstall)) {
94
- autoInstall[id] = true;
95
- }
96
- }
97
- settingsObj['auto_install_extensions'] = autoInstall;
98
- settingsJson = JSON.stringify(settingsObj, null, 4);
99
- if (!silent)
100
- p.log.info(`Injected ${color.cyan(String(extensionIds.length))} extension(s) into ${color.dim('auto_install_extensions')}`);
101
- }
51
+ const raw = await fs.readFile(localSettingsPath, 'utf-8');
52
+ settingsObj = JSON.parse(raw.replace(/\/\/[^\n]*/g, ''));
102
53
  }
103
54
  catch {
104
55
  if (!silent)
105
- p.log.warn(color.yellow('Could not parse extensions/index.json — skipping extension merge.'));
56
+ p.log.warn(color.yellow('Could not parse settings.json — skipping extension reconciliation.'));
57
+ return;
106
58
  }
107
59
  }
60
+ let installedIds = [];
61
+ try {
62
+ const indexJson = (await fs.readJson(localExtensionsIndexPath));
63
+ installedIds = Object.keys(indexJson.extensions ?? {}).filter(id => !indexJson.extensions[id]?.dev);
64
+ }
65
+ catch {
66
+ if (!silent)
67
+ p.log.warn(color.yellow('Could not parse extensions/index.json — skipping extension reconciliation.'));
68
+ return;
69
+ }
70
+ const existing = typeof settingsObj['auto_install_extensions'] === 'object' &&
71
+ settingsObj['auto_install_extensions'] !== null
72
+ ? settingsObj['auto_install_extensions']
73
+ : {};
74
+ const installedSet = new Set(installedIds);
75
+ const reconciled = {};
76
+ // Keep all explicit false entries (user said "never install this")
77
+ for (const [id, val] of Object.entries(existing)) {
78
+ if (val === false)
79
+ reconciled[id] = false;
80
+ }
81
+ // Add every currently installed extension as true
82
+ for (const id of installedIds) {
83
+ reconciled[id] = true;
84
+ }
85
+ // Drop true entries for extensions no longer installed
86
+ // (already handled — we only re-add what's in installedSet above)
87
+ const added = installedIds.filter(id => !(id in existing));
88
+ const removed = Object.keys(existing).filter(id => existing[id] === true && !installedSet.has(id));
89
+ if (added.length === 0 && removed.length === 0)
90
+ return;
91
+ settingsObj['auto_install_extensions'] = reconciled;
108
92
  await fs.ensureDir(path.dirname(localSettingsPath));
109
- await fs.writeFile(localSettingsPath, settingsJson, 'utf-8');
93
+ await fs.writeFile(localSettingsPath, JSON.stringify(settingsObj, null, 4), 'utf-8');
94
+ if (!silent) {
95
+ if (added.length > 0)
96
+ p.log.info(`Added ${color.cyan(String(added.length))} new extension(s) to ${color.dim('auto_install_extensions')}: ${added.join(', ')}`);
97
+ if (removed.length > 0)
98
+ p.log.info(`Removed ${color.cyan(String(removed.length))} uninstalled extension(s) from ${color.dim('auto_install_extensions')}: ${removed.join(', ')}`);
99
+ }
110
100
  }
111
101
  // zedx sync status
112
102
  export async function syncStatus() {
@@ -138,14 +128,18 @@ export async function syncStatus() {
138
128
  {
139
129
  repoPath: path.join(tmp, 'settings.json'),
140
130
  localPath: zedPaths.settings,
141
- label: 'settings.json',
131
+ label: 'Settings',
142
132
  },
143
133
  {
144
- repoPath: path.join(tmp, 'extensions', 'index.json'),
145
- localPath: zedPaths.extensions,
146
- label: 'extensions/index.json',
134
+ repoPath: path.join(tmp, 'keymap.json'),
135
+ localPath: zedPaths.keymap,
136
+ label: 'Key bindings',
147
137
  },
148
138
  ];
139
+ // Track per-file actions needed for the outro message
140
+ const toPush = [];
141
+ const toPull = [];
142
+ const toResolve = [];
149
143
  for (const file of files) {
150
144
  const localExists = await fs.pathExists(file.localPath);
151
145
  const remoteFileExists = remoteExists && (await fs.pathExists(file.repoPath));
@@ -155,10 +149,12 @@ export async function syncStatus() {
155
149
  }
156
150
  if (localExists && !remoteFileExists) {
157
151
  p.log.warn(`${color.bold(file.label)}: ${color.green('local only')} — not pushed yet`);
152
+ toPush.push(file.label);
158
153
  continue;
159
154
  }
160
155
  if (!localExists && remoteFileExists) {
161
156
  p.log.warn(`${color.bold(file.label)}: ${color.cyan('remote only')} — not pulled yet`);
157
+ toPull.push(file.label);
162
158
  continue;
163
159
  }
164
160
  const localContent = await fs.readFile(file.localPath, 'utf-8');
@@ -174,23 +170,41 @@ export async function syncStatus() {
174
170
  const remoteChanged = !lastSync || remoteMtime > lastSync;
175
171
  if (localChanged && !remoteChanged) {
176
172
  p.log.warn(`${color.bold(file.label)}: ${color.green('local ahead')} — modified ${color.dim(localMtime.toLocaleString())}`);
173
+ toPush.push(file.label);
177
174
  }
178
175
  else if (remoteChanged && !localChanged) {
179
176
  p.log.warn(`${color.bold(file.label)}: ${color.cyan('remote ahead')} — modified ${color.dim(remoteMtime.toLocaleString())}`);
177
+ toPull.push(file.label);
180
178
  }
181
179
  else {
182
180
  p.log.warn(`${color.bold(file.label)}: ${color.yellow('conflict')} — both changed since last sync`);
181
+ toResolve.push(file.label);
183
182
  }
184
183
  }
184
+ const singleAction = [toPush, toPull, toResolve].filter(a => a.length > 0).length === 1;
185
+ const needsSync = toPush.length > 0 || toPull.length > 0 || toResolve.length > 0;
186
+ if (needsSync) {
187
+ if (singleAction && toPush.length > 0) {
188
+ p.outro(`Run ${color.cyan('zedx sync')} to push ${toPush.map(l => color.bold(l)).join(', ')} to remote.`);
189
+ }
190
+ else if (singleAction && toPull.length > 0) {
191
+ p.outro(`Run ${color.cyan('zedx sync')} to pull ${toPull.map(l => color.bold(l)).join(', ')} from remote.`);
192
+ }
193
+ else {
194
+ p.outro(`Run ${color.cyan('zedx sync')} to resolve.`);
195
+ }
196
+ }
197
+ else {
198
+ p.outro('Everything is in sync.');
199
+ }
185
200
  });
186
- p.outro(`Run ${color.cyan('zedx sync')} to resolve.`);
187
201
  }
188
202
  // zedx sync init
189
203
  export async function syncInit() {
190
204
  console.log('');
191
205
  p.intro(`${color.bgBlue(color.bold(' zedx sync init '))} ${color.blue('Linking a git repo as the sync target…')}`);
192
206
  const repo = await p.text({
193
- message: 'GitHub repo URL (SSH or HTTPS)',
207
+ message: 'Git repo URL (SSH or HTTPS)',
194
208
  placeholder: 'https://github.com/you/zed-config.git',
195
209
  validate: v => {
196
210
  if (!v.trim())
@@ -221,7 +235,8 @@ export async function syncInit() {
221
235
  spinner.stop('Repo verified.');
222
236
  }
223
237
  catch {
224
- spinner.stop(color.yellow('Could not verify repo (may be empty or private — continuing anyway).'));
238
+ spinner.stop(color.red('Could not reach repo. Check the URL and your access.'));
239
+ process.exit(1);
225
240
  }
226
241
  const config = {
227
242
  syncRepo: repo.trim(),
@@ -239,13 +254,13 @@ export async function syncSelect() {
239
254
  const allFiles = [
240
255
  {
241
256
  value: 'settings',
242
- label: 'settings.json',
243
- hint: 'Zed editor settings',
257
+ label: 'Settings',
258
+ hint: 'settings.json',
244
259
  },
245
260
  {
246
- value: 'extensions',
247
- label: 'extensions/index.json',
248
- hint: 'Installed extensions list',
261
+ value: 'keymap',
262
+ label: 'Key bindings',
263
+ hint: 'keymap.json',
249
264
  },
250
265
  ];
251
266
  const selected = await p.multiselect({
@@ -283,7 +298,7 @@ export async function runSync(opts = {}) {
283
298
  };
284
299
  if (!silent) {
285
300
  console.log('');
286
- p.intro(`${color.bgBlue(color.bold(' zedx sync '))} ${color.blue('Syncing Zed settings and extensions…')}`);
301
+ p.intro(`${color.bgBlue(color.bold(' zedx sync '))} ${color.blue('Syncing Zed config…')}`);
287
302
  }
288
303
  const config = await requireSyncConfig();
289
304
  const zedPaths = resolveZedPaths();
@@ -316,13 +331,13 @@ export async function runSync(opts = {}) {
316
331
  key: 'settings',
317
332
  repoPath: path.join(tmp, 'settings.json'),
318
333
  localPath: zedPaths.settings,
319
- label: 'settings.json',
334
+ label: 'Settings',
320
335
  },
321
336
  {
322
- key: 'extensions',
323
- repoPath: path.join(tmp, 'extensions', 'index.json'),
324
- localPath: zedPaths.extensions,
325
- label: 'extensions/index.json',
337
+ key: 'keymap',
338
+ repoPath: path.join(tmp, 'keymap.json'),
339
+ localPath: zedPaths.keymap,
340
+ label: 'Key bindings',
326
341
  },
327
342
  ];
328
343
  const files = selectedFiles
@@ -337,29 +352,27 @@ export async function runSync(opts = {}) {
337
352
  log.warn(`${file.label}: not found locally or remotely — skipping.`);
338
353
  continue;
339
354
  }
340
- // Remote doesn't have it yet — push local
355
+ // Remote doesn't have it yet — first push.
356
+ // Bootstrap auto_install_extensions from local extensions/index.json so
357
+ // the synced settings.json is self-contained on a fresh machine.
341
358
  if (localExists && !remoteFileExists) {
342
359
  log.info(`${file.label}: ${color.green('pushing')} (not in remote yet)`);
343
- if (file.label === 'settings.json') {
344
- await prepareSettingsForPush(file.localPath, file.repoPath);
345
- }
346
- else {
347
- await fs.ensureDir(path.dirname(file.repoPath));
348
- await fs.copy(file.localPath, file.repoPath, { overwrite: true });
349
- }
360
+ await reconcileAutoInstallExtensions(file.localPath, zedPaths.extensionsIndex, silent);
361
+ await fs.ensureDir(path.dirname(file.repoPath));
362
+ await fs.copy(file.localPath, file.repoPath, { overwrite: true });
350
363
  anyChanges = true;
351
364
  continue;
352
365
  }
353
366
  // Local doesn't have it — pull remote
354
367
  if (!localExists && remoteFileExists) {
355
368
  log.info(`${file.label}: ${color.cyan('pulling')} (not found locally)`);
356
- if (file.label === 'settings.json') {
357
- await applyRemoteSettings(file.repoPath, path.join(tmp, 'extensions', 'index.json'), file.localPath, silent);
358
- }
359
- else {
360
- await fs.ensureDir(path.dirname(file.localPath));
361
- await fs.copy(file.repoPath, file.localPath, { overwrite: true });
369
+ if (await fs.pathExists(file.localPath)) {
370
+ await fs.copy(file.localPath, file.localPath + '.bak', { overwrite: true });
371
+ if (!silent)
372
+ p.log.info(`Backed up settings to ${color.dim(file.localPath + '.bak')}`);
362
373
  }
374
+ await fs.ensureDir(path.dirname(file.localPath));
375
+ await fs.copy(file.repoPath, file.localPath, { overwrite: true });
363
376
  continue;
364
377
  }
365
378
  // Both exist — compare content
@@ -377,27 +390,20 @@ export async function runSync(opts = {}) {
377
390
  const localChanged = !lastSync || localMtime > lastSync;
378
391
  const remoteChanged = !lastSync || remoteMtime > lastSync;
379
392
  if (localChanged && !remoteChanged) {
380
- // Only local changed → push
393
+ // Only local changed → reconcile extensions then push
381
394
  log.info(`${file.label}: ${color.green('pushing')} (local is newer)`);
382
- if (file.label === 'settings.json') {
383
- await prepareSettingsForPush(file.localPath, file.repoPath);
384
- }
385
- else {
386
- await fs.ensureDir(path.dirname(file.repoPath));
387
- await fs.copy(file.localPath, file.repoPath, { overwrite: true });
388
- }
395
+ await reconcileAutoInstallExtensions(file.localPath, zedPaths.extensionsIndex, silent);
396
+ await fs.ensureDir(path.dirname(file.repoPath));
397
+ await fs.copy(file.localPath, file.repoPath, { overwrite: true });
389
398
  anyChanges = true;
390
399
  }
391
400
  else if (remoteChanged && !localChanged) {
392
401
  // Only remote changed → pull
393
402
  log.info(`${file.label}: ${color.cyan('pulling')} (remote is newer)`);
394
- if (file.label === 'settings.json') {
395
- await applyRemoteSettings(file.repoPath, path.join(tmp, 'extensions', 'index.json'), file.localPath, silent);
396
- }
397
- else {
398
- await fs.ensureDir(path.dirname(file.localPath));
399
- await fs.copy(file.repoPath, file.localPath, { overwrite: true });
400
- }
403
+ await fs.copy(file.localPath, file.localPath + '.bak', { overwrite: true });
404
+ if (!silent)
405
+ p.log.info(`Backed up settings to ${color.dim(file.localPath + '.bak')}`);
406
+ await fs.copy(file.repoPath, file.localPath, { overwrite: true });
401
407
  }
402
408
  else {
403
409
  // Both changed — resolve based on strategy
@@ -441,32 +447,25 @@ export async function runSync(opts = {}) {
441
447
  if (resolution === 'local') {
442
448
  if (!silent && conflict === 'prompt')
443
449
  p.log.info(`${file.label}: ${color.green('keeping local, will push')}`);
444
- if (file.label === 'settings.json') {
445
- await prepareSettingsForPush(file.localPath, file.repoPath);
446
- }
447
- else {
448
- await fs.ensureDir(path.dirname(file.repoPath));
449
- await fs.copy(file.localPath, file.repoPath, { overwrite: true });
450
- }
450
+ await reconcileAutoInstallExtensions(file.localPath, zedPaths.extensionsIndex, silent);
451
+ await fs.ensureDir(path.dirname(file.repoPath));
452
+ await fs.copy(file.localPath, file.repoPath, { overwrite: true });
451
453
  anyChanges = true;
452
454
  }
453
455
  else {
454
456
  if (!silent && conflict === 'prompt')
455
457
  p.log.info(`${file.label}: ${color.cyan('applying remote')}`);
456
- if (file.label === 'settings.json') {
457
- await applyRemoteSettings(file.repoPath, path.join(tmp, 'extensions', 'index.json'), file.localPath, silent);
458
- }
459
- else {
460
- await fs.ensureDir(path.dirname(file.localPath));
461
- await fs.copy(file.repoPath, file.localPath, { overwrite: true });
462
- }
458
+ await fs.copy(file.localPath, file.localPath + '.bak', { overwrite: true });
459
+ if (!silent)
460
+ p.log.info(`Backed up settings to ${color.dim(file.localPath + '.bak')}`);
461
+ await fs.copy(file.repoPath, file.localPath, { overwrite: true });
463
462
  }
464
463
  }
465
464
  }
466
465
  // 3. Commit + push if any local files were written to the repo
467
466
  if (anyChanges) {
468
467
  spinner.start('Pushing changes to remote...');
469
- await git.add(['settings.json', path.join('extensions', 'index.json')]);
468
+ await git.add(files.map(f => path.basename(f.repoPath)));
470
469
  const status = await git.status();
471
470
  if (status.staged.length > 0) {
472
471
  const timestamp = new Date().toISOString();
@@ -1,7 +1,8 @@
1
1
  export type ExtensionType = 'theme' | 'language';
2
2
  export interface ZedPaths {
3
3
  settings: string;
4
- extensions: string;
4
+ keymap: string;
5
+ extensionsIndex: string;
5
6
  }
6
7
  export interface SyncConfig {
7
8
  syncRepo: string;
package/dist/zed-paths.js CHANGED
@@ -4,26 +4,32 @@ export function resolveZedPaths() {
4
4
  const home = os.homedir();
5
5
  const platform = process.platform;
6
6
  if (platform === 'darwin') {
7
+ const configDir = path.join(home, '.config', 'zed');
7
8
  return {
8
- settings: path.join(home, '.config', 'zed', 'settings.json'),
9
- extensions: path.join(home, 'Library', 'Application Support', 'Zed', 'extensions', 'index.json'),
9
+ settings: path.join(configDir, 'settings.json'),
10
+ keymap: path.join(configDir, 'keymap.json'),
11
+ extensionsIndex: path.join(home, 'Library', 'Application Support', 'Zed', 'extensions', 'index.json'),
10
12
  };
11
13
  }
12
14
  if (platform === 'linux') {
13
15
  const xdgData = process.env.FLATPAK_XDG_DATA_HOME ||
14
16
  process.env.XDG_DATA_HOME ||
15
17
  path.join(home, '.local', 'share');
18
+ const configDir = path.join(home, '.config', 'zed');
16
19
  return {
17
- settings: path.join(home, '.config', 'zed', 'settings.json'),
18
- extensions: path.join(xdgData, 'zed', 'extensions', 'index.json'),
20
+ settings: path.join(configDir, 'settings.json'),
21
+ keymap: path.join(configDir, 'keymap.json'),
22
+ extensionsIndex: path.join(xdgData, 'zed', 'extensions', 'index.json'),
19
23
  };
20
24
  }
21
25
  if (platform === 'win32') {
22
26
  const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
23
27
  const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
28
+ const configDir = path.join(appData, 'Zed');
24
29
  return {
25
- settings: path.join(appData, 'Zed', 'settings.json'),
26
- extensions: path.join(localAppData, 'Zed', 'extensions', 'index.json'),
30
+ settings: path.join(configDir, 'settings.json'),
31
+ keymap: path.join(configDir, 'keymap.json'),
32
+ extensionsIndex: path.join(localAppData, 'Zed', 'extensions', 'index.json'),
27
33
  };
28
34
  }
29
35
  throw new Error(`Unsupported platform: ${platform}`);
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "zedx",
3
- "version": "0.9.0",
4
- "description": "Scaffold Zed Editor extensions and sync your settings across machines.",
3
+ "version": "0.11.0",
4
+ "description": "The CLI toolkit for Zed Editor.",
5
5
  "keywords": [
6
6
  "boilerplate",
7
7
  "extension",
8
8
  "scaffold",
9
+ "settings",
10
+ "sync",
9
11
  "zed",
10
12
  "zed-editor"
11
13
  ],