editor-profile-sync 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sbetav
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # editor-profile-sync
2
+
3
+ Cross-platform CLI to share your VS Code-based editor profile between editors.
4
+
5
+ You can sync:
6
+ - extensions
7
+ - snippets
8
+ - `settings.json`
9
+
10
+ ## Requirements
11
+
12
+ - **Node.js** 18 or newer
13
+ - **npm** (comes with Node.js)
14
+ - At least one [supported editor](#supported-editors) installed, with its CLI on your PATH (e.g. `code`, `cursor`)
15
+
16
+ ## Install
17
+
18
+ Clone this repo, then install dependencies:
19
+
20
+ ```bash
21
+ npm install
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ From the project folder:
27
+
28
+ ```bash
29
+ npm start
30
+ ```
31
+
32
+ or:
33
+
34
+ ```bash
35
+ node index.js
36
+ ```
37
+
38
+ After linking (`npm link` in this folder), run from anywhere:
39
+
40
+ ```bash
41
+ editor-profile-sync
42
+ ```
43
+
44
+ ### Options
45
+
46
+ | Option | Description |
47
+ | ----------------- | ------------------- |
48
+ | `-h`, `--help` | Show help message |
49
+ | `-v`, `--version` | Show version number |
50
+
51
+ ## How it works
52
+
53
+ 1. Detect installed editors (only editors with CLI on PATH are shown).
54
+ 2. Choose a source editor.
55
+ 3. Choose what to share:
56
+ - `Extensions`
57
+ - `settings.json`
58
+ - `Snippets`
59
+ 4. Choose mode(s) for selected item types:
60
+ - Extensions:
61
+ - Install on top of existing (additive)
62
+ - Exact sync (replace all extensions)
63
+ - Snippets:
64
+ - Merge (source snippets override key conflicts in matching snippet files)
65
+ - Replace (target snippets folder is replaced)
66
+ 5. Choose one or more target editors.
67
+ 6. Run sync.
68
+
69
+ ### Settings merge behavior
70
+
71
+ For `settings.json`, each target is merged as:
72
+
73
+ ```js
74
+ const merged = {
75
+ ...targetSettings,
76
+ ...sourceSettings
77
+ }
78
+ ```
79
+
80
+ What this means:
81
+ - Shared/source settings win on key conflicts.
82
+ - Existing unrelated target settings stay.
83
+ - Editor-specific keys are preserved unless your source uses the same key.
84
+
85
+ Example:
86
+
87
+ Target:
88
+
89
+ ```json
90
+ {
91
+ "terminal.integrated.fontSize": 14
92
+ }
93
+ ```
94
+
95
+ Source:
96
+
97
+ ```json
98
+ {
99
+ "editor.formatOnSave": true
100
+ }
101
+ ```
102
+
103
+ Result:
104
+
105
+ ```json
106
+ {
107
+ "terminal.integrated.fontSize": 14,
108
+ "editor.formatOnSave": true
109
+ }
110
+ ```
111
+
112
+ ## Supported editors
113
+
114
+ These editors are currently supported:
115
+
116
+ | Editor | CLI command |
117
+ | ----------- | ------------- |
118
+ | Antigravity | `antigravity` |
119
+ | Cursor | `cursor` |
120
+ | Kiro | `kiro` |
121
+ | Trae | `trae` |
122
+ | VS Code | `code` |
123
+ | Windsurf | `windsurf` |
124
+
125
+ Each editor must be installed and available in your terminal (e.g. `code`, `cursor`).
126
+
127
+ ## Generated files
128
+
129
+ - `extensions.txt` is created when syncing extensions and contains the exported source extension IDs.
130
+
131
+ ## License
132
+
133
+ MIT
Binary file
package/index.js ADDED
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { dirname, join } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import ora from "ora";
7
+ import chalk from "chalk";
8
+ import {
9
+ EDITORS,
10
+ EXTENSION_MODES,
11
+ EXTENSIONS_FILE,
12
+ SNIPPET_MODES,
13
+ SYNC_ITEMS,
14
+ } from "./lib/constants.js";
15
+ import { isEditorInstalled } from "./lib/editor-cli.js";
16
+ import {
17
+ exportExtensions,
18
+ readExtensionsFile,
19
+ syncExtensions,
20
+ } from "./lib/extensions-sync.js";
21
+ import { getSettingsPath, getSnippetsPath } from "./lib/profile-paths.js";
22
+ import { readSourceSettings, syncSettings } from "./lib/settings-sync.js";
23
+ import { syncSnippets } from "./lib/snippets-sync.js";
24
+ import {
25
+ promptExtensionMode,
26
+ promptSnippetMode,
27
+ promptSourceEditor,
28
+ promptSyncItems,
29
+ promptTargetEditors,
30
+ } from "./lib/prompts.js";
31
+
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = dirname(__filename);
34
+ const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
35
+
36
+ async function main() {
37
+ const args = process.argv.slice(2);
38
+
39
+ if (args.includes("--help") || args.includes("-h")) {
40
+ console.log(`
41
+ ${pkg.name} v${pkg.version}
42
+ ${pkg.description}
43
+
44
+ Usage: ${Object.keys(pkg.bin)[0]} [options]
45
+
46
+ Options:
47
+ -h, --help Show this help message
48
+ -v, --version Show version number
49
+ `);
50
+ return;
51
+ }
52
+
53
+ if (args.includes("--version") || args.includes("-v")) {
54
+ console.log(pkg.version);
55
+ return;
56
+ }
57
+
58
+ console.log("\n Editor profile sync\n");
59
+
60
+ const detectSpinner = ora({
61
+ text: "Detecting installed editors...",
62
+ color: "cyan",
63
+ }).start();
64
+
65
+ const availableEditors = EDITORS.filter((editor) =>
66
+ isEditorInstalled(editor),
67
+ );
68
+
69
+ if (availableEditors.length === 0) {
70
+ detectSpinner.fail("No supported editors found on PATH.");
71
+ console.log(
72
+ chalk.yellow(
73
+ "\n Install at least one supported editor and make sure its CLI is on your PATH.\n",
74
+ ),
75
+ );
76
+ process.exit(1);
77
+ }
78
+
79
+ detectSpinner.succeed(
80
+ `Found ${availableEditors.length} editor(s): ${availableEditors
81
+ .map((e) => e.name)
82
+ .join(", ")}`,
83
+ );
84
+ console.log("");
85
+
86
+ const sourceId = await promptSourceEditor(availableEditors);
87
+ const sourceEditor = availableEditors.find((e) => e.id === sourceId);
88
+
89
+ const syncItems = await promptSyncItems(SYNC_ITEMS, chalk);
90
+
91
+ let extensionMode = "additive";
92
+ if (syncItems.includes("extensions")) {
93
+ extensionMode = await promptExtensionMode(EXTENSION_MODES);
94
+ }
95
+
96
+ let snippetMode = "merge";
97
+ if (syncItems.includes("snippets")) {
98
+ snippetMode = await promptSnippetMode(SNIPPET_MODES);
99
+ }
100
+
101
+ const targetChoices = availableEditors
102
+ .filter((e) => e.id !== sourceId)
103
+ .map((e) => ({ name: e.name, value: e.id }));
104
+
105
+ if (targetChoices.length === 0) {
106
+ console.log(
107
+ chalk.yellow("\n No other installed editors found to sync to.\n"),
108
+ );
109
+ process.exit(1);
110
+ }
111
+
112
+ const targetIds = await promptTargetEditors(targetChoices, chalk);
113
+ const targetEditors = availableEditors.filter((e) =>
114
+ targetIds.includes(e.id),
115
+ );
116
+
117
+ const cwd = process.cwd();
118
+ const extensionsPath = join(cwd, EXTENSIONS_FILE);
119
+
120
+ let desiredExtensions = [];
121
+ if (syncItems.includes("extensions")) {
122
+ const exportSpinner = ora({
123
+ text: `Exporting extensions from ${sourceEditor.name}...`,
124
+ color: "cyan",
125
+ }).start();
126
+ try {
127
+ await exportExtensions(sourceEditor, extensionsPath);
128
+ desiredExtensions = readExtensionsFile(extensionsPath);
129
+ exportSpinner.succeed(
130
+ `Exported ${desiredExtensions.length} extensions to ${EXTENSIONS_FILE}`,
131
+ );
132
+ } catch (err) {
133
+ exportSpinner.fail("Extension export failed");
134
+ console.error(" Error:", err.message, "\n");
135
+ process.exit(1);
136
+ }
137
+ }
138
+
139
+ let sourceSettings = {};
140
+ if (syncItems.includes("settings")) {
141
+ try {
142
+ sourceSettings = readSourceSettings(getSettingsPath(sourceEditor));
143
+ } catch (err) {
144
+ console.error(` ${err.message}\n`);
145
+ process.exit(1);
146
+ }
147
+ }
148
+
149
+ let sourceSnippetsDir = "";
150
+ if (syncItems.includes("snippets")) {
151
+ sourceSnippetsDir = getSnippetsPath(sourceEditor);
152
+ if (!existsSync(sourceSnippetsDir)) {
153
+ console.error(
154
+ ` Source snippets folder not found: ${sourceSnippetsDir}\n`,
155
+ );
156
+ process.exit(1);
157
+ }
158
+ }
159
+
160
+ console.log("");
161
+
162
+ for (const editor of targetEditors) {
163
+ try {
164
+ console.log(chalk.bold(editor.name));
165
+
166
+ if (syncItems.includes("extensions")) {
167
+ const spinner = ora({
168
+ text: `${chalk.bold(editor.name)}: Installing extensions...`,
169
+ color: "cyan",
170
+ }).start();
171
+ const result = await syncExtensions(
172
+ editor,
173
+ desiredExtensions,
174
+ extensionMode,
175
+ (i, extensionId) => {
176
+ spinner.text = `${chalk.bold(editor.name)}: [${i + 1}/${desiredExtensions.length}] Installing ${extensionId}`;
177
+ },
178
+ );
179
+ const parts = [chalk.green(`${result.synced} extensions synced`)];
180
+ if (result.failed.length > 0) {
181
+ parts.push(chalk.red(`${result.failed.length} failed`));
182
+ }
183
+ spinner.succeed(`${chalk.bold(editor.name)}: ${parts.join(", ")}`);
184
+ if (result.failed.length > 0) {
185
+ console.log(
186
+ ` ${chalk.red("Failed:")} ${result.failed.join(", ")}`,
187
+ );
188
+ }
189
+ }
190
+
191
+ if (syncItems.includes("settings")) {
192
+ const spinner = ora({
193
+ text: `${chalk.bold(editor.name)}: Syncing settings.json...`,
194
+ color: "cyan",
195
+ }).start();
196
+ try {
197
+ const targetPath = getSettingsPath(editor, { createIfMissing: true });
198
+ const merged = syncSettings(sourceSettings, targetPath);
199
+ spinner.succeed(
200
+ `${chalk.bold(editor.name)}: settings.json synced (${Object.keys(merged).length} keys)`,
201
+ );
202
+ } catch (err) {
203
+ spinner.fail(
204
+ `${chalk.bold(editor.name)}: settings sync failed (${err.message})`,
205
+ );
206
+ }
207
+ }
208
+
209
+ if (syncItems.includes("snippets")) {
210
+ const spinner = ora({
211
+ text: `${chalk.bold(editor.name)}: Syncing snippets (${snippetMode})...`,
212
+ color: "cyan",
213
+ }).start();
214
+ try {
215
+ const targetSnippetsDir = getSnippetsPath(editor, {
216
+ createIfMissing: true,
217
+ });
218
+ const fileCount = syncSnippets(
219
+ sourceSnippetsDir,
220
+ targetSnippetsDir,
221
+ snippetMode,
222
+ );
223
+ spinner.succeed(
224
+ `${chalk.bold(editor.name)}: snippets synced (${fileCount} file${fileCount === 1 ? "" : "s"}, ${snippetMode})`,
225
+ );
226
+ } catch (err) {
227
+ spinner.fail(
228
+ `${chalk.bold(editor.name)}: snippets sync failed (${err.message})`,
229
+ );
230
+ }
231
+ }
232
+
233
+ console.log("");
234
+ } catch (err) {
235
+ console.log(" ", editor.name, "error:", err.message, "\n");
236
+ }
237
+ }
238
+
239
+ console.log(chalk.green("✔ Sync complete.\n"));
240
+ }
241
+
242
+ main().catch((err) => {
243
+ console.error(err);
244
+ process.exit(1);
245
+ });
@@ -0,0 +1,66 @@
1
+ export const EXTENSIONS_FILE = "extensions.txt";
2
+ export const SETTINGS_FILE = "settings.json";
3
+ export const SNIPPETS_DIR = "snippets";
4
+
5
+ // Sorted alphabetically by display name.
6
+ export const EDITORS = [
7
+ { id: "antigravity", name: "Antigravity", cmd: "antigravity" },
8
+ { id: "cursor", name: "Cursor", cmd: "cursor" },
9
+ { id: "kiro", name: "Kiro", cmd: "kiro" },
10
+ { id: "trae", name: "Trae", cmd: "trae" },
11
+ { id: "code", name: "VS Code", cmd: "code" },
12
+ { id: "windsurf", name: "Windsurf", cmd: "windsurf" },
13
+ ];
14
+
15
+ export const EXTENSION_MODES = [
16
+ {
17
+ value: "additive",
18
+ name: "Install extensions on top of existing (Recommended)",
19
+ short: "Additive",
20
+ },
21
+ {
22
+ value: "strict",
23
+ name: "Exact sync (Replace all extensions)",
24
+ short: "Strict",
25
+ },
26
+ ];
27
+
28
+ export const SNIPPET_MODES = [
29
+ {
30
+ value: "merge",
31
+ name: "Merge snippet files (Recommended)",
32
+ short: "Merge",
33
+ },
34
+ {
35
+ value: "replace",
36
+ name: "Replace all target snippets",
37
+ short: "Replace",
38
+ },
39
+ ];
40
+
41
+ export const SYNC_ITEMS = [
42
+ {
43
+ value: "extensions",
44
+ name: "Extensions",
45
+ short: "Extensions",
46
+ },
47
+ {
48
+ value: "settings",
49
+ name: "settings.json",
50
+ short: "settings.json",
51
+ },
52
+ {
53
+ value: "snippets",
54
+ name: "Snippets",
55
+ short: "Snippets",
56
+ },
57
+ ];
58
+
59
+ export const EDITOR_DATA_DIRS = {
60
+ antigravity: ["Antigravity"],
61
+ code: ["Code", "Code - OSS"],
62
+ cursor: ["Cursor"],
63
+ kiro: ["Kiro"],
64
+ trae: ["Trae"],
65
+ windsurf: ["Windsurf"],
66
+ };
@@ -0,0 +1,78 @@
1
+ import { execSync, spawn } from "child_process";
2
+
3
+ export function isEditorInstalled(editor) {
4
+ const cmd = process.platform === "win32" ? "where" : "which";
5
+ try {
6
+ execSync(`${cmd} ${editor.cmd}`, { stdio: "ignore" });
7
+ return true;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ function escapeShellArg(arg) {
14
+ const s = String(arg);
15
+ return s.includes(" ") || s.includes('"') ? `"${s.replace(/"/g, '""')}"` : s;
16
+ }
17
+
18
+ function runEditorCmdAsync(editor, args, options = {}) {
19
+ const { ignoreError = false } = options;
20
+ const cmdString = [editor.cmd, ...args.map(escapeShellArg)].join(" ");
21
+
22
+ return new Promise((resolve, reject) => {
23
+ const child = spawn(cmdString, [], {
24
+ shell: true,
25
+ stdio: ["inherit", "pipe", "pipe"],
26
+ });
27
+
28
+ let stdout = "";
29
+ let stderr = "";
30
+
31
+ if (child.stdout) {
32
+ child.stdout.on("data", (d) => {
33
+ stdout += d.toString();
34
+ });
35
+ }
36
+
37
+ if (child.stderr) {
38
+ child.stderr.on("data", (d) => {
39
+ stderr += d.toString();
40
+ });
41
+ }
42
+
43
+ child.on("close", (code) => {
44
+ if (code === 0) resolve(stdout);
45
+ else if (ignoreError) resolve(null);
46
+ else reject(new Error(stderr || `Exit ${code}`));
47
+ });
48
+
49
+ child.on("error", (err) => reject(err));
50
+ });
51
+ }
52
+
53
+ export async function getInstalledExtensions(editor) {
54
+ const out = await runEditorCmdAsync(editor, ["--list-extensions"], {
55
+ ignoreError: true,
56
+ });
57
+ if (out == null) return null;
58
+ return out
59
+ .split(/\r?\n/)
60
+ .map((s) => s.trim())
61
+ .filter(Boolean);
62
+ }
63
+
64
+ export async function installExtension(editor, extensionId) {
65
+ const out = await runEditorCmdAsync(
66
+ editor,
67
+ ["--install-extension", extensionId],
68
+ {
69
+ ignoreError: true,
70
+ },
71
+ );
72
+ return out !== null;
73
+ }
74
+
75
+ export function uninstallExtension(editor, extensionId) {
76
+ const cmd = [editor.cmd, "--uninstall-extension", extensionId].join(" ");
77
+ execSync(cmd, { stdio: "ignore" });
78
+ }
@@ -0,0 +1,61 @@
1
+ import { readFileSync, writeFileSync } from "fs";
2
+ import {
3
+ getInstalledExtensions,
4
+ installExtension,
5
+ uninstallExtension,
6
+ } from "./editor-cli.js";
7
+
8
+ export async function exportExtensions(sourceEditor, filePath) {
9
+ const list = await getInstalledExtensions(sourceEditor);
10
+ if (list == null) {
11
+ throw new Error(`${sourceEditor.name} CLI not found or failed.`);
12
+ }
13
+
14
+ const sorted = [...list].sort();
15
+ writeFileSync(
16
+ filePath,
17
+ sorted.join("\n") + (sorted.length ? "\n" : ""),
18
+ "utf-8",
19
+ );
20
+
21
+ return sorted;
22
+ }
23
+
24
+ export function readExtensionsFile(filePath) {
25
+ const content = readFileSync(filePath, "utf-8");
26
+ return content
27
+ .split(/\r?\n/)
28
+ .map((s) => s.trim())
29
+ .filter(Boolean);
30
+ }
31
+
32
+ export async function syncExtensions(editor, desired, mode, onProgress) {
33
+ const current = await getInstalledExtensions(editor);
34
+ if (current == null) {
35
+ throw new Error(`${editor.name} CLI not found or failed.`);
36
+ }
37
+
38
+ if (mode === "strict") {
39
+ const toRemove = current.filter((id) => !desired.includes(id));
40
+ for (const extensionId of toRemove) {
41
+ try {
42
+ uninstallExtension(editor, extensionId);
43
+ } catch {
44
+ // Keep going; failures are naturally reflected when reinstalling.
45
+ }
46
+ }
47
+ }
48
+
49
+ let synced = 0;
50
+ const failed = [];
51
+
52
+ for (let i = 0; i < desired.length; i++) {
53
+ const extensionId = desired[i];
54
+ if (onProgress) onProgress(i, extensionId);
55
+ const ok = await installExtension(editor, extensionId);
56
+ if (ok) synced++;
57
+ else failed.push(extensionId);
58
+ }
59
+
60
+ return { synced, failed };
61
+ }
@@ -0,0 +1,36 @@
1
+ import { existsSync, mkdirSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import { EDITOR_DATA_DIRS, SETTINGS_FILE, SNIPPETS_DIR } from "./constants.js";
5
+
6
+ function getConfigBaseDir() {
7
+ if (process.platform === "win32") {
8
+ return process.env.APPDATA || join(homedir(), "AppData", "Roaming");
9
+ }
10
+ if (process.platform === "darwin") {
11
+ return join(homedir(), "Library", "Application Support");
12
+ }
13
+ return process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
14
+ }
15
+
16
+ function getEditorUserDir(editor, { createIfMissing = false } = {}) {
17
+ const base = getConfigBaseDir();
18
+ const names = EDITOR_DATA_DIRS[editor.id] || [editor.name];
19
+ const candidates = names.map((name) => join(base, name, "User"));
20
+ const existing = candidates.find((p) => existsSync(p));
21
+ const selected = existing || candidates[0];
22
+
23
+ if (createIfMissing) {
24
+ mkdirSync(selected, { recursive: true });
25
+ }
26
+
27
+ return selected;
28
+ }
29
+
30
+ export function getSettingsPath(editor, options = {}) {
31
+ return join(getEditorUserDir(editor, options), SETTINGS_FILE);
32
+ }
33
+
34
+ export function getSnippetsPath(editor, options = {}) {
35
+ return join(getEditorUserDir(editor, options), SNIPPETS_DIR);
36
+ }
package/lib/prompts.js ADDED
@@ -0,0 +1,96 @@
1
+ import inquirer from "inquirer";
2
+ import CheckboxPrompt from "inquirer/lib/prompts/checkbox.js";
3
+
4
+ class CleanCheckboxPrompt extends CheckboxPrompt {
5
+ constructor(questions, rl, answers) {
6
+ super(questions, rl, answers);
7
+ this.dontShowHints = true;
8
+ }
9
+ }
10
+
11
+ let isRegistered = false;
12
+
13
+ function ensurePromptRegistered() {
14
+ if (isRegistered) return;
15
+ inquirer.registerPrompt("clean-checkbox", CleanCheckboxPrompt);
16
+ isRegistered = true;
17
+ }
18
+
19
+ const CHECKBOX_HINT =
20
+ "(Press <space> to select, <a> to toggle all, <enter> to continue)";
21
+
22
+ export async function promptSourceEditor(availableEditors) {
23
+ ensurePromptRegistered();
24
+ const answer = await inquirer.prompt([
25
+ {
26
+ type: "list",
27
+ name: "sourceId",
28
+ message: "Share from:",
29
+ choices: availableEditors.map((e) => ({ name: e.name, value: e.id })),
30
+ pageSize: 10,
31
+ },
32
+ ]);
33
+ return answer.sourceId;
34
+ }
35
+
36
+ export async function promptSyncItems(syncItems, chalk) {
37
+ ensurePromptRegistered();
38
+ const answer = await inquirer.prompt([
39
+ {
40
+ type: "clean-checkbox",
41
+ name: "selectedItems",
42
+ message: `Share: ${chalk.dim(CHECKBOX_HINT)}`,
43
+ choices: syncItems,
44
+ validate: (v) => (v.length ? true : "Select at least one item."),
45
+ },
46
+ ]);
47
+ return answer.selectedItems;
48
+ }
49
+
50
+ export async function promptExtensionMode(extensionModes) {
51
+ const answer = await inquirer.prompt([
52
+ {
53
+ type: "list",
54
+ name: "extensionMode",
55
+ message: "Extension mode:",
56
+ choices: extensionModes.map((m) => ({
57
+ name: m.name,
58
+ value: m.value,
59
+ short: m.short,
60
+ })),
61
+ default: "additive",
62
+ },
63
+ ]);
64
+ return answer.extensionMode;
65
+ }
66
+
67
+ export async function promptSnippetMode(snippetModes) {
68
+ const answer = await inquirer.prompt([
69
+ {
70
+ type: "list",
71
+ name: "snippetMode",
72
+ message: "Snippet mode:",
73
+ choices: snippetModes.map((m) => ({
74
+ name: m.name,
75
+ value: m.value,
76
+ short: m.short,
77
+ })),
78
+ default: "merge",
79
+ },
80
+ ]);
81
+ return answer.snippetMode;
82
+ }
83
+
84
+ export async function promptTargetEditors(targetChoices, chalk) {
85
+ ensurePromptRegistered();
86
+ const answer = await inquirer.prompt([
87
+ {
88
+ type: "clean-checkbox",
89
+ name: "targetIds",
90
+ message: `Sync to: ${chalk.dim(CHECKBOX_HINT)}`,
91
+ choices: targetChoices,
92
+ validate: (v) => (v.length ? true : "Select at least one editor."),
93
+ },
94
+ ]);
95
+ return answer.targetIds;
96
+ }
@@ -0,0 +1,74 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { dirname } from "path";
3
+ import { parse, printParseErrorCode } from "jsonc-parser";
4
+
5
+ function isPlainObject(value) {
6
+ return value && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+
9
+ function readJsonFile(filePath, fallback = {}) {
10
+ if (!existsSync(filePath)) return fallback;
11
+ const raw = readFileSync(filePath, "utf-8");
12
+ const errors = [];
13
+ const parsed = parse(raw, errors, {
14
+ allowTrailingComma: true,
15
+ disallowComments: false,
16
+ });
17
+
18
+ if (errors.length > 0) {
19
+ const first = errors[0];
20
+ const position = getLineAndColumn(raw, first.offset);
21
+ throw new Error(
22
+ `${filePath} parse error (${printParseErrorCode(first.error)}) at line ${position.line}, column ${position.column}.`,
23
+ );
24
+ }
25
+
26
+ if (!isPlainObject(parsed)) {
27
+ throw new Error(`${filePath} must be a JSON object.`);
28
+ }
29
+ return parsed;
30
+ }
31
+
32
+ function getLineAndColumn(text, offset) {
33
+ let line = 1;
34
+ let column = 1;
35
+ for (let i = 0; i < offset; i++) {
36
+ if (text[i] === "\n") {
37
+ line++;
38
+ column = 1;
39
+ } else {
40
+ column++;
41
+ }
42
+ }
43
+ return { line, column };
44
+ }
45
+
46
+ function writeJsonFile(filePath, data) {
47
+ mkdirSync(dirname(filePath), { recursive: true });
48
+ writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
49
+ }
50
+
51
+ export function readSourceSettings(sourcePath) {
52
+ if (!existsSync(sourcePath)) {
53
+ throw new Error(`Source settings not found: ${sourcePath}`);
54
+ }
55
+ return readJsonFile(sourcePath, {});
56
+ }
57
+
58
+ export function syncSettings(sourceSettings, targetPath) {
59
+ const targetSettings = readJsonFile(targetPath, {});
60
+ const merged = {
61
+ ...targetSettings,
62
+ ...sourceSettings,
63
+ };
64
+ writeJsonFile(targetPath, merged);
65
+ return merged;
66
+ }
67
+
68
+ export function readJsonObject(filePath, fallback = {}) {
69
+ return readJsonFile(filePath, fallback);
70
+ }
71
+
72
+ export function writeJsonObject(filePath, data) {
73
+ writeJsonFile(filePath, data);
74
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ cpSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readdirSync,
6
+ rmSync,
7
+ statSync,
8
+ } from "fs";
9
+ import { dirname, join } from "path";
10
+ import { readJsonObject, writeJsonObject } from "./settings-sync.js";
11
+
12
+ function listFilesRecursive(dirPath, prefix = "") {
13
+ if (!existsSync(dirPath)) return [];
14
+
15
+ const entries = readdirSync(dirPath);
16
+ const files = [];
17
+
18
+ for (const entry of entries) {
19
+ const rel = prefix ? join(prefix, entry) : entry;
20
+ const abs = join(dirPath, entry);
21
+ const st = statSync(abs);
22
+ if (st.isDirectory()) {
23
+ files.push(...listFilesRecursive(abs, rel));
24
+ } else if (st.isFile()) {
25
+ files.push(rel);
26
+ }
27
+ }
28
+
29
+ return files;
30
+ }
31
+
32
+ function mergeSnippetFile(sourcePath, targetPath) {
33
+ const source = readJsonObject(sourcePath, {});
34
+ const target = readJsonObject(targetPath, {});
35
+ const merged = {
36
+ ...target,
37
+ ...source,
38
+ };
39
+ writeJsonObject(targetPath, merged);
40
+ }
41
+
42
+ export function syncSnippets(sourceDir, targetDir, mode) {
43
+ if (!existsSync(sourceDir)) {
44
+ throw new Error(`Source snippets folder not found: ${sourceDir}`);
45
+ }
46
+
47
+ if (mode === "replace") {
48
+ rmSync(targetDir, { recursive: true, force: true });
49
+ mkdirSync(dirname(targetDir), { recursive: true });
50
+ cpSync(sourceDir, targetDir, { recursive: true, force: true });
51
+ return listFilesRecursive(targetDir).length;
52
+ }
53
+
54
+ const sourceFiles = listFilesRecursive(sourceDir);
55
+ for (const relPath of sourceFiles) {
56
+ const sourcePath = join(sourceDir, relPath);
57
+ const targetPath = join(targetDir, relPath);
58
+ mkdirSync(dirname(targetPath), { recursive: true });
59
+
60
+ if (sourcePath.toLowerCase().endsWith(".json")) {
61
+ mergeSnippetFile(sourcePath, targetPath);
62
+ } else {
63
+ cpSync(sourcePath, targetPath, { force: true });
64
+ }
65
+ }
66
+
67
+ return sourceFiles.length;
68
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "editor-profile-sync",
3
+ "version": "1.0.0",
4
+ "description": "Cross-platform extensions, settings.json, and snippets sync for VS Code-based editors",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "editor-profile-sync": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "run": "node index.js"
13
+ },
14
+ "keywords": [
15
+ "vscode",
16
+ "cursor",
17
+ "windsurf",
18
+ "trae",
19
+ "kiro",
20
+ "extensions",
21
+ "settings",
22
+ "snippets",
23
+ "profile",
24
+ "sync",
25
+ "export",
26
+ "editor-profile-sync"
27
+ ],
28
+ "author": "sbetav",
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "dependencies": {
34
+ "chalk": "^5.6.2",
35
+ "inquirer": "^9.2.22",
36
+ "jsonc-parser": "^3.3.1",
37
+ "ora": "^9.3.0"
38
+ }
39
+ }