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 +17 -9
- package/dist/check.js +4 -4
- package/dist/daemon.js +16 -12
- package/dist/generator.js +6 -6
- package/dist/index.js +82 -25
- package/dist/install.d.ts +1 -0
- package/dist/install.js +188 -0
- package/dist/prompts.js +20 -16
- package/dist/snippet.d.ts +1 -0
- package/dist/snippet.js +168 -0
- package/dist/sync.js +89 -34
- package/dist/templates/base/extension.toml.ejs +4 -2
- package/dist/templates/language/config.toml.ejs +3 -1
- package/dist/templates/language/highlights.scm +1 -0
- package/dist/templates/lsp/Cargo.toml.ejs +12 -0
- package/dist/templates/lsp/lib.rs.ejs +30 -0
- package/dist/templates/theme/theme.json.ejs +1 -0
- package/package.json +59 -55
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
|
|
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
|
-
|
|
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'), {
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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('
|
|
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
|
-
|
|
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>;
|
package/dist/install.js
ADDED
|
@@ -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
|
+
}
|