cursor-keys 1.2.2 → 1.2.3
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/README.md +6 -4
- package/package.json +1 -1
- package/scripts/postinstall.mjs +41 -5
- package/src/commands.mjs +52 -28
- package/src/config.mjs +30 -8
package/README.md
CHANGED
|
@@ -48,6 +48,8 @@ npm install -g cursor-keys
|
|
|
48
48
|
|
|
49
49
|
Requires Node ≥18, the Cursor CLI (`Shell Command: Install 'cursor' in PATH`), and sqlite3 (preinstalled on macOS). Key injection is macOS-only; everything else is cross-platform shaped.
|
|
50
50
|
|
|
51
|
+
On global install, the companion Cursor extension is linked into `~/.cursor/extensions` automatically and a one-time hint prints (opt out of both: `CURSOR_KEYS_NO_POSTINSTALL=1`). Restart Cursor, then `Cmd+Shift+P` → "cursor-keys: Open Project...".
|
|
52
|
+
|
|
51
53
|
## Quick start
|
|
52
54
|
|
|
53
55
|
```bash
|
|
@@ -69,8 +71,8 @@ One-time extras for the full experience:
|
|
|
69
71
|
|
|
70
72
|
```bash
|
|
71
73
|
cursor-keys wrap --install # plain `cursor <path>` auto-picks the profile
|
|
72
|
-
cursor-keys aliases --install # per-project
|
|
73
|
-
cursor-keys install-extension # status bar
|
|
74
|
+
cursor-keys aliases --install # per-project aliases, `ck` shortcut, zsh tab completion
|
|
75
|
+
cursor-keys install-extension # status bar + wrong-window guard (auto-linked on global npm install)
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
## How it works
|
|
@@ -104,14 +106,14 @@ Settings flow one way, main to profiles: `sync` re-copies your main `settings.js
|
|
|
104
106
|
| `wrap [--install]` | zsh function so plain `cursor <path>` auto-picks the profile |
|
|
105
107
|
| `aliases [--install]` | Per-project shell aliases (also `ck` shortcut + tab completion) |
|
|
106
108
|
| `completion` | Print zsh tab completion (auto-sourced by `aliases --install`) |
|
|
107
|
-
| `install-extension` | Link the Cursor extension
|
|
109
|
+
| `install-extension` | Link the Cursor extension (auto-linked on global npm install) |
|
|
108
110
|
| `rm <name> [--delete]` | Unregister (optionally delete the profile) |
|
|
109
111
|
|
|
110
112
|
`cursor-keys ls` prints registered project names one per line for scripts and tab completion; it is hidden from `--help`.
|
|
111
113
|
|
|
112
114
|
## Config
|
|
113
115
|
|
|
114
|
-
`~/.config/cursor-keys/config.json
|
|
116
|
+
`~/.config/cursor-keys/config.json` (writes are atomic and merge-on-save, so overlapping runs never clobber registrations):
|
|
115
117
|
|
|
116
118
|
```json
|
|
117
119
|
{
|
package/package.json
CHANGED
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,20 +1,56 @@
|
|
|
1
|
-
// Postinstall
|
|
1
|
+
// Postinstall: one-time hint + best-effort companion extension install.
|
|
2
|
+
// Never breaks the install: the whole body is guarded and exits 0.
|
|
2
3
|
// Quiet installs: CURSOR_KEYS_NO_POSTINSTALL=1
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
3
9
|
try {
|
|
4
10
|
if (process.env.CURSOR_KEYS_NO_POSTINSTALL) process.exit(0);
|
|
5
11
|
const yellow = "\x1b[33m";
|
|
6
12
|
const dim = "\x1b[2m";
|
|
7
13
|
const reset = "\x1b[0m";
|
|
14
|
+
|
|
15
|
+
const pkgRoot = path.join(
|
|
16
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
17
|
+
"..",
|
|
18
|
+
);
|
|
19
|
+
const extSrc = path.join(pkgRoot, "extension");
|
|
20
|
+
const extPkgPath = path.join(extSrc, "package.json");
|
|
21
|
+
const cursorDir = path.join(os.homedir(), ".cursor");
|
|
22
|
+
|
|
23
|
+
// Auto-link the companion extension on global installs when Cursor exists.
|
|
24
|
+
let extLine = "";
|
|
25
|
+
const isGlobal = process.env.npm_config_global === "true";
|
|
26
|
+
if (isGlobal && fs.existsSync(cursorDir) && fs.existsSync(extPkgPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const extPkg = JSON.parse(fs.readFileSync(extPkgPath, "utf8"));
|
|
29
|
+
const extDir = path.join(cursorDir, "extensions");
|
|
30
|
+
fs.mkdirSync(extDir, { recursive: true });
|
|
31
|
+
const target = path.join(
|
|
32
|
+
extDir,
|
|
33
|
+
`${extPkg.publisher || "lubu"}.${extPkg.name}-${extPkg.version}`,
|
|
34
|
+
);
|
|
35
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
36
|
+
try {
|
|
37
|
+
fs.symlinkSync(extSrc, target, "dir");
|
|
38
|
+
} catch {
|
|
39
|
+
fs.cpSync(extSrc, target, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
extLine = `\n Cursor extension linked ${dim}· restart Cursor, then Cmd+Shift+P: "cursor-keys: Open Project..."${reset}`;
|
|
42
|
+
} catch {
|
|
43
|
+
extLine = `\n cursor-keys install-extension link the Cursor extension (status bar + warnings)`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
8
47
|
console.log(`
|
|
9
48
|
${yellow}cursor-keys installed${reset}
|
|
10
49
|
|
|
11
50
|
cursor-keys --help commands
|
|
12
51
|
cursor-keys aliases --install aliases, 'ck' shortcut, tab completion
|
|
13
52
|
cursor-keys wrap --install plain 'cursor <path>' picks the profile
|
|
14
|
-
|
|
15
|
-
Inside Cursor: Cmd+Shift+P then "cursor-keys: Open Project..." after
|
|
16
|
-
'cursor-keys install-extension' (or install 'cursor-keys' from Open VSX).
|
|
17
|
-
|
|
53
|
+
${extLine}
|
|
18
54
|
${dim}Quiet installs: CURSOR_KEYS_NO_POSTINSTALL=1${reset}
|
|
19
55
|
`);
|
|
20
56
|
} catch {
|
package/src/commands.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
loadConfig,
|
|
17
17
|
pickColorForName,
|
|
18
18
|
pickUnusedColor,
|
|
19
|
-
|
|
19
|
+
updateConfig,
|
|
20
20
|
profileStateDb,
|
|
21
21
|
} from "./config.mjs";
|
|
22
22
|
import {
|
|
@@ -238,8 +238,9 @@ async function cmdConfig(args) {
|
|
|
238
238
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
239
239
|
throw new Error(`baseUrl must start with http(s)://: ${value}`);
|
|
240
240
|
}
|
|
241
|
-
|
|
242
|
-
|
|
241
|
+
updateConfig((c) => {
|
|
242
|
+
c.baseUrl = value;
|
|
243
|
+
});
|
|
243
244
|
console.log(ok(`baseUrl set: ${value}`));
|
|
244
245
|
console.log(`${dim}doctor verifies this per profile.${reset}`);
|
|
245
246
|
}
|
|
@@ -304,20 +305,20 @@ async function cmdInit(args) {
|
|
|
304
305
|
}
|
|
305
306
|
}
|
|
306
307
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
308
|
+
updateConfig((config) => {
|
|
309
|
+
for (const repo of selected) {
|
|
310
|
+
const name = path.basename(repo);
|
|
311
|
+
if (config.projects[name]) {
|
|
312
|
+
console.log(warn(`Skip ${name} (already registered)`));
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
config.projects[name] = {
|
|
316
|
+
path: collapseHome(repo),
|
|
317
|
+
color: pickColorForName(name),
|
|
318
|
+
};
|
|
319
|
+
console.log(ok(`Registered ${name}`));
|
|
313
320
|
}
|
|
314
|
-
|
|
315
|
-
path: collapseHome(repo),
|
|
316
|
-
color: pickColorForName(name),
|
|
317
|
-
};
|
|
318
|
-
console.log(ok(`Registered ${name}`));
|
|
319
|
-
}
|
|
320
|
-
saveConfig(config);
|
|
321
|
+
});
|
|
321
322
|
console.log(
|
|
322
323
|
`\nNext: cursor-keys add <name> --clone main (creates profile + copies main Cursor settings)`,
|
|
323
324
|
);
|
|
@@ -360,10 +361,11 @@ async function cmdAdd(args) {
|
|
|
360
361
|
flags.color ||
|
|
361
362
|
config.projects[name]?.color ||
|
|
362
363
|
pickUnusedColor(name, usedColors);
|
|
363
|
-
|
|
364
|
-
|
|
364
|
+
const saved = updateConfig((c) => {
|
|
365
|
+
c.projects[name] = { path: collapseHome(repoPath), color };
|
|
366
|
+
});
|
|
365
367
|
|
|
366
|
-
const project = getProject(
|
|
368
|
+
const project = getProject(saved, name);
|
|
367
369
|
if (!project) throw new Error("Failed to resolve project");
|
|
368
370
|
|
|
369
371
|
let cloneFrom = null;
|
|
@@ -371,7 +373,7 @@ async function cmdAdd(args) {
|
|
|
371
373
|
if (flags.clone === "main") {
|
|
372
374
|
cloneFrom = mainCursorProfileRoot();
|
|
373
375
|
} else {
|
|
374
|
-
const donor = getProject(
|
|
376
|
+
const donor = getProject(saved, flags.clone);
|
|
375
377
|
if (!donor) throw new Error(`Unknown clone donor: ${flags.clone}`);
|
|
376
378
|
cloneFrom = donor.profileDir;
|
|
377
379
|
}
|
|
@@ -601,10 +603,10 @@ async function cmdAliases(args) {
|
|
|
601
603
|
}
|
|
602
604
|
fs.writeFileSync(zshrc, content, "utf8");
|
|
603
605
|
console.log(ok(`Updated ${zshrc}`));
|
|
604
|
-
if (!content
|
|
606
|
+
if (!compinitLikelyConfigured(content)) {
|
|
605
607
|
console.log(
|
|
606
608
|
warn(
|
|
607
|
-
"Note:
|
|
609
|
+
"Note: tab completion needs compinit. If `cursor-keys open <TAB>` does nothing after sourcing ~/.zshrc, add: autoload -Uz compinit && compinit",
|
|
608
610
|
),
|
|
609
611
|
);
|
|
610
612
|
}
|
|
@@ -614,6 +616,22 @@ function sanitizeAlias(name) {
|
|
|
614
616
|
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
615
617
|
}
|
|
616
618
|
|
|
619
|
+
const COMPINIT_HINT_RE = /compinit|oh-my-zsh|prezto|zinit|antigen|zplug|zimfw/;
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Best-effort check that the user's zsh setup loads compinit. Frameworks
|
|
623
|
+
* (oh-my-zsh, prezto, zinit, ...) run it from sourced files rather than
|
|
624
|
+
* ~/.zshrc itself, and Warp injects it via shell integration, so a plain
|
|
625
|
+
* text grep of ~/.zshrc alone gives false positives. Exported for tests.
|
|
626
|
+
*/
|
|
627
|
+
export function compinitLikelyConfigured(
|
|
628
|
+
zshrcContent,
|
|
629
|
+
termProgram = process.env.TERM_PROGRAM,
|
|
630
|
+
) {
|
|
631
|
+
if (COMPINIT_HINT_RE.test(zshrcContent)) return true;
|
|
632
|
+
return termProgram === "WarpTerminal";
|
|
633
|
+
}
|
|
634
|
+
|
|
617
635
|
function shellEscapeName(name) {
|
|
618
636
|
return name.replace(/'/g, `'\\''`);
|
|
619
637
|
}
|
|
@@ -680,10 +698,10 @@ function installZshrcBlock(startMarker, endMarker, block) {
|
|
|
680
698
|
content = content.trimEnd() + "\n\n" + block + "\n";
|
|
681
699
|
}
|
|
682
700
|
fs.writeFileSync(zshrc, content, "utf8");
|
|
683
|
-
if (!content
|
|
701
|
+
if (!compinitLikelyConfigured(content)) {
|
|
684
702
|
console.log(
|
|
685
703
|
warn(
|
|
686
|
-
"Note:
|
|
704
|
+
"Note: tab completion needs compinit. If `cursor-keys open <TAB>` does nothing after sourcing ~/.zshrc, add: autoload -Uz compinit && compinit",
|
|
687
705
|
),
|
|
688
706
|
);
|
|
689
707
|
}
|
|
@@ -807,12 +825,17 @@ async function cmdInstallExtension() {
|
|
|
807
825
|
"..",
|
|
808
826
|
);
|
|
809
827
|
const src = path.join(repoRoot, "extension");
|
|
810
|
-
|
|
828
|
+
const extPkgPath = path.join(src, "package.json");
|
|
829
|
+
if (!fs.existsSync(extPkgPath)) {
|
|
811
830
|
throw new Error(`Extension source missing: ${src}`);
|
|
812
831
|
}
|
|
832
|
+
const extPkg = JSON.parse(fs.readFileSync(extPkgPath, "utf8"));
|
|
813
833
|
const extDir = path.join(os.homedir(), ".cursor", "extensions");
|
|
814
834
|
fs.mkdirSync(extDir, { recursive: true });
|
|
815
|
-
const target = path.join(
|
|
835
|
+
const target = path.join(
|
|
836
|
+
extDir,
|
|
837
|
+
`${extPkg.publisher || "lubu"}.${extPkg.name}-${extPkg.version}`,
|
|
838
|
+
);
|
|
816
839
|
fs.rmSync(target, { recursive: true, force: true });
|
|
817
840
|
fs.symlinkSync(src, target, "dir");
|
|
818
841
|
console.log(ok(`Extension linked: ${target}`));
|
|
@@ -900,8 +923,9 @@ async function cmdRm(args) {
|
|
|
900
923
|
if (!config.projects[name]) throw new Error(`Unknown project: ${name}`);
|
|
901
924
|
|
|
902
925
|
const p = getProject(config, name);
|
|
903
|
-
|
|
904
|
-
|
|
926
|
+
updateConfig((c) => {
|
|
927
|
+
delete c.projects[name];
|
|
928
|
+
});
|
|
905
929
|
console.log(ok(`Removed ${name} from config`));
|
|
906
930
|
|
|
907
931
|
if (flags.delete && p) {
|
package/src/config.mjs
CHANGED
|
@@ -8,6 +8,15 @@ export const DEFAULT_CONFIG_DIR = path.join(
|
|
|
8
8
|
"cursor-keys",
|
|
9
9
|
);
|
|
10
10
|
export const CONFIG_FILE = path.join(DEFAULT_CONFIG_DIR, "config.json");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolved at call time so tests and scripts can point cursor-keys at a
|
|
14
|
+
* scratch config via CURSOR_KEYS_CONFIG_FILE.
|
|
15
|
+
*/
|
|
16
|
+
function configFilePath() {
|
|
17
|
+
const override = process.env.CURSOR_KEYS_CONFIG_FILE;
|
|
18
|
+
return override ? expandHome(override) : CONFIG_FILE;
|
|
19
|
+
}
|
|
11
20
|
export const DEFAULT_PROFILES_ROOT = path.join(
|
|
12
21
|
os.homedir(),
|
|
13
22
|
".cursor-profiles",
|
|
@@ -40,25 +49,38 @@ export function defaultConfig() {
|
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
export function loadConfig() {
|
|
43
|
-
|
|
52
|
+
const file = configFilePath();
|
|
53
|
+
if (!fs.existsSync(file)) {
|
|
44
54
|
return defaultConfig();
|
|
45
55
|
}
|
|
46
|
-
const raw = fs.readFileSync(
|
|
56
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
47
57
|
const data = JSON.parse(raw);
|
|
48
58
|
return validateConfig(data);
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
export function saveConfig(config) {
|
|
52
62
|
const validated = validateConfig(config);
|
|
53
|
-
|
|
54
|
-
fs.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
);
|
|
63
|
+
const file = configFilePath();
|
|
64
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
65
|
+
// Atomic write: a crash or concurrent reader never sees a torn file.
|
|
66
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
67
|
+
fs.writeFileSync(tmp, JSON.stringify(validated, null, 2) + "\n", "utf8");
|
|
68
|
+
fs.renameSync(tmp, file);
|
|
59
69
|
return validated;
|
|
60
70
|
}
|
|
61
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Load fresh, mutate, save: the only safe way to change config. Keeps the
|
|
74
|
+
* lost-update window (two concurrent runs dropping each other's projects)
|
|
75
|
+
* down to the few ms inside this function instead of a whole command run.
|
|
76
|
+
* @param {(config: ReturnType<typeof loadConfig>) => void} mutate
|
|
77
|
+
*/
|
|
78
|
+
export function updateConfig(mutate) {
|
|
79
|
+
const config = loadConfig();
|
|
80
|
+
mutate(config);
|
|
81
|
+
return saveConfig(config);
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
/**
|
|
63
85
|
* @param {unknown} data
|
|
64
86
|
*/
|