zedx 0.8.0 → 0.9.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 +4 -2
- package/dist/add.js +2 -0
- package/dist/check.js +1 -0
- package/dist/daemon.js +2 -0
- package/dist/index.js +29 -7
- package/dist/install.js +1 -0
- package/dist/prompts.js +2 -1
- package/dist/snippet.js +2 -1
- package/dist/sync.d.ts +4 -0
- package/dist/sync.js +72 -29
- package/package.json +1 -1
- package/dist/dev.d.ts +0 -1
- package/dist/dev.js +0 -243
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ brew install tahayvr/tap/zedx
|
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
# Create a new extension
|
|
20
|
-
zedx
|
|
20
|
+
zedx create
|
|
21
21
|
|
|
22
22
|
# Add a theme or language to an existing extension
|
|
23
23
|
zedx add theme "Midnight Blue"
|
|
@@ -33,7 +33,9 @@ zedx version major # 1.2.3 → 2.0.0
|
|
|
33
33
|
|
|
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
|
-
zedx sync # Sync local and remote config
|
|
36
|
+
zedx sync # Sync local and remote config (prompts on conflict)
|
|
37
|
+
zedx sync --local # Sync, always keeping local on conflict
|
|
38
|
+
zedx sync --remote # Sync, always using remote on conflict
|
|
37
39
|
zedx sync status # Show sync state between local config and the remote repo
|
|
38
40
|
zedx sync install # Install an OS daemon to auto-sync when Zed config changes
|
|
39
41
|
zedx sync uninstall # Remove the OS daemon
|
package/dist/add.js
CHANGED
|
@@ -24,6 +24,7 @@ function slugify(name) {
|
|
|
24
24
|
.replace(/[^a-z0-9-]/g, '');
|
|
25
25
|
}
|
|
26
26
|
export async function addTheme(callerDir, themeName) {
|
|
27
|
+
console.log('');
|
|
27
28
|
p.intro(`${color.bgBlue(color.bold(' zedx add theme '))} ${color.blue('Adding a theme to your extension…')}`);
|
|
28
29
|
const tomlPath = path.join(callerDir, 'extension.toml');
|
|
29
30
|
if (!(await fs.pathExists(tomlPath))) {
|
|
@@ -70,6 +71,7 @@ export async function addTheme(callerDir, themeName) {
|
|
|
70
71
|
`${color.dim('Run')} ${color.cyan('zedx check')} ${color.dim('to validate your extension.')}`);
|
|
71
72
|
}
|
|
72
73
|
export async function addLanguage(callerDir, languageId) {
|
|
74
|
+
console.log('');
|
|
73
75
|
p.intro(`${color.bgBlue(color.bold(' zedx add language '))} ${color.blue('Adding a language to your extension…')}`);
|
|
74
76
|
const tomlPath = path.join(callerDir, 'extension.toml');
|
|
75
77
|
if (!(await fs.pathExists(tomlPath))) {
|
package/dist/check.js
CHANGED
|
@@ -11,6 +11,7 @@ function tomlHasUncommentedKey(content, key) {
|
|
|
11
11
|
return new RegExp(`^${key}\\s*=`, 'm').test(content);
|
|
12
12
|
}
|
|
13
13
|
export async function runCheck(callerDir) {
|
|
14
|
+
console.log('');
|
|
14
15
|
p.intro(`${color.bgBlue(color.bold(' zedx check '))} ${color.blue('Validating extension config…')}`);
|
|
15
16
|
const tomlPath = path.join(callerDir, 'extension.toml');
|
|
16
17
|
if (!(await fs.pathExists(tomlPath))) {
|
package/dist/daemon.js
CHANGED
|
@@ -151,6 +151,7 @@ async function uninstallLinux() {
|
|
|
151
151
|
p.log.success('Daemon uninstalled.');
|
|
152
152
|
}
|
|
153
153
|
export async function syncInstall() {
|
|
154
|
+
console.log('');
|
|
154
155
|
p.intro(color.bold('zedx sync install'));
|
|
155
156
|
const platform = process.platform;
|
|
156
157
|
if (platform !== 'darwin' && platform !== 'linux')
|
|
@@ -173,6 +174,7 @@ export async function syncInstall() {
|
|
|
173
174
|
` Run ${color.cyan('zedx sync uninstall')} to remove the daemon at any time.`);
|
|
174
175
|
}
|
|
175
176
|
export async function syncUninstall() {
|
|
177
|
+
console.log('');
|
|
176
178
|
p.intro(color.bold('zedx sync uninstall'));
|
|
177
179
|
const platform = process.platform;
|
|
178
180
|
if (platform !== 'darwin' && platform !== 'linux')
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'module';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import * as p from '@clack/prompts';
|
|
4
5
|
import { Command } from 'commander';
|
|
@@ -11,7 +12,9 @@ import { generateExtension } from './generator.js';
|
|
|
11
12
|
import { installDevExtension } from './install.js';
|
|
12
13
|
import { promptUser, promptThemeDetails, promptLanguageDetails } from './prompts.js';
|
|
13
14
|
import { addLsp } from './snippet.js';
|
|
14
|
-
import { syncInit, runSync, syncStatus } from './sync.js';
|
|
15
|
+
import { syncInit, runSync, syncStatus, syncSelect } from './sync.js';
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const { version } = require('../package.json');
|
|
15
18
|
function bumpVersion(version, type) {
|
|
16
19
|
const [major, minor, patch] = version.split('.').map(Number);
|
|
17
20
|
switch (type) {
|
|
@@ -65,6 +68,7 @@ function printWelcome() {
|
|
|
65
68
|
['zedx install', 'Install as a Zed dev extension'],
|
|
66
69
|
['zedx version <major|minor|patch>', 'Bump extension version'],
|
|
67
70
|
['zedx sync', 'Sync Zed settings via a git repo'],
|
|
71
|
+
['zedx sync select', 'Choose which files to sync interactively'],
|
|
68
72
|
['zedx sync init', 'Link a git repo as the sync target'],
|
|
69
73
|
['zedx sync status', 'Show sync state between local and remote'],
|
|
70
74
|
['zedx sync install', 'Install the OS daemon for auto-sync'],
|
|
@@ -74,7 +78,7 @@ function printWelcome() {
|
|
|
74
78
|
for (const [cmd, desc] of commands) {
|
|
75
79
|
console.log(` ${color.cyan(cmd.padEnd(38))}${color.dim(desc)}`);
|
|
76
80
|
}
|
|
77
|
-
console.log(`\n ${color.dim('Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions'))}\n`);
|
|
81
|
+
console.log(`\n ${color.dim('Zed Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions'))}\n`);
|
|
78
82
|
}
|
|
79
83
|
async function runCreate() {
|
|
80
84
|
const options = await promptUser();
|
|
@@ -99,7 +103,7 @@ async function runCreate() {
|
|
|
99
103
|
}
|
|
100
104
|
async function main() {
|
|
101
105
|
const program = new Command();
|
|
102
|
-
program.name('zedx').description('The CLI toolkit for Zed Editor.').
|
|
106
|
+
program.name('zedx').description('The CLI toolkit for Zed Editor.').version(`zedx v${version}`);
|
|
103
107
|
program
|
|
104
108
|
.command('create')
|
|
105
109
|
.description('Scaffold a new Zed extension')
|
|
@@ -156,8 +160,19 @@ async function main() {
|
|
|
156
160
|
const syncCmd = program
|
|
157
161
|
.command('sync')
|
|
158
162
|
.description('Sync Zed settings and extensions via a GitHub repo')
|
|
159
|
-
.
|
|
160
|
-
|
|
163
|
+
.option('--local', 'On conflict, always keep the local version')
|
|
164
|
+
.option('--remote', 'On conflict, always use the remote version')
|
|
165
|
+
.action(async (opts) => {
|
|
166
|
+
if (opts.local && opts.remote) {
|
|
167
|
+
p.log.error(color.red('--local and --remote are mutually exclusive.'));
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
const conflict = opts.local
|
|
171
|
+
? 'local'
|
|
172
|
+
: opts.remote
|
|
173
|
+
? 'remote'
|
|
174
|
+
: 'prompt';
|
|
175
|
+
await runSync({ conflict });
|
|
161
176
|
});
|
|
162
177
|
syncCmd
|
|
163
178
|
.command('init')
|
|
@@ -171,6 +186,12 @@ async function main() {
|
|
|
171
186
|
.action(async () => {
|
|
172
187
|
await syncStatus();
|
|
173
188
|
});
|
|
189
|
+
syncCmd
|
|
190
|
+
.command('select')
|
|
191
|
+
.description('Interactively choose which files to sync')
|
|
192
|
+
.action(async () => {
|
|
193
|
+
await syncSelect();
|
|
194
|
+
});
|
|
174
195
|
syncCmd
|
|
175
196
|
.command('install')
|
|
176
197
|
.description('Install the OS daemon to auto-sync when Zed config changes')
|
|
@@ -183,10 +204,11 @@ async function main() {
|
|
|
183
204
|
.action(async () => {
|
|
184
205
|
await syncUninstall();
|
|
185
206
|
});
|
|
186
|
-
|
|
207
|
+
const argv = process.argv.filter(arg => arg !== '--');
|
|
208
|
+
if (argv.length <= 2) {
|
|
187
209
|
printWelcome();
|
|
188
210
|
return;
|
|
189
211
|
}
|
|
190
|
-
program.parse(
|
|
212
|
+
program.parse(argv);
|
|
191
213
|
}
|
|
192
214
|
main().catch(console.error);
|
package/dist/install.js
CHANGED
|
@@ -112,6 +112,7 @@ function buildManifest(extensionDir, toml) {
|
|
|
112
112
|
}
|
|
113
113
|
// Main install function
|
|
114
114
|
export async function installDevExtension(callerDir) {
|
|
115
|
+
console.log('');
|
|
115
116
|
p.intro(`${color.bgBlue(color.bold(' zedx install '))} ${color.blue('Installing as a Zed dev extension…')}`);
|
|
116
117
|
const tomlPath = path.join(callerDir, 'extension.toml');
|
|
117
118
|
if (!(await fs.pathExists(tomlPath))) {
|
package/dist/prompts.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
2
|
import color from 'picocolors';
|
|
3
3
|
export async function promptUser() {
|
|
4
|
-
|
|
4
|
+
console.log('');
|
|
5
|
+
p.intro(`${color.bgBlue(color.bold(' zedx create '))} ${color.blue('Boilerplate generator for Zed Editor extensions.')}`);
|
|
5
6
|
const nameDefault = 'my-zed-extension';
|
|
6
7
|
const name = await p.text({
|
|
7
8
|
message: 'Project name:',
|
package/dist/snippet.js
CHANGED
|
@@ -37,6 +37,7 @@ function detectLanguages(callerDir) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
export async function addLsp(callerDir) {
|
|
40
|
+
console.log('');
|
|
40
41
|
p.intro(`${color.bgBlue(color.bold(' zedx snippet add lsp '))} ${color.blue('Wiring up a language server…')}`);
|
|
41
42
|
const tomlPath = path.join(callerDir, 'extension.toml');
|
|
42
43
|
if (!(await fs.pathExists(tomlPath))) {
|
|
@@ -164,5 +165,5 @@ export async function addLsp(callerDir) {
|
|
|
164
165
|
p.outro(`${color.green('✓')} LSP snippet added.\n\n` +
|
|
165
166
|
` ${color.dim('1.')} Edit ${color.cyan('src/lib.rs')} — implement ${color.white('language_server_command')}\n` +
|
|
166
167
|
` ${color.dim('2.')} Edit ${color.cyan('Cargo.toml')} — pin ${color.white('zed_extension_api')} to latest version\n` +
|
|
167
|
-
` ${color.dim('3.')} ${color.dim('Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions/languages#language-servers'))}`);
|
|
168
|
+
` ${color.dim('3.')} ${color.dim('Zed Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions/languages#language-servers'))}`);
|
|
168
169
|
}
|
package/dist/sync.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export declare function syncStatus(): Promise<void>;
|
|
2
2
|
export declare function syncInit(): Promise<void>;
|
|
3
|
+
export type ConflictStrategy = 'local' | 'remote' | 'prompt';
|
|
4
|
+
export declare function syncSelect(): Promise<void>;
|
|
3
5
|
export declare function runSync(opts?: {
|
|
4
6
|
silent?: boolean;
|
|
7
|
+
conflict?: ConflictStrategy;
|
|
8
|
+
selectedFiles?: string[];
|
|
5
9
|
}): Promise<void>;
|
package/dist/sync.js
CHANGED
|
@@ -110,6 +110,7 @@ async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPa
|
|
|
110
110
|
}
|
|
111
111
|
// zedx sync status
|
|
112
112
|
export async function syncStatus() {
|
|
113
|
+
console.log('');
|
|
113
114
|
p.intro(`${color.bgBlue(color.bold(' zedx sync status '))} ${color.blue('Checking sync state…')}`);
|
|
114
115
|
const config = await requireSyncConfig();
|
|
115
116
|
const zedPaths = resolveZedPaths();
|
|
@@ -186,6 +187,7 @@ export async function syncStatus() {
|
|
|
186
187
|
}
|
|
187
188
|
// zedx sync init
|
|
188
189
|
export async function syncInit() {
|
|
190
|
+
console.log('');
|
|
189
191
|
p.intro(`${color.bgBlue(color.bold(' zedx sync init '))} ${color.blue('Linking a git repo as the sync target…')}`);
|
|
190
192
|
const repo = await p.text({
|
|
191
193
|
message: 'GitHub repo URL (SSH or HTTPS)',
|
|
@@ -229,9 +231,38 @@ export async function syncInit() {
|
|
|
229
231
|
p.outro(`${color.green('✓')} Sync config saved to ${color.cyan(ZEDX_CONFIG_PATH)}\n\n` +
|
|
230
232
|
` Run ${color.cyan('zedx sync')} to sync your Zed config.`);
|
|
231
233
|
}
|
|
234
|
+
// zedx sync select
|
|
235
|
+
export async function syncSelect() {
|
|
236
|
+
console.log('');
|
|
237
|
+
p.intro(`${color.bgBlue(color.bold(' zedx sync select '))} ${color.blue('Choose which files to sync…')}`);
|
|
238
|
+
await requireSyncConfig();
|
|
239
|
+
const allFiles = [
|
|
240
|
+
{
|
|
241
|
+
value: 'settings',
|
|
242
|
+
label: 'settings.json',
|
|
243
|
+
hint: 'Zed editor settings',
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
value: 'extensions',
|
|
247
|
+
label: 'extensions/index.json',
|
|
248
|
+
hint: 'Installed extensions list',
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
const selected = await p.multiselect({
|
|
252
|
+
message: 'Select files to sync',
|
|
253
|
+
options: allFiles,
|
|
254
|
+
required: true,
|
|
255
|
+
});
|
|
256
|
+
if (p.isCancel(selected)) {
|
|
257
|
+
p.cancel('Cancelled.');
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
const selectedFiles = selected;
|
|
261
|
+
await runSync({ selectedFiles });
|
|
262
|
+
}
|
|
232
263
|
// zedx sync
|
|
233
264
|
export async function runSync(opts = {}) {
|
|
234
|
-
const { silent = false } = opts;
|
|
265
|
+
const { silent = false, conflict = 'prompt', selectedFiles } = opts;
|
|
235
266
|
// In silent mode (daemon/watch), route all UI through plain console.log
|
|
236
267
|
// Interactive conflict prompts fall back to "local wins".
|
|
237
268
|
const log = {
|
|
@@ -250,8 +281,10 @@ export async function runSync(opts = {}) {
|
|
|
250
281
|
p.log.success(msg);
|
|
251
282
|
},
|
|
252
283
|
};
|
|
253
|
-
if (!silent)
|
|
284
|
+
if (!silent) {
|
|
285
|
+
console.log('');
|
|
254
286
|
p.intro(`${color.bgBlue(color.bold(' zedx sync '))} ${color.blue('Syncing Zed settings and extensions…')}`);
|
|
287
|
+
}
|
|
255
288
|
const config = await requireSyncConfig();
|
|
256
289
|
const zedPaths = resolveZedPaths();
|
|
257
290
|
// Spinner shim: in silent mode just log to stderr so daemons can capture it
|
|
@@ -278,18 +311,23 @@ export async function runSync(opts = {}) {
|
|
|
278
311
|
}
|
|
279
312
|
// 2. Determine what changed for each file
|
|
280
313
|
const lastSync = config.lastSync ? new Date(config.lastSync) : null;
|
|
281
|
-
const
|
|
314
|
+
const allFiles = [
|
|
282
315
|
{
|
|
316
|
+
key: 'settings',
|
|
283
317
|
repoPath: path.join(tmp, 'settings.json'),
|
|
284
318
|
localPath: zedPaths.settings,
|
|
285
319
|
label: 'settings.json',
|
|
286
320
|
},
|
|
287
321
|
{
|
|
322
|
+
key: 'extensions',
|
|
288
323
|
repoPath: path.join(tmp, 'extensions', 'index.json'),
|
|
289
324
|
localPath: zedPaths.extensions,
|
|
290
325
|
label: 'extensions/index.json',
|
|
291
326
|
},
|
|
292
327
|
];
|
|
328
|
+
const files = selectedFiles
|
|
329
|
+
? allFiles.filter(f => selectedFiles.includes(f.key))
|
|
330
|
+
: allFiles;
|
|
293
331
|
let anyChanges = false;
|
|
294
332
|
for (const file of files) {
|
|
295
333
|
const localExists = await fs.pathExists(file.localPath);
|
|
@@ -362,18 +400,20 @@ export async function runSync(opts = {}) {
|
|
|
362
400
|
}
|
|
363
401
|
}
|
|
364
402
|
else {
|
|
365
|
-
// Both changed
|
|
366
|
-
|
|
403
|
+
// Both changed — resolve based on strategy
|
|
404
|
+
// Determine the effective resolution:
|
|
405
|
+
// - explicit --local / --remote flag always wins
|
|
406
|
+
// - silent (daemon) mode falls back to local
|
|
407
|
+
// - otherwise prompt interactively
|
|
408
|
+
let resolution;
|
|
409
|
+
if (conflict === 'local' || conflict === 'remote') {
|
|
410
|
+
resolution = conflict;
|
|
411
|
+
log.warn(`${file.label}: conflict — using ${color.bold(resolution)} (--${resolution} flag).`);
|
|
412
|
+
}
|
|
413
|
+
else if (silent) {
|
|
367
414
|
// Daemon can't prompt — local wins, will be pushed
|
|
415
|
+
resolution = 'local';
|
|
368
416
|
log.warn(`${file.label}: conflict detected in unattended mode — keeping local.`);
|
|
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
|
-
}
|
|
376
|
-
anyChanges = true;
|
|
377
417
|
}
|
|
378
418
|
else {
|
|
379
419
|
p.log.warn(color.yellow(`conflict between local and remote ${file.label}`));
|
|
@@ -396,26 +436,29 @@ export async function runSync(opts = {}) {
|
|
|
396
436
|
p.cancel('Cancelled.');
|
|
397
437
|
process.exit(0);
|
|
398
438
|
}
|
|
399
|
-
|
|
439
|
+
resolution = choice;
|
|
440
|
+
}
|
|
441
|
+
if (resolution === 'local') {
|
|
442
|
+
if (!silent && conflict === 'prompt')
|
|
400
443
|
p.log.info(`${file.label}: ${color.green('keeping local, will push')}`);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
else {
|
|
405
|
-
await fs.ensureDir(path.dirname(file.repoPath));
|
|
406
|
-
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
407
|
-
}
|
|
408
|
-
anyChanges = true;
|
|
444
|
+
if (file.label === 'settings.json') {
|
|
445
|
+
await prepareSettingsForPush(file.localPath, file.repoPath);
|
|
409
446
|
}
|
|
410
447
|
else {
|
|
448
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
449
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
450
|
+
}
|
|
451
|
+
anyChanges = true;
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
if (!silent && conflict === 'prompt')
|
|
411
455
|
p.log.info(`${file.label}: ${color.cyan('applying remote')}`);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
456
|
+
if (file.label === 'settings.json') {
|
|
457
|
+
await applyRemoteSettings(file.repoPath, path.join(tmp, 'extensions', 'index.json'), file.localPath, silent);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
await fs.ensureDir(path.dirname(file.localPath));
|
|
461
|
+
await fs.copy(file.repoPath, file.localPath, { overwrite: true });
|
|
419
462
|
}
|
|
420
463
|
}
|
|
421
464
|
}
|
package/package.json
CHANGED
package/dist/dev.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function runCheck(callerDir: string): Promise<void>;
|
package/dist/dev.js
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
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
|
-
}
|