zedx 0.7.0 → 0.8.1

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"
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
@@ -8,7 +8,9 @@ import { addTheme, addLanguage } from './add.js';
8
8
  import { runCheck } from './check.js';
9
9
  import { syncInstall, syncUninstall } from './daemon.js';
10
10
  import { generateExtension } from './generator.js';
11
+ import { installDevExtension } from './install.js';
11
12
  import { promptUser, promptThemeDetails, promptLanguageDetails } from './prompts.js';
13
+ import { addLsp } from './snippet.js';
12
14
  import { syncInit, runSync, syncStatus } from './sync.js';
13
15
  function bumpVersion(version, type) {
14
16
  const [major, minor, patch] = version.split('.').map(Number);
@@ -43,9 +45,67 @@ async function bumpExtensionVersion(type) {
43
45
  await fs.writeFile(tomlPath, newContent);
44
46
  p.log.success(color.green(`Bumped version from ${currentVersion} to ${newVersion}`));
45
47
  }
48
+ function printWelcome() {
49
+ const ascii = String.raw `
50
+ ░ ░░ ░░ ░░░ ░░░░ ░
51
+ ▒▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒ ▒▒▒ ▒▒ ▒▒
52
+ ▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓ ▓▓▓▓ ▓▓▓▓ ▓▓▓
53
+ ██ ███████ ████████ ████ ███ ██ ██
54
+ █ ██ ██ ███ ████ █
55
+
56
+ `.trim();
57
+ console.log('\n' + color.cyan(color.bold(ascii)) + '\n');
58
+ console.log(color.bold(' The CLI toolkit for Zed Editor') + '\n');
59
+ const commands = [
60
+ ['zedx create', 'Scaffold a new Zed extension'],
61
+ ['zedx add theme <name>', 'Add a theme to an existing extension'],
62
+ ['zedx add language <id>', 'Add a language to an existing extension'],
63
+ ['zedx snippet add lsp', 'Wire up a language server into the extension'],
64
+ ['zedx check', 'Validate your extension config'],
65
+ ['zedx install', 'Install as a Zed dev extension'],
66
+ ['zedx version <major|minor|patch>', 'Bump extension version'],
67
+ ['zedx sync', 'Sync Zed settings via a git repo'],
68
+ ['zedx sync init', 'Link a git repo as the sync target'],
69
+ ['zedx sync status', 'Show sync state between local and remote'],
70
+ ['zedx sync install', 'Install the OS daemon for auto-sync'],
71
+ ['zedx sync uninstall', 'Remove the auto-sync daemon'],
72
+ ];
73
+ console.log(color.dim(' Commands:\n'));
74
+ for (const [cmd, desc] of commands) {
75
+ console.log(` ${color.cyan(cmd.padEnd(38))}${color.dim(desc)}`);
76
+ }
77
+ console.log(`\n ${color.dim('Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions'))}\n`);
78
+ }
79
+ async function runCreate() {
80
+ const options = await promptUser();
81
+ if (options.types.includes('theme')) {
82
+ const themeDetails = await promptThemeDetails();
83
+ Object.assign(options, themeDetails);
84
+ }
85
+ if (options.types.includes('language')) {
86
+ const languageDetails = await promptLanguageDetails();
87
+ Object.assign(options, languageDetails);
88
+ }
89
+ const targetDir = path.join(getCallerDir(), options.id);
90
+ await generateExtension(options, targetDir);
91
+ p.outro(`${color.green('✓')} ${color.bold('Extension created successfully!')}\n` +
92
+ `${color.gray('─'.repeat(40))}\n` +
93
+ `${color.dim('Location:')} ${color.cyan(targetDir)}`);
94
+ p.outro(`${color.yellow('⚡')} ${color.bold('Next steps')}\n\n` +
95
+ ` ${color.gray('1.')} Open Zed\n` +
96
+ ` ${color.gray('2.')} ${color.white('Extensions > Install Dev Extension')}\n` +
97
+ ` ${color.gray('3.')} Select ${color.cyan(options.id)} folder\n\n` +
98
+ `${color.dim('Learn more:')} ${color.underline(color.blue('https://zed.dev/docs/extensions/developing-extensions'))}`);
99
+ }
46
100
  async function main() {
47
101
  const program = new Command();
48
- program.name('zedx').description('Boilerplate generator for Zed Editor extensions.');
102
+ program.name('zedx').description('The CLI toolkit for Zed Editor.').helpOption(false);
103
+ program
104
+ .command('create')
105
+ .description('Scaffold a new Zed extension')
106
+ .action(async () => {
107
+ await runCreate();
108
+ });
49
109
  program
50
110
  .command('version')
51
111
  .description('Bump the version of the extension')
@@ -63,6 +123,12 @@ async function main() {
63
123
  .action(async () => {
64
124
  await runCheck(getCallerDir());
65
125
  });
126
+ program
127
+ .command('install')
128
+ .description('Install the current extension as a Zed dev extension')
129
+ .action(async () => {
130
+ await installDevExtension(getCallerDir());
131
+ });
66
132
  const addCmd = program
67
133
  .command('add')
68
134
  .description('Add a theme or language to an existing extension');
@@ -78,6 +144,15 @@ async function main() {
78
144
  .action(async (id) => {
79
145
  await addLanguage(getCallerDir(), id);
80
146
  });
147
+ const snippetCmd = program
148
+ .command('snippet')
149
+ .description('Inject a code snippet into an existing extension');
150
+ snippetCmd
151
+ .command('add lsp')
152
+ .description('Wire up a language server (Rust + WASM) into the extension')
153
+ .action(async () => {
154
+ await addLsp(getCallerDir());
155
+ });
81
156
  const syncCmd = program
82
157
  .command('sync')
83
158
  .description('Sync Zed settings and extensions via a GitHub repo')
@@ -109,25 +184,7 @@ async function main() {
109
184
  await syncUninstall();
110
185
  });
111
186
  if (process.argv.length <= 2) {
112
- const options = await promptUser();
113
- if (options.types.includes('theme')) {
114
- const themeDetails = await promptThemeDetails();
115
- Object.assign(options, themeDetails);
116
- }
117
- if (options.types.includes('language')) {
118
- const languageDetails = await promptLanguageDetails();
119
- Object.assign(options, languageDetails);
120
- }
121
- const targetDir = path.join(getCallerDir(), options.id);
122
- await generateExtension(options, targetDir);
123
- p.outro(`${color.green('✓')} ${color.bold('Extension created successfully!')}\n` +
124
- `${color.gray('─'.repeat(40))}\n` +
125
- `${color.dim('Location:')} ${color.cyan(targetDir)}`);
126
- p.outro(`${color.yellow('⚡')} ${color.bold('Next steps')}\n\n` +
127
- ` ${color.gray('1.')} Open Zed\n` +
128
- ` ${color.gray('2.')} ${color.white('Extensions > Install Dev Extension')}\n` +
129
- ` ${color.gray('3.')} Select ${color.cyan(options.id)} folder\n\n` +
130
- `${color.dim('Learn more:')} ${color.underline(color.blue('https://zed.dev/docs/extensions/developing-extensions'))}`);
187
+ printWelcome();
131
188
  return;
132
189
  }
133
190
  program.parse(process.argv);
@@ -0,0 +1 @@
1
+ export declare function installDevExtension(callerDir: string): Promise<void>;
@@ -0,0 +1,189 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import * as p from '@clack/prompts';
4
+ import fs from 'fs-extra';
5
+ import color from 'picocolors';
6
+ // TOML helpers (regex-based — no parser dependency needed for these fields)
7
+ function tomlGetString(content, key) {
8
+ const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, 'm'));
9
+ return match?.[1];
10
+ }
11
+ function tomlGetNumber(content, key) {
12
+ const match = content.match(new RegExp(`^${key}\\s*=\\s*(\\d+)`, 'm'));
13
+ return match ? Number(match[1]) : undefined;
14
+ }
15
+ function tomlGetAuthors(content) {
16
+ const match = content.match(/^authors\s*=\s*\[([^\]]*)\]/m);
17
+ if (!match)
18
+ return [];
19
+ return [...match[1].matchAll(/"([^"]*)"/g)].map(m => m[1]);
20
+ }
21
+ // Filesystem helpers
22
+ function resolveZedExtensionsDir() {
23
+ const home = os.homedir();
24
+ const platform = process.platform;
25
+ if (platform === 'darwin') {
26
+ return path.join(home, 'Library', 'Application Support', 'Zed', 'extensions');
27
+ }
28
+ if (platform === 'linux') {
29
+ const xdgData = process.env.FLATPAK_XDG_DATA_HOME ||
30
+ process.env.XDG_DATA_HOME ||
31
+ path.join(home, '.local', 'share');
32
+ return path.join(xdgData, 'zed', 'extensions');
33
+ }
34
+ if (platform === 'win32') {
35
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
36
+ return path.join(localAppData, 'Zed', 'extensions');
37
+ }
38
+ throw new Error(`Unsupported platform: ${platform}`);
39
+ }
40
+ function listSubdirs(dir) {
41
+ try {
42
+ return fs
43
+ .readdirSync(dir, { withFileTypes: true })
44
+ .filter(d => d.isDirectory())
45
+ .map(d => d.name);
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ }
51
+ function buildManifest(extensionDir, toml) {
52
+ const id = tomlGetString(toml, 'id') ?? 'unknown';
53
+ const name = tomlGetString(toml, 'name') ?? id;
54
+ const version = tomlGetString(toml, 'version') ?? '0.0.1';
55
+ const schemaVersion = tomlGetNumber(toml, 'schema_version') ?? 1;
56
+ const description = tomlGetString(toml, 'description') ?? '';
57
+ const repository = tomlGetString(toml, 'repository') ?? '';
58
+ const authors = tomlGetAuthors(toml);
59
+ // Detect themes
60
+ const themesDir = path.join(extensionDir, 'themes');
61
+ const themes = fs.pathExistsSync(themesDir)
62
+ ? fs
63
+ .readdirSync(themesDir)
64
+ .filter(f => f.endsWith('.json'))
65
+ .map(f => `themes/${f}`)
66
+ : [];
67
+ // Detect languages
68
+ const langsDir = path.join(extensionDir, 'languages');
69
+ const languages = fs.pathExistsSync(langsDir)
70
+ ? listSubdirs(langsDir).map(d => `languages/${d}`)
71
+ : [];
72
+ // Detect grammars from extension.toml [grammars.<id>] blocks
73
+ const grammars = {};
74
+ const grammarMatches = toml.matchAll(/^\[grammars\.([^\]]+)\]\s*\nrepository\s*=\s*"([^"]*)"\s*\nrev\s*=\s*"([^"]*)"/gm);
75
+ for (const m of grammarMatches) {
76
+ grammars[m[1]] = { repository: m[2], rev: m[3], path: null };
77
+ }
78
+ // Detect language_servers from extension.toml [language_servers.<id>] blocks
79
+ const languageServers = {};
80
+ const lsMatches = toml.matchAll(/^\[language_servers\.([^\]]+)\]\s*\nname\s*=\s*"([^"]*)"\s*\nlanguages\s*=\s*\[([^\]]*)\]/gm);
81
+ for (const m of lsMatches) {
82
+ const langs = [...m[3].matchAll(/"([^"]*)"/g)].map(x => x[1]);
83
+ languageServers[m[1]] = {
84
+ language: langs[0] ?? '',
85
+ languages: langs.slice(1),
86
+ language_ids: {},
87
+ code_action_kinds: null,
88
+ };
89
+ }
90
+ // Detect whether Rust lib is present
91
+ const hasLib = fs.pathExistsSync(path.join(extensionDir, 'Cargo.toml'));
92
+ return {
93
+ id,
94
+ name,
95
+ version,
96
+ schema_version: schemaVersion,
97
+ description,
98
+ repository,
99
+ authors,
100
+ lib: { kind: hasLib ? 'Rust' : null, version: null },
101
+ themes,
102
+ icon_themes: [],
103
+ languages,
104
+ grammars,
105
+ language_servers: languageServers,
106
+ context_servers: {},
107
+ agent_servers: {},
108
+ slash_commands: {},
109
+ snippets: null,
110
+ capabilities: [],
111
+ };
112
+ }
113
+ // Main install function
114
+ export async function installDevExtension(callerDir) {
115
+ console.log('');
116
+ p.intro(`${color.bgBlue(color.bold(' zedx install '))} ${color.blue('Installing as a Zed dev extension…')}`);
117
+ const tomlPath = path.join(callerDir, 'extension.toml');
118
+ if (!(await fs.pathExists(tomlPath))) {
119
+ p.log.error(color.red('No extension.toml found. Run zedx from an extension directory.'));
120
+ process.exit(1);
121
+ }
122
+ const toml = await fs.readFile(tomlPath, 'utf-8');
123
+ const extensionId = tomlGetString(toml, 'id');
124
+ if (!extensionId) {
125
+ p.log.error(color.red('Could not read extension id from extension.toml.'));
126
+ process.exit(1);
127
+ }
128
+ let extensionsDir;
129
+ try {
130
+ extensionsDir = resolveZedExtensionsDir();
131
+ }
132
+ catch (err) {
133
+ p.log.error(color.red(String(err)));
134
+ process.exit(1);
135
+ }
136
+ const installedDir = path.join(extensionsDir, 'installed');
137
+ const indexPath = path.join(extensionsDir, 'index.json');
138
+ const symlinkPath = path.join(installedDir, extensionId);
139
+ await fs.ensureDir(installedDir);
140
+ // --- Handle existing symlink / directory ---
141
+ if (await fs.pathExists(symlinkPath)) {
142
+ const stat = await fs.lstat(symlinkPath);
143
+ if (stat.isSymbolicLink()) {
144
+ const existing = await fs.readlink(symlinkPath);
145
+ if (existing === callerDir) {
146
+ p.log.warn(`${color.yellow(`${extensionId}`)} is already installed and points to this directory.`);
147
+ }
148
+ else {
149
+ const overwrite = await p.confirm({
150
+ message: `${extensionId} is already installed (→ ${existing}). Replace it?`,
151
+ initialValue: true,
152
+ });
153
+ if (p.isCancel(overwrite) || !overwrite) {
154
+ p.cancel('Cancelled.');
155
+ process.exit(0);
156
+ }
157
+ await fs.remove(symlinkPath);
158
+ await fs.symlink(callerDir, symlinkPath);
159
+ p.log.success(`Replaced symlink ${color.cyan(`installed/${extensionId}`)} → ${color.dim(callerDir)}`);
160
+ }
161
+ }
162
+ else {
163
+ p.log.error(color.red(`${symlinkPath} exists and is not a symlink. Remove it manually first.`));
164
+ process.exit(1);
165
+ }
166
+ }
167
+ else {
168
+ await fs.symlink(callerDir, symlinkPath);
169
+ p.log.success(`Created symlink ${color.cyan(`installed/${extensionId}`)} → ${color.dim(callerDir)}`);
170
+ }
171
+ // --- Upsert index.json ---
172
+ let index = { extensions: {} };
173
+ if (await fs.pathExists(indexPath)) {
174
+ try {
175
+ index = await fs.readJson(indexPath);
176
+ }
177
+ catch {
178
+ // malformed — start fresh
179
+ }
180
+ }
181
+ const manifest = buildManifest(callerDir, toml);
182
+ index.extensions[extensionId] = { manifest, dev: true };
183
+ await fs.writeJson(indexPath, index, { spaces: 2 });
184
+ p.log.success(`Updated ${color.cyan('index.json')}`);
185
+ p.outro(`${color.green('✓')} ${color.bold(`${manifest.name} v${manifest.version}`)} installed as a dev extension.\n\n` +
186
+ ` ${color.dim('Reload Zed to pick up the changes:')}\n` +
187
+ ` ${color.white('Extensions')} ${color.dim('→')} ${color.white('Reload Extensions')} ${color.dim('(or restart Zed)')}\n\n` +
188
+ ` ${color.dim('Run')} ${color.cyan('zedx check')} ${color.dim('to validate your extension.')}`);
189
+ }
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:',
@@ -0,0 +1 @@
1
+ export declare function addLsp(callerDir: string): Promise<void>;
@@ -0,0 +1,169 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import * as p from '@clack/prompts';
4
+ import ejs from 'ejs';
5
+ import fs from 'fs-extra';
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 toPascalCase(str) {
21
+ return str
22
+ .replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
23
+ .replace(/^(.)/, (c) => c.toUpperCase());
24
+ }
25
+ function detectLanguages(callerDir) {
26
+ try {
27
+ const langsDir = path.join(callerDir, 'languages');
28
+ if (!fs.pathExistsSync(langsDir))
29
+ return [];
30
+ return fs
31
+ .readdirSync(langsDir, { withFileTypes: true })
32
+ .filter(d => d.isDirectory())
33
+ .map(d => d.name);
34
+ }
35
+ catch {
36
+ return [];
37
+ }
38
+ }
39
+ export async function addLsp(callerDir) {
40
+ console.log('');
41
+ p.intro(`${color.bgBlue(color.bold(' zedx snippet add lsp '))} ${color.blue('Wiring up a language server…')}`);
42
+ const tomlPath = path.join(callerDir, 'extension.toml');
43
+ if (!(await fs.pathExists(tomlPath))) {
44
+ p.log.error(color.red('No extension.toml found. Run zedx from an extension directory.'));
45
+ process.exit(1);
46
+ }
47
+ const tomlContent = await fs.readFile(tomlPath, 'utf-8');
48
+ const extensionId = tomlGet(tomlContent, 'id') ?? 'my-extension';
49
+ const extensionName = tomlGet(tomlContent, 'name') ?? extensionId;
50
+ // --- LSP server name ---
51
+ const lspNameDefault = `${extensionName} LSP`;
52
+ const lspName = await p.text({
53
+ message: 'Language server display name:',
54
+ placeholder: lspNameDefault,
55
+ });
56
+ if (p.isCancel(lspName)) {
57
+ p.cancel('Cancelled.');
58
+ process.exit(0);
59
+ }
60
+ const lspNameValue = String(lspName || lspNameDefault);
61
+ // Derive a TOML-safe ID from the display name
62
+ const lspId = lspNameValue
63
+ .toLowerCase()
64
+ .replace(/\s+/g, '-')
65
+ .replace(/[^a-z0-9-]/g, '');
66
+ // --- Check for duplicate ---
67
+ if (new RegExp(`^\\[language_servers\\.${lspId}\\]`, 'm').test(tomlContent)) {
68
+ p.log.error(color.red(`[language_servers.${lspId}] already exists in extension.toml.`));
69
+ process.exit(1);
70
+ }
71
+ // --- Language association ---
72
+ const detectedLanguages = detectLanguages(callerDir);
73
+ let languageName;
74
+ if (detectedLanguages.length > 0) {
75
+ const choice = await p.select({
76
+ message: 'Which language does this LSP serve?',
77
+ options: [
78
+ ...detectedLanguages.map(l => ({
79
+ value: l,
80
+ label: toPascalCase(l),
81
+ })),
82
+ { value: '__custom__', label: 'Enter manually' },
83
+ ],
84
+ });
85
+ if (p.isCancel(choice)) {
86
+ p.cancel('Cancelled.');
87
+ process.exit(0);
88
+ }
89
+ if (choice === '__custom__') {
90
+ const custom = await p.text({
91
+ message: 'Language name (must match name in config.toml):',
92
+ placeholder: extensionName,
93
+ });
94
+ if (p.isCancel(custom)) {
95
+ p.cancel('Cancelled.');
96
+ process.exit(0);
97
+ }
98
+ languageName = String(custom || extensionName);
99
+ }
100
+ else {
101
+ languageName = toPascalCase(String(choice));
102
+ }
103
+ }
104
+ else {
105
+ const custom = await p.text({
106
+ message: 'Language name (must match name in config.toml):',
107
+ placeholder: extensionName,
108
+ });
109
+ if (p.isCancel(custom)) {
110
+ p.cancel('Cancelled.');
111
+ process.exit(0);
112
+ }
113
+ languageName = String(custom || extensionName);
114
+ }
115
+ // --- LSP binary command ---
116
+ const lspCommand = await p.text({
117
+ message: 'LSP binary command (the executable name or path):',
118
+ placeholder: `${lspId}-server`,
119
+ });
120
+ if (p.isCancel(lspCommand)) {
121
+ p.cancel('Cancelled.');
122
+ process.exit(0);
123
+ }
124
+ const lspCommandValue = String(lspCommand || `${lspId}-server`);
125
+ // --- Append [language_servers.*] block to extension.toml ---
126
+ const lspTomlBlock = `\n[language_servers.${lspId}]\n` +
127
+ `name = "${lspNameValue}"\n` +
128
+ `languages = ["${languageName}"]\n`;
129
+ await fs.appendFile(tomlPath, lspTomlBlock);
130
+ p.log.success(`Updated ${color.cyan('extension.toml')} with [language_servers.${lspId}]`);
131
+ // --- Add lib.path to extension.toml if not present ---
132
+ const updatedToml = await fs.readFile(tomlPath, 'utf-8');
133
+ if (!/^lib\s*=/m.test(updatedToml)) {
134
+ await fs.appendFile(tomlPath, `\nlib.path = "extension.wasm"\n`);
135
+ p.log.success(`Updated ${color.cyan('extension.toml')} with lib.path`);
136
+ }
137
+ const structName = toPascalCase(extensionId).replace(/-/g, '');
138
+ // --- Generate Cargo.toml if not present ---
139
+ const cargoPath = path.join(callerDir, 'Cargo.toml');
140
+ if (!(await fs.pathExists(cargoPath))) {
141
+ const cargoToml = await renderTemplate(path.join(TEMPLATE_DIR, 'lsp/Cargo.toml.ejs'), {
142
+ extensionId,
143
+ });
144
+ await fs.writeFile(cargoPath, cargoToml);
145
+ p.log.success(`Created ${color.cyan('Cargo.toml')}`);
146
+ }
147
+ else {
148
+ p.log.warn(`${color.yellow('Cargo.toml already exists')} — skipped`);
149
+ }
150
+ // --- Generate src/lib.rs if not present ---
151
+ const srcDir = path.join(callerDir, 'src');
152
+ const libRsPath = path.join(srcDir, 'lib.rs');
153
+ if (!(await fs.pathExists(libRsPath))) {
154
+ await fs.ensureDir(srcDir);
155
+ const libRs = await renderTemplate(path.join(TEMPLATE_DIR, 'lsp/lib.rs.ejs'), {
156
+ structName,
157
+ lspCommand: lspCommandValue,
158
+ });
159
+ await fs.writeFile(libRsPath, libRs);
160
+ p.log.success(`Created ${color.cyan('src/lib.rs')}`);
161
+ }
162
+ else {
163
+ p.log.warn(`${color.yellow('src/lib.rs already exists')} — skipped`);
164
+ }
165
+ p.outro(`${color.green('✓')} LSP snippet added.\n\n` +
166
+ ` ${color.dim('1.')} Edit ${color.cyan('src/lib.rs')} — implement ${color.white('language_server_command')}\n` +
167
+ ` ${color.dim('2.')} Edit ${color.cyan('Cargo.toml')} — pin ${color.white('zed_extension_api')} to latest version\n` +
168
+ ` ${color.dim('3.')} ${color.dim('Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions/languages#language-servers'))}`);
169
+ }
package/dist/sync.js CHANGED
@@ -110,7 +110,8 @@ async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPa
110
110
  }
111
111
  // zedx sync status
112
112
  export async function syncStatus() {
113
- p.intro(color.bold('zedx sync status'));
113
+ console.log('');
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();
116
117
  p.log.info(`Repo: ${color.dim(config.syncRepo)} ${color.dim(`(${config.branch})`)}`);
@@ -186,7 +187,8 @@ export async function syncStatus() {
186
187
  }
187
188
  // zedx sync init
188
189
  export async function syncInit() {
189
- p.intro(color.bold('zedx sync init'));
190
+ console.log('');
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)',
192
194
  placeholder: 'https://github.com/you/zed-config.git',
@@ -250,8 +252,10 @@ export async function runSync(opts = {}) {
250
252
  p.log.success(msg);
251
253
  },
252
254
  };
253
- if (!silent)
254
- p.intro(color.bold('zedx sync'));
255
+ if (!silent) {
256
+ console.log('');
257
+ p.intro(`${color.bgBlue(color.bold(' zedx sync '))} ${color.blue('Syncing Zed settings and extensions…')}`);
258
+ }
255
259
  const config = await requireSyncConfig();
256
260
  const zedPaths = resolveZedPaths();
257
261
  // Spinner shim: in silent mode just log to stderr so daemons can capture it
@@ -2,21 +2,23 @@ id = "<%= id %>"
2
2
  name = "<%= name %>"
3
3
  version = "0.0.1"
4
4
  schema_version = 1
5
- authors = ["<%- author %>"]
5
+ authors = ["<%- author %>"] # TODO(edit-me): replace with your name and email
6
6
  description = "<%= description %>"
7
- repository = "<%= repository %>"
7
+ repository = "<%= repository %>" # TODO(edit-me): update to your actual repository URL
8
8
 
9
9
  <% if (types.includes('language') && grammarRepo) { %>
10
10
  [grammars.<%= languageId %>]
11
11
  repository = "<%= grammarRepo %>"
12
12
  rev = "<%= grammarRev || 'main' %>"
13
13
  <% } else if (types.includes('language')) { %>
14
+ # TODO(edit-me): uncomment and fill in the grammar block once you have a Tree-sitter grammar repo
14
15
  # [grammars.<%= languageId %>]
15
16
  # repository = "https://github.com/user/tree-sitter-<%= languageId %>"
16
17
  # rev = "main"
17
18
  <% } %>
18
19
 
19
20
  <% if (types.includes('language')) { %>
21
+ # TODO(edit-me): uncomment and fill in the LSP block if your language has a language server
20
22
  # [language_servers.my-lsp]
21
23
  # name = "My Language LSP"
22
24
  # languages = ["<%= languageName %>"]
@@ -4,10 +4,12 @@ name = "<%= languageName %>"
4
4
  # grammar (required): The name of a Tree-sitter grammar (must match grammar registration in extension.toml)
5
5
  grammar = "<%= languageId %>"
6
6
 
7
- # path_suffixes: Array of file suffixes associated with this language (e.g., ["myl", "my"])
7
+ # TODO(edit-me): uncomment and set the file extensions for your language (e.g., ["myl", "my"])
8
+ # path_suffixes: Array of file suffixes associated with this language
8
9
  # Unlike file_types in settings, this does not support glob patterns
9
10
  # path_suffixes = []
10
11
 
12
+ # TODO(edit-me): uncomment and set the comment syntax for your language (e.g., ["// ", "# "])
11
13
  # line_comments: Array of strings used to identify line comments
12
14
  # Used for editor::ToggleComments keybind (cmd-/ or ctrl-/)
13
15
  # line_comments = ["// ", "# "]
@@ -27,6 +27,7 @@
27
27
  ; Learn more about Tree-sitter queries:
28
28
  ; https://tree-sitter.github.io/tree-sitter/using-parsers/queries
29
29
 
30
+ ; TODO(edit-me): replace these with the actual node type names from your Tree-sitter grammar
30
31
  (string) @string
31
32
  (number) @number
32
33
  (comment) @comment
@@ -0,0 +1,12 @@
1
+ [package]
2
+ name = "<%= extensionId %>"
3
+ version = "0.0.1"
4
+ edition = "2024"
5
+
6
+ [lib]
7
+ crate-type = ["cdylib"]
8
+
9
+ [dependencies]
10
+ # TODO(edit-me): pin to the latest version compatible with your target Zed version
11
+ # See: https://crates.io/crates/zed_extension_api
12
+ zed_extension_api = "0.4"
@@ -0,0 +1,30 @@
1
+ use zed_extension_api::{self as zed, LanguageServerId, Result};
2
+
3
+ struct <%= structName %> {
4
+ // TODO(edit-me): add any cached state here (e.g. cached binary path)
5
+ }
6
+
7
+ impl zed::Extension for <%= structName %> {
8
+ fn new() -> Self {
9
+ Self {}
10
+ }
11
+
12
+ fn language_server_command(
13
+ &mut self,
14
+ _language_server_id: &LanguageServerId,
15
+ _worktree: &zed::Worktree,
16
+ ) -> Result<zed::Command> {
17
+ // TODO(edit-me): resolve the path to the LSP binary.
18
+ // Common patterns:
19
+ // - zed::Command { command: worktree.which("<%= lspCommand %>")?.ok_or("...")?, ... }
20
+ // - download the binary via zed::download_file and cache the path
21
+ // See: https://docs.rs/zed_extension_api
22
+ Ok(zed::Command {
23
+ command: "<%= lspCommand %>".to_string(), // TODO(edit-me): replace with actual binary path
24
+ args: vec![], // TODO(edit-me): add required args
25
+ env: vec![],
26
+ })
27
+ }
28
+ }
29
+
30
+ zed::register_extension!(<%= structName %>);
@@ -2,6 +2,7 @@
2
2
  "$schema": "https://zed.dev/schema/themes/v0.2.0.json",
3
3
  "name": "<%= themeName %>",
4
4
  "author": "<%= author %>",
5
+ "_todo": "TODO(edit-me): replace the placeholder grayscale colors in 'style' with your actual palette. Search for hex values like #d4d4d4ff to find them.",
5
6
  "themes": [
6
7
  <% appearances.forEach((app, index) => { %>
7
8
  {
package/package.json CHANGED
@@ -1,59 +1,60 @@
1
1
  {
2
- "name": "zedx",
3
- "version": "0.7.0",
4
- "description": "Scaffold Zed Editor extensions and sync your settings across machines.",
5
- "keywords": [
6
- "boilerplate",
7
- "extension",
8
- "scaffold",
9
- "zed",
10
- "zed-editor"
11
- ],
12
- "homepage": "https://github.com/tahayvr/zedx#readme",
13
- "license": "Apache-2.0",
14
- "author": "Taha Nejad <taha@noiserandom.com>",
15
- "repository": {
16
- "type": "git",
17
- "url": "https://github.com/tahayvr/zedx.git"
18
- },
19
- "bin": {
20
- "zedx": "dist/index.js"
21
- },
22
- "files": [
23
- "dist"
24
- ],
25
- "type": "module",
26
- "main": "dist/index.js",
27
- "publishConfig": {
28
- "access": "public"
29
- },
30
- "dependencies": {
31
- "@clack/prompts": "^0.10.1",
32
- "commander": "^14.0.3",
33
- "ejs": "^4.0.1",
34
- "fs-extra": "^11.3.3",
35
- "picocolors": "^1.1.1",
36
- "simple-git": "^3.33.0"
37
- },
38
- "devDependencies": {
39
- "@types/ejs": "^3.1.5",
40
- "@types/fs-extra": "^11.0.4",
41
- "@types/node": "^25.2.3",
42
- "oxfmt": "^0.42.0",
43
- "oxlint": "^1.57.0",
44
- "tsx": "^4.21.0",
45
- "typescript": "^5.9.3"
46
- },
47
- "engines": {
48
- "node": ">=18"
49
- },
50
- "scripts": {
51
- "build": "tsc && cp -r src/templates dist/",
52
- "start": "node dist/index.js",
53
- "dev": "tsx src/index.ts",
54
- "lint": "oxlint",
55
- "lint:fix": "oxlint --fix",
56
- "fmt": "oxfmt",
57
- "fmt:check": "oxfmt --check"
58
- }
59
- }
2
+ "name": "zedx",
3
+ "version": "0.8.1",
4
+ "description": "Scaffold Zed Editor extensions and sync your settings across machines.",
5
+ "keywords": [
6
+ "boilerplate",
7
+ "extension",
8
+ "scaffold",
9
+ "zed",
10
+ "zed-editor"
11
+ ],
12
+ "homepage": "https://github.com/tahayvr/zedx#readme",
13
+ "license": "Apache-2.0",
14
+ "author": "Taha Nejad <taha@noiserandom.com>",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/tahayvr/zedx.git"
18
+ },
19
+ "bin": {
20
+ "zedx": "dist/index.js"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "type": "module",
26
+ "main": "dist/index.js",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc && cp -r src/templates dist/",
32
+ "start": "node dist/index.js",
33
+ "dev": "tsx src/index.ts",
34
+ "lint": "oxlint",
35
+ "lint:fix": "oxlint --fix",
36
+ "fmt": "oxfmt",
37
+ "fmt:check": "oxfmt --check"
38
+ },
39
+ "dependencies": {
40
+ "@clack/prompts": "^0.10.1",
41
+ "commander": "^14.0.3",
42
+ "ejs": "^4.0.1",
43
+ "fs-extra": "^11.3.3",
44
+ "picocolors": "^1.1.1",
45
+ "simple-git": "^3.33.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/ejs": "^3.1.5",
49
+ "@types/fs-extra": "^11.0.4",
50
+ "@types/node": "^25.2.3",
51
+ "oxfmt": "^0.42.0",
52
+ "oxlint": "^1.57.0",
53
+ "tsx": "^4.21.0",
54
+ "typescript": "^5.9.3"
55
+ },
56
+ "engines": {
57
+ "node": ">=18"
58
+ },
59
+ "packageManager": "pnpm@10.30.0+sha512.2b5753de015d480eeb88f5b5b61e0051f05b4301808a82ec8b840c9d2adf7748eb352c83f5c1593ca703ff1017295bc3fdd3119abb9686efc96b9fcb18200937"
60
+ }
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
- }