zedx 0.8.1 → 0.10.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 +73 -16
- package/dist/daemon.js +1 -1
- package/dist/index.js +29 -7
- package/dist/snippet.js +1 -1
- package/dist/sync.d.ts +4 -0
- package/dist/sync.js +169 -132
- package/dist/types/index.d.ts +2 -1
- package/dist/zed-paths.js +12 -6
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
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
|

|
|
6
10
|
|
|
@@ -13,7 +17,9 @@ npm install -g zedx
|
|
|
13
17
|
brew install tahayvr/tap/zedx
|
|
14
18
|
```
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Scaffolding an extension
|
|
17
23
|
|
|
18
24
|
```bash
|
|
19
25
|
# Create a new extension
|
|
@@ -22,26 +28,77 @@ 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
|
+
```
|
|
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
|
|
25
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 GitHub 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 GitHub 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**
|
|
28
63
|
|
|
29
|
-
|
|
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
|
+
### Versioning
|
|
93
|
+
|
|
94
|
+
Bump the extension version:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
30
97
|
zedx version patch # 1.2.3 → 1.2.4
|
|
31
98
|
zedx version minor # 1.2.3 → 1.3.0
|
|
32
99
|
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 status # Show sync state between local config and the remote repo
|
|
38
|
-
zedx sync install # Install an OS daemon to auto-sync when Zed config changes
|
|
39
|
-
zedx sync uninstall # Remove the OS daemon
|
|
40
100
|
```
|
|
41
101
|
|
|
42
|
-
###
|
|
102
|
+
### License
|
|
43
103
|
|
|
44
|
-
|
|
45
|
-
2. **Languages** - Syntax highlighting, indentation, and optional LSP support
|
|
46
|
-
|
|
47
|
-
You can choose to include theme, language, or both when creating an extension.
|
|
104
|
+
License is Apache-2.0. See [LICENSE](./LICENSE) for details.
|
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.
|
|
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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'module';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import * as p from '@clack/prompts';
|
|
4
5
|
import { Command } from 'commander';
|
|
@@ -11,7 +12,9 @@ import { generateExtension } from './generator.js';
|
|
|
11
12
|
import { installDevExtension } from './install.js';
|
|
12
13
|
import { promptUser, promptThemeDetails, promptLanguageDetails } from './prompts.js';
|
|
13
14
|
import { addLsp } from './snippet.js';
|
|
14
|
-
import { syncInit, runSync, syncStatus } from './sync.js';
|
|
15
|
+
import { syncInit, runSync, syncStatus, syncSelect } from './sync.js';
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const { version } = require('../package.json');
|
|
15
18
|
function bumpVersion(version, type) {
|
|
16
19
|
const [major, minor, patch] = version.split('.').map(Number);
|
|
17
20
|
switch (type) {
|
|
@@ -65,6 +68,7 @@ function printWelcome() {
|
|
|
65
68
|
['zedx install', 'Install as a Zed dev extension'],
|
|
66
69
|
['zedx version <major|minor|patch>', 'Bump extension version'],
|
|
67
70
|
['zedx sync', 'Sync Zed settings via a git repo'],
|
|
71
|
+
['zedx sync select', 'Choose which files to sync interactively'],
|
|
68
72
|
['zedx sync init', 'Link a git repo as the sync target'],
|
|
69
73
|
['zedx sync status', 'Show sync state between local and remote'],
|
|
70
74
|
['zedx sync install', 'Install the OS daemon for auto-sync'],
|
|
@@ -74,7 +78,7 @@ function printWelcome() {
|
|
|
74
78
|
for (const [cmd, desc] of commands) {
|
|
75
79
|
console.log(` ${color.cyan(cmd.padEnd(38))}${color.dim(desc)}`);
|
|
76
80
|
}
|
|
77
|
-
console.log(`\n ${color.dim('Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions'))}\n`);
|
|
81
|
+
console.log(`\n ${color.dim('Zed Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions'))}\n`);
|
|
78
82
|
}
|
|
79
83
|
async function runCreate() {
|
|
80
84
|
const options = await promptUser();
|
|
@@ -99,7 +103,7 @@ async function runCreate() {
|
|
|
99
103
|
}
|
|
100
104
|
async function main() {
|
|
101
105
|
const program = new Command();
|
|
102
|
-
program.name('zedx').description('The CLI toolkit for Zed Editor.').
|
|
106
|
+
program.name('zedx').description('The CLI toolkit for Zed Editor.').version(`zedx v${version}`);
|
|
103
107
|
program
|
|
104
108
|
.command('create')
|
|
105
109
|
.description('Scaffold a new Zed extension')
|
|
@@ -156,8 +160,19 @@ async function main() {
|
|
|
156
160
|
const syncCmd = program
|
|
157
161
|
.command('sync')
|
|
158
162
|
.description('Sync Zed settings and extensions via a GitHub repo')
|
|
159
|
-
.
|
|
160
|
-
|
|
163
|
+
.option('--local', 'On conflict, always keep the local version')
|
|
164
|
+
.option('--remote', 'On conflict, always use the remote version')
|
|
165
|
+
.action(async (opts) => {
|
|
166
|
+
if (opts.local && opts.remote) {
|
|
167
|
+
p.log.error(color.red('--local and --remote are mutually exclusive.'));
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
const conflict = opts.local
|
|
171
|
+
? 'local'
|
|
172
|
+
: opts.remote
|
|
173
|
+
? 'remote'
|
|
174
|
+
: 'prompt';
|
|
175
|
+
await runSync({ conflict });
|
|
161
176
|
});
|
|
162
177
|
syncCmd
|
|
163
178
|
.command('init')
|
|
@@ -171,6 +186,12 @@ async function main() {
|
|
|
171
186
|
.action(async () => {
|
|
172
187
|
await syncStatus();
|
|
173
188
|
});
|
|
189
|
+
syncCmd
|
|
190
|
+
.command('select')
|
|
191
|
+
.description('Interactively choose which files to sync')
|
|
192
|
+
.action(async () => {
|
|
193
|
+
await syncSelect();
|
|
194
|
+
});
|
|
174
195
|
syncCmd
|
|
175
196
|
.command('install')
|
|
176
197
|
.description('Install the OS daemon to auto-sync when Zed config changes')
|
|
@@ -183,10 +204,11 @@ async function main() {
|
|
|
183
204
|
.action(async () => {
|
|
184
205
|
await syncUninstall();
|
|
185
206
|
});
|
|
186
|
-
|
|
207
|
+
const argv = process.argv.filter(arg => arg !== '--');
|
|
208
|
+
if (argv.length <= 2) {
|
|
187
209
|
printWelcome();
|
|
188
210
|
return;
|
|
189
211
|
}
|
|
190
|
-
program.parse(
|
|
212
|
+
program.parse(argv);
|
|
191
213
|
}
|
|
192
214
|
main().catch(console.error);
|
package/dist/snippet.js
CHANGED
|
@@ -165,5 +165,5 @@ export async function addLsp(callerDir) {
|
|
|
165
165
|
p.outro(`${color.green('✓')} LSP snippet added.\n\n` +
|
|
166
166
|
` ${color.dim('1.')} Edit ${color.cyan('src/lib.rs')} — implement ${color.white('language_server_command')}\n` +
|
|
167
167
|
` ${color.dim('2.')} Edit ${color.cyan('Cargo.toml')} — pin ${color.white('zed_extension_api')} to latest version\n` +
|
|
168
|
-
` ${color.dim('3.')} ${color.dim('Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions/languages#language-servers'))}`);
|
|
168
|
+
` ${color.dim('3.')} ${color.dim('Zed Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions/languages#language-servers'))}`);
|
|
169
169
|
}
|
package/dist/sync.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export declare function syncStatus(): Promise<void>;
|
|
2
2
|
export declare function syncInit(): Promise<void>;
|
|
3
|
+
export type ConflictStrategy = 'local' | 'remote' | 'prompt';
|
|
4
|
+
export declare function syncSelect(): Promise<void>;
|
|
3
5
|
export declare function runSync(opts?: {
|
|
4
6
|
silent?: boolean;
|
|
7
|
+
conflict?: ConflictStrategy;
|
|
8
|
+
selectedFiles?: string[];
|
|
5
9
|
}): Promise<void>;
|
package/dist/sync.js
CHANGED
|
@@ -37,76 +37,66 @@ async function withTempDir(fn) {
|
|
|
37
37
|
await fs.remove(tmp);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
73
|
-
|
|
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
|
|
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,
|
|
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: '
|
|
131
|
+
label: 'Settings',
|
|
142
132
|
},
|
|
143
133
|
{
|
|
144
|
-
repoPath: path.join(tmp, '
|
|
145
|
-
localPath: zedPaths.
|
|
146
|
-
label: '
|
|
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,16 +170,34 @@ 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() {
|
|
@@ -231,9 +245,38 @@ export async function syncInit() {
|
|
|
231
245
|
p.outro(`${color.green('✓')} Sync config saved to ${color.cyan(ZEDX_CONFIG_PATH)}\n\n` +
|
|
232
246
|
` Run ${color.cyan('zedx sync')} to sync your Zed config.`);
|
|
233
247
|
}
|
|
248
|
+
// zedx sync select
|
|
249
|
+
export async function syncSelect() {
|
|
250
|
+
console.log('');
|
|
251
|
+
p.intro(`${color.bgBlue(color.bold(' zedx sync select '))} ${color.blue('Choose which files to sync…')}`);
|
|
252
|
+
await requireSyncConfig();
|
|
253
|
+
const allFiles = [
|
|
254
|
+
{
|
|
255
|
+
value: 'settings',
|
|
256
|
+
label: 'Settings',
|
|
257
|
+
hint: 'settings.json',
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
value: 'keymap',
|
|
261
|
+
label: 'Key bindings',
|
|
262
|
+
hint: 'keymap.json',
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
const selected = await p.multiselect({
|
|
266
|
+
message: 'Select files to sync',
|
|
267
|
+
options: allFiles,
|
|
268
|
+
required: true,
|
|
269
|
+
});
|
|
270
|
+
if (p.isCancel(selected)) {
|
|
271
|
+
p.cancel('Cancelled.');
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
const selectedFiles = selected;
|
|
275
|
+
await runSync({ selectedFiles });
|
|
276
|
+
}
|
|
234
277
|
// zedx sync
|
|
235
278
|
export async function runSync(opts = {}) {
|
|
236
|
-
const { silent = false } = opts;
|
|
279
|
+
const { silent = false, conflict = 'prompt', selectedFiles } = opts;
|
|
237
280
|
// In silent mode (daemon/watch), route all UI through plain console.log
|
|
238
281
|
// Interactive conflict prompts fall back to "local wins".
|
|
239
282
|
const log = {
|
|
@@ -254,7 +297,7 @@ export async function runSync(opts = {}) {
|
|
|
254
297
|
};
|
|
255
298
|
if (!silent) {
|
|
256
299
|
console.log('');
|
|
257
|
-
p.intro(`${color.bgBlue(color.bold(' zedx sync '))} ${color.blue('Syncing Zed
|
|
300
|
+
p.intro(`${color.bgBlue(color.bold(' zedx sync '))} ${color.blue('Syncing Zed config…')}`);
|
|
258
301
|
}
|
|
259
302
|
const config = await requireSyncConfig();
|
|
260
303
|
const zedPaths = resolveZedPaths();
|
|
@@ -282,18 +325,23 @@ export async function runSync(opts = {}) {
|
|
|
282
325
|
}
|
|
283
326
|
// 2. Determine what changed for each file
|
|
284
327
|
const lastSync = config.lastSync ? new Date(config.lastSync) : null;
|
|
285
|
-
const
|
|
328
|
+
const allFiles = [
|
|
286
329
|
{
|
|
330
|
+
key: 'settings',
|
|
287
331
|
repoPath: path.join(tmp, 'settings.json'),
|
|
288
332
|
localPath: zedPaths.settings,
|
|
289
|
-
label: '
|
|
333
|
+
label: 'Settings',
|
|
290
334
|
},
|
|
291
335
|
{
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
336
|
+
key: 'keymap',
|
|
337
|
+
repoPath: path.join(tmp, 'keymap.json'),
|
|
338
|
+
localPath: zedPaths.keymap,
|
|
339
|
+
label: 'Key bindings',
|
|
295
340
|
},
|
|
296
341
|
];
|
|
342
|
+
const files = selectedFiles
|
|
343
|
+
? allFiles.filter(f => selectedFiles.includes(f.key))
|
|
344
|
+
: allFiles;
|
|
297
345
|
let anyChanges = false;
|
|
298
346
|
for (const file of files) {
|
|
299
347
|
const localExists = await fs.pathExists(file.localPath);
|
|
@@ -303,29 +351,27 @@ export async function runSync(opts = {}) {
|
|
|
303
351
|
log.warn(`${file.label}: not found locally or remotely — skipping.`);
|
|
304
352
|
continue;
|
|
305
353
|
}
|
|
306
|
-
// Remote doesn't have it yet — push
|
|
354
|
+
// Remote doesn't have it yet — first push.
|
|
355
|
+
// Bootstrap auto_install_extensions from local extensions/index.json so
|
|
356
|
+
// the synced settings.json is self-contained on a fresh machine.
|
|
307
357
|
if (localExists && !remoteFileExists) {
|
|
308
358
|
log.info(`${file.label}: ${color.green('pushing')} (not in remote yet)`);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
await fs.ensureDir(path.dirname(file.repoPath));
|
|
314
|
-
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
315
|
-
}
|
|
359
|
+
await reconcileAutoInstallExtensions(file.localPath, zedPaths.extensionsIndex, silent);
|
|
360
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
361
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
316
362
|
anyChanges = true;
|
|
317
363
|
continue;
|
|
318
364
|
}
|
|
319
365
|
// Local doesn't have it — pull remote
|
|
320
366
|
if (!localExists && remoteFileExists) {
|
|
321
367
|
log.info(`${file.label}: ${color.cyan('pulling')} (not found locally)`);
|
|
322
|
-
if (file.
|
|
323
|
-
await
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
await fs.ensureDir(path.dirname(file.localPath));
|
|
327
|
-
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
368
|
+
if (await fs.pathExists(file.localPath)) {
|
|
369
|
+
await fs.copy(file.localPath, file.localPath + '.bak', { overwrite: true });
|
|
370
|
+
if (!silent)
|
|
371
|
+
p.log.info(`Backed up settings to ${color.dim(file.localPath + '.bak')}`);
|
|
328
372
|
}
|
|
373
|
+
await fs.ensureDir(path.dirname(file.localPath));
|
|
374
|
+
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
329
375
|
continue;
|
|
330
376
|
}
|
|
331
377
|
// Both exist — compare content
|
|
@@ -343,41 +389,36 @@ export async function runSync(opts = {}) {
|
|
|
343
389
|
const localChanged = !lastSync || localMtime > lastSync;
|
|
344
390
|
const remoteChanged = !lastSync || remoteMtime > lastSync;
|
|
345
391
|
if (localChanged && !remoteChanged) {
|
|
346
|
-
// Only local changed → push
|
|
392
|
+
// Only local changed → reconcile extensions then push
|
|
347
393
|
log.info(`${file.label}: ${color.green('pushing')} (local is newer)`);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
await fs.ensureDir(path.dirname(file.repoPath));
|
|
353
|
-
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
354
|
-
}
|
|
394
|
+
await reconcileAutoInstallExtensions(file.localPath, zedPaths.extensionsIndex, silent);
|
|
395
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
396
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
355
397
|
anyChanges = true;
|
|
356
398
|
}
|
|
357
399
|
else if (remoteChanged && !localChanged) {
|
|
358
400
|
// Only remote changed → pull
|
|
359
401
|
log.info(`${file.label}: ${color.cyan('pulling')} (remote is newer)`);
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
await fs.ensureDir(path.dirname(file.localPath));
|
|
365
|
-
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
366
|
-
}
|
|
402
|
+
await fs.copy(file.localPath, file.localPath + '.bak', { overwrite: true });
|
|
403
|
+
if (!silent)
|
|
404
|
+
p.log.info(`Backed up settings to ${color.dim(file.localPath + '.bak')}`);
|
|
405
|
+
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
367
406
|
}
|
|
368
407
|
else {
|
|
369
|
-
// Both changed
|
|
370
|
-
|
|
408
|
+
// Both changed — resolve based on strategy
|
|
409
|
+
// Determine the effective resolution:
|
|
410
|
+
// - explicit --local / --remote flag always wins
|
|
411
|
+
// - silent (daemon) mode falls back to local
|
|
412
|
+
// - otherwise prompt interactively
|
|
413
|
+
let resolution;
|
|
414
|
+
if (conflict === 'local' || conflict === 'remote') {
|
|
415
|
+
resolution = conflict;
|
|
416
|
+
log.warn(`${file.label}: conflict — using ${color.bold(resolution)} (--${resolution} flag).`);
|
|
417
|
+
}
|
|
418
|
+
else if (silent) {
|
|
371
419
|
// Daemon can't prompt — local wins, will be pushed
|
|
420
|
+
resolution = 'local';
|
|
372
421
|
log.warn(`${file.label}: conflict detected in unattended mode — keeping local.`);
|
|
373
|
-
if (file.label === 'settings.json') {
|
|
374
|
-
await prepareSettingsForPush(file.localPath, file.repoPath);
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
await fs.ensureDir(path.dirname(file.repoPath));
|
|
378
|
-
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
379
|
-
}
|
|
380
|
-
anyChanges = true;
|
|
381
422
|
}
|
|
382
423
|
else {
|
|
383
424
|
p.log.warn(color.yellow(`conflict between local and remote ${file.label}`));
|
|
@@ -400,34 +441,30 @@ export async function runSync(opts = {}) {
|
|
|
400
441
|
p.cancel('Cancelled.');
|
|
401
442
|
process.exit(0);
|
|
402
443
|
}
|
|
403
|
-
|
|
444
|
+
resolution = choice;
|
|
445
|
+
}
|
|
446
|
+
if (resolution === 'local') {
|
|
447
|
+
if (!silent && conflict === 'prompt')
|
|
404
448
|
p.log.info(`${file.label}: ${color.green('keeping local, will push')}`);
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
anyChanges = true;
|
|
413
|
-
}
|
|
414
|
-
else {
|
|
449
|
+
await reconcileAutoInstallExtensions(file.localPath, zedPaths.extensionsIndex, silent);
|
|
450
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
451
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
452
|
+
anyChanges = true;
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
if (!silent && conflict === 'prompt')
|
|
415
456
|
p.log.info(`${file.label}: ${color.cyan('applying remote')}`);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
await fs.ensureDir(path.dirname(file.localPath));
|
|
421
|
-
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
422
|
-
}
|
|
423
|
-
}
|
|
457
|
+
await fs.copy(file.localPath, file.localPath + '.bak', { overwrite: true });
|
|
458
|
+
if (!silent)
|
|
459
|
+
p.log.info(`Backed up settings to ${color.dim(file.localPath + '.bak')}`);
|
|
460
|
+
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
424
461
|
}
|
|
425
462
|
}
|
|
426
463
|
}
|
|
427
464
|
// 3. Commit + push if any local files were written to the repo
|
|
428
465
|
if (anyChanges) {
|
|
429
466
|
spinner.start('Pushing changes to remote...');
|
|
430
|
-
await git.add(
|
|
467
|
+
await git.add(files.map(f => path.basename(f.repoPath)));
|
|
431
468
|
const status = await git.status();
|
|
432
469
|
if (status.staged.length > 0) {
|
|
433
470
|
const timestamp = new Date().toISOString();
|
package/dist/types/index.d.ts
CHANGED
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(
|
|
9
|
-
|
|
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(
|
|
18
|
-
|
|
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(
|
|
26
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.10.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
|
],
|