zedx 0.6.0 → 0.7.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,15 @@
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 { promptUser, promptThemeDetails, promptLanguageDetails } from './prompts.js';
12
+ import { syncInit, runSync, syncStatus } from './sync.js';
13
13
  function bumpVersion(version, type) {
14
14
  const [major, minor, patch] = version.split('.').map(Number);
15
15
  switch (type) {
package/dist/prompts.js CHANGED
@@ -5,7 +5,7 @@ export async function promptUser() {
5
5
  const nameDefault = 'my-zed-extension';
6
6
  const name = await p.text({
7
7
  message: 'Project name:',
8
- placeholder: nameDefault
8
+ placeholder: nameDefault,
9
9
  });
10
10
  if (p.isCancel(name)) {
11
11
  p.cancel('Cancelled.');
@@ -21,7 +21,7 @@ export async function promptUser() {
21
21
  return 'ID cannot contain spaces';
22
22
  if (value && value !== value.toLowerCase())
23
23
  return 'ID must be lowercase';
24
- }
24
+ },
25
25
  });
26
26
  if (p.isCancel(id)) {
27
27
  p.cancel('Cancelled.');
@@ -31,7 +31,7 @@ export async function promptUser() {
31
31
  const descriptionDefault = 'A Zed extension';
32
32
  const description = await p.text({
33
33
  message: 'Description:',
34
- placeholder: descriptionDefault
34
+ placeholder: descriptionDefault,
35
35
  });
36
36
  if (p.isCancel(description)) {
37
37
  p.cancel('Cancelled.');
@@ -44,7 +44,7 @@ export async function promptUser() {
44
44
  validate: (value) => {
45
45
  if (!value || value.length === 0)
46
46
  return 'Author is required';
47
- }
47
+ },
48
48
  });
49
49
  if (p.isCancel(author)) {
50
50
  p.cancel('Cancelled.');
@@ -53,7 +53,7 @@ export async function promptUser() {
53
53
  const repositoryDefault = `https://github.com/username/${idValue}.git`;
54
54
  const repository = await p.text({
55
55
  message: 'GitHub repository URL:',
56
- initialValue: repositoryDefault
56
+ initialValue: repositoryDefault,
57
57
  });
58
58
  if (p.isCancel(repository)) {
59
59
  p.cancel('Cancelled.');
@@ -69,9 +69,9 @@ export async function promptUser() {
69
69
  { value: 'GPL-3.0', label: 'GNU GPLv3' },
70
70
  { value: 'LGPL-3.0', label: 'GNU LGPLv3' },
71
71
  { value: 'MIT', label: 'MIT' },
72
- { value: 'Zlib', label: 'zlib' }
72
+ { value: 'Zlib', label: 'zlib' },
73
73
  ],
74
- initialValue: 'MIT'
74
+ initialValue: 'MIT',
75
75
  });
76
76
  if (p.isCancel(license)) {
77
77
  p.cancel('Cancelled.');
@@ -81,9 +81,13 @@ export async function promptUser() {
81
81
  message: 'What do you want to include in your extension?',
82
82
  options: [
83
83
  { value: 'theme', label: 'Theme', hint: 'Color scheme for the editor' },
84
- { value: 'language', label: 'Language', hint: 'Syntax highlighting, indentation, etc.' }
84
+ {
85
+ value: 'language',
86
+ label: 'Language',
87
+ hint: 'Syntax highlighting, indentation, etc.',
88
+ },
85
89
  ],
86
- required: true
90
+ required: true,
87
91
  });
88
92
  if (p.isCancel(extensionTypes)) {
89
93
  p.cancel('Cancelled.');
@@ -96,14 +100,14 @@ export async function promptUser() {
96
100
  author: String(author),
97
101
  repository: repositoryValue,
98
102
  license: license,
99
- types: extensionTypes
103
+ types: extensionTypes,
100
104
  };
101
105
  return options;
102
106
  }
103
107
  export async function promptThemeDetails() {
104
108
  const themeName = await p.text({
105
109
  message: 'Theme name:',
106
- placeholder: 'My Theme'
110
+ placeholder: 'My Theme',
107
111
  });
108
112
  if (p.isCancel(themeName)) {
109
113
  p.cancel('Cancelled.');
@@ -114,9 +118,9 @@ export async function promptThemeDetails() {
114
118
  options: [
115
119
  { value: 'dark', label: 'Dark' },
116
120
  { value: 'light', label: 'Light' },
117
- { value: 'both', label: 'Both (Dark & Light)' }
121
+ { value: 'both', label: 'Both (Dark & Light)' },
118
122
  ],
119
- initialValue: 'dark'
123
+ initialValue: 'dark',
120
124
  });
121
125
  if (p.isCancel(appearance)) {
122
126
  p.cancel('Cancelled.');
@@ -124,13 +128,13 @@ export async function promptThemeDetails() {
124
128
  }
125
129
  return {
126
130
  themeName: String(themeName),
127
- appearance: appearance
131
+ appearance: appearance,
128
132
  };
129
133
  }
130
134
  export async function promptLanguageDetails() {
131
135
  const languageName = await p.text({
132
136
  message: 'Language name:',
133
- placeholder: 'My Language'
137
+ placeholder: 'My Language',
134
138
  });
135
139
  if (p.isCancel(languageName)) {
136
140
  p.cancel('Cancelled.');
@@ -138,7 +142,7 @@ export async function promptLanguageDetails() {
138
142
  }
139
143
  const result = {
140
144
  languageName: String(languageName),
141
- languageId: String(languageName).toLowerCase().replace(/\s+/g, '-')
145
+ languageId: String(languageName).toLowerCase().replace(/\s+/g, '-'),
142
146
  };
143
147
  return result;
144
148
  }
package/dist/sync.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import os from 'os';
2
2
  import path from 'path';
3
- import fs from 'fs-extra';
4
3
  import * as p from '@clack/prompts';
4
+ import fs from 'fs-extra';
5
5
  import color from 'picocolors';
6
6
  import simpleGit from 'simple-git';
7
7
  import { resolveZedPaths } from './zed-paths.js';
@@ -16,7 +16,9 @@ async function readSyncConfig() {
16
16
  async function requireSyncConfig() {
17
17
  const config = await readSyncConfig();
18
18
  if (!config) {
19
- p.log.error(color.red('No sync config found. Run ') + color.cyan('zedx sync init') + color.red(' first.'));
19
+ p.log.error(color.red('No sync config found. Run ') +
20
+ color.cyan('zedx sync init') +
21
+ color.red(' first.'));
20
22
  process.exit(1);
21
23
  }
22
24
  return config;
@@ -35,6 +37,26 @@ async function withTempDir(fn) {
35
37
  await fs.remove(tmp);
36
38
  }
37
39
  }
40
+ // Prepare settings.json for pushing to the repo by stripping auto_install_extensions.
41
+ // That field is derived from extensions/index.json on pull, so storing it in the
42
+ // remote would create stale/conflicting data across machines.
43
+ async function prepareSettingsForPush(localSettingsPath, repoSettingsPath) {
44
+ const raw = await fs.readFile(localSettingsPath, 'utf-8');
45
+ const stripped = raw.replace(/\/\/[^\n]*/g, '');
46
+ let settingsObj = {};
47
+ try {
48
+ settingsObj = JSON.parse(stripped);
49
+ }
50
+ catch {
51
+ // If we can't parse it (e.g. complex comments), push as-is
52
+ await fs.ensureDir(path.dirname(repoSettingsPath));
53
+ await fs.copy(localSettingsPath, repoSettingsPath, { overwrite: true });
54
+ return;
55
+ }
56
+ delete settingsObj['auto_install_extensions'];
57
+ await fs.ensureDir(path.dirname(repoSettingsPath));
58
+ await fs.writeFile(repoSettingsPath, JSON.stringify(settingsObj, null, 4), 'utf-8');
59
+ }
38
60
  // Extension merge helper
39
61
  async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPath, silent = false) {
40
62
  // Backup existing settings
@@ -48,7 +70,7 @@ async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPa
48
70
  if (await fs.pathExists(repoExtensions)) {
49
71
  try {
50
72
  const indexJson = (await fs.readJson(repoExtensions));
51
- const extensionIds = Object.keys(indexJson.extensions ?? {}).filter((id) => !indexJson.extensions[id]?.dev);
73
+ const extensionIds = Object.keys(indexJson.extensions ?? {}).filter(id => !indexJson.extensions[id]?.dev);
52
74
  if (extensionIds.length > 0) {
53
75
  const stripped = settingsJson.replace(/\/\/[^\n]*/g, '');
54
76
  let settingsObj = {};
@@ -59,12 +81,23 @@ async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPa
59
81
  if (!silent)
60
82
  p.log.warn(color.yellow('Could not parse settings.json — skipping extension merge.'));
61
83
  }
62
- const autoInstall = {};
84
+ // Preserve any existing entries (e.g. false entries for "never install"),
85
+ // then add true for every extension recorded in index.json.
86
+ const existing = typeof settingsObj['auto_install_extensions'] === 'object' &&
87
+ settingsObj['auto_install_extensions'] !== null
88
+ ? settingsObj['auto_install_extensions']
89
+ : {};
90
+ const autoInstall = { ...existing };
63
91
  for (const id of extensionIds) {
64
- autoInstall[id] = true;
92
+ // Only set to true if there is no explicit user preference already
93
+ if (!(id in autoInstall)) {
94
+ autoInstall[id] = true;
95
+ }
65
96
  }
66
97
  settingsObj['auto_install_extensions'] = autoInstall;
67
98
  settingsJson = JSON.stringify(settingsObj, null, 4);
99
+ if (!silent)
100
+ p.log.info(`Injected ${color.cyan(String(extensionIds.length))} extension(s) into ${color.dim('auto_install_extensions')}`);
68
101
  }
69
102
  }
70
103
  catch {
@@ -104,13 +137,13 @@ export async function syncStatus() {
104
137
  {
105
138
  repoPath: path.join(tmp, 'settings.json'),
106
139
  localPath: zedPaths.settings,
107
- label: 'settings.json'
140
+ label: 'settings.json',
108
141
  },
109
142
  {
110
143
  repoPath: path.join(tmp, 'extensions', 'index.json'),
111
144
  localPath: zedPaths.extensions,
112
- label: 'extensions/index.json'
113
- }
145
+ label: 'extensions/index.json',
146
+ },
114
147
  ];
115
148
  for (const file of files) {
116
149
  const localExists = await fs.pathExists(file.localPath);
@@ -157,13 +190,13 @@ export async function syncInit() {
157
190
  const repo = await p.text({
158
191
  message: 'GitHub repo URL (SSH or HTTPS)',
159
192
  placeholder: 'https://github.com/you/zed-config.git',
160
- validate: (v) => {
193
+ validate: v => {
161
194
  if (!v.trim())
162
195
  return 'Repo URL is required';
163
196
  if (!v.startsWith('https://') && !v.startsWith('git@')) {
164
197
  return 'Must be a valid HTTPS or SSH git URL';
165
198
  }
166
- }
199
+ },
167
200
  });
168
201
  if (p.isCancel(repo)) {
169
202
  p.cancel('Cancelled.');
@@ -172,7 +205,7 @@ export async function syncInit() {
172
205
  const branch = await p.text({
173
206
  message: 'Branch name',
174
207
  placeholder: 'main',
175
- defaultValue: 'main'
208
+ defaultValue: 'main',
176
209
  });
177
210
  if (p.isCancel(branch)) {
178
211
  p.cancel('Cancelled.');
@@ -190,7 +223,7 @@ export async function syncInit() {
190
223
  }
191
224
  const config = {
192
225
  syncRepo: repo.trim(),
193
- branch: (branch || 'main').trim()
226
+ branch: (branch || 'main').trim(),
194
227
  };
195
228
  await writeSyncConfig(config);
196
229
  p.outro(`${color.green('✓')} Sync config saved to ${color.cyan(ZEDX_CONFIG_PATH)}\n\n` +
@@ -215,7 +248,7 @@ export async function runSync(opts = {}) {
215
248
  success: (msg) => {
216
249
  if (!silent)
217
250
  p.log.success(msg);
218
- }
251
+ },
219
252
  };
220
253
  if (!silent)
221
254
  p.intro(color.bold('zedx sync'));
@@ -225,7 +258,7 @@ export async function runSync(opts = {}) {
225
258
  const spinner = silent
226
259
  ? {
227
260
  start: (m) => console.error(`[zedx] ${m}`),
228
- stop: (m) => console.error(`[zedx] ${m}`)
261
+ stop: (m) => console.error(`[zedx] ${m}`),
229
262
  }
230
263
  : p.spinner();
231
264
  await withTempDir(async (tmp) => {
@@ -249,13 +282,13 @@ export async function runSync(opts = {}) {
249
282
  {
250
283
  repoPath: path.join(tmp, 'settings.json'),
251
284
  localPath: zedPaths.settings,
252
- label: 'settings.json'
285
+ label: 'settings.json',
253
286
  },
254
287
  {
255
288
  repoPath: path.join(tmp, 'extensions', 'index.json'),
256
289
  localPath: zedPaths.extensions,
257
- label: 'extensions/index.json'
258
- }
290
+ label: 'extensions/index.json',
291
+ },
259
292
  ];
260
293
  let anyChanges = false;
261
294
  for (const file of files) {
@@ -269,8 +302,13 @@ export async function runSync(opts = {}) {
269
302
  // Remote doesn't have it yet — push local
270
303
  if (localExists && !remoteFileExists) {
271
304
  log.info(`${file.label}: ${color.green('pushing')} (not in remote yet)`);
272
- await fs.ensureDir(path.dirname(file.repoPath));
273
- await fs.copy(file.localPath, file.repoPath, { overwrite: true });
305
+ if (file.label === 'settings.json') {
306
+ await prepareSettingsForPush(file.localPath, file.repoPath);
307
+ }
308
+ else {
309
+ await fs.ensureDir(path.dirname(file.repoPath));
310
+ await fs.copy(file.localPath, file.repoPath, { overwrite: true });
311
+ }
274
312
  anyChanges = true;
275
313
  continue;
276
314
  }
@@ -295,14 +333,21 @@ export async function runSync(opts = {}) {
295
333
  }
296
334
  // Detect which side changed since last sync via mtime
297
335
  const localMtime = (await fs.stat(file.localPath)).mtime;
298
- const remoteMtime = remoteFileExists ? (await fs.stat(file.repoPath)).mtime : new Date(0);
336
+ const remoteMtime = remoteFileExists
337
+ ? (await fs.stat(file.repoPath)).mtime
338
+ : new Date(0);
299
339
  const localChanged = !lastSync || localMtime > lastSync;
300
340
  const remoteChanged = !lastSync || remoteMtime > lastSync;
301
341
  if (localChanged && !remoteChanged) {
302
342
  // Only local changed → push
303
343
  log.info(`${file.label}: ${color.green('pushing')} (local is newer)`);
304
- await fs.ensureDir(path.dirname(file.repoPath));
305
- await fs.copy(file.localPath, file.repoPath, { overwrite: true });
344
+ if (file.label === 'settings.json') {
345
+ await prepareSettingsForPush(file.localPath, file.repoPath);
346
+ }
347
+ else {
348
+ await fs.ensureDir(path.dirname(file.repoPath));
349
+ await fs.copy(file.localPath, file.repoPath, { overwrite: true });
350
+ }
306
351
  anyChanges = true;
307
352
  }
308
353
  else if (remoteChanged && !localChanged) {
@@ -321,26 +366,31 @@ export async function runSync(opts = {}) {
321
366
  if (silent) {
322
367
  // Daemon can't prompt — local wins, will be pushed
323
368
  log.warn(`${file.label}: conflict detected in unattended mode — keeping local.`);
324
- await fs.ensureDir(path.dirname(file.repoPath));
325
- await fs.copy(file.localPath, file.repoPath, { overwrite: true });
369
+ if (file.label === 'settings.json') {
370
+ await prepareSettingsForPush(file.localPath, file.repoPath);
371
+ }
372
+ else {
373
+ await fs.ensureDir(path.dirname(file.repoPath));
374
+ await fs.copy(file.localPath, file.repoPath, { overwrite: true });
375
+ }
326
376
  anyChanges = true;
327
377
  }
328
378
  else {
329
- p.log.warn(color.yellow(`${file.label}: both local and remote changed.`));
379
+ p.log.warn(color.yellow(`conflict between local and remote ${file.label}`));
330
380
  const choice = await p.select({
331
381
  message: `Which version of ${color.bold(file.label)} should win?`,
332
382
  options: [
333
383
  {
334
384
  value: 'local',
335
385
  label: 'Keep local',
336
- hint: `modified ${localMtime.toLocaleString()}`
386
+ hint: `modified ${localMtime.toLocaleString()}`,
337
387
  },
338
388
  {
339
389
  value: 'remote',
340
390
  label: 'Use remote',
341
- hint: `modified ${remoteMtime.toLocaleString()}`
342
- }
343
- ]
391
+ hint: `modified ${remoteMtime.toLocaleString()}`,
392
+ },
393
+ ],
344
394
  });
345
395
  if (p.isCancel(choice)) {
346
396
  p.cancel('Cancelled.');
@@ -348,8 +398,13 @@ export async function runSync(opts = {}) {
348
398
  }
349
399
  if (choice === 'local') {
350
400
  p.log.info(`${file.label}: ${color.green('keeping local, will push')}`);
351
- await fs.ensureDir(path.dirname(file.repoPath));
352
- await fs.copy(file.localPath, file.repoPath, { overwrite: true });
401
+ if (file.label === 'settings.json') {
402
+ await prepareSettingsForPush(file.localPath, file.repoPath);
403
+ }
404
+ else {
405
+ await fs.ensureDir(path.dirname(file.repoPath));
406
+ await fs.copy(file.localPath, file.repoPath, { overwrite: true });
407
+ }
353
408
  anyChanges = true;
354
409
  }
355
410
  else {
package/package.json CHANGED
@@ -1,56 +1,59 @@
1
1
  {
2
2
  "name": "zedx",
3
- "version": "0.6.0",
4
- "description": "Boilerplate generator for Zed Edittor extensions.",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "bin": {
8
- "zedx": "dist/index.js"
9
- },
10
- "files": [
11
- "dist"
12
- ],
3
+ "version": "0.7.0",
4
+ "description": "Scaffold Zed Editor extensions and sync your settings across machines.",
13
5
  "keywords": [
14
- "zed",
15
- "zed-editor",
16
- "extension",
17
6
  "boilerplate",
18
- "scaffold"
7
+ "extension",
8
+ "scaffold",
9
+ "zed",
10
+ "zed-editor"
19
11
  ],
12
+ "homepage": "https://github.com/tahayvr/zedx#readme",
13
+ "license": "Apache-2.0",
20
14
  "author": "Taha Nejad <taha@noiserandom.com>",
21
15
  "repository": {
22
16
  "type": "git",
23
17
  "url": "https://github.com/tahayvr/zedx.git"
24
18
  },
25
- "homepage": "https://github.com/tahayvr/zedx#readme",
26
- "license": "Apache-2.0",
19
+ "bin": {
20
+ "zedx": "dist/index.js"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "type": "module",
26
+ "main": "dist/index.js",
27
27
  "publishConfig": {
28
28
  "access": "public"
29
29
  },
30
- "engines": {
31
- "node": ">=18"
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"
32
37
  },
33
38
  "devDependencies": {
34
39
  "@types/ejs": "^3.1.5",
35
40
  "@types/fs-extra": "^11.0.4",
36
41
  "@types/node": "^25.2.3",
42
+ "oxfmt": "^0.42.0",
37
43
  "oxlint": "^1.57.0",
38
44
  "tsx": "^4.21.0",
39
45
  "typescript": "^5.9.3"
40
46
  },
41
- "dependencies": {
42
- "@clack/prompts": "^0.10.1",
43
- "commander": "^14.0.3",
44
- "ejs": "^4.0.1",
45
- "fs-extra": "^11.3.3",
46
- "picocolors": "^1.1.1",
47
- "simple-git": "^3.33.0"
47
+ "engines": {
48
+ "node": ">=18"
48
49
  },
49
50
  "scripts": {
50
51
  "build": "tsc && cp -r src/templates dist/",
51
52
  "start": "node dist/index.js",
52
53
  "dev": "tsx src/index.ts",
53
54
  "lint": "oxlint",
54
- "lint:fix": "oxlint --fix"
55
+ "lint:fix": "oxlint --fix",
56
+ "fmt": "oxfmt",
57
+ "fmt:check": "oxfmt --check"
55
58
  }
56
59
  }