zedx 0.5.0 → 0.7.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 +1 -0
- package/dist/add.js +17 -9
- package/dist/check.js +4 -8
- package/dist/daemon.js +16 -12
- package/dist/generator.js +6 -6
- package/dist/index.js +11 -5
- package/dist/prompts.js +20 -16
- package/dist/sync.d.ts +1 -0
- package/dist/sync.js +159 -28
- package/package.json +58 -53
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ zedx version major # 1.2.3 → 2.0.0
|
|
|
34
34
|
# Sync Zed settings and extensions via a GitHub repo
|
|
35
35
|
zedx sync init # Link a GitHub repo as the sync target (run once)
|
|
36
36
|
zedx sync # Sync local and remote config automatically
|
|
37
|
+
zedx sync status # Show sync state between local config and the remote repo
|
|
37
38
|
zedx sync install # Install an OS daemon to auto-sync when Zed config changes
|
|
38
39
|
zedx sync uninstall # Remove the OS daemon
|
|
39
40
|
```
|
package/dist/add.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
1
|
import path from 'path';
|
|
3
|
-
import ejs from 'ejs';
|
|
4
2
|
import { fileURLToPath } from 'url';
|
|
5
3
|
import * as p from '@clack/prompts';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
6
|
import color from 'picocolors';
|
|
7
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
8
|
const __dirname = path.dirname(__filename);
|
|
@@ -18,7 +18,10 @@ function tomlGet(content, key) {
|
|
|
18
18
|
return match?.[1];
|
|
19
19
|
}
|
|
20
20
|
function slugify(name) {
|
|
21
|
-
return name
|
|
21
|
+
return name
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/\s+/g, '-')
|
|
24
|
+
.replace(/[^a-z0-9-]/g, '');
|
|
22
25
|
}
|
|
23
26
|
export async function addTheme(callerDir, themeName) {
|
|
24
27
|
p.intro(`${color.bgBlue(color.bold(' zedx add theme '))} ${color.blue('Adding a theme to your extension…')}`);
|
|
@@ -29,9 +32,9 @@ export async function addTheme(callerDir, themeName) {
|
|
|
29
32
|
}
|
|
30
33
|
const tomlContent = await fs.readFile(tomlPath, 'utf-8');
|
|
31
34
|
const extensionId = tomlGet(tomlContent, 'id') ?? 'extension';
|
|
32
|
-
const author = tomlGet(tomlContent, 'authors')
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
const author = tomlGet(tomlContent, 'authors') ??
|
|
36
|
+
tomlContent.match(/^authors\s*=\s*\["([^"]+)"\]/m)?.[1] ??
|
|
37
|
+
'';
|
|
35
38
|
const appearance = await p.select({
|
|
36
39
|
message: 'Appearance:',
|
|
37
40
|
options: [
|
|
@@ -55,7 +58,12 @@ export async function addTheme(callerDir, themeName) {
|
|
|
55
58
|
process.exit(1);
|
|
56
59
|
}
|
|
57
60
|
await fs.ensureDir(themesDir);
|
|
58
|
-
const themeJson = await renderTemplate(path.join(TEMPLATE_DIR, 'theme/theme.json.ejs'), {
|
|
61
|
+
const themeJson = await renderTemplate(path.join(TEMPLATE_DIR, 'theme/theme.json.ejs'), {
|
|
62
|
+
id: extensionId,
|
|
63
|
+
author,
|
|
64
|
+
themeName,
|
|
65
|
+
appearances,
|
|
66
|
+
});
|
|
59
67
|
await fs.writeFile(themePath, themeJson);
|
|
60
68
|
p.log.success(`Created ${color.cyan(`themes/${themeFile}`)}`);
|
|
61
69
|
p.outro(`${color.green('✓')} Theme added.\n` +
|
|
@@ -70,8 +78,8 @@ export async function addLanguage(callerDir, languageId) {
|
|
|
70
78
|
}
|
|
71
79
|
const tomlContent = await fs.readFile(tomlPath, 'utf-8');
|
|
72
80
|
// Check for duplicate
|
|
73
|
-
const alreadyExists = new RegExp(`^\\[grammars\\.${languageId}\\]`, 'm').test(tomlContent)
|
|
74
|
-
|
|
81
|
+
const alreadyExists = new RegExp(`^\\[grammars\\.${languageId}\\]`, 'm').test(tomlContent) ||
|
|
82
|
+
new RegExp(`^#\\s*\\[grammars\\.${languageId}\\]`, 'm').test(tomlContent);
|
|
75
83
|
if (alreadyExists) {
|
|
76
84
|
p.log.error(color.red(`Language "${languageId}" already exists in extension.toml.`));
|
|
77
85
|
process.exit(1);
|
package/dist/check.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
1
|
import path from 'path';
|
|
3
2
|
import * as p from '@clack/prompts';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
4
|
import color from 'picocolors';
|
|
5
5
|
// Minimal TOML key extraction — handles `key = "value"` and `key = ["a", "b"]`
|
|
6
6
|
function tomlGet(content, key) {
|
|
@@ -10,10 +10,6 @@ function tomlGet(content, key) {
|
|
|
10
10
|
function tomlHasUncommentedKey(content, key) {
|
|
11
11
|
return new RegExp(`^${key}\\s*=`, 'm').test(content);
|
|
12
12
|
}
|
|
13
|
-
function tomlHasSection(content, section) {
|
|
14
|
-
// Looks for an uncommented [section] or [section.something] header
|
|
15
|
-
return new RegExp(`^\\[${section.replace('.', '\\.')}`, 'm').test(content);
|
|
16
|
-
}
|
|
17
13
|
export async function runCheck(callerDir) {
|
|
18
14
|
p.intro(`${color.bgBlue(color.bold(' zedx check '))} ${color.blue('Validating extension config…')}`);
|
|
19
15
|
const tomlPath = path.join(callerDir, 'extension.toml');
|
|
@@ -179,7 +175,7 @@ export async function runCheck(callerDir) {
|
|
|
179
175
|
if (!tomlHasUncommentedKey(configContent, 'path_suffixes')) {
|
|
180
176
|
configIssues.push({
|
|
181
177
|
file: `languages/${langId}/config.toml`,
|
|
182
|
-
message:
|
|
178
|
+
message: "path_suffixes is not set — files won't be associated with this language",
|
|
183
179
|
hint: 'Uncomment and fill in path_suffixes (e.g., ["myl"])',
|
|
184
180
|
});
|
|
185
181
|
}
|
|
@@ -187,7 +183,7 @@ export async function runCheck(callerDir) {
|
|
|
187
183
|
if (!tomlHasUncommentedKey(configContent, 'line_comments')) {
|
|
188
184
|
configIssues.push({
|
|
189
185
|
file: `languages/${langId}/config.toml`,
|
|
190
|
-
message:
|
|
186
|
+
message: "line_comments is not set — toggle-comment keybind won't work",
|
|
191
187
|
hint: 'Uncomment and set line_comments (e.g., ["// "])',
|
|
192
188
|
});
|
|
193
189
|
}
|
|
@@ -213,7 +209,7 @@ export async function runCheck(callerDir) {
|
|
|
213
209
|
highlightIssues.push({
|
|
214
210
|
file: `languages/${langId}/highlights.scm`,
|
|
215
211
|
message: 'Only scaffold starter patterns present — no real grammar queries added yet',
|
|
216
|
-
hint:
|
|
212
|
+
hint: "Add tree-sitter queries matching your language's grammar node types",
|
|
217
213
|
});
|
|
218
214
|
}
|
|
219
215
|
}
|
package/dist/daemon.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
1
2
|
import os from 'os';
|
|
2
3
|
import path from 'path';
|
|
3
|
-
import fs from 'fs-extra';
|
|
4
|
-
import { execSync } from 'child_process';
|
|
5
4
|
import * as p from '@clack/prompts';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
6
|
import color from 'picocolors';
|
|
7
7
|
import { resolveZedPaths } from './zed-paths.js';
|
|
8
8
|
const LAUNCHD_LABEL = 'dev.zedx.sync';
|
|
@@ -17,7 +17,9 @@ function resolveZedxBinary() {
|
|
|
17
17
|
if (bin)
|
|
18
18
|
return bin;
|
|
19
19
|
}
|
|
20
|
-
catch {
|
|
20
|
+
catch {
|
|
21
|
+
/* fall through */
|
|
22
|
+
}
|
|
21
23
|
return `${process.execPath} ${process.argv[1]}`;
|
|
22
24
|
}
|
|
23
25
|
function unsupportedPlatform() {
|
|
@@ -25,9 +27,7 @@ function unsupportedPlatform() {
|
|
|
25
27
|
process.exit(1);
|
|
26
28
|
}
|
|
27
29
|
function buildPlist(zedxBin, watchPaths) {
|
|
28
|
-
const watchEntries = watchPaths
|
|
29
|
-
.map((wp) => ` <string>${wp}</string>`)
|
|
30
|
-
.join('\n');
|
|
30
|
+
const watchEntries = watchPaths.map(wp => ` <string>${wp}</string>`).join('\n');
|
|
31
31
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
32
32
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
33
33
|
<plist version="1.0">
|
|
@@ -68,7 +68,9 @@ async function installMacos(zedxBin, watchPaths) {
|
|
|
68
68
|
try {
|
|
69
69
|
execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null`, { stdio: 'pipe' });
|
|
70
70
|
}
|
|
71
|
-
catch {
|
|
71
|
+
catch {
|
|
72
|
+
/* not loaded yet */
|
|
73
|
+
}
|
|
72
74
|
execSync(`launchctl load "${LAUNCHD_PLIST_PATH}"`);
|
|
73
75
|
p.log.success(`Daemon installed: ${color.dim(LAUNCHD_PLIST_PATH)}`);
|
|
74
76
|
p.log.info(`Logs: ${color.dim(`${os.homedir()}/Library/Logs/zedx-sync.log`)}`);
|
|
@@ -82,7 +84,9 @@ async function uninstallMacos() {
|
|
|
82
84
|
try {
|
|
83
85
|
execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}"`, { stdio: 'pipe' });
|
|
84
86
|
}
|
|
85
|
-
catch {
|
|
87
|
+
catch {
|
|
88
|
+
/* already unloaded */
|
|
89
|
+
}
|
|
86
90
|
await fs.remove(LAUNCHD_PLIST_PATH);
|
|
87
91
|
p.log.success('Daemon uninstalled.');
|
|
88
92
|
}
|
|
@@ -103,9 +107,7 @@ WantedBy=default.target
|
|
|
103
107
|
`;
|
|
104
108
|
}
|
|
105
109
|
function buildSystemdPath(watchPaths) {
|
|
106
|
-
const pathChangedEntries = watchPaths
|
|
107
|
-
.map((wp) => `PathChanged=${wp}`)
|
|
108
|
-
.join('\n');
|
|
110
|
+
const pathChangedEntries = watchPaths.map(wp => `PathChanged=${wp}`).join('\n');
|
|
109
111
|
return `[Unit]
|
|
110
112
|
Description=Watch Zed config files for zedx sync
|
|
111
113
|
|
|
@@ -138,7 +140,9 @@ async function uninstallLinux() {
|
|
|
138
140
|
try {
|
|
139
141
|
execSync(`systemctl --user disable --now ${SYSTEMD_SERVICE_NAME}.path`, { stdio: 'pipe' });
|
|
140
142
|
}
|
|
141
|
-
catch {
|
|
143
|
+
catch {
|
|
144
|
+
/* already inactive */
|
|
145
|
+
}
|
|
142
146
|
if (serviceExists)
|
|
143
147
|
await fs.remove(SYSTEMD_SERVICE_PATH);
|
|
144
148
|
if (pathExists)
|
package/dist/generator.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
1
|
import path from 'path';
|
|
3
2
|
import { fileURLToPath } from 'url';
|
|
4
3
|
import ejs from 'ejs';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
const isDev = __dirname.includes('/src/');
|
|
@@ -17,13 +17,13 @@ export async function generateExtension(options, targetDir) {
|
|
|
17
17
|
...options,
|
|
18
18
|
grammarRepo: options.grammarRepo || '',
|
|
19
19
|
grammarRev: options.grammarRev || '',
|
|
20
|
-
languageName: options.languageName || 'My Language'
|
|
20
|
+
languageName: options.languageName || 'My Language',
|
|
21
21
|
};
|
|
22
22
|
const extToml = await renderTemplate(path.join(TEMPLATE_DIR, 'base/extension.toml.ejs'), extData);
|
|
23
23
|
await fs.writeFile(path.join(targetDir, 'extension.toml'), extToml);
|
|
24
24
|
const readmeData = {
|
|
25
25
|
...extData,
|
|
26
|
-
languageId: options.languageId || 'my-language'
|
|
26
|
+
languageId: options.languageId || 'my-language',
|
|
27
27
|
};
|
|
28
28
|
const readme = await renderTemplate(path.join(TEMPLATE_DIR, 'base/readme.md.ejs'), readmeData);
|
|
29
29
|
await fs.writeFile(path.join(targetDir, 'README.md'), readme);
|
|
@@ -47,7 +47,7 @@ async function generateTheme(options, targetDir) {
|
|
|
47
47
|
const themeData = {
|
|
48
48
|
...options,
|
|
49
49
|
themeName: options.themeName || 'My Theme',
|
|
50
|
-
appearances
|
|
50
|
+
appearances,
|
|
51
51
|
};
|
|
52
52
|
const themeJson = await renderTemplate(path.join(TEMPLATE_DIR, 'theme/theme.json.ejs'), themeData);
|
|
53
53
|
await fs.writeFile(path.join(themeDir, `${options.id}.json`), themeJson);
|
|
@@ -58,7 +58,7 @@ async function generateLanguage(options, targetDir) {
|
|
|
58
58
|
const data = {
|
|
59
59
|
...options,
|
|
60
60
|
pathSuffixes: options.pathSuffixes || [],
|
|
61
|
-
lineComments: options.lineComments || ['//', '#']
|
|
61
|
+
lineComments: options.lineComments || ['//', '#'],
|
|
62
62
|
};
|
|
63
63
|
const configToml = await renderTemplate(path.join(TEMPLATE_DIR, 'language/config.toml.ejs'), data);
|
|
64
64
|
await fs.writeFile(path.join(languageDir, 'config.toml'), configToml);
|
|
@@ -71,7 +71,7 @@ async function generateLanguage(options, targetDir) {
|
|
|
71
71
|
'overrides.scm',
|
|
72
72
|
'textobjects.scm',
|
|
73
73
|
'redactions.scm',
|
|
74
|
-
'runnables.scm'
|
|
74
|
+
'runnables.scm',
|
|
75
75
|
];
|
|
76
76
|
for (const file of queryFiles) {
|
|
77
77
|
const templatePath = path.join(TEMPLATE_DIR, 'language', file);
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import * as p from '@clack/prompts';
|
|
4
|
-
import color from 'picocolors';
|
|
5
4
|
import { Command } from 'commander';
|
|
6
5
|
import fs from 'fs-extra';
|
|
7
|
-
import
|
|
8
|
-
import { generateExtension } from './generator.js';
|
|
9
|
-
import { runCheck } from './check.js';
|
|
6
|
+
import color from 'picocolors';
|
|
10
7
|
import { addTheme, addLanguage } from './add.js';
|
|
11
|
-
import {
|
|
8
|
+
import { runCheck } from './check.js';
|
|
12
9
|
import { syncInstall, syncUninstall } from './daemon.js';
|
|
10
|
+
import { generateExtension } from './generator.js';
|
|
11
|
+
import { promptUser, promptThemeDetails, promptLanguageDetails } from './prompts.js';
|
|
12
|
+
import { syncInit, runSync, syncStatus } from './sync.js';
|
|
13
13
|
function bumpVersion(version, type) {
|
|
14
14
|
const [major, minor, patch] = version.split('.').map(Number);
|
|
15
15
|
switch (type) {
|
|
@@ -90,6 +90,12 @@ async function main() {
|
|
|
90
90
|
.action(async () => {
|
|
91
91
|
await syncInit();
|
|
92
92
|
});
|
|
93
|
+
syncCmd
|
|
94
|
+
.command('status')
|
|
95
|
+
.description('Show sync state between local Zed config and the remote repo')
|
|
96
|
+
.action(async () => {
|
|
97
|
+
await syncStatus();
|
|
98
|
+
});
|
|
93
99
|
syncCmd
|
|
94
100
|
.command('install')
|
|
95
101
|
.description('Install the OS daemon to auto-sync when Zed config changes')
|
package/dist/prompts.js
CHANGED
|
@@ -5,7 +5,7 @@ export async function promptUser() {
|
|
|
5
5
|
const nameDefault = 'my-zed-extension';
|
|
6
6
|
const name = await p.text({
|
|
7
7
|
message: 'Project name:',
|
|
8
|
-
placeholder: nameDefault
|
|
8
|
+
placeholder: nameDefault,
|
|
9
9
|
});
|
|
10
10
|
if (p.isCancel(name)) {
|
|
11
11
|
p.cancel('Cancelled.');
|
|
@@ -21,7 +21,7 @@ export async function promptUser() {
|
|
|
21
21
|
return 'ID cannot contain spaces';
|
|
22
22
|
if (value && value !== value.toLowerCase())
|
|
23
23
|
return 'ID must be lowercase';
|
|
24
|
-
}
|
|
24
|
+
},
|
|
25
25
|
});
|
|
26
26
|
if (p.isCancel(id)) {
|
|
27
27
|
p.cancel('Cancelled.');
|
|
@@ -31,7 +31,7 @@ export async function promptUser() {
|
|
|
31
31
|
const descriptionDefault = 'A Zed extension';
|
|
32
32
|
const description = await p.text({
|
|
33
33
|
message: 'Description:',
|
|
34
|
-
placeholder: descriptionDefault
|
|
34
|
+
placeholder: descriptionDefault,
|
|
35
35
|
});
|
|
36
36
|
if (p.isCancel(description)) {
|
|
37
37
|
p.cancel('Cancelled.');
|
|
@@ -44,7 +44,7 @@ export async function promptUser() {
|
|
|
44
44
|
validate: (value) => {
|
|
45
45
|
if (!value || value.length === 0)
|
|
46
46
|
return 'Author is required';
|
|
47
|
-
}
|
|
47
|
+
},
|
|
48
48
|
});
|
|
49
49
|
if (p.isCancel(author)) {
|
|
50
50
|
p.cancel('Cancelled.');
|
|
@@ -53,7 +53,7 @@ export async function promptUser() {
|
|
|
53
53
|
const repositoryDefault = `https://github.com/username/${idValue}.git`;
|
|
54
54
|
const repository = await p.text({
|
|
55
55
|
message: 'GitHub repository URL:',
|
|
56
|
-
initialValue: repositoryDefault
|
|
56
|
+
initialValue: repositoryDefault,
|
|
57
57
|
});
|
|
58
58
|
if (p.isCancel(repository)) {
|
|
59
59
|
p.cancel('Cancelled.');
|
|
@@ -69,9 +69,9 @@ export async function promptUser() {
|
|
|
69
69
|
{ value: 'GPL-3.0', label: 'GNU GPLv3' },
|
|
70
70
|
{ value: 'LGPL-3.0', label: 'GNU LGPLv3' },
|
|
71
71
|
{ value: 'MIT', label: 'MIT' },
|
|
72
|
-
{ value: 'Zlib', label: 'zlib' }
|
|
72
|
+
{ value: 'Zlib', label: 'zlib' },
|
|
73
73
|
],
|
|
74
|
-
initialValue: 'MIT'
|
|
74
|
+
initialValue: 'MIT',
|
|
75
75
|
});
|
|
76
76
|
if (p.isCancel(license)) {
|
|
77
77
|
p.cancel('Cancelled.');
|
|
@@ -81,9 +81,13 @@ export async function promptUser() {
|
|
|
81
81
|
message: 'What do you want to include in your extension?',
|
|
82
82
|
options: [
|
|
83
83
|
{ value: 'theme', label: 'Theme', hint: 'Color scheme for the editor' },
|
|
84
|
-
{
|
|
84
|
+
{
|
|
85
|
+
value: 'language',
|
|
86
|
+
label: 'Language',
|
|
87
|
+
hint: 'Syntax highlighting, indentation, etc.',
|
|
88
|
+
},
|
|
85
89
|
],
|
|
86
|
-
required: true
|
|
90
|
+
required: true,
|
|
87
91
|
});
|
|
88
92
|
if (p.isCancel(extensionTypes)) {
|
|
89
93
|
p.cancel('Cancelled.');
|
|
@@ -96,14 +100,14 @@ export async function promptUser() {
|
|
|
96
100
|
author: String(author),
|
|
97
101
|
repository: repositoryValue,
|
|
98
102
|
license: license,
|
|
99
|
-
types: extensionTypes
|
|
103
|
+
types: extensionTypes,
|
|
100
104
|
};
|
|
101
105
|
return options;
|
|
102
106
|
}
|
|
103
107
|
export async function promptThemeDetails() {
|
|
104
108
|
const themeName = await p.text({
|
|
105
109
|
message: 'Theme name:',
|
|
106
|
-
placeholder: 'My Theme'
|
|
110
|
+
placeholder: 'My Theme',
|
|
107
111
|
});
|
|
108
112
|
if (p.isCancel(themeName)) {
|
|
109
113
|
p.cancel('Cancelled.');
|
|
@@ -114,9 +118,9 @@ export async function promptThemeDetails() {
|
|
|
114
118
|
options: [
|
|
115
119
|
{ value: 'dark', label: 'Dark' },
|
|
116
120
|
{ value: 'light', label: 'Light' },
|
|
117
|
-
{ value: 'both', label: 'Both (Dark & Light)' }
|
|
121
|
+
{ value: 'both', label: 'Both (Dark & Light)' },
|
|
118
122
|
],
|
|
119
|
-
initialValue: 'dark'
|
|
123
|
+
initialValue: 'dark',
|
|
120
124
|
});
|
|
121
125
|
if (p.isCancel(appearance)) {
|
|
122
126
|
p.cancel('Cancelled.');
|
|
@@ -124,13 +128,13 @@ export async function promptThemeDetails() {
|
|
|
124
128
|
}
|
|
125
129
|
return {
|
|
126
130
|
themeName: String(themeName),
|
|
127
|
-
appearance: appearance
|
|
131
|
+
appearance: appearance,
|
|
128
132
|
};
|
|
129
133
|
}
|
|
130
134
|
export async function promptLanguageDetails() {
|
|
131
135
|
const languageName = await p.text({
|
|
132
136
|
message: 'Language name:',
|
|
133
|
-
placeholder: 'My Language'
|
|
137
|
+
placeholder: 'My Language',
|
|
134
138
|
});
|
|
135
139
|
if (p.isCancel(languageName)) {
|
|
136
140
|
p.cancel('Cancelled.');
|
|
@@ -138,7 +142,7 @@ export async function promptLanguageDetails() {
|
|
|
138
142
|
}
|
|
139
143
|
const result = {
|
|
140
144
|
languageName: String(languageName),
|
|
141
|
-
languageId: String(languageName).toLowerCase().replace(/\s+/g, '-')
|
|
145
|
+
languageId: String(languageName).toLowerCase().replace(/\s+/g, '-'),
|
|
142
146
|
};
|
|
143
147
|
return result;
|
|
144
148
|
}
|
package/dist/sync.d.ts
CHANGED
package/dist/sync.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import fs from 'fs-extra';
|
|
4
3
|
import * as p from '@clack/prompts';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
5
|
import color from 'picocolors';
|
|
6
6
|
import simpleGit from 'simple-git';
|
|
7
7
|
import { resolveZedPaths } from './zed-paths.js';
|
|
@@ -16,7 +16,9 @@ async function readSyncConfig() {
|
|
|
16
16
|
async function requireSyncConfig() {
|
|
17
17
|
const config = await readSyncConfig();
|
|
18
18
|
if (!config) {
|
|
19
|
-
p.log.error(color.red('No sync config found. Run ') +
|
|
19
|
+
p.log.error(color.red('No sync config found. Run ') +
|
|
20
|
+
color.cyan('zedx sync init') +
|
|
21
|
+
color.red(' first.'));
|
|
20
22
|
process.exit(1);
|
|
21
23
|
}
|
|
22
24
|
return config;
|
|
@@ -35,6 +37,26 @@ async function withTempDir(fn) {
|
|
|
35
37
|
await fs.remove(tmp);
|
|
36
38
|
}
|
|
37
39
|
}
|
|
40
|
+
// Prepare settings.json for pushing to the repo by stripping auto_install_extensions.
|
|
41
|
+
// That field is derived from extensions/index.json on pull, so storing it in the
|
|
42
|
+
// remote would create stale/conflicting data across machines.
|
|
43
|
+
async function prepareSettingsForPush(localSettingsPath, repoSettingsPath) {
|
|
44
|
+
const raw = await fs.readFile(localSettingsPath, 'utf-8');
|
|
45
|
+
const stripped = raw.replace(/\/\/[^\n]*/g, '');
|
|
46
|
+
let settingsObj = {};
|
|
47
|
+
try {
|
|
48
|
+
settingsObj = JSON.parse(stripped);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// If we can't parse it (e.g. complex comments), push as-is
|
|
52
|
+
await fs.ensureDir(path.dirname(repoSettingsPath));
|
|
53
|
+
await fs.copy(localSettingsPath, repoSettingsPath, { overwrite: true });
|
|
54
|
+
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
|
+
}
|
|
38
60
|
// Extension merge helper
|
|
39
61
|
async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPath, silent = false) {
|
|
40
62
|
// Backup existing settings
|
|
@@ -48,7 +70,7 @@ async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPa
|
|
|
48
70
|
if (await fs.pathExists(repoExtensions)) {
|
|
49
71
|
try {
|
|
50
72
|
const indexJson = (await fs.readJson(repoExtensions));
|
|
51
|
-
const extensionIds = Object.keys(indexJson.extensions ?? {}).filter(
|
|
73
|
+
const extensionIds = Object.keys(indexJson.extensions ?? {}).filter(id => !indexJson.extensions[id]?.dev);
|
|
52
74
|
if (extensionIds.length > 0) {
|
|
53
75
|
const stripped = settingsJson.replace(/\/\/[^\n]*/g, '');
|
|
54
76
|
let settingsObj = {};
|
|
@@ -59,12 +81,23 @@ async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPa
|
|
|
59
81
|
if (!silent)
|
|
60
82
|
p.log.warn(color.yellow('Could not parse settings.json — skipping extension merge.'));
|
|
61
83
|
}
|
|
62
|
-
|
|
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 };
|
|
63
91
|
for (const id of extensionIds) {
|
|
64
|
-
|
|
92
|
+
// Only set to true if there is no explicit user preference already
|
|
93
|
+
if (!(id in autoInstall)) {
|
|
94
|
+
autoInstall[id] = true;
|
|
95
|
+
}
|
|
65
96
|
}
|
|
66
97
|
settingsObj['auto_install_extensions'] = autoInstall;
|
|
67
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')}`);
|
|
68
101
|
}
|
|
69
102
|
}
|
|
70
103
|
catch {
|
|
@@ -75,19 +108,95 @@ async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPa
|
|
|
75
108
|
await fs.ensureDir(path.dirname(localSettingsPath));
|
|
76
109
|
await fs.writeFile(localSettingsPath, settingsJson, 'utf-8');
|
|
77
110
|
}
|
|
111
|
+
// zedx sync status
|
|
112
|
+
export async function syncStatus() {
|
|
113
|
+
p.intro(color.bold('zedx sync status'));
|
|
114
|
+
const config = await requireSyncConfig();
|
|
115
|
+
const zedPaths = resolveZedPaths();
|
|
116
|
+
p.log.info(`Repo: ${color.dim(config.syncRepo)} ${color.dim(`(${config.branch})`)}`);
|
|
117
|
+
if (config.lastSync) {
|
|
118
|
+
p.log.info(`Last sync: ${color.dim(new Date(config.lastSync).toLocaleString())}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
p.log.info(`Last sync: ${color.dim('never')}`);
|
|
122
|
+
}
|
|
123
|
+
const spinner = p.spinner();
|
|
124
|
+
await withTempDir(async (tmp) => {
|
|
125
|
+
spinner.start(`Fetching ${config.syncRepo}...`);
|
|
126
|
+
let remoteExists = true;
|
|
127
|
+
try {
|
|
128
|
+
const git = simpleGit(tmp);
|
|
129
|
+
await git.clone(config.syncRepo, tmp, ['--depth', '1', '--branch', config.branch]);
|
|
130
|
+
spinner.stop('Remote fetched.');
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
remoteExists = false;
|
|
134
|
+
spinner.stop(color.yellow('Remote is empty or branch not found.'));
|
|
135
|
+
}
|
|
136
|
+
const files = [
|
|
137
|
+
{
|
|
138
|
+
repoPath: path.join(tmp, 'settings.json'),
|
|
139
|
+
localPath: zedPaths.settings,
|
|
140
|
+
label: 'settings.json',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
repoPath: path.join(tmp, 'extensions', 'index.json'),
|
|
144
|
+
localPath: zedPaths.extensions,
|
|
145
|
+
label: 'extensions/index.json',
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
for (const file of files) {
|
|
149
|
+
const localExists = await fs.pathExists(file.localPath);
|
|
150
|
+
const remoteFileExists = remoteExists && (await fs.pathExists(file.repoPath));
|
|
151
|
+
if (!localExists && !remoteFileExists) {
|
|
152
|
+
p.log.warn(`${color.bold(file.label)}: not found locally or remotely`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (localExists && !remoteFileExists) {
|
|
156
|
+
p.log.warn(`${color.bold(file.label)}: ${color.green('local only')} — not pushed yet`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!localExists && remoteFileExists) {
|
|
160
|
+
p.log.warn(`${color.bold(file.label)}: ${color.cyan('remote only')} — not pulled yet`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const localContent = await fs.readFile(file.localPath, 'utf-8');
|
|
164
|
+
const remoteContent = await fs.readFile(file.repoPath, 'utf-8');
|
|
165
|
+
if (localContent === remoteContent) {
|
|
166
|
+
p.log.success(`${color.bold(file.label)}: in sync`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const localMtime = (await fs.stat(file.localPath)).mtime;
|
|
170
|
+
const remoteMtime = (await fs.stat(file.repoPath)).mtime;
|
|
171
|
+
const lastSync = config.lastSync ? new Date(config.lastSync) : null;
|
|
172
|
+
const localChanged = !lastSync || localMtime > lastSync;
|
|
173
|
+
const remoteChanged = !lastSync || remoteMtime > lastSync;
|
|
174
|
+
if (localChanged && !remoteChanged) {
|
|
175
|
+
p.log.warn(`${color.bold(file.label)}: ${color.green('local ahead')} — modified ${color.dim(localMtime.toLocaleString())}`);
|
|
176
|
+
}
|
|
177
|
+
else if (remoteChanged && !localChanged) {
|
|
178
|
+
p.log.warn(`${color.bold(file.label)}: ${color.cyan('remote ahead')} — modified ${color.dim(remoteMtime.toLocaleString())}`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
p.log.warn(`${color.bold(file.label)}: ${color.yellow('conflict')} — both changed since last sync`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
p.outro(`Run ${color.cyan('zedx sync')} to resolve.`);
|
|
186
|
+
}
|
|
78
187
|
// zedx sync init
|
|
79
188
|
export async function syncInit() {
|
|
80
189
|
p.intro(color.bold('zedx sync init'));
|
|
81
190
|
const repo = await p.text({
|
|
82
191
|
message: 'GitHub repo URL (SSH or HTTPS)',
|
|
83
192
|
placeholder: 'https://github.com/you/zed-config.git',
|
|
84
|
-
validate:
|
|
193
|
+
validate: v => {
|
|
85
194
|
if (!v.trim())
|
|
86
195
|
return 'Repo URL is required';
|
|
87
196
|
if (!v.startsWith('https://') && !v.startsWith('git@')) {
|
|
88
197
|
return 'Must be a valid HTTPS or SSH git URL';
|
|
89
198
|
}
|
|
90
|
-
}
|
|
199
|
+
},
|
|
91
200
|
});
|
|
92
201
|
if (p.isCancel(repo)) {
|
|
93
202
|
p.cancel('Cancelled.');
|
|
@@ -96,7 +205,7 @@ export async function syncInit() {
|
|
|
96
205
|
const branch = await p.text({
|
|
97
206
|
message: 'Branch name',
|
|
98
207
|
placeholder: 'main',
|
|
99
|
-
defaultValue: 'main'
|
|
208
|
+
defaultValue: 'main',
|
|
100
209
|
});
|
|
101
210
|
if (p.isCancel(branch)) {
|
|
102
211
|
p.cancel('Cancelled.');
|
|
@@ -114,7 +223,7 @@ export async function syncInit() {
|
|
|
114
223
|
}
|
|
115
224
|
const config = {
|
|
116
225
|
syncRepo: repo.trim(),
|
|
117
|
-
branch: (branch || 'main').trim()
|
|
226
|
+
branch: (branch || 'main').trim(),
|
|
118
227
|
};
|
|
119
228
|
await writeSyncConfig(config);
|
|
120
229
|
p.outro(`${color.green('✓')} Sync config saved to ${color.cyan(ZEDX_CONFIG_PATH)}\n\n` +
|
|
@@ -139,7 +248,7 @@ export async function runSync(opts = {}) {
|
|
|
139
248
|
success: (msg) => {
|
|
140
249
|
if (!silent)
|
|
141
250
|
p.log.success(msg);
|
|
142
|
-
}
|
|
251
|
+
},
|
|
143
252
|
};
|
|
144
253
|
if (!silent)
|
|
145
254
|
p.intro(color.bold('zedx sync'));
|
|
@@ -149,7 +258,7 @@ export async function runSync(opts = {}) {
|
|
|
149
258
|
const spinner = silent
|
|
150
259
|
? {
|
|
151
260
|
start: (m) => console.error(`[zedx] ${m}`),
|
|
152
|
-
stop: (m) => console.error(`[zedx] ${m}`)
|
|
261
|
+
stop: (m) => console.error(`[zedx] ${m}`),
|
|
153
262
|
}
|
|
154
263
|
: p.spinner();
|
|
155
264
|
await withTempDir(async (tmp) => {
|
|
@@ -173,13 +282,13 @@ export async function runSync(opts = {}) {
|
|
|
173
282
|
{
|
|
174
283
|
repoPath: path.join(tmp, 'settings.json'),
|
|
175
284
|
localPath: zedPaths.settings,
|
|
176
|
-
label: 'settings.json'
|
|
285
|
+
label: 'settings.json',
|
|
177
286
|
},
|
|
178
287
|
{
|
|
179
288
|
repoPath: path.join(tmp, 'extensions', 'index.json'),
|
|
180
289
|
localPath: zedPaths.extensions,
|
|
181
|
-
label: 'extensions/index.json'
|
|
182
|
-
}
|
|
290
|
+
label: 'extensions/index.json',
|
|
291
|
+
},
|
|
183
292
|
];
|
|
184
293
|
let anyChanges = false;
|
|
185
294
|
for (const file of files) {
|
|
@@ -193,8 +302,13 @@ export async function runSync(opts = {}) {
|
|
|
193
302
|
// Remote doesn't have it yet — push local
|
|
194
303
|
if (localExists && !remoteFileExists) {
|
|
195
304
|
log.info(`${file.label}: ${color.green('pushing')} (not in remote yet)`);
|
|
196
|
-
|
|
197
|
-
|
|
305
|
+
if (file.label === 'settings.json') {
|
|
306
|
+
await prepareSettingsForPush(file.localPath, file.repoPath);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
310
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
311
|
+
}
|
|
198
312
|
anyChanges = true;
|
|
199
313
|
continue;
|
|
200
314
|
}
|
|
@@ -219,14 +333,21 @@ export async function runSync(opts = {}) {
|
|
|
219
333
|
}
|
|
220
334
|
// Detect which side changed since last sync via mtime
|
|
221
335
|
const localMtime = (await fs.stat(file.localPath)).mtime;
|
|
222
|
-
const remoteMtime = remoteFileExists
|
|
336
|
+
const remoteMtime = remoteFileExists
|
|
337
|
+
? (await fs.stat(file.repoPath)).mtime
|
|
338
|
+
: new Date(0);
|
|
223
339
|
const localChanged = !lastSync || localMtime > lastSync;
|
|
224
340
|
const remoteChanged = !lastSync || remoteMtime > lastSync;
|
|
225
341
|
if (localChanged && !remoteChanged) {
|
|
226
342
|
// Only local changed → push
|
|
227
343
|
log.info(`${file.label}: ${color.green('pushing')} (local is newer)`);
|
|
228
|
-
|
|
229
|
-
|
|
344
|
+
if (file.label === 'settings.json') {
|
|
345
|
+
await prepareSettingsForPush(file.localPath, file.repoPath);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
349
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
350
|
+
}
|
|
230
351
|
anyChanges = true;
|
|
231
352
|
}
|
|
232
353
|
else if (remoteChanged && !localChanged) {
|
|
@@ -245,26 +366,31 @@ export async function runSync(opts = {}) {
|
|
|
245
366
|
if (silent) {
|
|
246
367
|
// Daemon can't prompt — local wins, will be pushed
|
|
247
368
|
log.warn(`${file.label}: conflict detected in unattended mode — keeping local.`);
|
|
248
|
-
|
|
249
|
-
|
|
369
|
+
if (file.label === 'settings.json') {
|
|
370
|
+
await prepareSettingsForPush(file.localPath, file.repoPath);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
374
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
375
|
+
}
|
|
250
376
|
anyChanges = true;
|
|
251
377
|
}
|
|
252
378
|
else {
|
|
253
|
-
p.log.warn(color.yellow(
|
|
379
|
+
p.log.warn(color.yellow(`conflict between local and remote ${file.label}`));
|
|
254
380
|
const choice = await p.select({
|
|
255
381
|
message: `Which version of ${color.bold(file.label)} should win?`,
|
|
256
382
|
options: [
|
|
257
383
|
{
|
|
258
384
|
value: 'local',
|
|
259
385
|
label: 'Keep local',
|
|
260
|
-
hint: `modified ${localMtime.toLocaleString()}
|
|
386
|
+
hint: `modified ${localMtime.toLocaleString()}`,
|
|
261
387
|
},
|
|
262
388
|
{
|
|
263
389
|
value: 'remote',
|
|
264
390
|
label: 'Use remote',
|
|
265
|
-
hint: `modified ${remoteMtime.toLocaleString()}
|
|
266
|
-
}
|
|
267
|
-
]
|
|
391
|
+
hint: `modified ${remoteMtime.toLocaleString()}`,
|
|
392
|
+
},
|
|
393
|
+
],
|
|
268
394
|
});
|
|
269
395
|
if (p.isCancel(choice)) {
|
|
270
396
|
p.cancel('Cancelled.');
|
|
@@ -272,8 +398,13 @@ export async function runSync(opts = {}) {
|
|
|
272
398
|
}
|
|
273
399
|
if (choice === 'local') {
|
|
274
400
|
p.log.info(`${file.label}: ${color.green('keeping local, will push')}`);
|
|
275
|
-
|
|
276
|
-
|
|
401
|
+
if (file.label === 'settings.json') {
|
|
402
|
+
await prepareSettingsForPush(file.localPath, file.repoPath);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
406
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
407
|
+
}
|
|
277
408
|
anyChanges = true;
|
|
278
409
|
}
|
|
279
410
|
else {
|
package/package.json
CHANGED
|
@@ -1,54 +1,59 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
2
|
+
"name": "zedx",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Scaffold Zed Editor extensions and sync your settings across machines.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"boilerplate",
|
|
7
|
+
"extension",
|
|
8
|
+
"scaffold",
|
|
9
|
+
"zed",
|
|
10
|
+
"zed-editor"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/tahayvr/zedx#readme",
|
|
13
|
+
"license": "Apache-2.0",
|
|
14
|
+
"author": "Taha Nejad <taha@noiserandom.com>",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/tahayvr/zedx.git"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"zedx": "dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "dist/index.js",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@clack/prompts": "^0.10.1",
|
|
32
|
+
"commander": "^14.0.3",
|
|
33
|
+
"ejs": "^4.0.1",
|
|
34
|
+
"fs-extra": "^11.3.3",
|
|
35
|
+
"picocolors": "^1.1.1",
|
|
36
|
+
"simple-git": "^3.33.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/ejs": "^3.1.5",
|
|
40
|
+
"@types/fs-extra": "^11.0.4",
|
|
41
|
+
"@types/node": "^25.2.3",
|
|
42
|
+
"oxfmt": "^0.42.0",
|
|
43
|
+
"oxlint": "^1.57.0",
|
|
44
|
+
"tsx": "^4.21.0",
|
|
45
|
+
"typescript": "^5.9.3"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc && cp -r src/templates dist/",
|
|
52
|
+
"start": "node dist/index.js",
|
|
53
|
+
"dev": "tsx src/index.ts",
|
|
54
|
+
"lint": "oxlint",
|
|
55
|
+
"lint:fix": "oxlint --fix",
|
|
56
|
+
"fmt": "oxfmt",
|
|
57
|
+
"fmt:check": "oxfmt --check"
|
|
58
|
+
}
|
|
59
|
+
}
|