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.
- package/LICENSE +21 -0
- package/README.md +129 -2
- package/bin/cursor-keys.mjs +68 -0
- package/extension/LICENSE +21 -0
- package/extension/extension.js +248 -0
- package/extension/package.json +35 -0
- package/package.json +35 -4
- package/src/ansi.mjs +55 -0
- package/src/commands.mjs +819 -0
- package/src/config.mjs +176 -0
- package/src/detect.mjs +214 -0
- package/src/jsonc.mjs +109 -0
- package/src/keychain.mjs +136 -0
- package/src/profile.mjs +191 -0
package/src/commands.mjs
ADDED
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
baseUrlFragment,
|
|
9
|
+
collapseHome,
|
|
10
|
+
CONFIG_FILE,
|
|
11
|
+
expandHome,
|
|
12
|
+
findProjectByPath,
|
|
13
|
+
getProject,
|
|
14
|
+
loadConfig,
|
|
15
|
+
pickColorForName,
|
|
16
|
+
pickUnusedColor,
|
|
17
|
+
saveConfig,
|
|
18
|
+
profileStateDb,
|
|
19
|
+
} from "./config.mjs";
|
|
20
|
+
import {
|
|
21
|
+
blockSwatch,
|
|
22
|
+
bold,
|
|
23
|
+
dim,
|
|
24
|
+
err,
|
|
25
|
+
ok,
|
|
26
|
+
printTable,
|
|
27
|
+
reset,
|
|
28
|
+
warn,
|
|
29
|
+
} from "./ansi.mjs";
|
|
30
|
+
import {
|
|
31
|
+
SECRET_OPENAI_KEY,
|
|
32
|
+
SUBAGENT_OVERRIDES_KEY,
|
|
33
|
+
findCursorCli,
|
|
34
|
+
listRunningCursorProfiles,
|
|
35
|
+
mainCursorProfileRoot,
|
|
36
|
+
mainCursorUserDir,
|
|
37
|
+
stateDbHasKey,
|
|
38
|
+
stateDbValueContains,
|
|
39
|
+
stateDbValuesMatch,
|
|
40
|
+
hasSqlite3,
|
|
41
|
+
} from "./detect.mjs";
|
|
42
|
+
import {
|
|
43
|
+
cloneStateDb,
|
|
44
|
+
createProfile,
|
|
45
|
+
markSetupDone,
|
|
46
|
+
needsFirstRunChecklist,
|
|
47
|
+
printFirstRunChecklist,
|
|
48
|
+
profileIntegrityChecks,
|
|
49
|
+
writeProfileSettings,
|
|
50
|
+
} from "./profile.mjs";
|
|
51
|
+
import { setProfileApiKey } from "./keychain.mjs";
|
|
52
|
+
|
|
53
|
+
const ZSHRC_MARKER_START = "# >>> cursor-keys aliases >>>";
|
|
54
|
+
const ZSHRC_MARKER_END = "# <<< cursor-keys aliases <<<";
|
|
55
|
+
const ZSHRC_WRAP_START = "# >>> cursor-keys cursor wrapper >>>";
|
|
56
|
+
const ZSHRC_WRAP_END = "# <<< cursor-keys cursor wrapper <<<";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string[]} argv
|
|
60
|
+
*/
|
|
61
|
+
export async function runCommand(argv) {
|
|
62
|
+
const [cmd, ...rest] = argv;
|
|
63
|
+
switch (cmd) {
|
|
64
|
+
case "init":
|
|
65
|
+
return cmdInit(rest);
|
|
66
|
+
case "add":
|
|
67
|
+
return cmdAdd(rest);
|
|
68
|
+
case "open":
|
|
69
|
+
return cmdOpen(rest);
|
|
70
|
+
case "status":
|
|
71
|
+
return cmdStatus(rest);
|
|
72
|
+
case "current":
|
|
73
|
+
return cmdCurrent(rest);
|
|
74
|
+
case "config":
|
|
75
|
+
return cmdConfig(rest);
|
|
76
|
+
case "sync":
|
|
77
|
+
return cmdSync(rest);
|
|
78
|
+
case "doctor":
|
|
79
|
+
return cmdDoctor(rest);
|
|
80
|
+
case "aliases":
|
|
81
|
+
return cmdAliases(rest);
|
|
82
|
+
case "wrap":
|
|
83
|
+
return cmdWrap(rest);
|
|
84
|
+
case "__cursor-wrap":
|
|
85
|
+
return cmdCursorWrap(rest);
|
|
86
|
+
case "install-extension":
|
|
87
|
+
return cmdInstallExtension(rest);
|
|
88
|
+
case "set-key":
|
|
89
|
+
return cmdSetKey(rest);
|
|
90
|
+
case "clone":
|
|
91
|
+
return cmdClone(rest);
|
|
92
|
+
case "rm":
|
|
93
|
+
return cmdRm(rest);
|
|
94
|
+
default:
|
|
95
|
+
throw new Error(`Unknown command: ${cmd}. Run cursor-keys --help`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseFlags(args) {
|
|
100
|
+
const flags = {};
|
|
101
|
+
const positional = [];
|
|
102
|
+
for (let i = 0; i < args.length; i++) {
|
|
103
|
+
const a = args[i];
|
|
104
|
+
if (a === "--help" || a === "-h") {
|
|
105
|
+
flags.help = true;
|
|
106
|
+
} else if (a === "--all") {
|
|
107
|
+
flags.all = true;
|
|
108
|
+
} else if (a === "--install") {
|
|
109
|
+
flags.install = true;
|
|
110
|
+
} else if (a === "--delete") {
|
|
111
|
+
flags.delete = true;
|
|
112
|
+
} else if (a === "--root" && args[i + 1]) {
|
|
113
|
+
flags.root = args[++i];
|
|
114
|
+
} else if (a === "--path" && args[i + 1]) {
|
|
115
|
+
flags.path = args[++i];
|
|
116
|
+
} else if (a === "--color" && args[i + 1]) {
|
|
117
|
+
flags.color = args[++i];
|
|
118
|
+
} else if (a === "--clone" && args[i + 1]) {
|
|
119
|
+
flags.clone = args[++i];
|
|
120
|
+
} else if (a === "--key" && args[i + 1]) {
|
|
121
|
+
flags.key = args[++i];
|
|
122
|
+
} else if (a === "--from" && args[i + 1]) {
|
|
123
|
+
flags.from = args[++i];
|
|
124
|
+
} else if (a.startsWith("-")) {
|
|
125
|
+
throw new Error(`Unknown option: ${a}`);
|
|
126
|
+
} else {
|
|
127
|
+
positional.push(a);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { flags, positional };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function scanGitRepos(rootDir, maxDepth = 2) {
|
|
134
|
+
const root = path.resolve(expandHome(rootDir));
|
|
135
|
+
if (!fs.existsSync(root)) return [];
|
|
136
|
+
|
|
137
|
+
/** @type {string[]} */
|
|
138
|
+
const repos = [];
|
|
139
|
+
|
|
140
|
+
function walk(dir, depth) {
|
|
141
|
+
if (depth > maxDepth) return;
|
|
142
|
+
let entries;
|
|
143
|
+
try {
|
|
144
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
145
|
+
} catch {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const hasGit = entries.some((e) => e.isDirectory() && e.name === ".git");
|
|
149
|
+
if (hasGit) {
|
|
150
|
+
repos.push(dir);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
for (const e of entries) {
|
|
154
|
+
if (!e.isDirectory()) continue;
|
|
155
|
+
if (e.name === "node_modules" || e.name === ".git") continue;
|
|
156
|
+
walk(path.join(dir, e.name), depth + 1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
walk(root, 0);
|
|
161
|
+
return repos.sort();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function ask(question) {
|
|
165
|
+
const rl = readline.createInterface({
|
|
166
|
+
input: process.stdin,
|
|
167
|
+
output: process.stdout,
|
|
168
|
+
});
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
rl.question(question, (answer) => {
|
|
171
|
+
rl.close();
|
|
172
|
+
resolve(answer.trim());
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function cmdConfig(args) {
|
|
178
|
+
const { flags, positional } = parseFlags(args);
|
|
179
|
+
if (flags.help) {
|
|
180
|
+
console.log("Usage: cursor-keys config [baseUrl [URL]]");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const config = loadConfig();
|
|
184
|
+
const [key, value] = positional;
|
|
185
|
+
|
|
186
|
+
if (!key) {
|
|
187
|
+
const count = Object.keys(config.projects).length;
|
|
188
|
+
console.log(
|
|
189
|
+
`${bold}Config${reset} ${dim}(${collapseHome(CONFIG_FILE)})${reset}`,
|
|
190
|
+
);
|
|
191
|
+
console.log(` baseUrl: ${config.baseUrl}`);
|
|
192
|
+
console.log(` profilesRoot: ${config.profilesRoot}`);
|
|
193
|
+
console.log(
|
|
194
|
+
` projects: ${count} registered ${dim}(see cursor-keys status)${reset}`,
|
|
195
|
+
);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (key !== "baseUrl") {
|
|
200
|
+
throw new Error(`Unknown config key: ${key}. Known keys: baseUrl`);
|
|
201
|
+
}
|
|
202
|
+
if (!value) {
|
|
203
|
+
console.log(config.baseUrl);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
let parsed;
|
|
207
|
+
try {
|
|
208
|
+
parsed = new URL(value);
|
|
209
|
+
} catch {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Invalid baseUrl: ${value} (example: https://gateway.example.com/v1)`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
215
|
+
throw new Error(`baseUrl must start with http(s)://: ${value}`);
|
|
216
|
+
}
|
|
217
|
+
config.baseUrl = value;
|
|
218
|
+
saveConfig(config);
|
|
219
|
+
console.log(ok(`baseUrl set: ${value}`));
|
|
220
|
+
console.log(`${dim}doctor verifies this per profile.${reset}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function cmdSync(args) {
|
|
224
|
+
const { flags, positional } = parseFlags(args);
|
|
225
|
+
if (flags.help) {
|
|
226
|
+
console.log("Usage: cursor-keys sync [name]");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const config = loadConfig();
|
|
230
|
+
const names = positional[0]
|
|
231
|
+
? [positional[0]]
|
|
232
|
+
: Object.keys(config.projects).sort();
|
|
233
|
+
if (!names.length) {
|
|
234
|
+
console.log("No projects registered. Run: cursor-keys init");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
for (const name of names) {
|
|
238
|
+
const project = getProject(config, name);
|
|
239
|
+
if (!project) throw new Error(`Unknown project: ${name}`);
|
|
240
|
+
if (!fs.existsSync(project.profileDir)) {
|
|
241
|
+
console.log(warn(`skip ${name}: profile missing (run add first)`));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
writeProfileSettings(project.profileDir, name, project.color);
|
|
245
|
+
console.log(ok(`Synced settings from main -> ${name}`));
|
|
246
|
+
}
|
|
247
|
+
console.log(
|
|
248
|
+
`${dim}Open Cursor windows reload settings automatically.${reset}`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function cmdInit(args) {
|
|
253
|
+
const { flags, positional } = parseFlags(args);
|
|
254
|
+
if (flags.help) {
|
|
255
|
+
console.log("Usage: cursor-keys init [--root DIR] [--all]");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const root =
|
|
259
|
+
flags.root || positional[0] || path.join(os.homedir(), "Projects");
|
|
260
|
+
const repos = scanGitRepos(root);
|
|
261
|
+
if (!repos.length) {
|
|
262
|
+
console.log(`No git repos found under ${root}`);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let selected = repos;
|
|
267
|
+
if (!flags.all) {
|
|
268
|
+
console.log(`Found ${repos.length} repos under ${root}:\n`);
|
|
269
|
+
repos.forEach((r, i) => console.log(` [${i + 1}] ${collapseHome(r)}`));
|
|
270
|
+
console.log("\nEnter numbers to register (e.g. 1,3,5) or 'all':");
|
|
271
|
+
const answer = await ask("> ");
|
|
272
|
+
if (/^all$/i.test(answer)) {
|
|
273
|
+
selected = repos;
|
|
274
|
+
} else {
|
|
275
|
+
const nums = answer
|
|
276
|
+
.split(/[,\s]+/)
|
|
277
|
+
.map((n) => parseInt(n, 10))
|
|
278
|
+
.filter((n) => n >= 1 && n <= repos.length);
|
|
279
|
+
selected = [...new Set(nums)].map((n) => repos[n - 1]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const config = loadConfig();
|
|
284
|
+
for (const repo of selected) {
|
|
285
|
+
const name = path.basename(repo);
|
|
286
|
+
if (config.projects[name]) {
|
|
287
|
+
console.log(warn(`Skip ${name} (already registered)`));
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
config.projects[name] = {
|
|
291
|
+
path: collapseHome(repo),
|
|
292
|
+
color: pickColorForName(name),
|
|
293
|
+
};
|
|
294
|
+
console.log(ok(`Registered ${name}`));
|
|
295
|
+
}
|
|
296
|
+
saveConfig(config);
|
|
297
|
+
console.log(
|
|
298
|
+
`\nNext: cursor-keys add <name> --clone main (creates profile + copies Bonzai settings)`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function shouldShowFirstRunChecklist(profileDir) {
|
|
303
|
+
if (!needsFirstRunChecklist(profileDir)) return false;
|
|
304
|
+
const db = profileStateDb(profileDir);
|
|
305
|
+
// A cloned profile already carries a key row: setup is effectively done.
|
|
306
|
+
return !(fs.existsSync(db) && stateDbHasKey(db, SECRET_OPENAI_KEY));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function cmdAdd(args) {
|
|
310
|
+
const { flags, positional } = parseFlags(args);
|
|
311
|
+
if (flags.help) {
|
|
312
|
+
console.log(
|
|
313
|
+
"Usage: cursor-keys add <name> [--path REPO] [--color #RRGGBB] [--clone main|donorName]",
|
|
314
|
+
);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const name = positional[0];
|
|
318
|
+
if (!name)
|
|
319
|
+
throw new Error("Missing project name. Usage: cursor-keys add <name>");
|
|
320
|
+
|
|
321
|
+
const config = loadConfig();
|
|
322
|
+
const repoPath = flags.path
|
|
323
|
+
? path.resolve(expandHome(flags.path))
|
|
324
|
+
: config.projects[name]
|
|
325
|
+
? path.resolve(expandHome(config.projects[name].path))
|
|
326
|
+
: path.resolve(process.cwd());
|
|
327
|
+
|
|
328
|
+
if (!fs.existsSync(repoPath)) {
|
|
329
|
+
throw new Error(`Repo path does not exist: ${repoPath}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const usedColors = Object.entries(config.projects)
|
|
333
|
+
.filter(([other]) => other !== name)
|
|
334
|
+
.map(([, entry]) => entry.color);
|
|
335
|
+
const color =
|
|
336
|
+
flags.color ||
|
|
337
|
+
config.projects[name]?.color ||
|
|
338
|
+
pickUnusedColor(name, usedColors);
|
|
339
|
+
config.projects[name] = { path: collapseHome(repoPath), color };
|
|
340
|
+
saveConfig(config);
|
|
341
|
+
|
|
342
|
+
const project = getProject(config, name);
|
|
343
|
+
if (!project) throw new Error("Failed to resolve project");
|
|
344
|
+
|
|
345
|
+
let cloneFrom = null;
|
|
346
|
+
if (flags.clone) {
|
|
347
|
+
if (flags.clone === "main") {
|
|
348
|
+
cloneFrom = mainCursorProfileRoot();
|
|
349
|
+
} else {
|
|
350
|
+
const donor = getProject(config, flags.clone);
|
|
351
|
+
if (!donor) throw new Error(`Unknown clone donor: ${flags.clone}`);
|
|
352
|
+
cloneFrom = donor.profileDir;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
createProfile({
|
|
357
|
+
profileDir: project.profileDir,
|
|
358
|
+
projectName: name,
|
|
359
|
+
color,
|
|
360
|
+
cloneFrom,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
console.log(ok(`Profile ready: ${collapseHome(project.profileDir)}`));
|
|
364
|
+
console.log(` Repo: ${collapseHome(project.path)}`);
|
|
365
|
+
console.log(` Color: ${color} ${blockSwatch(color)}`);
|
|
366
|
+
if (shouldShowFirstRunChecklist(project.profileDir)) {
|
|
367
|
+
printFirstRunChecklist(name, config.baseUrl);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function cmdOpen(args) {
|
|
372
|
+
const { flags, positional } = parseFlags(args);
|
|
373
|
+
if (flags.help) {
|
|
374
|
+
console.log("Usage: cursor-keys open [name]");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const cursor = findCursorCli();
|
|
379
|
+
if (!cursor)
|
|
380
|
+
throw new Error(
|
|
381
|
+
"cursor CLI not found. Install from Cursor: Shell Command: Install 'cursor' in PATH",
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const config = loadConfig();
|
|
385
|
+
let name = positional[0];
|
|
386
|
+
let project;
|
|
387
|
+
if (name) {
|
|
388
|
+
project = getProject(config, name);
|
|
389
|
+
if (!project)
|
|
390
|
+
throw new Error(`Unknown project: ${name}. Run cursor-keys init or add.`);
|
|
391
|
+
} else {
|
|
392
|
+
project = findProjectByPath(config, process.cwd());
|
|
393
|
+
if (!project) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
"No project matches cwd. Pass a name: cursor-keys open <name>",
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
name = project.name;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!fs.existsSync(project.profileDir)) {
|
|
402
|
+
throw new Error(`Profile missing. Run: cursor-keys add ${name}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Refresh settings from main so every open picks up the latest tweaks.
|
|
406
|
+
try {
|
|
407
|
+
writeProfileSettings(project.profileDir, name, project.color);
|
|
408
|
+
} catch {
|
|
409
|
+
// Never block an open on a settings rewrite failure.
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const child = spawn(
|
|
413
|
+
cursor,
|
|
414
|
+
["--user-data-dir", project.profileDir, "--new-window", project.path],
|
|
415
|
+
{ detached: true, stdio: "ignore" },
|
|
416
|
+
);
|
|
417
|
+
child.unref();
|
|
418
|
+
|
|
419
|
+
console.log(ok(`Opening ${name}`));
|
|
420
|
+
console.log(` ${dim}${collapseHome(project.path)}${reset}`);
|
|
421
|
+
console.log(` Profile ${blockSwatch(project.color)} ${project.color}`);
|
|
422
|
+
|
|
423
|
+
if (shouldShowFirstRunChecklist(project.profileDir)) {
|
|
424
|
+
printFirstRunChecklist(name, config.baseUrl);
|
|
425
|
+
markSetupDone(project.profileDir);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function cmdStatus() {
|
|
430
|
+
const config = loadConfig();
|
|
431
|
+
const running = listRunningCursorProfiles();
|
|
432
|
+
const names = Object.keys(config.projects).sort();
|
|
433
|
+
|
|
434
|
+
if (!names.length) {
|
|
435
|
+
console.log("No projects registered. Run: cursor-keys init");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const rows = names.map((name) => {
|
|
440
|
+
const p = getProject(config, name);
|
|
441
|
+
if (!p) return [name, "-", "-", "-", "-"];
|
|
442
|
+
const profResolved = path.resolve(p.profileDir);
|
|
443
|
+
const runCount = running.get(profResolved) || 0;
|
|
444
|
+
const keyOk = fs.existsSync(profileStateDb(p.profileDir))
|
|
445
|
+
? stateDbHasKey(profileStateDb(p.profileDir), SECRET_OPENAI_KEY)
|
|
446
|
+
: false;
|
|
447
|
+
return [
|
|
448
|
+
name,
|
|
449
|
+
blockSwatch(p.color) + " " + p.color,
|
|
450
|
+
runCount ? ok(String(runCount)) : `${dim}0${reset}`,
|
|
451
|
+
keyOk ? ok("yes") : warn("no"),
|
|
452
|
+
collapseHome(p.path),
|
|
453
|
+
];
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
printTable(["Project", "Color", "Windows", "Key", "Repo"], rows);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function cmdCurrent() {
|
|
460
|
+
const config = loadConfig();
|
|
461
|
+
const project = findProjectByPath(config, process.cwd());
|
|
462
|
+
if (!project) {
|
|
463
|
+
console.log("cwd is not under any registered project path.");
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
console.log(`${bold}Project:${reset} ${project.name}`);
|
|
467
|
+
console.log(`${bold}Repo:${reset} ${collapseHome(project.path)}`);
|
|
468
|
+
console.log(`${bold}Profile:${reset} ${collapseHome(project.profileDir)}`);
|
|
469
|
+
console.log(
|
|
470
|
+
`${bold}Color:${reset} ${blockSwatch(project.color)} ${project.color}`,
|
|
471
|
+
);
|
|
472
|
+
const db = profileStateDb(project.profileDir);
|
|
473
|
+
const keyOk = stateDbHasKey(db, SECRET_OPENAI_KEY);
|
|
474
|
+
console.log(
|
|
475
|
+
`${bold}API key:${reset} ${keyOk ? ok("configured in profile") : warn("not set in profile yet")}`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function cmdDoctor(args) {
|
|
480
|
+
const { flags, positional } = parseFlags(args);
|
|
481
|
+
if (flags.help) {
|
|
482
|
+
console.log("Usage: cursor-keys doctor [name]");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const cursor = findCursorCli();
|
|
487
|
+
console.log(
|
|
488
|
+
cursor ? ok(`cursor CLI: ${cursor}`) : err("cursor CLI: not found"),
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
if (!hasSqlite3()) {
|
|
492
|
+
console.log(warn("sqlite3 not on PATH (key checks limited)"));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const config = loadConfig();
|
|
496
|
+
const names = positional[0]
|
|
497
|
+
? [positional[0]]
|
|
498
|
+
: Object.keys(config.projects).sort();
|
|
499
|
+
|
|
500
|
+
if (!names.length) {
|
|
501
|
+
console.log("No projects to check.");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
for (const name of names) {
|
|
506
|
+
const p = getProject(config, name);
|
|
507
|
+
if (!p) {
|
|
508
|
+
console.log(err(`\n${name}: not in config`));
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
console.log(`\n${bold}${name}${reset}`);
|
|
512
|
+
for (const issue of profileIntegrityChecks(p.profileDir)) {
|
|
513
|
+
console.log(` ${err("✗")} ${issue}`);
|
|
514
|
+
}
|
|
515
|
+
const db = profileStateDb(p.profileDir);
|
|
516
|
+
if (fs.existsSync(db)) {
|
|
517
|
+
const hasKey = stateDbHasKey(db, SECRET_OPENAI_KEY);
|
|
518
|
+
console.log(
|
|
519
|
+
` ${hasKey ? ok("✓") : warn("✗")} OpenAI/Bonzai API key row present`,
|
|
520
|
+
);
|
|
521
|
+
if (hasKey) {
|
|
522
|
+
const mainDb = path.join(
|
|
523
|
+
mainCursorUserDir(),
|
|
524
|
+
"globalStorage",
|
|
525
|
+
"state.vscdb",
|
|
526
|
+
);
|
|
527
|
+
if (stateDbValuesMatch(db, mainDb, SECRET_OPENAI_KEY)) {
|
|
528
|
+
console.log(
|
|
529
|
+
` ${warn("!")} Key is identical to the main profile (inherited via clone); run: cursor-keys set-key ${name}`,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
console.log(
|
|
534
|
+
` ${stateDbHasKey(db, SUBAGENT_OVERRIDES_KEY) ? ok("✓") : warn("✗")} Explore subagent overrides present`,
|
|
535
|
+
);
|
|
536
|
+
console.log(
|
|
537
|
+
` ${stateDbValueContains(db, baseUrlFragment(config)) ? ok("✓") : warn("✗")} Base URL (${baseUrlFragment(config)}) in profile state`,
|
|
538
|
+
);
|
|
539
|
+
} else {
|
|
540
|
+
console.log(` ${warn("✗")} Open profile once: cursor-keys open ${name}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function cmdAliases(args) {
|
|
546
|
+
const { flags } = parseFlags(args);
|
|
547
|
+
const config = loadConfig();
|
|
548
|
+
const names = Object.keys(config.projects).sort();
|
|
549
|
+
const lines = names.map(
|
|
550
|
+
(n) => `alias ${sanitizeAlias(n)}='cursor-keys open ${shellEscapeName(n)}'`,
|
|
551
|
+
);
|
|
552
|
+
const block = [ZSHRC_MARKER_START, ...lines, ZSHRC_MARKER_END].join("\n");
|
|
553
|
+
|
|
554
|
+
if (!flags.install) {
|
|
555
|
+
console.log(block);
|
|
556
|
+
console.log(`\n${dim}Run: cursor-keys aliases --install${reset}`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const zshrc = path.join(os.homedir(), ".zshrc");
|
|
561
|
+
let content = fs.existsSync(zshrc) ? fs.readFileSync(zshrc, "utf8") : "";
|
|
562
|
+
const startIdx = content.indexOf(ZSHRC_MARKER_START);
|
|
563
|
+
const endIdx = content.indexOf(ZSHRC_MARKER_END);
|
|
564
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
565
|
+
content =
|
|
566
|
+
content.slice(0, startIdx) +
|
|
567
|
+
block +
|
|
568
|
+
content.slice(endIdx + ZSHRC_MARKER_END.length);
|
|
569
|
+
} else {
|
|
570
|
+
content = content.trimEnd() + "\n\n" + block + "\n";
|
|
571
|
+
}
|
|
572
|
+
fs.writeFileSync(zshrc, content, "utf8");
|
|
573
|
+
console.log(ok(`Updated ${zshrc}`));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function sanitizeAlias(name) {
|
|
577
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function shellEscapeName(name) {
|
|
581
|
+
return name.replace(/'/g, `'\\''`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function installZshrcBlock(startMarker, endMarker, block) {
|
|
585
|
+
const zshrc = path.join(os.homedir(), ".zshrc");
|
|
586
|
+
let content = fs.existsSync(zshrc) ? fs.readFileSync(zshrc, "utf8") : "";
|
|
587
|
+
const startIdx = content.indexOf(startMarker);
|
|
588
|
+
const endIdx = content.indexOf(endMarker);
|
|
589
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
590
|
+
content =
|
|
591
|
+
content.slice(0, startIdx) +
|
|
592
|
+
block +
|
|
593
|
+
content.slice(endIdx + endMarker.length);
|
|
594
|
+
} else {
|
|
595
|
+
content = content.trimEnd() + "\n\n" + block + "\n";
|
|
596
|
+
}
|
|
597
|
+
fs.writeFileSync(zshrc, content, "utf8");
|
|
598
|
+
return zshrc;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function cmdWrap(args) {
|
|
602
|
+
const { flags } = parseFlags(args);
|
|
603
|
+
if (flags.help) {
|
|
604
|
+
console.log("Usage: cursor-keys wrap [--install]");
|
|
605
|
+
console.log(
|
|
606
|
+
"Makes plain `cursor <path>` auto-use the right project profile.",
|
|
607
|
+
);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const block = [
|
|
611
|
+
ZSHRC_WRAP_START,
|
|
612
|
+
'cursor() { cursor-keys __cursor-wrap "$@"; }',
|
|
613
|
+
ZSHRC_WRAP_END,
|
|
614
|
+
].join("\n");
|
|
615
|
+
|
|
616
|
+
if (!flags.install) {
|
|
617
|
+
console.log(block);
|
|
618
|
+
console.log(`\n${dim}Run: cursor-keys wrap --install${reset}`);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const zshrc = installZshrcBlock(ZSHRC_WRAP_START, ZSHRC_WRAP_END, block);
|
|
622
|
+
console.log(ok(`Updated ${zshrc}`));
|
|
623
|
+
console.log(`Run: source ~/.zshrc (then plain \`cursor .\` just works)`);
|
|
624
|
+
console.log(`${dim}Bypass anytime: CURSOR_KEYS_NO_WRAP=1 cursor .${reset}`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Hidden command: called by the zsh `cursor()` wrapper function.
|
|
629
|
+
* Injects --user-data-dir when the target lives in a registered project,
|
|
630
|
+
* otherwise delegates to the real cursor binary unchanged.
|
|
631
|
+
*/
|
|
632
|
+
async function cmdCursorWrap(args) {
|
|
633
|
+
const real = findCursorCli();
|
|
634
|
+
const delegate = () => {
|
|
635
|
+
if (!real) {
|
|
636
|
+
console.error("cursor-keys: real cursor binary not found");
|
|
637
|
+
process.exit(127);
|
|
638
|
+
}
|
|
639
|
+
const child = spawn(real, args, { stdio: "inherit" });
|
|
640
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
641
|
+
child.on("error", () => process.exit(127));
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
if (!real) return delegate();
|
|
645
|
+
if (process.env.CURSOR_KEYS_NO_WRAP) return delegate();
|
|
646
|
+
if (args.some((a) => ["-h", "--help", "-v", "--version"].includes(a))) {
|
|
647
|
+
return delegate();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
let config;
|
|
651
|
+
try {
|
|
652
|
+
config = loadConfig();
|
|
653
|
+
} catch {
|
|
654
|
+
return delegate();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
let target = null;
|
|
658
|
+
for (const a of args) {
|
|
659
|
+
if (a.startsWith("-")) continue;
|
|
660
|
+
const p = path.resolve(expandHome(a));
|
|
661
|
+
if (fs.existsSync(p)) {
|
|
662
|
+
target = p;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (!target) target = process.cwd();
|
|
667
|
+
const stat = fs.statSync(target, { throwIfNoEntry: false });
|
|
668
|
+
const dir = stat?.isDirectory() ? target : path.dirname(target);
|
|
669
|
+
|
|
670
|
+
const project = findProjectByPath(config, dir);
|
|
671
|
+
if (!project || !fs.existsSync(project.profileDir)) return delegate();
|
|
672
|
+
|
|
673
|
+
console.log(dim(`cursor-keys: profile "${project.name}"${reset}`));
|
|
674
|
+
const child = spawn(real, ["--user-data-dir", project.profileDir, ...args], {
|
|
675
|
+
stdio: "inherit",
|
|
676
|
+
});
|
|
677
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
678
|
+
child.on("error", () => delegate());
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function cmdClone(args) {
|
|
682
|
+
const { flags, positional } = parseFlags(args);
|
|
683
|
+
if (flags.help) {
|
|
684
|
+
console.log("Usage: cursor-keys clone <name> [--from main|otherProject]");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const name = positional[0];
|
|
688
|
+
if (!name) throw new Error("Usage: cursor-keys clone <name>");
|
|
689
|
+
|
|
690
|
+
const config = loadConfig();
|
|
691
|
+
const project = getProject(config, name);
|
|
692
|
+
if (!project) throw new Error(`Unknown project: ${name}`);
|
|
693
|
+
|
|
694
|
+
const from = flags.from || "main";
|
|
695
|
+
let donorRoot;
|
|
696
|
+
if (from === "main") {
|
|
697
|
+
donorRoot = mainCursorProfileRoot();
|
|
698
|
+
} else {
|
|
699
|
+
const donor = getProject(config, from);
|
|
700
|
+
if (!donor) throw new Error(`Unknown donor project: ${from}`);
|
|
701
|
+
donorRoot = donor.profileDir;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
cloneStateDb(donorRoot, project.profileDir);
|
|
705
|
+
console.log(ok(`Cloned state from ${from} into ${name}`));
|
|
706
|
+
console.log(
|
|
707
|
+
`${dim}Sign-in, settings, and model config carried over. Now set the project key:${reset}`,
|
|
708
|
+
);
|
|
709
|
+
console.log(` cursor-keys set-key ${name}`);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function cmdInstallExtension() {
|
|
713
|
+
const repoRoot = path.resolve(
|
|
714
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
715
|
+
"..",
|
|
716
|
+
);
|
|
717
|
+
const src = path.join(repoRoot, "extension");
|
|
718
|
+
if (!fs.existsSync(path.join(src, "package.json"))) {
|
|
719
|
+
throw new Error(`Extension source missing: ${src}`);
|
|
720
|
+
}
|
|
721
|
+
const extDir = path.join(os.homedir(), ".cursor", "extensions");
|
|
722
|
+
fs.mkdirSync(extDir, { recursive: true });
|
|
723
|
+
const target = path.join(extDir, "lubu.cursor-keys-1.0.0");
|
|
724
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
725
|
+
fs.symlinkSync(src, target, "dir");
|
|
726
|
+
console.log(ok(`Extension linked: ${target}`));
|
|
727
|
+
console.log("Restart any open Cursor windows to load it.");
|
|
728
|
+
console.log(`Look for ${bold}$(key) <project>${reset} in the status bar.`);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function promptHidden(question) {
|
|
732
|
+
return new Promise((resolve) => {
|
|
733
|
+
const rl = readline.createInterface({
|
|
734
|
+
input: process.stdin,
|
|
735
|
+
output: process.stdout,
|
|
736
|
+
terminal: true,
|
|
737
|
+
});
|
|
738
|
+
rl._writeToOutput = () => {};
|
|
739
|
+
process.stdout.write(question);
|
|
740
|
+
rl.question("", (value) => {
|
|
741
|
+
rl.close();
|
|
742
|
+
process.stdout.write("\n");
|
|
743
|
+
resolve(value.trim());
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function cmdSetKey(args) {
|
|
749
|
+
const { flags, positional } = parseFlags(args);
|
|
750
|
+
if (flags.help) {
|
|
751
|
+
console.log("Usage: cursor-keys set-key <name> [--key sk-...]");
|
|
752
|
+
console.log(
|
|
753
|
+
"Writes the key into the profile's encrypted store (Cursor must",
|
|
754
|
+
);
|
|
755
|
+
console.log(
|
|
756
|
+
"not have that profile window open). Prompts hidden if --key omitted.",
|
|
757
|
+
);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const name = positional[0];
|
|
761
|
+
if (!name)
|
|
762
|
+
throw new Error("Usage: cursor-keys set-key <name> [--key sk-...]");
|
|
763
|
+
|
|
764
|
+
const config = loadConfig();
|
|
765
|
+
const project = getProject(config, name);
|
|
766
|
+
if (!project) throw new Error(`Unknown project: ${name}`);
|
|
767
|
+
|
|
768
|
+
const db = profileStateDb(project.profileDir);
|
|
769
|
+
if (!fs.existsSync(db)) {
|
|
770
|
+
throw new Error(
|
|
771
|
+
`Profile has no state db yet. Run: cursor-keys clone ${name} (or open it once)`,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (listRunningCursorProfiles().has(path.resolve(project.profileDir))) {
|
|
775
|
+
throw new Error(`Close the "${name}" Cursor window first, then retry.`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
let key = flags.key;
|
|
779
|
+
if (!key) {
|
|
780
|
+
console.log(
|
|
781
|
+
`\nPaste the API key for ${bold}${name}${reset} below, then press Enter.`,
|
|
782
|
+
);
|
|
783
|
+
console.log(
|
|
784
|
+
`${dim}Input is hidden: nothing echoes while you paste or type.${reset}`,
|
|
785
|
+
);
|
|
786
|
+
key = await promptHidden("API key: ");
|
|
787
|
+
}
|
|
788
|
+
if (!key) {
|
|
789
|
+
throw new Error("No key entered. Aborted, nothing was written.");
|
|
790
|
+
}
|
|
791
|
+
const result = setProfileApiKey(db, key);
|
|
792
|
+
console.log(
|
|
793
|
+
ok(`Key set for ${name}`) +
|
|
794
|
+
` ${dim}(encrypted with Cursor safeStorage, round-trip verified, len ${result.length})${reset}`,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async function cmdRm(args) {
|
|
799
|
+
const { flags, positional } = parseFlags(args);
|
|
800
|
+
if (flags.help) {
|
|
801
|
+
console.log("Usage: cursor-keys rm <name> [--delete]");
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const name = positional[0];
|
|
805
|
+
if (!name) throw new Error("Usage: cursor-keys rm <name>");
|
|
806
|
+
|
|
807
|
+
const config = loadConfig();
|
|
808
|
+
if (!config.projects[name]) throw new Error(`Unknown project: ${name}`);
|
|
809
|
+
|
|
810
|
+
const p = getProject(config, name);
|
|
811
|
+
delete config.projects[name];
|
|
812
|
+
saveConfig(config);
|
|
813
|
+
console.log(ok(`Removed ${name} from config`));
|
|
814
|
+
|
|
815
|
+
if (flags.delete && p) {
|
|
816
|
+
fs.rmSync(p.profileDir, { recursive: true, force: true });
|
|
817
|
+
console.log(ok(`Deleted profile dir ${collapseHome(p.profileDir)}`));
|
|
818
|
+
}
|
|
819
|
+
}
|