zedx 0.6.0 → 0.8.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/dist/add.js CHANGED
@@ -1,8 +1,8 @@
1
- import fs from 'fs-extra';
2
1
  import path from 'path';
3
- import ejs from 'ejs';
4
2
  import { fileURLToPath } from 'url';
5
3
  import * as p from '@clack/prompts';
4
+ import ejs from 'ejs';
5
+ import fs from 'fs-extra';
6
6
  import color from 'picocolors';
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
@@ -18,7 +18,10 @@ function tomlGet(content, key) {
18
18
  return match?.[1];
19
19
  }
20
20
  function slugify(name) {
21
- return name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
21
+ return name
22
+ .toLowerCase()
23
+ .replace(/\s+/g, '-')
24
+ .replace(/[^a-z0-9-]/g, '');
22
25
  }
23
26
  export async function addTheme(callerDir, themeName) {
24
27
  p.intro(`${color.bgBlue(color.bold(' zedx add theme '))} ${color.blue('Adding a theme to your extension…')}`);
@@ -29,9 +32,9 @@ export async function addTheme(callerDir, themeName) {
29
32
  }
30
33
  const tomlContent = await fs.readFile(tomlPath, 'utf-8');
31
34
  const extensionId = tomlGet(tomlContent, 'id') ?? 'extension';
32
- const author = tomlGet(tomlContent, 'authors')
33
- ?? tomlContent.match(/^authors\s*=\s*\["([^"]+)"\]/m)?.[1]
34
- ?? '';
35
+ const author = tomlGet(tomlContent, 'authors') ??
36
+ tomlContent.match(/^authors\s*=\s*\["([^"]+)"\]/m)?.[1] ??
37
+ '';
35
38
  const appearance = await p.select({
36
39
  message: 'Appearance:',
37
40
  options: [
@@ -55,7 +58,12 @@ export async function addTheme(callerDir, themeName) {
55
58
  process.exit(1);
56
59
  }
57
60
  await fs.ensureDir(themesDir);
58
- const themeJson = await renderTemplate(path.join(TEMPLATE_DIR, 'theme/theme.json.ejs'), { id: extensionId, author, themeName, appearances });
61
+ const themeJson = await renderTemplate(path.join(TEMPLATE_DIR, 'theme/theme.json.ejs'), {
62
+ id: extensionId,
63
+ author,
64
+ themeName,
65
+ appearances,
66
+ });
59
67
  await fs.writeFile(themePath, themeJson);
60
68
  p.log.success(`Created ${color.cyan(`themes/${themeFile}`)}`);
61
69
  p.outro(`${color.green('✓')} Theme added.\n` +
@@ -70,8 +78,8 @@ export async function addLanguage(callerDir, languageId) {
70
78
  }
71
79
  const tomlContent = await fs.readFile(tomlPath, 'utf-8');
72
80
  // Check for duplicate
73
- const alreadyExists = new RegExp(`^\\[grammars\\.${languageId}\\]`, 'm').test(tomlContent)
74
- || new RegExp(`^#\\s*\\[grammars\\.${languageId}\\]`, 'm').test(tomlContent);
81
+ const alreadyExists = new RegExp(`^\\[grammars\\.${languageId}\\]`, 'm').test(tomlContent) ||
82
+ new RegExp(`^#\\s*\\[grammars\\.${languageId}\\]`, 'm').test(tomlContent);
75
83
  if (alreadyExists) {
76
84
  p.log.error(color.red(`Language "${languageId}" already exists in extension.toml.`));
77
85
  process.exit(1);
package/dist/check.js CHANGED
@@ -1,6 +1,6 @@
1
- import fs from 'fs-extra';
2
1
  import path from 'path';
3
2
  import * as p from '@clack/prompts';
3
+ import fs from 'fs-extra';
4
4
  import color from 'picocolors';
5
5
  // Minimal TOML key extraction — handles `key = "value"` and `key = ["a", "b"]`
6
6
  function tomlGet(content, key) {
@@ -175,7 +175,7 @@ export async function runCheck(callerDir) {
175
175
  if (!tomlHasUncommentedKey(configContent, 'path_suffixes')) {
176
176
  configIssues.push({
177
177
  file: `languages/${langId}/config.toml`,
178
- message: 'path_suffixes is not set — files won\'t be associated with this language',
178
+ message: "path_suffixes is not set — files won't be associated with this language",
179
179
  hint: 'Uncomment and fill in path_suffixes (e.g., ["myl"])',
180
180
  });
181
181
  }
@@ -183,7 +183,7 @@ export async function runCheck(callerDir) {
183
183
  if (!tomlHasUncommentedKey(configContent, 'line_comments')) {
184
184
  configIssues.push({
185
185
  file: `languages/${langId}/config.toml`,
186
- message: 'line_comments is not set — toggle-comment keybind won\'t work',
186
+ message: "line_comments is not set — toggle-comment keybind won't work",
187
187
  hint: 'Uncomment and set line_comments (e.g., ["// "])',
188
188
  });
189
189
  }
@@ -209,7 +209,7 @@ export async function runCheck(callerDir) {
209
209
  highlightIssues.push({
210
210
  file: `languages/${langId}/highlights.scm`,
211
211
  message: 'Only scaffold starter patterns present — no real grammar queries added yet',
212
- hint: 'Add tree-sitter queries matching your language\'s grammar node types',
212
+ hint: "Add tree-sitter queries matching your language's grammar node types",
213
213
  });
214
214
  }
215
215
  }
package/dist/daemon.js CHANGED
@@ -1,8 +1,8 @@
1
+ import { execSync } from 'child_process';
1
2
  import os from 'os';
2
3
  import path from 'path';
3
- import fs from 'fs-extra';
4
- import { execSync } from 'child_process';
5
4
  import * as p from '@clack/prompts';
5
+ import fs from 'fs-extra';
6
6
  import color from 'picocolors';
7
7
  import { resolveZedPaths } from './zed-paths.js';
8
8
  const LAUNCHD_LABEL = 'dev.zedx.sync';
@@ -17,7 +17,9 @@ function resolveZedxBinary() {
17
17
  if (bin)
18
18
  return bin;
19
19
  }
20
- catch { /* fall through */ }
20
+ catch {
21
+ /* fall through */
22
+ }
21
23
  return `${process.execPath} ${process.argv[1]}`;
22
24
  }
23
25
  function unsupportedPlatform() {
@@ -25,9 +27,7 @@ function unsupportedPlatform() {
25
27
  process.exit(1);
26
28
  }
27
29
  function buildPlist(zedxBin, watchPaths) {
28
- const watchEntries = watchPaths
29
- .map((wp) => ` <string>${wp}</string>`)
30
- .join('\n');
30
+ const watchEntries = watchPaths.map(wp => ` <string>${wp}</string>`).join('\n');
31
31
  return `<?xml version="1.0" encoding="UTF-8"?>
32
32
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
33
  <plist version="1.0">
@@ -68,7 +68,9 @@ async function installMacos(zedxBin, watchPaths) {
68
68
  try {
69
69
  execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null`, { stdio: 'pipe' });
70
70
  }
71
- catch { /* not loaded yet */ }
71
+ catch {
72
+ /* not loaded yet */
73
+ }
72
74
  execSync(`launchctl load "${LAUNCHD_PLIST_PATH}"`);
73
75
  p.log.success(`Daemon installed: ${color.dim(LAUNCHD_PLIST_PATH)}`);
74
76
  p.log.info(`Logs: ${color.dim(`${os.homedir()}/Library/Logs/zedx-sync.log`)}`);
@@ -82,7 +84,9 @@ async function uninstallMacos() {
82
84
  try {
83
85
  execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}"`, { stdio: 'pipe' });
84
86
  }
85
- catch { /* already unloaded */ }
87
+ catch {
88
+ /* already unloaded */
89
+ }
86
90
  await fs.remove(LAUNCHD_PLIST_PATH);
87
91
  p.log.success('Daemon uninstalled.');
88
92
  }
@@ -103,9 +107,7 @@ WantedBy=default.target
103
107
  `;
104
108
  }
105
109
  function buildSystemdPath(watchPaths) {
106
- const pathChangedEntries = watchPaths
107
- .map((wp) => `PathChanged=${wp}`)
108
- .join('\n');
110
+ const pathChangedEntries = watchPaths.map(wp => `PathChanged=${wp}`).join('\n');
109
111
  return `[Unit]
110
112
  Description=Watch Zed config files for zedx sync
111
113
 
@@ -138,7 +140,9 @@ async function uninstallLinux() {
138
140
  try {
139
141
  execSync(`systemctl --user disable --now ${SYSTEMD_SERVICE_NAME}.path`, { stdio: 'pipe' });
140
142
  }
141
- catch { /* already inactive */ }
143
+ catch {
144
+ /* already inactive */
145
+ }
142
146
  if (serviceExists)
143
147
  await fs.remove(SYSTEMD_SERVICE_PATH);
144
148
  if (pathExists)
package/dist/generator.js CHANGED
@@ -1,7 +1,7 @@
1
- import fs from 'fs-extra';
2
1
  import path from 'path';
3
2
  import { fileURLToPath } from 'url';
4
3
  import ejs from 'ejs';
4
+ import fs from 'fs-extra';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
  const isDev = __dirname.includes('/src/');
@@ -17,13 +17,13 @@ export async function generateExtension(options, targetDir) {
17
17
  ...options,
18
18
  grammarRepo: options.grammarRepo || '',
19
19
  grammarRev: options.grammarRev || '',
20
- languageName: options.languageName || 'My Language'
20
+ languageName: options.languageName || 'My Language',
21
21
  };
22
22
  const extToml = await renderTemplate(path.join(TEMPLATE_DIR, 'base/extension.toml.ejs'), extData);
23
23
  await fs.writeFile(path.join(targetDir, 'extension.toml'), extToml);
24
24
  const readmeData = {
25
25
  ...extData,
26
- languageId: options.languageId || 'my-language'
26
+ languageId: options.languageId || 'my-language',
27
27
  };
28
28
  const readme = await renderTemplate(path.join(TEMPLATE_DIR, 'base/readme.md.ejs'), readmeData);
29
29
  await fs.writeFile(path.join(targetDir, 'README.md'), readme);
@@ -47,7 +47,7 @@ async function generateTheme(options, targetDir) {
47
47
  const themeData = {
48
48
  ...options,
49
49
  themeName: options.themeName || 'My Theme',
50
- appearances
50
+ appearances,
51
51
  };
52
52
  const themeJson = await renderTemplate(path.join(TEMPLATE_DIR, 'theme/theme.json.ejs'), themeData);
53
53
  await fs.writeFile(path.join(themeDir, `${options.id}.json`), themeJson);
@@ -58,7 +58,7 @@ async function generateLanguage(options, targetDir) {
58
58
  const data = {
59
59
  ...options,
60
60
  pathSuffixes: options.pathSuffixes || [],
61
- lineComments: options.lineComments || ['//', '#']
61
+ lineComments: options.lineComments || ['//', '#'],
62
62
  };
63
63
  const configToml = await renderTemplate(path.join(TEMPLATE_DIR, 'language/config.toml.ejs'), data);
64
64
  await fs.writeFile(path.join(languageDir, 'config.toml'), configToml);
@@ -71,7 +71,7 @@ async function generateLanguage(options, targetDir) {
71
71
  'overrides.scm',
72
72
  'textobjects.scm',
73
73
  'redactions.scm',
74
- 'runnables.scm'
74
+ 'runnables.scm',
75
75
  ];
76
76
  for (const file of queryFiles) {
77
77
  const templatePath = path.join(TEMPLATE_DIR, 'language', file);
package/dist/index.js CHANGED
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'path';
3
3
  import * as p from '@clack/prompts';
4
- import color from 'picocolors';
5
4
  import { Command } from 'commander';
6
5
  import fs from 'fs-extra';
7
- import { promptUser, promptThemeDetails, promptLanguageDetails } from './prompts.js';
8
- import { generateExtension } from './generator.js';
9
- import { runCheck } from './check.js';
6
+ import color from 'picocolors';
10
7
  import { addTheme, addLanguage } from './add.js';
11
- import { syncInit, runSync, syncStatus } from './sync.js';
8
+ import { runCheck } from './check.js';
12
9
  import { syncInstall, syncUninstall } from './daemon.js';
10
+ import { generateExtension } from './generator.js';
11
+ import { installDevExtension } from './install.js';
12
+ import { promptUser, promptThemeDetails, promptLanguageDetails } from './prompts.js';
13
+ import { addLsp } from './snippet.js';
14
+ import { syncInit, runSync, syncStatus } from './sync.js';
13
15
  function bumpVersion(version, type) {
14
16
  const [major, minor, patch] = version.split('.').map(Number);
15
17
  switch (type) {
@@ -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,188 @@
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
+ p.intro(`${color.bgBlue(color.bold(' zedx install '))} ${color.blue('Installing as a Zed dev extension…')}`);
116
+ const tomlPath = path.join(callerDir, 'extension.toml');
117
+ if (!(await fs.pathExists(tomlPath))) {
118
+ p.log.error(color.red('No extension.toml found. Run zedx from an extension directory.'));
119
+ process.exit(1);
120
+ }
121
+ const toml = await fs.readFile(tomlPath, 'utf-8');
122
+ const extensionId = tomlGetString(toml, 'id');
123
+ if (!extensionId) {
124
+ p.log.error(color.red('Could not read extension id from extension.toml.'));
125
+ process.exit(1);
126
+ }
127
+ let extensionsDir;
128
+ try {
129
+ extensionsDir = resolveZedExtensionsDir();
130
+ }
131
+ catch (err) {
132
+ p.log.error(color.red(String(err)));
133
+ process.exit(1);
134
+ }
135
+ const installedDir = path.join(extensionsDir, 'installed');
136
+ const indexPath = path.join(extensionsDir, 'index.json');
137
+ const symlinkPath = path.join(installedDir, extensionId);
138
+ await fs.ensureDir(installedDir);
139
+ // --- Handle existing symlink / directory ---
140
+ if (await fs.pathExists(symlinkPath)) {
141
+ const stat = await fs.lstat(symlinkPath);
142
+ if (stat.isSymbolicLink()) {
143
+ const existing = await fs.readlink(symlinkPath);
144
+ if (existing === callerDir) {
145
+ p.log.warn(`${color.yellow(`${extensionId}`)} is already installed and points to this directory.`);
146
+ }
147
+ else {
148
+ const overwrite = await p.confirm({
149
+ message: `${extensionId} is already installed (→ ${existing}). Replace it?`,
150
+ initialValue: true,
151
+ });
152
+ if (p.isCancel(overwrite) || !overwrite) {
153
+ p.cancel('Cancelled.');
154
+ process.exit(0);
155
+ }
156
+ await fs.remove(symlinkPath);
157
+ await fs.symlink(callerDir, symlinkPath);
158
+ p.log.success(`Replaced symlink ${color.cyan(`installed/${extensionId}`)} → ${color.dim(callerDir)}`);
159
+ }
160
+ }
161
+ else {
162
+ p.log.error(color.red(`${symlinkPath} exists and is not a symlink. Remove it manually first.`));
163
+ process.exit(1);
164
+ }
165
+ }
166
+ else {
167
+ await fs.symlink(callerDir, symlinkPath);
168
+ p.log.success(`Created symlink ${color.cyan(`installed/${extensionId}`)} → ${color.dim(callerDir)}`);
169
+ }
170
+ // --- Upsert index.json ---
171
+ let index = { extensions: {} };
172
+ if (await fs.pathExists(indexPath)) {
173
+ try {
174
+ index = await fs.readJson(indexPath);
175
+ }
176
+ catch {
177
+ // malformed — start fresh
178
+ }
179
+ }
180
+ const manifest = buildManifest(callerDir, toml);
181
+ index.extensions[extensionId] = { manifest, dev: true };
182
+ await fs.writeJson(indexPath, index, { spaces: 2 });
183
+ p.log.success(`Updated ${color.cyan('index.json')}`);
184
+ p.outro(`${color.green('✓')} ${color.bold(`${manifest.name} v${manifest.version}`)} installed as a dev extension.\n\n` +
185
+ ` ${color.dim('Reload Zed to pick up the changes:')}\n` +
186
+ ` ${color.white('Extensions')} ${color.dim('→')} ${color.white('Reload Extensions')} ${color.dim('(or restart Zed)')}\n\n` +
187
+ ` ${color.dim('Run')} ${color.cyan('zedx check')} ${color.dim('to validate your extension.')}`);
188
+ }