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 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 automatically
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.').helpOption(false);
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
- .action(async () => {
160
- await runSync();
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
- if (process.argv.length <= 2) {
207
+ const argv = process.argv.filter(arg => arg !== '--');
208
+ if (argv.length <= 2) {
187
209
  printWelcome();
188
210
  return;
189
211
  }
190
- program.parse(process.argv);
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
- p.intro(`${color.bgBlue(color.bold(' zedx '))} ${color.blue('Boilerplate generator for Zed Editor extensions.')}`);
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 files = [
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
- if (silent) {
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
- if (choice === 'local') {
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
- 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
- }
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
- if (file.label === 'settings.json') {
413
- await applyRemoteSettings(file.repoPath, path.join(tmp, 'extensions', 'index.json'), file.localPath, silent);
414
- }
415
- else {
416
- await fs.ensureDir(path.dirname(file.localPath));
417
- await fs.copy(file.repoPath, file.localPath, { overwrite: true });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zedx",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Scaffold Zed Editor extensions and sync your settings across machines.",
5
5
  "keywords": [
6
6
  "boilerplate",
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
- }