cursor-keys 1.0.0 → 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/bin/cursor-keys.mjs +6 -0
- package/extension/LICENSE +21 -0
- package/package.json +1 -1
- package/src/commands.mjs +45 -5
- package/src/config.mjs +22 -13
- package/src/detect.mjs +22 -0
- package/src/profile.mjs +8 -1
- package/extension/cursor-keys-1.0.0.vsix +0 -0
package/bin/cursor-keys.mjs
CHANGED
|
@@ -17,6 +17,12 @@ function printHelp() {
|
|
|
17
17
|
" init [--root DIR] [--all] Scan for git repos and register projects",
|
|
18
18
|
);
|
|
19
19
|
console.log(" add <name> [options] Create profile for a project");
|
|
20
|
+
console.log(
|
|
21
|
+
" clone <name> [--from main] Copy sign-in + settings from a donor profile",
|
|
22
|
+
);
|
|
23
|
+
console.log(
|
|
24
|
+
" set-key <name> Inject the project API key (encrypted)",
|
|
25
|
+
);
|
|
20
26
|
console.log(
|
|
21
27
|
" open [name] Open Cursor with this project's profile",
|
|
22
28
|
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 L-ubu
|
|
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/package.json
CHANGED
package/src/commands.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getProject,
|
|
14
14
|
loadConfig,
|
|
15
15
|
pickColorForName,
|
|
16
|
+
pickUnusedColor,
|
|
16
17
|
saveConfig,
|
|
17
18
|
profileStateDb,
|
|
18
19
|
} from "./config.mjs";
|
|
@@ -32,8 +33,10 @@ import {
|
|
|
32
33
|
findCursorCli,
|
|
33
34
|
listRunningCursorProfiles,
|
|
34
35
|
mainCursorProfileRoot,
|
|
36
|
+
mainCursorUserDir,
|
|
35
37
|
stateDbHasKey,
|
|
36
38
|
stateDbValueContains,
|
|
39
|
+
stateDbValuesMatch,
|
|
37
40
|
hasSqlite3,
|
|
38
41
|
} from "./detect.mjs";
|
|
39
42
|
import {
|
|
@@ -296,6 +299,13 @@ async function cmdInit(args) {
|
|
|
296
299
|
);
|
|
297
300
|
}
|
|
298
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
|
+
|
|
299
309
|
async function cmdAdd(args) {
|
|
300
310
|
const { flags, positional } = parseFlags(args);
|
|
301
311
|
if (flags.help) {
|
|
@@ -319,8 +329,13 @@ async function cmdAdd(args) {
|
|
|
319
329
|
throw new Error(`Repo path does not exist: ${repoPath}`);
|
|
320
330
|
}
|
|
321
331
|
|
|
332
|
+
const usedColors = Object.entries(config.projects)
|
|
333
|
+
.filter(([other]) => other !== name)
|
|
334
|
+
.map(([, entry]) => entry.color);
|
|
322
335
|
const color =
|
|
323
|
-
flags.color ||
|
|
336
|
+
flags.color ||
|
|
337
|
+
config.projects[name]?.color ||
|
|
338
|
+
pickUnusedColor(name, usedColors);
|
|
324
339
|
config.projects[name] = { path: collapseHome(repoPath), color };
|
|
325
340
|
saveConfig(config);
|
|
326
341
|
|
|
@@ -348,7 +363,7 @@ async function cmdAdd(args) {
|
|
|
348
363
|
console.log(ok(`Profile ready: ${collapseHome(project.profileDir)}`));
|
|
349
364
|
console.log(` Repo: ${collapseHome(project.path)}`);
|
|
350
365
|
console.log(` Color: ${color} ${blockSwatch(color)}`);
|
|
351
|
-
if (
|
|
366
|
+
if (shouldShowFirstRunChecklist(project.profileDir)) {
|
|
352
367
|
printFirstRunChecklist(name, config.baseUrl);
|
|
353
368
|
}
|
|
354
369
|
}
|
|
@@ -405,7 +420,7 @@ async function cmdOpen(args) {
|
|
|
405
420
|
console.log(` ${dim}${collapseHome(project.path)}${reset}`);
|
|
406
421
|
console.log(` Profile ${blockSwatch(project.color)} ${project.color}`);
|
|
407
422
|
|
|
408
|
-
if (
|
|
423
|
+
if (shouldShowFirstRunChecklist(project.profileDir)) {
|
|
409
424
|
printFirstRunChecklist(name, config.baseUrl);
|
|
410
425
|
markSetupDone(project.profileDir);
|
|
411
426
|
}
|
|
@@ -499,9 +514,22 @@ async function cmdDoctor(args) {
|
|
|
499
514
|
}
|
|
500
515
|
const db = profileStateDb(p.profileDir);
|
|
501
516
|
if (fs.existsSync(db)) {
|
|
517
|
+
const hasKey = stateDbHasKey(db, SECRET_OPENAI_KEY);
|
|
502
518
|
console.log(
|
|
503
|
-
` ${
|
|
519
|
+
` ${hasKey ? ok("✓") : warn("✗")} OpenAI/Bonzai API key row present`,
|
|
504
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
|
+
}
|
|
505
533
|
console.log(
|
|
506
534
|
` ${stateDbHasKey(db, SUBAGENT_OVERRIDES_KEY) ? ok("✓") : warn("✗")} Explore subagent overrides present`,
|
|
507
535
|
);
|
|
@@ -747,7 +775,19 @@ async function cmdSetKey(args) {
|
|
|
747
775
|
throw new Error(`Close the "${name}" Cursor window first, then retry.`);
|
|
748
776
|
}
|
|
749
777
|
|
|
750
|
-
|
|
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
|
+
}
|
|
751
791
|
const result = setProfileApiKey(db, key);
|
|
752
792
|
console.log(
|
|
753
793
|
ok(`Key set for ${name}`) +
|
package/src/config.mjs
CHANGED
|
@@ -98,25 +98,34 @@ export function baseUrlFragment(config) {
|
|
|
98
98
|
return String(config.baseUrl || "").replace(/^https?:\/\//, "");
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
const COLOR_PALETTE = [
|
|
102
|
+
"#4f8cff",
|
|
103
|
+
"#ff6b6b",
|
|
104
|
+
"#51cf66",
|
|
105
|
+
"#ffd43b",
|
|
106
|
+
"#845ef7",
|
|
107
|
+
"#20c997",
|
|
108
|
+
"#ff922b",
|
|
109
|
+
"#e64980",
|
|
110
|
+
"#339af0",
|
|
111
|
+
"#94d82d",
|
|
112
|
+
];
|
|
113
|
+
|
|
101
114
|
/** Deterministic palette color from project name */
|
|
102
115
|
export function pickColorForName(name) {
|
|
103
|
-
const palette = [
|
|
104
|
-
"#4f8cff",
|
|
105
|
-
"#ff6b6b",
|
|
106
|
-
"#51cf66",
|
|
107
|
-
"#ffd43b",
|
|
108
|
-
"#845ef7",
|
|
109
|
-
"#20c997",
|
|
110
|
-
"#ff922b",
|
|
111
|
-
"#e64980",
|
|
112
|
-
"#339af0",
|
|
113
|
-
"#94d82d",
|
|
114
|
-
];
|
|
115
116
|
let hash = 0;
|
|
116
117
|
for (let i = 0; i < name.length; i++) {
|
|
117
118
|
hash = (hash * 31 + name.charCodeAt(i)) >>> 0;
|
|
118
119
|
}
|
|
119
|
-
return
|
|
120
|
+
return COLOR_PALETTE[hash % COLOR_PALETTE.length];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Like pickColorForName, but avoids colors already used by other projects */
|
|
124
|
+
export function pickUnusedColor(name, usedColors) {
|
|
125
|
+
const used = new Set(usedColors);
|
|
126
|
+
const preferred = pickColorForName(name);
|
|
127
|
+
if (!used.has(preferred)) return preferred;
|
|
128
|
+
return COLOR_PALETTE.find((c) => !used.has(c)) || preferred;
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
/**
|
package/src/detect.mjs
CHANGED
|
@@ -183,6 +183,28 @@ export function stateDbValueContains(stateDbPath, urlFragment) {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* True if the value for `key` is byte-identical in both state DBs.
|
|
188
|
+
* Comparison happens inside SQL; the value never enters this process.
|
|
189
|
+
* @param {string} dbA
|
|
190
|
+
* @param {string} dbB
|
|
191
|
+
* @param {string} key
|
|
192
|
+
*/
|
|
193
|
+
export function stateDbValuesMatch(dbA, dbB, key) {
|
|
194
|
+
if (!fs.existsSync(dbA) || !fs.existsSync(dbB) || !hasSqlite3()) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const out = execSync(
|
|
199
|
+
`sqlite3 ${shellQuote(dbA)} "ATTACH DATABASE ${shellQuote(dbB)} AS other; SELECT CASE WHEN (SELECT value FROM ItemTable WHERE key = ${shellQuote(key)}) IS NOT NULL AND (SELECT value FROM ItemTable WHERE key = ${shellQuote(key)}) = (SELECT value FROM other.ItemTable WHERE key = ${shellQuote(key)}) THEN 1 ELSE 0 END;"`,
|
|
200
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] },
|
|
201
|
+
);
|
|
202
|
+
return out.trim() === "1";
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
186
208
|
function shellQuote(s) {
|
|
187
209
|
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
188
210
|
}
|
package/src/profile.mjs
CHANGED
|
@@ -116,7 +116,14 @@ export function cloneStateDb(donorProfileDir, targetProfileDir) {
|
|
|
116
116
|
ensureProfileStructure(targetProfileDir);
|
|
117
117
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
118
118
|
|
|
119
|
-
|
|
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)}"`, {
|
|
120
127
|
stdio: "ignore",
|
|
121
128
|
});
|
|
122
129
|
}
|
|
Binary file
|