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 +80 -18
- package/dist/check.js +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +95 -0
- package/dist/daemon.js +1 -1
- package/dist/index.js +35 -13
- package/dist/sync.js +122 -123
- 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,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
|
-
|
|
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
|
-
###
|
|
45
|
-
|
|
46
|
-
1. **Themes** - Color schemes for the editor
|
|
47
|
-
2. **Languages** - Syntax highlighting, indentation, and optional LSP support
|
|
109
|
+
### License
|
|
48
110
|
|
|
49
|
-
|
|
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
|
|
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
|
package/dist/config.d.ts
ADDED
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.
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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('
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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,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: '
|
|
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.
|
|
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: '
|
|
243
|
-
hint: '
|
|
257
|
+
label: 'Settings',
|
|
258
|
+
hint: 'settings.json',
|
|
244
259
|
},
|
|
245
260
|
{
|
|
246
|
-
value: '
|
|
247
|
-
label: '
|
|
248
|
-
hint: '
|
|
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
|
|
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: '
|
|
334
|
+
label: 'Settings',
|
|
320
335
|
},
|
|
321
336
|
{
|
|
322
|
-
key: '
|
|
323
|
-
repoPath: path.join(tmp, '
|
|
324
|
-
localPath: zedPaths.
|
|
325
|
-
label: '
|
|
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
|
|
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
|
-
|
|
344
|
-
|
|
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.
|
|
357
|
-
await
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
383
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
445
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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(
|
|
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();
|
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.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
|
],
|