cursor-keys 0.0.0-stage → 1.1.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.
@@ -0,0 +1,191 @@
1
+ import { execSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ profileSettingsPath,
6
+ profileStateDb,
7
+ profileUserDir,
8
+ } from "./config.mjs";
9
+ import { parseJsonc } from "./jsonc.mjs";
10
+ import { listRunningCursorProfiles, mainCursorUserDir } from "./detect.mjs";
11
+
12
+ const SETUP_MARKER = ".cursor-keys-setup-done";
13
+
14
+ export function ensureProfileStructure(profileDir) {
15
+ const userDir = profileUserDir(profileDir);
16
+ fs.mkdirSync(path.join(userDir, "globalStorage"), { recursive: true });
17
+ return userDir;
18
+ }
19
+
20
+ /**
21
+ * @param {string} profileDir
22
+ * @param {string} projectName
23
+ * @param {string} color
24
+ */
25
+ export function writeProfileSettings(profileDir, projectName, color) {
26
+ const userDir = ensureProfileStructure(profileDir);
27
+ const mainSettings = path.join(mainCursorUserDir(), "settings.json");
28
+ let base = {};
29
+ if (fs.existsSync(mainSettings)) {
30
+ const raw = fs.readFileSync(mainSettings, "utf8");
31
+ try {
32
+ base = parseJsonc(raw);
33
+ } catch {
34
+ base = {};
35
+ }
36
+ }
37
+ if (typeof base !== "object" || base === null || Array.isArray(base)) {
38
+ base = {};
39
+ }
40
+
41
+ const colorCustom = {
42
+ ...(base.workbench?.colorCustomizations || {}),
43
+ "titleBar.activeBackground": color,
44
+ "titleBar.activeForeground": "#ffffff",
45
+ "titleBar.inactiveBackground": color + "99",
46
+ "titleBar.inactiveForeground": "#ffffffcc",
47
+ };
48
+
49
+ base.window = {
50
+ ...(base.window || {}),
51
+ title: `${projectName} — \${activeEditorShort}\${separator}\${rootName}`,
52
+ };
53
+ base.workbench = {
54
+ ...(base.workbench || {}),
55
+ colorCustomizations: colorCustom,
56
+ };
57
+
58
+ const outPath = profileSettingsPath(profileDir);
59
+ fs.writeFileSync(outPath, JSON.stringify(base, null, 2) + "\n", "utf8");
60
+ }
61
+
62
+ export function symlinkSharedUserFiles(profileDir) {
63
+ const userDir = profileUserDir(profileDir);
64
+ const mainUser = mainCursorUserDir();
65
+ const links = ["keybindings.json", "snippets"];
66
+ for (const name of links) {
67
+ const src = path.join(mainUser, name);
68
+ const dest = path.join(userDir, name);
69
+ if (!fs.existsSync(src)) continue;
70
+ if (fs.existsSync(dest)) {
71
+ try {
72
+ const st = fs.lstatSync(dest);
73
+ if (st.isSymbolicLink()) fs.unlinkSync(dest);
74
+ else continue;
75
+ } catch {
76
+ continue;
77
+ }
78
+ }
79
+ fs.symlinkSync(src, dest);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * @param {string} donorProfileDir - main Cursor or another profile root (parent of User)
85
+ * @param {string} targetProfileDir
86
+ */
87
+ export function cloneStateDb(donorProfileDir, targetProfileDir) {
88
+ const runningProfiles = listRunningCursorProfiles();
89
+ if (runningProfiles.has(path.resolve(targetProfileDir))) {
90
+ throw new Error(
91
+ `Profile is currently running: ${targetProfileDir}. Close that window first.`,
92
+ );
93
+ }
94
+ let donorRoot = donorProfileDir;
95
+ if (
96
+ fs.existsSync(
97
+ path.join(donorProfileDir, "User", "globalStorage", "state.vscdb"),
98
+ )
99
+ ) {
100
+ donorRoot = donorProfileDir;
101
+ } else if (
102
+ fs.existsSync(
103
+ path.join(mainCursorUserDir(), "globalStorage", "state.vscdb"),
104
+ )
105
+ ) {
106
+ donorRoot = path.dirname(mainCursorUserDir());
107
+ } else {
108
+ throw new Error(`Donor profile has no state.vscdb: ${donorProfileDir}`);
109
+ }
110
+
111
+ const src = path.join(donorRoot, "User", "globalStorage", "state.vscdb");
112
+ const dest = profileStateDb(targetProfileDir);
113
+ if (!fs.existsSync(src)) {
114
+ throw new Error(`Missing donor state database: ${src}`);
115
+ }
116
+ ensureProfileStructure(targetProfileDir);
117
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
118
+
119
+ // VACUUM INTO requires a fresh destination.
120
+ for (const suffix of ["", "-wal", "-shm", "-journal"]) {
121
+ fs.rmSync(dest + suffix, { force: true });
122
+ }
123
+
124
+ // VACUUM INTO snapshots a consistent copy even while the donor Cursor is
125
+ // running; `.backup` can spin at full CPU against a write-heavy donor.
126
+ execSync(`sqlite3 ${shellQuote(src)} "VACUUM INTO ${shellQuote(dest)}"`, {
127
+ stdio: "ignore",
128
+ });
129
+ }
130
+
131
+ function shellQuote(s) {
132
+ return `'${String(s).replace(/'/g, `'\\''`)}'`;
133
+ }
134
+
135
+ export function markSetupDone(profileDir) {
136
+ fs.writeFileSync(
137
+ path.join(profileDir, SETUP_MARKER),
138
+ new Date().toISOString() + "\n",
139
+ );
140
+ }
141
+
142
+ export function needsFirstRunChecklist(profileDir) {
143
+ return !fs.existsSync(path.join(profileDir, SETUP_MARKER));
144
+ }
145
+
146
+ export function printFirstRunChecklist(
147
+ projectName,
148
+ baseUrl = "https://connect.iobonzai.com/cursor",
149
+ ) {
150
+ console.log(`
151
+ First-time setup for "${projectName}" in this Cursor profile:
152
+
153
+ 1. Cursor Settings → Models → API keys
154
+ 2. Paste your project Bonzai API key under OpenAI API Key
155
+ 3. Enable "Use OpenAI API Key" and "Override OpenAI Base URL"
156
+ 4. Base URL: ${baseUrl}
157
+ 5. Explore Subagent Model: Composer 2.5
158
+ 6. Add custom Bonzai models and disable non-Bonzai models
159
+
160
+ Then run: cursor-keys doctor ${projectName}
161
+ `);
162
+ }
163
+
164
+ export function createProfile({ profileDir, projectName, color, cloneFrom }) {
165
+ fs.mkdirSync(profileDir, { recursive: true });
166
+ writeProfileSettings(profileDir, projectName, color);
167
+ symlinkSharedUserFiles(profileDir);
168
+ if (cloneFrom) {
169
+ cloneStateDb(cloneFrom, profileDir);
170
+ }
171
+ }
172
+
173
+ export function profileIntegrityChecks(profileDir) {
174
+ const issues = [];
175
+ const settings = profileSettingsPath(profileDir);
176
+ const stateDb = profileStateDb(profileDir);
177
+ if (!fs.existsSync(settings)) issues.push("missing settings.json");
178
+ if (!fs.existsSync(stateDb))
179
+ issues.push("missing state.vscdb (open Cursor once in this profile)");
180
+ const kb = path.join(profileUserDir(profileDir), "keybindings.json");
181
+ if (fs.existsSync(kb)) {
182
+ try {
183
+ if (!fs.lstatSync(kb).isSymbolicLink()) {
184
+ issues.push("keybindings.json is not a symlink to main profile");
185
+ }
186
+ } catch {
187
+ /* ignore */
188
+ }
189
+ }
190
+ return issues;
191
+ }