zedx 0.3.1 → 0.5.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 +14 -2
- package/dist/add.d.ts +2 -0
- package/dist/add.js +137 -0
- package/dist/check.d.ts +1 -0
- package/dist/check.js +243 -0
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +183 -0
- package/dist/dev.d.ts +1 -0
- package/dist/dev.js +243 -0
- package/dist/index.js +49 -0
- package/dist/sync.d.ts +4 -0
- package/dist/sync.js +317 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/zed-paths.d.ts +2 -0
- package/dist/zed-paths.js +30 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# zedx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scaffold [Zed Editor](https://zed.dev/) extensions and sync your settings across machines.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -19,11 +19,23 @@ brew install tahayvr/tap/zedx
|
|
|
19
19
|
# Create a new extension
|
|
20
20
|
zedx
|
|
21
21
|
|
|
22
|
+
# Add a theme or language to an existing extension
|
|
23
|
+
zedx add theme "Midnight Blue"
|
|
24
|
+
zedx add language rust
|
|
25
|
+
|
|
26
|
+
# Validate extension config and show what is missing or incomplete
|
|
27
|
+
zedx check
|
|
28
|
+
|
|
22
29
|
# Bump extension version
|
|
23
|
-
# Run inside extension dir
|
|
24
30
|
zedx version patch # 1.2.3 → 1.2.4
|
|
25
31
|
zedx version minor # 1.2.3 → 1.3.0
|
|
26
32
|
zedx version major # 1.2.3 → 2.0.0
|
|
33
|
+
|
|
34
|
+
# Sync Zed settings and extensions via a GitHub repo
|
|
35
|
+
zedx sync init # Link a GitHub repo as the sync target (run once)
|
|
36
|
+
zedx sync # Sync local and remote config automatically
|
|
37
|
+
zedx sync install # Install an OS daemon to auto-sync when Zed config changes
|
|
38
|
+
zedx sync uninstall # Remove the OS daemon
|
|
27
39
|
```
|
|
28
40
|
|
|
29
41
|
### Supported extension types:
|
package/dist/add.d.ts
ADDED
package/dist/add.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import ejs from 'ejs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import * as p from '@clack/prompts';
|
|
6
|
+
import color from 'picocolors';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const isDev = __dirname.includes('/src/');
|
|
10
|
+
const PROJECT_ROOT = isDev ? path.join(__dirname, '..') : __dirname;
|
|
11
|
+
const TEMPLATE_DIR = path.join(PROJECT_ROOT, 'templates');
|
|
12
|
+
async function renderTemplate(templatePath, data) {
|
|
13
|
+
const template = await fs.readFile(templatePath, 'utf-8');
|
|
14
|
+
return ejs.render(template, data);
|
|
15
|
+
}
|
|
16
|
+
function tomlGet(content, key) {
|
|
17
|
+
const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, 'm'));
|
|
18
|
+
return match?.[1];
|
|
19
|
+
}
|
|
20
|
+
function slugify(name) {
|
|
21
|
+
return name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
22
|
+
}
|
|
23
|
+
export async function addTheme(callerDir, themeName) {
|
|
24
|
+
p.intro(`${color.bgBlue(color.bold(' zedx add theme '))} ${color.blue('Adding a theme to your extension…')}`);
|
|
25
|
+
const tomlPath = path.join(callerDir, 'extension.toml');
|
|
26
|
+
if (!(await fs.pathExists(tomlPath))) {
|
|
27
|
+
p.log.error(color.red('No extension.toml found. Run zedx from an extension directory.'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const tomlContent = await fs.readFile(tomlPath, 'utf-8');
|
|
31
|
+
const extensionId = tomlGet(tomlContent, 'id') ?? 'extension';
|
|
32
|
+
const author = tomlGet(tomlContent, 'authors')
|
|
33
|
+
?? tomlContent.match(/^authors\s*=\s*\["([^"]+)"\]/m)?.[1]
|
|
34
|
+
?? '';
|
|
35
|
+
const appearance = await p.select({
|
|
36
|
+
message: 'Appearance:',
|
|
37
|
+
options: [
|
|
38
|
+
{ value: 'dark', label: 'Dark' },
|
|
39
|
+
{ value: 'light', label: 'Light' },
|
|
40
|
+
{ value: 'both', label: 'Both (Dark & Light)' },
|
|
41
|
+
],
|
|
42
|
+
initialValue: 'dark',
|
|
43
|
+
});
|
|
44
|
+
if (p.isCancel(appearance)) {
|
|
45
|
+
p.cancel('Cancelled.');
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
const appearances = appearance === 'both' ? ['dark', 'light'] : [appearance];
|
|
49
|
+
const themeSlug = slugify(themeName);
|
|
50
|
+
const themeFile = `${themeSlug}.json`;
|
|
51
|
+
const themesDir = path.join(callerDir, 'themes');
|
|
52
|
+
const themePath = path.join(themesDir, themeFile);
|
|
53
|
+
if (await fs.pathExists(themePath)) {
|
|
54
|
+
p.log.error(color.red(`themes/${themeFile} already exists.`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
await fs.ensureDir(themesDir);
|
|
58
|
+
const themeJson = await renderTemplate(path.join(TEMPLATE_DIR, 'theme/theme.json.ejs'), { id: extensionId, author, themeName, appearances });
|
|
59
|
+
await fs.writeFile(themePath, themeJson);
|
|
60
|
+
p.log.success(`Created ${color.cyan(`themes/${themeFile}`)}`);
|
|
61
|
+
p.outro(`${color.green('✓')} Theme added.\n` +
|
|
62
|
+
`${color.dim('Run')} ${color.cyan('zedx check')} ${color.dim('to validate your extension.')}`);
|
|
63
|
+
}
|
|
64
|
+
export async function addLanguage(callerDir, languageId) {
|
|
65
|
+
p.intro(`${color.bgBlue(color.bold(' zedx add language '))} ${color.blue('Adding a language to your extension…')}`);
|
|
66
|
+
const tomlPath = path.join(callerDir, 'extension.toml');
|
|
67
|
+
if (!(await fs.pathExists(tomlPath))) {
|
|
68
|
+
p.log.error(color.red('No extension.toml found. Run zedx from an extension directory.'));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const tomlContent = await fs.readFile(tomlPath, 'utf-8');
|
|
72
|
+
// Check for duplicate
|
|
73
|
+
const alreadyExists = new RegExp(`^\\[grammars\\.${languageId}\\]`, 'm').test(tomlContent)
|
|
74
|
+
|| new RegExp(`^#\\s*\\[grammars\\.${languageId}\\]`, 'm').test(tomlContent);
|
|
75
|
+
if (alreadyExists) {
|
|
76
|
+
p.log.error(color.red(`Language "${languageId}" already exists in extension.toml.`));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const languageName = await p.text({
|
|
80
|
+
message: 'Language display name:',
|
|
81
|
+
placeholder: languageId,
|
|
82
|
+
});
|
|
83
|
+
if (p.isCancel(languageName)) {
|
|
84
|
+
p.cancel('Cancelled.');
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
const languageNameValue = String(languageName || languageId);
|
|
88
|
+
const langDir = path.join(callerDir, 'languages', languageId);
|
|
89
|
+
if (await fs.pathExists(langDir)) {
|
|
90
|
+
p.log.error(color.red(`languages/${languageId}/ already exists.`));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
await fs.ensureDir(langDir);
|
|
94
|
+
// Write config.toml
|
|
95
|
+
const data = {
|
|
96
|
+
languageName: languageNameValue,
|
|
97
|
+
languageId,
|
|
98
|
+
pathSuffixes: [],
|
|
99
|
+
lineComments: ['//', '#'],
|
|
100
|
+
grammarRepo: '',
|
|
101
|
+
grammarRev: '',
|
|
102
|
+
};
|
|
103
|
+
const configToml = await renderTemplate(path.join(TEMPLATE_DIR, 'language/config.toml.ejs'), data);
|
|
104
|
+
await fs.writeFile(path.join(langDir, 'config.toml'), configToml);
|
|
105
|
+
p.log.success(`Created ${color.cyan(`languages/${languageId}/config.toml`)}`);
|
|
106
|
+
// Write .scm query files
|
|
107
|
+
const queryFiles = [
|
|
108
|
+
'highlights.scm',
|
|
109
|
+
'brackets.scm',
|
|
110
|
+
'outline.scm',
|
|
111
|
+
'indents.scm',
|
|
112
|
+
'injections.scm',
|
|
113
|
+
'overrides.scm',
|
|
114
|
+
'textobjects.scm',
|
|
115
|
+
'redactions.scm',
|
|
116
|
+
'runnables.scm',
|
|
117
|
+
];
|
|
118
|
+
for (const file of queryFiles) {
|
|
119
|
+
const templatePath = path.join(TEMPLATE_DIR, 'language', file);
|
|
120
|
+
if (await fs.pathExists(templatePath)) {
|
|
121
|
+
const content = ejs.render(await fs.readFile(templatePath, 'utf-8'), data);
|
|
122
|
+
await fs.writeFile(path.join(langDir, file), content);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
p.log.success(`Created ${color.cyan(`languages/${languageId}/`)} query files`);
|
|
126
|
+
// Append grammar block to extension.toml
|
|
127
|
+
const grammarBlock = `\n# [grammars.${languageId}]\n` +
|
|
128
|
+
`# repository = "https://github.com/user/tree-sitter-${languageId}"\n` +
|
|
129
|
+
`# rev = "main"\n` +
|
|
130
|
+
`\n# [language_servers.${languageId}-lsp]\n` +
|
|
131
|
+
`# name = "${languageNameValue} LSP"\n` +
|
|
132
|
+
`# languages = ["${languageNameValue}"]\n`;
|
|
133
|
+
await fs.appendFile(tomlPath, grammarBlock);
|
|
134
|
+
p.log.success(`Updated ${color.cyan('extension.toml')} with grammar block`);
|
|
135
|
+
p.outro(`${color.green('✓')} Language added.\n` +
|
|
136
|
+
`${color.dim('Run')} ${color.cyan('zedx check')} ${color.dim('to validate your extension.')}`);
|
|
137
|
+
}
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCheck(callerDir: string): Promise<void>;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import color from 'picocolors';
|
|
5
|
+
// Minimal TOML key extraction — handles `key = "value"` and `key = ["a", "b"]`
|
|
6
|
+
function tomlGet(content, key) {
|
|
7
|
+
const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, 'm'));
|
|
8
|
+
return match?.[1];
|
|
9
|
+
}
|
|
10
|
+
function tomlHasUncommentedKey(content, key) {
|
|
11
|
+
return new RegExp(`^${key}\\s*=`, 'm').test(content);
|
|
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
|
+
export async function runCheck(callerDir) {
|
|
18
|
+
p.intro(`${color.bgBlue(color.bold(' zedx check '))} ${color.blue('Validating extension config…')}`);
|
|
19
|
+
const tomlPath = path.join(callerDir, 'extension.toml');
|
|
20
|
+
if (!(await fs.pathExists(tomlPath))) {
|
|
21
|
+
p.log.error(color.red('No extension.toml found in current directory.'));
|
|
22
|
+
p.log.info(`Run ${color.cyan('zedx')} to scaffold a new extension first.`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const tomlContent = await fs.readFile(tomlPath, 'utf-8');
|
|
26
|
+
const extensionId = tomlGet(tomlContent, 'id');
|
|
27
|
+
const extensionName = tomlGet(tomlContent, 'name');
|
|
28
|
+
const repository = tomlGet(tomlContent, 'repository');
|
|
29
|
+
const results = [];
|
|
30
|
+
// ── extension.toml ────────────────────────────────────────────────────────
|
|
31
|
+
const extIssues = [];
|
|
32
|
+
if (!extensionId) {
|
|
33
|
+
extIssues.push({
|
|
34
|
+
file: 'extension.toml',
|
|
35
|
+
message: 'Missing required field: id',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (!extensionName) {
|
|
39
|
+
extIssues.push({
|
|
40
|
+
file: 'extension.toml',
|
|
41
|
+
message: 'Missing required field: name',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (!repository || repository.includes('username')) {
|
|
45
|
+
extIssues.push({
|
|
46
|
+
file: 'extension.toml',
|
|
47
|
+
message: 'repository still uses the default placeholder URL',
|
|
48
|
+
hint: 'Set it to your actual GitHub repository URL',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// Detect language entries by looking for uncommented [grammars.*] sections
|
|
52
|
+
const grammarMatches = [...tomlContent.matchAll(/^\[grammars\.(\S+)\]/gm)];
|
|
53
|
+
const commentedGrammarMatches = [...tomlContent.matchAll(/^#\s*\[grammars\.(\S+)\]/gm)];
|
|
54
|
+
const languageIds = grammarMatches.map(m => m[1]);
|
|
55
|
+
const hasLanguage = languageIds.length > 0 || commentedGrammarMatches.length > 0;
|
|
56
|
+
if (commentedGrammarMatches.length > 0 && grammarMatches.length === 0) {
|
|
57
|
+
const ids = commentedGrammarMatches.map(m => m[1]);
|
|
58
|
+
extIssues.push({
|
|
59
|
+
file: 'extension.toml',
|
|
60
|
+
message: `Grammar section is commented out for: ${ids.join(', ')}`,
|
|
61
|
+
hint: 'Uncomment [grammars.<id>] and set a real tree-sitter repository URL and rev',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Detect theme entries by looking for themes/ directory
|
|
65
|
+
const themesDir = path.join(callerDir, 'themes');
|
|
66
|
+
const hasTheme = await fs.pathExists(themesDir);
|
|
67
|
+
results.push({ file: 'extension.toml', issues: extIssues });
|
|
68
|
+
// ── theme validation ──────────────────────────────────────────────────────
|
|
69
|
+
if (hasTheme) {
|
|
70
|
+
const themeIssues = [];
|
|
71
|
+
const themeFiles = (await fs.readdir(themesDir)).filter(f => f.endsWith('.json'));
|
|
72
|
+
if (themeFiles.length === 0) {
|
|
73
|
+
themeIssues.push({
|
|
74
|
+
file: 'themes/',
|
|
75
|
+
message: 'No .json theme files found in themes/ directory',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
for (const themeFile of themeFiles) {
|
|
79
|
+
const themePath = path.join(themesDir, themeFile);
|
|
80
|
+
const themeIssuesForFile = [];
|
|
81
|
+
let themeJson;
|
|
82
|
+
try {
|
|
83
|
+
themeJson = await fs.readJson(themePath);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
themeIssuesForFile.push({
|
|
87
|
+
file: `themes/${themeFile}`,
|
|
88
|
+
message: 'Invalid JSON — file could not be parsed',
|
|
89
|
+
});
|
|
90
|
+
results.push({ file: `themes/${themeFile}`, issues: themeIssuesForFile });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const themes = themeJson['themes'];
|
|
94
|
+
if (!themes || themes.length === 0) {
|
|
95
|
+
themeIssuesForFile.push({
|
|
96
|
+
file: `themes/${themeFile}`,
|
|
97
|
+
message: 'No theme variants found under the "themes" key',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
for (const variant of themes) {
|
|
102
|
+
const variantName = String(variant['name'] ?? 'unknown');
|
|
103
|
+
const style = variant['style'];
|
|
104
|
+
if (!style) {
|
|
105
|
+
themeIssuesForFile.push({
|
|
106
|
+
file: `themes/${themeFile}`,
|
|
107
|
+
message: `Variant "${variantName}": missing "style" block`,
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Check for placeholder-like neutral grays that indicate untouched scaffold
|
|
112
|
+
const background = style['background'];
|
|
113
|
+
const placeholderBgs = ['#1e1e1e', '#f5f5f5', '#ffffff', '#000000'];
|
|
114
|
+
if (background && placeholderBgs.includes(background.toLowerCase())) {
|
|
115
|
+
themeIssuesForFile.push({
|
|
116
|
+
file: `themes/${themeFile}`,
|
|
117
|
+
message: `Variant "${variantName}": background color is still the scaffold placeholder (${background})`,
|
|
118
|
+
hint: 'Replace with your actual theme colors',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// Check that syntax block is populated
|
|
122
|
+
const syntax = style['syntax'];
|
|
123
|
+
if (!syntax || Object.keys(syntax).length === 0) {
|
|
124
|
+
themeIssuesForFile.push({
|
|
125
|
+
file: `themes/${themeFile}`,
|
|
126
|
+
message: `Variant "${variantName}": "syntax" block is empty or missing`,
|
|
127
|
+
hint: 'Add syntax token color definitions',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
themeIssues.push(...themeIssuesForFile);
|
|
133
|
+
}
|
|
134
|
+
results.push({ file: 'themes/', issues: themeIssues });
|
|
135
|
+
}
|
|
136
|
+
// ── language validation ───────────────────────────────────────────────────
|
|
137
|
+
if (hasLanguage) {
|
|
138
|
+
// Collect all language IDs from both uncommented and commented grammar sections
|
|
139
|
+
const allLanguageIds = [
|
|
140
|
+
...grammarMatches.map(m => m[1]),
|
|
141
|
+
...commentedGrammarMatches.map(m => m[1]),
|
|
142
|
+
];
|
|
143
|
+
for (const langId of allLanguageIds) {
|
|
144
|
+
const langDir = path.join(callerDir, 'languages', langId);
|
|
145
|
+
const langIssues = [];
|
|
146
|
+
if (!(await fs.pathExists(langDir))) {
|
|
147
|
+
langIssues.push({
|
|
148
|
+
file: `languages/${langId}/`,
|
|
149
|
+
message: `Language directory does not exist`,
|
|
150
|
+
hint: `Expected at ${path.join('languages', langId)}`,
|
|
151
|
+
});
|
|
152
|
+
results.push({ file: `languages/${langId}/`, issues: langIssues });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
// config.toml checks
|
|
156
|
+
const configPath = path.join(langDir, 'config.toml');
|
|
157
|
+
const configIssues = [];
|
|
158
|
+
if (!(await fs.pathExists(configPath))) {
|
|
159
|
+
configIssues.push({
|
|
160
|
+
file: `languages/${langId}/config.toml`,
|
|
161
|
+
message: 'config.toml is missing',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
166
|
+
if (!tomlHasUncommentedKey(configContent, 'name')) {
|
|
167
|
+
configIssues.push({
|
|
168
|
+
file: `languages/${langId}/config.toml`,
|
|
169
|
+
message: 'Missing required field: name',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (!tomlHasUncommentedKey(configContent, 'grammar')) {
|
|
173
|
+
configIssues.push({
|
|
174
|
+
file: `languages/${langId}/config.toml`,
|
|
175
|
+
message: 'Missing required field: grammar',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// path_suffixes is commented out in scaffold — flag it
|
|
179
|
+
if (!tomlHasUncommentedKey(configContent, 'path_suffixes')) {
|
|
180
|
+
configIssues.push({
|
|
181
|
+
file: `languages/${langId}/config.toml`,
|
|
182
|
+
message: 'path_suffixes is not set — files won\'t be associated with this language',
|
|
183
|
+
hint: 'Uncomment and fill in path_suffixes (e.g., ["myl"])',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// line_comments is commented out in scaffold — flag it
|
|
187
|
+
if (!tomlHasUncommentedKey(configContent, 'line_comments')) {
|
|
188
|
+
configIssues.push({
|
|
189
|
+
file: `languages/${langId}/config.toml`,
|
|
190
|
+
message: 'line_comments is not set — toggle-comment keybind won\'t work',
|
|
191
|
+
hint: 'Uncomment and set line_comments (e.g., ["// "])',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
results.push({ file: `languages/${langId}/config.toml`, issues: configIssues });
|
|
196
|
+
// highlights.scm checks
|
|
197
|
+
const highlightsPath = path.join(langDir, 'highlights.scm');
|
|
198
|
+
const highlightIssues = [];
|
|
199
|
+
if (!(await fs.pathExists(highlightsPath))) {
|
|
200
|
+
highlightIssues.push({
|
|
201
|
+
file: `languages/${langId}/highlights.scm`,
|
|
202
|
+
message: 'highlights.scm is missing',
|
|
203
|
+
hint: 'Without it, no syntax highlighting will appear',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const highlightsContent = await fs.readFile(highlightsPath, 'utf-8');
|
|
208
|
+
// Count non-comment, non-empty lines with actual query patterns
|
|
209
|
+
const activeLines = highlightsContent
|
|
210
|
+
.split('\n')
|
|
211
|
+
.filter(l => l.trim() && !l.trim().startsWith(';'));
|
|
212
|
+
if (activeLines.length <= 3) {
|
|
213
|
+
highlightIssues.push({
|
|
214
|
+
file: `languages/${langId}/highlights.scm`,
|
|
215
|
+
message: 'Only scaffold starter patterns present — no real grammar queries added yet',
|
|
216
|
+
hint: 'Add tree-sitter queries matching your language\'s grammar node types',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
results.push({ file: `languages/${langId}/highlights.scm`, issues: highlightIssues });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// ── render results ────────────────────────────────────────────────────────
|
|
224
|
+
const allIssues = results.flatMap(r => r.issues);
|
|
225
|
+
const fileGroups = results.filter(r => r.issues.length > 0);
|
|
226
|
+
if (fileGroups.length === 0) {
|
|
227
|
+
p.log.success(color.green('No issues found. Your extension config looks good!'));
|
|
228
|
+
p.outro(`${color.dim('Load it in Zed:')} Extensions ${color.dim('>')} Install Dev Extension`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
for (const group of fileGroups) {
|
|
232
|
+
p.log.warn(`${color.yellow(color.bold(group.issues[0].file))}`);
|
|
233
|
+
for (const issue of group.issues) {
|
|
234
|
+
process.stdout.write(` ${color.red('✗')} ${issue.message}\n`);
|
|
235
|
+
if (issue.hint) {
|
|
236
|
+
process.stdout.write(` ${color.dim('→')} ${color.dim(issue.hint)}\n`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
process.stdout.write('\n');
|
|
240
|
+
}
|
|
241
|
+
const issueCount = allIssues.length;
|
|
242
|
+
p.outro(`${color.red(`${issueCount} issue${issueCount === 1 ? '' : 's'} found`)} — fix the above before publishing`);
|
|
243
|
+
}
|
package/dist/daemon.d.ts
ADDED
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import * as p from '@clack/prompts';
|
|
6
|
+
import color from 'picocolors';
|
|
7
|
+
import { resolveZedPaths } from './zed-paths.js';
|
|
8
|
+
const LAUNCHD_LABEL = 'dev.zedx.sync';
|
|
9
|
+
const LAUNCHD_PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
10
|
+
const SYSTEMD_SERVICE_NAME = 'zedx-sync';
|
|
11
|
+
const SYSTEMD_UNIT_DIR = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
12
|
+
const SYSTEMD_SERVICE_PATH = path.join(SYSTEMD_UNIT_DIR, `${SYSTEMD_SERVICE_NAME}.service`);
|
|
13
|
+
const SYSTEMD_PATH_PATH = path.join(SYSTEMD_UNIT_DIR, `${SYSTEMD_SERVICE_NAME}.path`);
|
|
14
|
+
function resolveZedxBinary() {
|
|
15
|
+
try {
|
|
16
|
+
const bin = execSync('which zedx', { encoding: 'utf-8' }).trim();
|
|
17
|
+
if (bin)
|
|
18
|
+
return bin;
|
|
19
|
+
}
|
|
20
|
+
catch { /* fall through */ }
|
|
21
|
+
return `${process.execPath} ${process.argv[1]}`;
|
|
22
|
+
}
|
|
23
|
+
function unsupportedPlatform() {
|
|
24
|
+
p.log.error(color.red(`zedx sync install is only supported on macOS and Linux.`));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
function buildPlist(zedxBin, watchPaths) {
|
|
28
|
+
const watchEntries = watchPaths
|
|
29
|
+
.map((wp) => ` <string>${wp}</string>`)
|
|
30
|
+
.join('\n');
|
|
31
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
32
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
33
|
+
<plist version="1.0">
|
|
34
|
+
<dict>
|
|
35
|
+
<key>Label</key>
|
|
36
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
37
|
+
|
|
38
|
+
<key>ProgramArguments</key>
|
|
39
|
+
<array>
|
|
40
|
+
<string>${zedxBin}</string>
|
|
41
|
+
<string>sync</string>
|
|
42
|
+
</array>
|
|
43
|
+
|
|
44
|
+
<key>RunAtLoad</key>
|
|
45
|
+
<true/>
|
|
46
|
+
|
|
47
|
+
<key>WatchPaths</key>
|
|
48
|
+
<array>
|
|
49
|
+
${watchEntries}
|
|
50
|
+
</array>
|
|
51
|
+
|
|
52
|
+
<key>ThrottleInterval</key>
|
|
53
|
+
<integer>30</integer>
|
|
54
|
+
|
|
55
|
+
<key>StandardOutPath</key>
|
|
56
|
+
<string>${os.homedir()}/Library/Logs/zedx-sync.log</string>
|
|
57
|
+
|
|
58
|
+
<key>StandardErrorPath</key>
|
|
59
|
+
<string>${os.homedir()}/Library/Logs/zedx-sync.log</string>
|
|
60
|
+
</dict>
|
|
61
|
+
</plist>
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
async function installMacos(zedxBin, watchPaths) {
|
|
65
|
+
const plist = buildPlist(zedxBin, watchPaths);
|
|
66
|
+
await fs.ensureDir(path.dirname(LAUNCHD_PLIST_PATH));
|
|
67
|
+
await fs.writeFile(LAUNCHD_PLIST_PATH, plist, 'utf-8');
|
|
68
|
+
try {
|
|
69
|
+
execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null`, { stdio: 'pipe' });
|
|
70
|
+
}
|
|
71
|
+
catch { /* not loaded yet */ }
|
|
72
|
+
execSync(`launchctl load "${LAUNCHD_PLIST_PATH}"`);
|
|
73
|
+
p.log.success(`Daemon installed: ${color.dim(LAUNCHD_PLIST_PATH)}`);
|
|
74
|
+
p.log.info(`Logs: ${color.dim(`${os.homedir()}/Library/Logs/zedx-sync.log`)}`);
|
|
75
|
+
p.log.info(`To check status: ${color.cyan(`launchctl list ${LAUNCHD_LABEL}`)}`);
|
|
76
|
+
}
|
|
77
|
+
async function uninstallMacos() {
|
|
78
|
+
if (!(await fs.pathExists(LAUNCHD_PLIST_PATH))) {
|
|
79
|
+
p.log.warn(color.yellow('No launchd agent found — nothing to uninstall.'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}"`, { stdio: 'pipe' });
|
|
84
|
+
}
|
|
85
|
+
catch { /* already unloaded */ }
|
|
86
|
+
await fs.remove(LAUNCHD_PLIST_PATH);
|
|
87
|
+
p.log.success('Daemon uninstalled.');
|
|
88
|
+
}
|
|
89
|
+
function buildSystemdService(zedxBin) {
|
|
90
|
+
return `[Unit]
|
|
91
|
+
Description=zedx Zed config sync
|
|
92
|
+
After=network-online.target
|
|
93
|
+
Wants=network-online.target
|
|
94
|
+
|
|
95
|
+
[Service]
|
|
96
|
+
Type=oneshot
|
|
97
|
+
ExecStart=${zedxBin} sync
|
|
98
|
+
StandardOutput=journal
|
|
99
|
+
StandardError=journal
|
|
100
|
+
|
|
101
|
+
[Install]
|
|
102
|
+
WantedBy=default.target
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
function buildSystemdPath(watchPaths) {
|
|
106
|
+
const pathChangedEntries = watchPaths
|
|
107
|
+
.map((wp) => `PathChanged=${wp}`)
|
|
108
|
+
.join('\n');
|
|
109
|
+
return `[Unit]
|
|
110
|
+
Description=Watch Zed config files for zedx sync
|
|
111
|
+
|
|
112
|
+
[Path]
|
|
113
|
+
${pathChangedEntries}
|
|
114
|
+
Unit=${SYSTEMD_SERVICE_NAME}.service
|
|
115
|
+
|
|
116
|
+
[Install]
|
|
117
|
+
WantedBy=default.target
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
async function installLinux(zedxBin, watchPaths) {
|
|
121
|
+
await fs.ensureDir(SYSTEMD_UNIT_DIR);
|
|
122
|
+
await fs.writeFile(SYSTEMD_SERVICE_PATH, buildSystemdService(zedxBin), 'utf-8');
|
|
123
|
+
await fs.writeFile(SYSTEMD_PATH_PATH, buildSystemdPath(watchPaths), 'utf-8');
|
|
124
|
+
execSync('systemctl --user daemon-reload');
|
|
125
|
+
execSync(`systemctl --user enable --now ${SYSTEMD_SERVICE_NAME}.path`);
|
|
126
|
+
p.log.success(`Service installed: ${color.dim(SYSTEMD_SERVICE_PATH)}`);
|
|
127
|
+
p.log.success(`Path unit installed: ${color.dim(SYSTEMD_PATH_PATH)}`);
|
|
128
|
+
p.log.info(`To check status: ${color.cyan(`systemctl --user status ${SYSTEMD_SERVICE_NAME}.path`)}`);
|
|
129
|
+
p.log.info(`Logs: ${color.cyan(`journalctl --user -u ${SYSTEMD_SERVICE_NAME}.service`)}`);
|
|
130
|
+
}
|
|
131
|
+
async function uninstallLinux() {
|
|
132
|
+
const serviceExists = await fs.pathExists(SYSTEMD_SERVICE_PATH);
|
|
133
|
+
const pathExists = await fs.pathExists(SYSTEMD_PATH_PATH);
|
|
134
|
+
if (!serviceExists && !pathExists) {
|
|
135
|
+
p.log.warn(color.yellow('No systemd units found — nothing to uninstall.'));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
execSync(`systemctl --user disable --now ${SYSTEMD_SERVICE_NAME}.path`, { stdio: 'pipe' });
|
|
140
|
+
}
|
|
141
|
+
catch { /* already inactive */ }
|
|
142
|
+
if (serviceExists)
|
|
143
|
+
await fs.remove(SYSTEMD_SERVICE_PATH);
|
|
144
|
+
if (pathExists)
|
|
145
|
+
await fs.remove(SYSTEMD_PATH_PATH);
|
|
146
|
+
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
|
|
147
|
+
p.log.success('Daemon uninstalled.');
|
|
148
|
+
}
|
|
149
|
+
export async function syncInstall() {
|
|
150
|
+
p.intro(color.bold('zedx sync install'));
|
|
151
|
+
const platform = process.platform;
|
|
152
|
+
if (platform !== 'darwin' && platform !== 'linux')
|
|
153
|
+
unsupportedPlatform();
|
|
154
|
+
const zedPaths = resolveZedPaths();
|
|
155
|
+
const watchPaths = [zedPaths.settings, zedPaths.extensions];
|
|
156
|
+
const zedxBin = resolveZedxBinary();
|
|
157
|
+
p.log.info(`Binary: ${color.dim(zedxBin)}`);
|
|
158
|
+
p.log.info(`Watching:`);
|
|
159
|
+
for (const wp of watchPaths) {
|
|
160
|
+
p.log.info(` ${color.dim(wp)}`);
|
|
161
|
+
}
|
|
162
|
+
if (platform === 'darwin') {
|
|
163
|
+
await installMacos(zedxBin, watchPaths);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
await installLinux(zedxBin, watchPaths);
|
|
167
|
+
}
|
|
168
|
+
p.outro(`${color.green('✓')} zedx sync will now run automatically whenever your Zed config changes.\n\n` +
|
|
169
|
+
` Run ${color.cyan('zedx sync uninstall')} to remove the daemon at any time.`);
|
|
170
|
+
}
|
|
171
|
+
export async function syncUninstall() {
|
|
172
|
+
p.intro(color.bold('zedx sync uninstall'));
|
|
173
|
+
const platform = process.platform;
|
|
174
|
+
if (platform !== 'darwin' && platform !== 'linux')
|
|
175
|
+
unsupportedPlatform();
|
|
176
|
+
if (platform === 'darwin') {
|
|
177
|
+
await uninstallMacos();
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
await uninstallLinux();
|
|
181
|
+
}
|
|
182
|
+
p.outro(`${color.green('✓')} Done.`);
|
|
183
|
+
}
|
package/dist/dev.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCheck(callerDir: string): Promise<void>;
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import color from 'picocolors';
|
|
5
|
+
// Minimal TOML key extraction — handles `key = "value"` and `key = ["a", "b"]`
|
|
6
|
+
function tomlGet(content, key) {
|
|
7
|
+
const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, 'm'));
|
|
8
|
+
return match?.[1];
|
|
9
|
+
}
|
|
10
|
+
function tomlHasUncommentedKey(content, key) {
|
|
11
|
+
return new RegExp(`^${key}\\s*=`, 'm').test(content);
|
|
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
|
+
export async function runCheck(callerDir) {
|
|
18
|
+
p.intro(`${color.bgBlue(color.bold(' zedx check '))} ${color.blue('Validating extension config…')}`);
|
|
19
|
+
const tomlPath = path.join(callerDir, 'extension.toml');
|
|
20
|
+
if (!(await fs.pathExists(tomlPath))) {
|
|
21
|
+
p.log.error(color.red('No extension.toml found in current directory.'));
|
|
22
|
+
p.log.info(`Run ${color.cyan('zedx')} to scaffold a new extension first.`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const tomlContent = await fs.readFile(tomlPath, 'utf-8');
|
|
26
|
+
const extensionId = tomlGet(tomlContent, 'id');
|
|
27
|
+
const extensionName = tomlGet(tomlContent, 'name');
|
|
28
|
+
const repository = tomlGet(tomlContent, 'repository');
|
|
29
|
+
const results = [];
|
|
30
|
+
// ── extension.toml ────────────────────────────────────────────────────────
|
|
31
|
+
const extIssues = [];
|
|
32
|
+
if (!extensionId) {
|
|
33
|
+
extIssues.push({
|
|
34
|
+
file: 'extension.toml',
|
|
35
|
+
message: 'Missing required field: id',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (!extensionName) {
|
|
39
|
+
extIssues.push({
|
|
40
|
+
file: 'extension.toml',
|
|
41
|
+
message: 'Missing required field: name',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (!repository || repository.includes('username')) {
|
|
45
|
+
extIssues.push({
|
|
46
|
+
file: 'extension.toml',
|
|
47
|
+
message: 'repository still uses the default placeholder URL',
|
|
48
|
+
hint: 'Set it to your actual GitHub repository URL',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// Detect language entries by looking for uncommented [grammars.*] sections
|
|
52
|
+
const grammarMatches = [...tomlContent.matchAll(/^\[grammars\.(\S+)\]/gm)];
|
|
53
|
+
const commentedGrammarMatches = [...tomlContent.matchAll(/^#\s*\[grammars\.(\S+)\]/gm)];
|
|
54
|
+
const languageIds = grammarMatches.map(m => m[1]);
|
|
55
|
+
const hasLanguage = languageIds.length > 0 || commentedGrammarMatches.length > 0;
|
|
56
|
+
if (commentedGrammarMatches.length > 0 && grammarMatches.length === 0) {
|
|
57
|
+
const ids = commentedGrammarMatches.map(m => m[1]);
|
|
58
|
+
extIssues.push({
|
|
59
|
+
file: 'extension.toml',
|
|
60
|
+
message: `Grammar section is commented out for: ${ids.join(', ')}`,
|
|
61
|
+
hint: 'Uncomment [grammars.<id>] and set a real tree-sitter repository URL and rev',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Detect theme entries by looking for themes/ directory
|
|
65
|
+
const themesDir = path.join(callerDir, 'themes');
|
|
66
|
+
const hasTheme = await fs.pathExists(themesDir);
|
|
67
|
+
results.push({ file: 'extension.toml', issues: extIssues });
|
|
68
|
+
// ── theme validation ──────────────────────────────────────────────────────
|
|
69
|
+
if (hasTheme) {
|
|
70
|
+
const themeIssues = [];
|
|
71
|
+
const themeFiles = (await fs.readdir(themesDir)).filter(f => f.endsWith('.json'));
|
|
72
|
+
if (themeFiles.length === 0) {
|
|
73
|
+
themeIssues.push({
|
|
74
|
+
file: 'themes/',
|
|
75
|
+
message: 'No .json theme files found in themes/ directory',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
for (const themeFile of themeFiles) {
|
|
79
|
+
const themePath = path.join(themesDir, themeFile);
|
|
80
|
+
const themeIssuesForFile = [];
|
|
81
|
+
let themeJson;
|
|
82
|
+
try {
|
|
83
|
+
themeJson = await fs.readJson(themePath);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
themeIssuesForFile.push({
|
|
87
|
+
file: `themes/${themeFile}`,
|
|
88
|
+
message: 'Invalid JSON — file could not be parsed',
|
|
89
|
+
});
|
|
90
|
+
results.push({ file: `themes/${themeFile}`, issues: themeIssuesForFile });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const themes = themeJson['themes'];
|
|
94
|
+
if (!themes || themes.length === 0) {
|
|
95
|
+
themeIssuesForFile.push({
|
|
96
|
+
file: `themes/${themeFile}`,
|
|
97
|
+
message: 'No theme variants found under the "themes" key',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
for (const variant of themes) {
|
|
102
|
+
const variantName = String(variant['name'] ?? 'unknown');
|
|
103
|
+
const style = variant['style'];
|
|
104
|
+
if (!style) {
|
|
105
|
+
themeIssuesForFile.push({
|
|
106
|
+
file: `themes/${themeFile}`,
|
|
107
|
+
message: `Variant "${variantName}": missing "style" block`,
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Check for placeholder-like neutral grays that indicate untouched scaffold
|
|
112
|
+
const background = style['background'];
|
|
113
|
+
const placeholderBgs = ['#1e1e1e', '#f5f5f5', '#ffffff', '#000000'];
|
|
114
|
+
if (background && placeholderBgs.includes(background.toLowerCase())) {
|
|
115
|
+
themeIssuesForFile.push({
|
|
116
|
+
file: `themes/${themeFile}`,
|
|
117
|
+
message: `Variant "${variantName}": background color is still the scaffold placeholder (${background})`,
|
|
118
|
+
hint: 'Replace with your actual theme colors',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// Check that syntax block is populated
|
|
122
|
+
const syntax = style['syntax'];
|
|
123
|
+
if (!syntax || Object.keys(syntax).length === 0) {
|
|
124
|
+
themeIssuesForFile.push({
|
|
125
|
+
file: `themes/${themeFile}`,
|
|
126
|
+
message: `Variant "${variantName}": "syntax" block is empty or missing`,
|
|
127
|
+
hint: 'Add syntax token color definitions',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
themeIssues.push(...themeIssuesForFile);
|
|
133
|
+
}
|
|
134
|
+
results.push({ file: 'themes/', issues: themeIssues });
|
|
135
|
+
}
|
|
136
|
+
// ── language validation ───────────────────────────────────────────────────
|
|
137
|
+
if (hasLanguage) {
|
|
138
|
+
// Collect all language IDs from both uncommented and commented grammar sections
|
|
139
|
+
const allLanguageIds = [
|
|
140
|
+
...grammarMatches.map(m => m[1]),
|
|
141
|
+
...commentedGrammarMatches.map(m => m[1]),
|
|
142
|
+
];
|
|
143
|
+
for (const langId of allLanguageIds) {
|
|
144
|
+
const langDir = path.join(callerDir, 'languages', langId);
|
|
145
|
+
const langIssues = [];
|
|
146
|
+
if (!(await fs.pathExists(langDir))) {
|
|
147
|
+
langIssues.push({
|
|
148
|
+
file: `languages/${langId}/`,
|
|
149
|
+
message: `Language directory does not exist`,
|
|
150
|
+
hint: `Expected at ${path.join('languages', langId)}`,
|
|
151
|
+
});
|
|
152
|
+
results.push({ file: `languages/${langId}/`, issues: langIssues });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
// config.toml checks
|
|
156
|
+
const configPath = path.join(langDir, 'config.toml');
|
|
157
|
+
const configIssues = [];
|
|
158
|
+
if (!(await fs.pathExists(configPath))) {
|
|
159
|
+
configIssues.push({
|
|
160
|
+
file: `languages/${langId}/config.toml`,
|
|
161
|
+
message: 'config.toml is missing',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
166
|
+
if (!tomlHasUncommentedKey(configContent, 'name')) {
|
|
167
|
+
configIssues.push({
|
|
168
|
+
file: `languages/${langId}/config.toml`,
|
|
169
|
+
message: 'Missing required field: name',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (!tomlHasUncommentedKey(configContent, 'grammar')) {
|
|
173
|
+
configIssues.push({
|
|
174
|
+
file: `languages/${langId}/config.toml`,
|
|
175
|
+
message: 'Missing required field: grammar',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// path_suffixes is commented out in scaffold — flag it
|
|
179
|
+
if (!tomlHasUncommentedKey(configContent, 'path_suffixes')) {
|
|
180
|
+
configIssues.push({
|
|
181
|
+
file: `languages/${langId}/config.toml`,
|
|
182
|
+
message: 'path_suffixes is not set — files won\'t be associated with this language',
|
|
183
|
+
hint: 'Uncomment and fill in path_suffixes (e.g., ["myl"])',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// line_comments is commented out in scaffold — flag it
|
|
187
|
+
if (!tomlHasUncommentedKey(configContent, 'line_comments')) {
|
|
188
|
+
configIssues.push({
|
|
189
|
+
file: `languages/${langId}/config.toml`,
|
|
190
|
+
message: 'line_comments is not set — toggle-comment keybind won\'t work',
|
|
191
|
+
hint: 'Uncomment and set line_comments (e.g., ["// "])',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
results.push({ file: `languages/${langId}/config.toml`, issues: configIssues });
|
|
196
|
+
// highlights.scm checks
|
|
197
|
+
const highlightsPath = path.join(langDir, 'highlights.scm');
|
|
198
|
+
const highlightIssues = [];
|
|
199
|
+
if (!(await fs.pathExists(highlightsPath))) {
|
|
200
|
+
highlightIssues.push({
|
|
201
|
+
file: `languages/${langId}/highlights.scm`,
|
|
202
|
+
message: 'highlights.scm is missing',
|
|
203
|
+
hint: 'Without it, no syntax highlighting will appear',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const highlightsContent = await fs.readFile(highlightsPath, 'utf-8');
|
|
208
|
+
// Count non-comment, non-empty lines with actual query patterns
|
|
209
|
+
const activeLines = highlightsContent
|
|
210
|
+
.split('\n')
|
|
211
|
+
.filter(l => l.trim() && !l.trim().startsWith(';'));
|
|
212
|
+
if (activeLines.length <= 3) {
|
|
213
|
+
highlightIssues.push({
|
|
214
|
+
file: `languages/${langId}/highlights.scm`,
|
|
215
|
+
message: 'Only scaffold starter patterns present — no real grammar queries added yet',
|
|
216
|
+
hint: 'Add tree-sitter queries matching your language\'s grammar node types',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
results.push({ file: `languages/${langId}/highlights.scm`, issues: highlightIssues });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// ── render results ────────────────────────────────────────────────────────
|
|
224
|
+
const allIssues = results.flatMap(r => r.issues);
|
|
225
|
+
const fileGroups = results.filter(r => r.issues.length > 0);
|
|
226
|
+
if (fileGroups.length === 0) {
|
|
227
|
+
p.log.success(color.green('No issues found. Your extension config looks good!'));
|
|
228
|
+
p.outro(`${color.dim('Load it in Zed:')} Extensions ${color.dim('>')} Install Dev Extension`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
for (const group of fileGroups) {
|
|
232
|
+
p.log.warn(`${color.yellow(color.bold(group.issues[0].file))}`);
|
|
233
|
+
for (const issue of group.issues) {
|
|
234
|
+
process.stdout.write(` ${color.red('✗')} ${issue.message}\n`);
|
|
235
|
+
if (issue.hint) {
|
|
236
|
+
process.stdout.write(` ${color.dim('→')} ${color.dim(issue.hint)}\n`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
process.stdout.write('\n');
|
|
240
|
+
}
|
|
241
|
+
const issueCount = allIssues.length;
|
|
242
|
+
p.outro(`${color.red(`${issueCount} issue${issueCount === 1 ? '' : 's'} found`)} — fix the above before publishing`);
|
|
243
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,10 @@ import { Command } from 'commander';
|
|
|
6
6
|
import fs from 'fs-extra';
|
|
7
7
|
import { promptUser, promptThemeDetails, promptLanguageDetails } from './prompts.js';
|
|
8
8
|
import { generateExtension } from './generator.js';
|
|
9
|
+
import { runCheck } from './check.js';
|
|
10
|
+
import { addTheme, addLanguage } from './add.js';
|
|
11
|
+
import { syncInit, runSync } from './sync.js';
|
|
12
|
+
import { syncInstall, syncUninstall } from './daemon.js';
|
|
9
13
|
function bumpVersion(version, type) {
|
|
10
14
|
const [major, minor, patch] = version.split('.').map(Number);
|
|
11
15
|
switch (type) {
|
|
@@ -53,6 +57,51 @@ async function main() {
|
|
|
53
57
|
}
|
|
54
58
|
await bumpExtensionVersion(type);
|
|
55
59
|
});
|
|
60
|
+
program
|
|
61
|
+
.command('check')
|
|
62
|
+
.description('Validate extension config and show what is missing or incomplete')
|
|
63
|
+
.action(async () => {
|
|
64
|
+
await runCheck(getCallerDir());
|
|
65
|
+
});
|
|
66
|
+
const addCmd = program
|
|
67
|
+
.command('add')
|
|
68
|
+
.description('Add a theme or language to an existing extension');
|
|
69
|
+
addCmd
|
|
70
|
+
.command('theme <name>')
|
|
71
|
+
.description('Add a new theme to the extension')
|
|
72
|
+
.action(async (name) => {
|
|
73
|
+
await addTheme(getCallerDir(), name);
|
|
74
|
+
});
|
|
75
|
+
addCmd
|
|
76
|
+
.command('language <id>')
|
|
77
|
+
.description('Add a new language to the extension')
|
|
78
|
+
.action(async (id) => {
|
|
79
|
+
await addLanguage(getCallerDir(), id);
|
|
80
|
+
});
|
|
81
|
+
const syncCmd = program
|
|
82
|
+
.command('sync')
|
|
83
|
+
.description('Sync Zed settings and extensions via a GitHub repo')
|
|
84
|
+
.action(async () => {
|
|
85
|
+
await runSync();
|
|
86
|
+
});
|
|
87
|
+
syncCmd
|
|
88
|
+
.command('init')
|
|
89
|
+
.description('Link a GitHub repo as the sync target')
|
|
90
|
+
.action(async () => {
|
|
91
|
+
await syncInit();
|
|
92
|
+
});
|
|
93
|
+
syncCmd
|
|
94
|
+
.command('install')
|
|
95
|
+
.description('Install the OS daemon to auto-sync when Zed config changes')
|
|
96
|
+
.action(async () => {
|
|
97
|
+
await syncInstall();
|
|
98
|
+
});
|
|
99
|
+
syncCmd
|
|
100
|
+
.command('uninstall')
|
|
101
|
+
.description('Remove the OS daemon')
|
|
102
|
+
.action(async () => {
|
|
103
|
+
await syncUninstall();
|
|
104
|
+
});
|
|
56
105
|
if (process.argv.length <= 2) {
|
|
57
106
|
const options = await promptUser();
|
|
58
107
|
if (options.types.includes('theme')) {
|
package/dist/sync.d.ts
ADDED
package/dist/sync.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import color from 'picocolors';
|
|
6
|
+
import simpleGit from 'simple-git';
|
|
7
|
+
import { resolveZedPaths } from './zed-paths.js';
|
|
8
|
+
const ZEDX_CONFIG_DIR = path.join(os.homedir(), '.config', 'zedx');
|
|
9
|
+
const ZEDX_CONFIG_PATH = path.join(ZEDX_CONFIG_DIR, 'config.json');
|
|
10
|
+
async function readSyncConfig() {
|
|
11
|
+
if (!(await fs.pathExists(ZEDX_CONFIG_PATH))) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return fs.readJson(ZEDX_CONFIG_PATH);
|
|
15
|
+
}
|
|
16
|
+
async function requireSyncConfig() {
|
|
17
|
+
const config = await readSyncConfig();
|
|
18
|
+
if (!config) {
|
|
19
|
+
p.log.error(color.red('No sync config found. Run ') + color.cyan('zedx sync init') + color.red(' first.'));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
return config;
|
|
23
|
+
}
|
|
24
|
+
async function writeSyncConfig(config) {
|
|
25
|
+
await fs.ensureDir(ZEDX_CONFIG_DIR);
|
|
26
|
+
await fs.writeJson(ZEDX_CONFIG_PATH, config, { spaces: 4 });
|
|
27
|
+
}
|
|
28
|
+
// Temp dir helper
|
|
29
|
+
async function withTempDir(fn) {
|
|
30
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'zedx-sync-'));
|
|
31
|
+
try {
|
|
32
|
+
return await fn(tmp);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await fs.remove(tmp);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Extension merge helper
|
|
39
|
+
async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPath, silent = false) {
|
|
40
|
+
// Backup existing settings
|
|
41
|
+
if (await fs.pathExists(localSettingsPath)) {
|
|
42
|
+
await fs.copy(localSettingsPath, localSettingsPath + '.bak', { overwrite: true });
|
|
43
|
+
if (!silent)
|
|
44
|
+
p.log.info(`Backed up settings to ${color.dim(localSettingsPath + '.bak')}`);
|
|
45
|
+
}
|
|
46
|
+
let settingsJson = await fs.readFile(repoSettings, 'utf-8');
|
|
47
|
+
// Merge auto_install_extensions from index.json into settings
|
|
48
|
+
if (await fs.pathExists(repoExtensions)) {
|
|
49
|
+
try {
|
|
50
|
+
const indexJson = (await fs.readJson(repoExtensions));
|
|
51
|
+
const extensionIds = Object.keys(indexJson.extensions ?? {}).filter((id) => !indexJson.extensions[id]?.dev);
|
|
52
|
+
if (extensionIds.length > 0) {
|
|
53
|
+
const stripped = settingsJson.replace(/\/\/[^\n]*/g, '');
|
|
54
|
+
let settingsObj = {};
|
|
55
|
+
try {
|
|
56
|
+
settingsObj = JSON.parse(stripped);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
if (!silent)
|
|
60
|
+
p.log.warn(color.yellow('Could not parse settings.json — skipping extension merge.'));
|
|
61
|
+
}
|
|
62
|
+
const autoInstall = {};
|
|
63
|
+
for (const id of extensionIds) {
|
|
64
|
+
autoInstall[id] = true;
|
|
65
|
+
}
|
|
66
|
+
settingsObj['auto_install_extensions'] = autoInstall;
|
|
67
|
+
settingsJson = JSON.stringify(settingsObj, null, 4);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
if (!silent)
|
|
72
|
+
p.log.warn(color.yellow('Could not parse extensions/index.json — skipping extension merge.'));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
await fs.ensureDir(path.dirname(localSettingsPath));
|
|
76
|
+
await fs.writeFile(localSettingsPath, settingsJson, 'utf-8');
|
|
77
|
+
}
|
|
78
|
+
// zedx sync init
|
|
79
|
+
export async function syncInit() {
|
|
80
|
+
p.intro(color.bold('zedx sync init'));
|
|
81
|
+
const repo = await p.text({
|
|
82
|
+
message: 'GitHub repo URL (SSH or HTTPS)',
|
|
83
|
+
placeholder: 'https://github.com/you/zed-config.git',
|
|
84
|
+
validate: (v) => {
|
|
85
|
+
if (!v.trim())
|
|
86
|
+
return 'Repo URL is required';
|
|
87
|
+
if (!v.startsWith('https://') && !v.startsWith('git@')) {
|
|
88
|
+
return 'Must be a valid HTTPS or SSH git URL';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
if (p.isCancel(repo)) {
|
|
93
|
+
p.cancel('Cancelled.');
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
const branch = await p.text({
|
|
97
|
+
message: 'Branch name',
|
|
98
|
+
placeholder: 'main',
|
|
99
|
+
defaultValue: 'main'
|
|
100
|
+
});
|
|
101
|
+
if (p.isCancel(branch)) {
|
|
102
|
+
p.cancel('Cancelled.');
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
const spinner = p.spinner();
|
|
106
|
+
spinner.start('Verifying repo is reachable...');
|
|
107
|
+
try {
|
|
108
|
+
const git = simpleGit();
|
|
109
|
+
await git.listRemote(['--heads', repo]);
|
|
110
|
+
spinner.stop('Repo verified.');
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
spinner.stop(color.yellow('Could not verify repo (may be empty or private — continuing anyway).'));
|
|
114
|
+
}
|
|
115
|
+
const config = {
|
|
116
|
+
syncRepo: repo.trim(),
|
|
117
|
+
branch: (branch || 'main').trim()
|
|
118
|
+
};
|
|
119
|
+
await writeSyncConfig(config);
|
|
120
|
+
p.outro(`${color.green('✓')} Sync config saved to ${color.cyan(ZEDX_CONFIG_PATH)}\n\n` +
|
|
121
|
+
` Run ${color.cyan('zedx sync')} to sync your Zed config.`);
|
|
122
|
+
}
|
|
123
|
+
// zedx sync
|
|
124
|
+
export async function runSync(opts = {}) {
|
|
125
|
+
const { silent = false } = opts;
|
|
126
|
+
// In silent mode (daemon/watch), route all UI through plain console.log
|
|
127
|
+
// Interactive conflict prompts fall back to "local wins".
|
|
128
|
+
const log = {
|
|
129
|
+
info: (msg) => {
|
|
130
|
+
if (!silent)
|
|
131
|
+
p.log.info(msg);
|
|
132
|
+
},
|
|
133
|
+
warn: (msg) => {
|
|
134
|
+
if (!silent)
|
|
135
|
+
p.log.warn(msg);
|
|
136
|
+
else
|
|
137
|
+
console.error(`[zedx] warn: ${msg}`);
|
|
138
|
+
},
|
|
139
|
+
success: (msg) => {
|
|
140
|
+
if (!silent)
|
|
141
|
+
p.log.success(msg);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
if (!silent)
|
|
145
|
+
p.intro(color.bold('zedx sync'));
|
|
146
|
+
const config = await requireSyncConfig();
|
|
147
|
+
const zedPaths = resolveZedPaths();
|
|
148
|
+
// Spinner shim: in silent mode just log to stderr so daemons can capture it
|
|
149
|
+
const spinner = silent
|
|
150
|
+
? {
|
|
151
|
+
start: (m) => console.error(`[zedx] ${m}`),
|
|
152
|
+
stop: (m) => console.error(`[zedx] ${m}`)
|
|
153
|
+
}
|
|
154
|
+
: p.spinner();
|
|
155
|
+
await withTempDir(async (tmp) => {
|
|
156
|
+
// 1. Clone the remote repo
|
|
157
|
+
const git = simpleGit(tmp);
|
|
158
|
+
let remoteExists = true;
|
|
159
|
+
spinner.start(`Fetching ${config.syncRepo}...`);
|
|
160
|
+
try {
|
|
161
|
+
await git.clone(config.syncRepo, tmp, ['--depth', '1', '--branch', config.branch]);
|
|
162
|
+
spinner.stop('Remote fetched.');
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
remoteExists = false;
|
|
166
|
+
spinner.stop('Remote is empty or branch not found — will push fresh.');
|
|
167
|
+
await git.init();
|
|
168
|
+
await git.addRemote('origin', config.syncRepo);
|
|
169
|
+
}
|
|
170
|
+
// 2. Determine what changed for each file
|
|
171
|
+
const lastSync = config.lastSync ? new Date(config.lastSync) : null;
|
|
172
|
+
const files = [
|
|
173
|
+
{
|
|
174
|
+
repoPath: path.join(tmp, 'settings.json'),
|
|
175
|
+
localPath: zedPaths.settings,
|
|
176
|
+
label: 'settings.json'
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
repoPath: path.join(tmp, 'extensions', 'index.json'),
|
|
180
|
+
localPath: zedPaths.extensions,
|
|
181
|
+
label: 'extensions/index.json'
|
|
182
|
+
}
|
|
183
|
+
];
|
|
184
|
+
let anyChanges = false;
|
|
185
|
+
for (const file of files) {
|
|
186
|
+
const localExists = await fs.pathExists(file.localPath);
|
|
187
|
+
const remoteFileExists = remoteExists && (await fs.pathExists(file.repoPath));
|
|
188
|
+
// Both missing — skip
|
|
189
|
+
if (!localExists && !remoteFileExists) {
|
|
190
|
+
log.warn(`${file.label}: not found locally or remotely — skipping.`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Remote doesn't have it yet — push local
|
|
194
|
+
if (localExists && !remoteFileExists) {
|
|
195
|
+
log.info(`${file.label}: ${color.green('pushing')} (not in remote yet)`);
|
|
196
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
197
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
198
|
+
anyChanges = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
// Local doesn't have it — pull remote
|
|
202
|
+
if (!localExists && remoteFileExists) {
|
|
203
|
+
log.info(`${file.label}: ${color.cyan('pulling')} (not found locally)`);
|
|
204
|
+
if (file.label === 'settings.json') {
|
|
205
|
+
await applyRemoteSettings(file.repoPath, path.join(tmp, 'extensions', 'index.json'), file.localPath, silent);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
await fs.ensureDir(path.dirname(file.localPath));
|
|
209
|
+
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
// Both exist — compare content
|
|
214
|
+
const localContent = await fs.readFile(file.localPath, 'utf-8');
|
|
215
|
+
const remoteContent = await fs.readFile(file.repoPath, 'utf-8');
|
|
216
|
+
if (localContent === remoteContent) {
|
|
217
|
+
log.success(`${file.label}: ${color.dim('already in sync')}`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
// Detect which side changed since last sync via mtime
|
|
221
|
+
const localMtime = (await fs.stat(file.localPath)).mtime;
|
|
222
|
+
const remoteMtime = remoteFileExists ? (await fs.stat(file.repoPath)).mtime : new Date(0);
|
|
223
|
+
const localChanged = !lastSync || localMtime > lastSync;
|
|
224
|
+
const remoteChanged = !lastSync || remoteMtime > lastSync;
|
|
225
|
+
if (localChanged && !remoteChanged) {
|
|
226
|
+
// Only local changed → push
|
|
227
|
+
log.info(`${file.label}: ${color.green('pushing')} (local is newer)`);
|
|
228
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
229
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
230
|
+
anyChanges = true;
|
|
231
|
+
}
|
|
232
|
+
else if (remoteChanged && !localChanged) {
|
|
233
|
+
// Only remote changed → pull
|
|
234
|
+
log.info(`${file.label}: ${color.cyan('pulling')} (remote is newer)`);
|
|
235
|
+
if (file.label === 'settings.json') {
|
|
236
|
+
await applyRemoteSettings(file.repoPath, path.join(tmp, 'extensions', 'index.json'), file.localPath, silent);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
await fs.ensureDir(path.dirname(file.localPath));
|
|
240
|
+
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// Both changed
|
|
245
|
+
if (silent) {
|
|
246
|
+
// Daemon can't prompt — local wins, will be pushed
|
|
247
|
+
log.warn(`${file.label}: conflict detected in unattended mode — keeping local.`);
|
|
248
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
249
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
250
|
+
anyChanges = true;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
p.log.warn(color.yellow(`${file.label}: both local and remote changed.`));
|
|
254
|
+
const choice = await p.select({
|
|
255
|
+
message: `Which version of ${color.bold(file.label)} should win?`,
|
|
256
|
+
options: [
|
|
257
|
+
{
|
|
258
|
+
value: 'local',
|
|
259
|
+
label: 'Keep local',
|
|
260
|
+
hint: `modified ${localMtime.toLocaleString()}`
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
value: 'remote',
|
|
264
|
+
label: 'Use remote',
|
|
265
|
+
hint: `modified ${remoteMtime.toLocaleString()}`
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
});
|
|
269
|
+
if (p.isCancel(choice)) {
|
|
270
|
+
p.cancel('Cancelled.');
|
|
271
|
+
process.exit(0);
|
|
272
|
+
}
|
|
273
|
+
if (choice === 'local') {
|
|
274
|
+
p.log.info(`${file.label}: ${color.green('keeping local, will push')}`);
|
|
275
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
276
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
277
|
+
anyChanges = true;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
p.log.info(`${file.label}: ${color.cyan('applying remote')}`);
|
|
281
|
+
if (file.label === 'settings.json') {
|
|
282
|
+
await applyRemoteSettings(file.repoPath, path.join(tmp, 'extensions', 'index.json'), file.localPath, silent);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
await fs.ensureDir(path.dirname(file.localPath));
|
|
286
|
+
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// 3. Commit + push if any local files were written to the repo
|
|
293
|
+
if (anyChanges) {
|
|
294
|
+
spinner.start('Pushing changes to remote...');
|
|
295
|
+
await git.add(['settings.json', path.join('extensions', 'index.json')]);
|
|
296
|
+
const status = await git.status();
|
|
297
|
+
if (status.staged.length > 0) {
|
|
298
|
+
const timestamp = new Date().toISOString();
|
|
299
|
+
await git.commit(`sync: ${timestamp}`);
|
|
300
|
+
try {
|
|
301
|
+
await git.push('origin', config.branch, ['--set-upstream']);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
await git.push('origin', config.branch);
|
|
305
|
+
}
|
|
306
|
+
spinner.stop('Pushed.');
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
spinner.stop('Nothing staged to push.');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
// 4. Save last sync timestamp
|
|
314
|
+
await writeSyncConfig({ ...config, lastSync: new Date().toISOString() });
|
|
315
|
+
if (!silent)
|
|
316
|
+
p.outro(`${color.green('✓')} Sync complete.`);
|
|
317
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export type ExtensionType = 'theme' | 'language';
|
|
2
|
+
export interface ZedPaths {
|
|
3
|
+
settings: string;
|
|
4
|
+
extensions: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SyncConfig {
|
|
7
|
+
syncRepo: string;
|
|
8
|
+
branch: string;
|
|
9
|
+
}
|
|
2
10
|
export type License = 'Apache-2.0' | 'BSD-2-Clause' | 'BSD-3-Clause' | 'GPL-3.0' | 'LGPL-3.0' | 'MIT' | 'Zlib';
|
|
3
11
|
export interface ExtensionOptions {
|
|
4
12
|
name: string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export function resolveZedPaths() {
|
|
4
|
+
const home = os.homedir();
|
|
5
|
+
const platform = process.platform;
|
|
6
|
+
if (platform === 'darwin') {
|
|
7
|
+
return {
|
|
8
|
+
settings: path.join(home, '.config', 'zed', 'settings.json'),
|
|
9
|
+
extensions: path.join(home, 'Library', 'Application Support', 'Zed', 'extensions', 'index.json'),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (platform === 'linux') {
|
|
13
|
+
const xdgData = process.env.FLATPAK_XDG_DATA_HOME ||
|
|
14
|
+
process.env.XDG_DATA_HOME ||
|
|
15
|
+
path.join(home, '.local', 'share');
|
|
16
|
+
return {
|
|
17
|
+
settings: path.join(home, '.config', 'zed', 'settings.json'),
|
|
18
|
+
extensions: path.join(xdgData, 'zed', 'extensions', 'index.json'),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (platform === 'win32') {
|
|
22
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
23
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
24
|
+
return {
|
|
25
|
+
settings: path.join(appData, 'Zed', 'settings.json'),
|
|
26
|
+
extensions: path.join(localAppData, 'Zed', 'extensions', 'index.json'),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zedx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Boilerplate generator for Zed Edittor extensions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"commander": "^14.0.3",
|
|
48
48
|
"ejs": "^4.0.1",
|
|
49
49
|
"fs-extra": "^11.3.3",
|
|
50
|
-
"picocolors": "^1.1.1"
|
|
50
|
+
"picocolors": "^1.1.1",
|
|
51
|
+
"simple-git": "^3.33.0"
|
|
51
52
|
},
|
|
52
53
|
"packageManager": "pnpm@10.30.0+sha512.2b5753de015d480eeb88f5b5b61e0051f05b4301808a82ec8b840c9d2adf7748eb352c83f5c1593ca703ff1017295bc3fdd3119abb9686efc96b9fcb18200937"
|
|
53
54
|
}
|