zedx 0.9.0 → 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 -18
- package/dist/daemon.js +1 -1
- package/dist/sync.js +119 -121
- 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,28 +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 (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
100
|
```
|
|
43
101
|
|
|
44
|
-
###
|
|
102
|
+
### License
|
|
45
103
|
|
|
46
|
-
|
|
47
|
-
2. **Languages** - Syntax highlighting, indentation, and optional LSP support
|
|
48
|
-
|
|
49
|
-
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/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);
|
|
182
|
+
}
|
|
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.`);
|
|
183
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.');
|
|
184
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() {
|
|
@@ -239,13 +253,13 @@ export async function syncSelect() {
|
|
|
239
253
|
const allFiles = [
|
|
240
254
|
{
|
|
241
255
|
value: 'settings',
|
|
242
|
-
label: '
|
|
243
|
-
hint: '
|
|
256
|
+
label: 'Settings',
|
|
257
|
+
hint: 'settings.json',
|
|
244
258
|
},
|
|
245
259
|
{
|
|
246
|
-
value: '
|
|
247
|
-
label: '
|
|
248
|
-
hint: '
|
|
260
|
+
value: 'keymap',
|
|
261
|
+
label: 'Key bindings',
|
|
262
|
+
hint: 'keymap.json',
|
|
249
263
|
},
|
|
250
264
|
];
|
|
251
265
|
const selected = await p.multiselect({
|
|
@@ -283,7 +297,7 @@ export async function runSync(opts = {}) {
|
|
|
283
297
|
};
|
|
284
298
|
if (!silent) {
|
|
285
299
|
console.log('');
|
|
286
|
-
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…')}`);
|
|
287
301
|
}
|
|
288
302
|
const config = await requireSyncConfig();
|
|
289
303
|
const zedPaths = resolveZedPaths();
|
|
@@ -316,13 +330,13 @@ export async function runSync(opts = {}) {
|
|
|
316
330
|
key: 'settings',
|
|
317
331
|
repoPath: path.join(tmp, 'settings.json'),
|
|
318
332
|
localPath: zedPaths.settings,
|
|
319
|
-
label: '
|
|
333
|
+
label: 'Settings',
|
|
320
334
|
},
|
|
321
335
|
{
|
|
322
|
-
key: '
|
|
323
|
-
repoPath: path.join(tmp, '
|
|
324
|
-
localPath: zedPaths.
|
|
325
|
-
label: '
|
|
336
|
+
key: 'keymap',
|
|
337
|
+
repoPath: path.join(tmp, 'keymap.json'),
|
|
338
|
+
localPath: zedPaths.keymap,
|
|
339
|
+
label: 'Key bindings',
|
|
326
340
|
},
|
|
327
341
|
];
|
|
328
342
|
const files = selectedFiles
|
|
@@ -337,29 +351,27 @@ export async function runSync(opts = {}) {
|
|
|
337
351
|
log.warn(`${file.label}: not found locally or remotely — skipping.`);
|
|
338
352
|
continue;
|
|
339
353
|
}
|
|
340
|
-
// 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.
|
|
341
357
|
if (localExists && !remoteFileExists) {
|
|
342
358
|
log.info(`${file.label}: ${color.green('pushing')} (not in remote yet)`);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
await fs.ensureDir(path.dirname(file.repoPath));
|
|
348
|
-
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
349
|
-
}
|
|
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 });
|
|
350
362
|
anyChanges = true;
|
|
351
363
|
continue;
|
|
352
364
|
}
|
|
353
365
|
// Local doesn't have it — pull remote
|
|
354
366
|
if (!localExists && remoteFileExists) {
|
|
355
367
|
log.info(`${file.label}: ${color.cyan('pulling')} (not found locally)`);
|
|
356
|
-
if (file.
|
|
357
|
-
await
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
await fs.ensureDir(path.dirname(file.localPath));
|
|
361
|
-
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')}`);
|
|
362
372
|
}
|
|
373
|
+
await fs.ensureDir(path.dirname(file.localPath));
|
|
374
|
+
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
363
375
|
continue;
|
|
364
376
|
}
|
|
365
377
|
// Both exist — compare content
|
|
@@ -377,27 +389,20 @@ export async function runSync(opts = {}) {
|
|
|
377
389
|
const localChanged = !lastSync || localMtime > lastSync;
|
|
378
390
|
const remoteChanged = !lastSync || remoteMtime > lastSync;
|
|
379
391
|
if (localChanged && !remoteChanged) {
|
|
380
|
-
// Only local changed → push
|
|
392
|
+
// Only local changed → reconcile extensions then push
|
|
381
393
|
log.info(`${file.label}: ${color.green('pushing')} (local is newer)`);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
else {
|
|
386
|
-
await fs.ensureDir(path.dirname(file.repoPath));
|
|
387
|
-
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
388
|
-
}
|
|
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 });
|
|
389
397
|
anyChanges = true;
|
|
390
398
|
}
|
|
391
399
|
else if (remoteChanged && !localChanged) {
|
|
392
400
|
// Only remote changed → pull
|
|
393
401
|
log.info(`${file.label}: ${color.cyan('pulling')} (remote is newer)`);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
await fs.ensureDir(path.dirname(file.localPath));
|
|
399
|
-
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
400
|
-
}
|
|
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 });
|
|
401
406
|
}
|
|
402
407
|
else {
|
|
403
408
|
// Both changed — resolve based on strategy
|
|
@@ -441,32 +446,25 @@ export async function runSync(opts = {}) {
|
|
|
441
446
|
if (resolution === 'local') {
|
|
442
447
|
if (!silent && conflict === 'prompt')
|
|
443
448
|
p.log.info(`${file.label}: ${color.green('keeping local, will push')}`);
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
else {
|
|
448
|
-
await fs.ensureDir(path.dirname(file.repoPath));
|
|
449
|
-
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
450
|
-
}
|
|
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 });
|
|
451
452
|
anyChanges = true;
|
|
452
453
|
}
|
|
453
454
|
else {
|
|
454
455
|
if (!silent && conflict === 'prompt')
|
|
455
456
|
p.log.info(`${file.label}: ${color.cyan('applying remote')}`);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
await fs.ensureDir(path.dirname(file.localPath));
|
|
461
|
-
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
462
|
-
}
|
|
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 });
|
|
463
461
|
}
|
|
464
462
|
}
|
|
465
463
|
}
|
|
466
464
|
// 3. Commit + push if any local files were written to the repo
|
|
467
465
|
if (anyChanges) {
|
|
468
466
|
spinner.start('Pushing changes to remote...');
|
|
469
|
-
await git.add(
|
|
467
|
+
await git.add(files.map(f => path.basename(f.repoPath)));
|
|
470
468
|
const status = await git.status();
|
|
471
469
|
if (status.staged.length > 0) {
|
|
472
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
|
],
|