cursor-keys 1.2.1 → 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 +10 -5
- package/bin/cursor-keys.mjs +7 -3
- package/package.json +4 -2
- package/scripts/postinstall.mjs +58 -0
- package/src/ansi.mjs +14 -4
- package/src/commands.mjs +121 -25
- 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
|
|
@@ -102,13 +104,16 @@ Settings flow one way, main to profiles: `sync` re-copies your main `settings.js
|
|
|
102
104
|
| `sync [name]` | Re-copy main Cursor settings into profiles |
|
|
103
105
|
| `doctor [name]` | Health checks: key row, base URL, subagent overrides |
|
|
104
106
|
| `wrap [--install]` | zsh function so plain `cursor <path>` auto-picks the profile |
|
|
105
|
-
| `aliases [--install]` | Per-project shell aliases
|
|
106
|
-
| `
|
|
107
|
+
| `aliases [--install]` | Per-project shell aliases (also `ck` shortcut + tab completion) |
|
|
108
|
+
| `completion` | Print zsh tab completion (auto-sourced by `aliases --install`) |
|
|
109
|
+
| `install-extension` | Link the Cursor extension (auto-linked on global npm install) |
|
|
107
110
|
| `rm <name> [--delete]` | Unregister (optionally delete the profile) |
|
|
108
111
|
|
|
112
|
+
`cursor-keys ls` prints registered project names one per line for scripts and tab completion; it is hidden from `--help`.
|
|
113
|
+
|
|
109
114
|
## Config
|
|
110
115
|
|
|
111
|
-
`~/.config/cursor-keys/config.json
|
|
116
|
+
`~/.config/cursor-keys/config.json` (writes are atomic and merge-on-save, so overlapping runs never clobber registrations):
|
|
112
117
|
|
|
113
118
|
```json
|
|
114
119
|
{
|
package/bin/cursor-keys.mjs
CHANGED
|
@@ -9,12 +9,13 @@ const BANNER = `
|
|
|
9
9
|
${yellow}██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ █████╔╝ █████╗ ╚████╔╝ ███████╗
|
|
10
10
|
${yellow}██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ ██╔═██╗ ██╔══╝ ╚██╔╝ ╚════██║
|
|
11
11
|
${yellow}╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ ██║ ██╗███████╗ ██║ ███████║
|
|
12
|
-
${yellow} ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝${reset}
|
|
12
|
+
${yellow} ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝${reset}
|
|
13
|
+
${dim}per-project Cursor API keys${reset}
|
|
13
14
|
`;
|
|
14
15
|
|
|
15
16
|
function printHelp() {
|
|
16
17
|
console.log(BANNER);
|
|
17
|
-
console.log(`${bold}Usage:${reset} cursor-keys <command> [options]\n`);
|
|
18
|
+
console.log(`${bold}Usage:${reset} cursor-keys (or ck, ckeys) <command> [options]\n`);
|
|
18
19
|
console.log(`${bold}Commands:${reset}`);
|
|
19
20
|
console.log(
|
|
20
21
|
" init [--root DIR] [--all] Scan for git repos and register projects",
|
|
@@ -42,7 +43,10 @@ function printHelp() {
|
|
|
42
43
|
console.log(
|
|
43
44
|
" doctor [name] Verify setup for one or all projects",
|
|
44
45
|
);
|
|
45
|
-
console.log(" aliases [--install]
|
|
46
|
+
console.log(" aliases [--install] Shell aliases (incl. 'ck' + tab completion)");
|
|
47
|
+
console.log(
|
|
48
|
+
" completion Print zsh tab completion (auto-sourced by aliases)",
|
|
49
|
+
);
|
|
46
50
|
console.log(
|
|
47
51
|
" wrap [--install] Make plain `cursor <path>` auto-pick profile",
|
|
48
52
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-keys",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Per-project Cursor profiles so each window uses the correct BYOK API key. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"bin",
|
|
15
15
|
"src",
|
|
16
|
-
"extension"
|
|
16
|
+
"extension",
|
|
17
|
+
"scripts"
|
|
17
18
|
],
|
|
18
19
|
"keywords": [
|
|
19
20
|
"cursor",
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
},
|
|
32
33
|
"scripts": {
|
|
33
34
|
"test": "node --test",
|
|
35
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
34
36
|
"prepublishOnly": "npm test"
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Postinstall: one-time hint + best-effort companion extension install.
|
|
2
|
+
// Never breaks the install: the whole body is guarded and exits 0.
|
|
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
|
+
|
|
9
|
+
try {
|
|
10
|
+
if (process.env.CURSOR_KEYS_NO_POSTINSTALL) process.exit(0);
|
|
11
|
+
const yellow = "\x1b[33m";
|
|
12
|
+
const dim = "\x1b[2m";
|
|
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
|
+
|
|
47
|
+
console.log(`
|
|
48
|
+
${yellow}cursor-keys installed${reset}
|
|
49
|
+
|
|
50
|
+
cursor-keys --help commands
|
|
51
|
+
cursor-keys aliases --install aliases, 'ck' shortcut, tab completion
|
|
52
|
+
cursor-keys wrap --install plain 'cursor <path>' picks the profile
|
|
53
|
+
${extLine}
|
|
54
|
+
${dim}Quiet installs: CURSOR_KEYS_NO_POSTINSTALL=1${reset}
|
|
55
|
+
`);
|
|
56
|
+
} catch {
|
|
57
|
+
/* postinstall must never break install */
|
|
58
|
+
}
|
package/src/ansi.mjs
CHANGED
|
@@ -26,15 +26,25 @@ export function err(text) {
|
|
|
26
26
|
return colorize(text, red);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
30
|
+
|
|
31
|
+
export function stripAnsi(s) {
|
|
32
|
+
return String(s).replace(ANSI_RE, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
export function padEnd(str, len) {
|
|
30
36
|
const s = String(str);
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
const vis = stripAnsi(s).length;
|
|
38
|
+
if (vis >= len) return s;
|
|
39
|
+
return s + " ".repeat(len - vis);
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
export function printTable(headers, rows) {
|
|
36
43
|
const widths = headers.map((h, i) =>
|
|
37
|
-
Math.max(
|
|
44
|
+
Math.max(
|
|
45
|
+
h.length,
|
|
46
|
+
...rows.map((r) => stripAnsi(String(r[i] ?? "")).length),
|
|
47
|
+
),
|
|
38
48
|
);
|
|
39
49
|
const line = widths.map((w) => "-".repeat(w)).join("-+-");
|
|
40
50
|
console.log(widths.map((w, i) => padEnd(headers[i], w)).join(" | "));
|
|
@@ -51,5 +61,5 @@ export function blockSwatch(hex) {
|
|
|
51
61
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
52
62
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
53
63
|
const b = parseInt(hex.slice(5, 7), 16);
|
|
54
|
-
return
|
|
64
|
+
return `\x1b[48;2;${r};${g};${b}m ${reset}`;
|
|
55
65
|
}
|
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 {
|
|
@@ -93,6 +93,10 @@ export async function runCommand(argv) {
|
|
|
93
93
|
return cmdClone(rest);
|
|
94
94
|
case "rm":
|
|
95
95
|
return cmdRm(rest);
|
|
96
|
+
case "ls":
|
|
97
|
+
return cmdLs(rest);
|
|
98
|
+
case "completion":
|
|
99
|
+
return cmdCompletion(rest);
|
|
96
100
|
default:
|
|
97
101
|
throw new Error(`Unknown command: ${cmd}. Run cursor-keys --help`);
|
|
98
102
|
}
|
|
@@ -234,8 +238,9 @@ async function cmdConfig(args) {
|
|
|
234
238
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
235
239
|
throw new Error(`baseUrl must start with http(s)://: ${value}`);
|
|
236
240
|
}
|
|
237
|
-
|
|
238
|
-
|
|
241
|
+
updateConfig((c) => {
|
|
242
|
+
c.baseUrl = value;
|
|
243
|
+
});
|
|
239
244
|
console.log(ok(`baseUrl set: ${value}`));
|
|
240
245
|
console.log(`${dim}doctor verifies this per profile.${reset}`);
|
|
241
246
|
}
|
|
@@ -300,20 +305,20 @@ async function cmdInit(args) {
|
|
|
300
305
|
}
|
|
301
306
|
}
|
|
302
307
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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}`));
|
|
309
320
|
}
|
|
310
|
-
|
|
311
|
-
path: collapseHome(repo),
|
|
312
|
-
color: pickColorForName(name),
|
|
313
|
-
};
|
|
314
|
-
console.log(ok(`Registered ${name}`));
|
|
315
|
-
}
|
|
316
|
-
saveConfig(config);
|
|
321
|
+
});
|
|
317
322
|
console.log(
|
|
318
323
|
`\nNext: cursor-keys add <name> --clone main (creates profile + copies main Cursor settings)`,
|
|
319
324
|
);
|
|
@@ -356,10 +361,11 @@ async function cmdAdd(args) {
|
|
|
356
361
|
flags.color ||
|
|
357
362
|
config.projects[name]?.color ||
|
|
358
363
|
pickUnusedColor(name, usedColors);
|
|
359
|
-
|
|
360
|
-
|
|
364
|
+
const saved = updateConfig((c) => {
|
|
365
|
+
c.projects[name] = { path: collapseHome(repoPath), color };
|
|
366
|
+
});
|
|
361
367
|
|
|
362
|
-
const project = getProject(
|
|
368
|
+
const project = getProject(saved, name);
|
|
363
369
|
if (!project) throw new Error("Failed to resolve project");
|
|
364
370
|
|
|
365
371
|
let cloneFrom = null;
|
|
@@ -367,7 +373,7 @@ async function cmdAdd(args) {
|
|
|
367
373
|
if (flags.clone === "main") {
|
|
368
374
|
cloneFrom = mainCursorProfileRoot();
|
|
369
375
|
} else {
|
|
370
|
-
const donor = getProject(
|
|
376
|
+
const donor = getProject(saved, flags.clone);
|
|
371
377
|
if (!donor) throw new Error(`Unknown clone donor: ${flags.clone}`);
|
|
372
378
|
cloneFrom = donor.profileDir;
|
|
373
379
|
}
|
|
@@ -569,7 +575,13 @@ async function cmdAliases(args) {
|
|
|
569
575
|
const lines = names.map(
|
|
570
576
|
(n) => `alias ${sanitizeAlias(n)}='cursor-keys open ${shellEscapeName(n)}'`,
|
|
571
577
|
);
|
|
572
|
-
const block = [
|
|
578
|
+
const block = [
|
|
579
|
+
ZSHRC_MARKER_START,
|
|
580
|
+
"alias ck='cursor-keys'",
|
|
581
|
+
'eval "$(cursor-keys completion)" # tab completion',
|
|
582
|
+
...lines,
|
|
583
|
+
ZSHRC_MARKER_END,
|
|
584
|
+
].join("\n");
|
|
573
585
|
|
|
574
586
|
if (!flags.install) {
|
|
575
587
|
console.log(block);
|
|
@@ -591,16 +603,87 @@ async function cmdAliases(args) {
|
|
|
591
603
|
}
|
|
592
604
|
fs.writeFileSync(zshrc, content, "utf8");
|
|
593
605
|
console.log(ok(`Updated ${zshrc}`));
|
|
606
|
+
if (!compinitLikelyConfigured(content)) {
|
|
607
|
+
console.log(
|
|
608
|
+
warn(
|
|
609
|
+
"Note: tab completion needs compinit. If `cursor-keys open <TAB>` does nothing after sourcing ~/.zshrc, add: autoload -Uz compinit && compinit",
|
|
610
|
+
),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
594
613
|
}
|
|
595
614
|
|
|
596
615
|
function sanitizeAlias(name) {
|
|
597
616
|
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
598
617
|
}
|
|
599
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
|
+
|
|
600
635
|
function shellEscapeName(name) {
|
|
601
636
|
return name.replace(/'/g, `'\\''`);
|
|
602
637
|
}
|
|
603
638
|
|
|
639
|
+
const COMPLETION_COMMANDS =
|
|
640
|
+
"init add clone set-key open status current config sync doctor aliases wrap install-extension completion ls rm";
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Pure builder for the zsh completion script. Sourced via
|
|
644
|
+
* `eval "$(cursor-keys completion)"` from the aliases/wrap zshrc blocks,
|
|
645
|
+
* so updates ship with `npm update` and never go stale on disk.
|
|
646
|
+
*/
|
|
647
|
+
export function completionScript() {
|
|
648
|
+
return `#compdef cursor-keys ckeys ck
|
|
649
|
+
_cursor_keys() {
|
|
650
|
+
local -a projects
|
|
651
|
+
if (( CURRENT == 3 )) && [[ \${words[2]} == (open|clone|set-key|doctor|sync|rm) ]]; then
|
|
652
|
+
projects=(\${(f)"$(cursor-keys ls 2>/dev/null)"})
|
|
653
|
+
_describe 'project' projects
|
|
654
|
+
else
|
|
655
|
+
_arguments '1:command:(${COMPLETION_COMMANDS})'
|
|
656
|
+
fi
|
|
657
|
+
}
|
|
658
|
+
compdef _cursor_keys cursor-keys ckeys ck
|
|
659
|
+
`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function cmdCompletion(args) {
|
|
663
|
+
const { flags } = parseFlags(args);
|
|
664
|
+
if (flags.help) {
|
|
665
|
+
console.log("Usage: cursor-keys completion");
|
|
666
|
+
console.log("Prints the zsh tab completion script to stdout.");
|
|
667
|
+
console.log("Auto-sourced by `cursor-keys aliases --install`; no separate install needed.");
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
process.stdout.write(completionScript());
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Hidden command: bare project names, one per line, for completion/scripts.
|
|
675
|
+
*/
|
|
676
|
+
function cmdLs(args) {
|
|
677
|
+
const { flags } = parseFlags(args);
|
|
678
|
+
if (flags.help) {
|
|
679
|
+
console.log("Usage: cursor-keys ls");
|
|
680
|
+
console.log("Prints registered project names, one per line (for scripts and tab completion).");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const config = loadConfig();
|
|
684
|
+
console.log(Object.keys(config.projects).sort().join("\n"));
|
|
685
|
+
}
|
|
686
|
+
|
|
604
687
|
function installZshrcBlock(startMarker, endMarker, block) {
|
|
605
688
|
const zshrc = path.join(os.homedir(), ".zshrc");
|
|
606
689
|
let content = fs.existsSync(zshrc) ? fs.readFileSync(zshrc, "utf8") : "";
|
|
@@ -615,6 +698,13 @@ function installZshrcBlock(startMarker, endMarker, block) {
|
|
|
615
698
|
content = content.trimEnd() + "\n\n" + block + "\n";
|
|
616
699
|
}
|
|
617
700
|
fs.writeFileSync(zshrc, content, "utf8");
|
|
701
|
+
if (!compinitLikelyConfigured(content)) {
|
|
702
|
+
console.log(
|
|
703
|
+
warn(
|
|
704
|
+
"Note: tab completion needs compinit. If `cursor-keys open <TAB>` does nothing after sourcing ~/.zshrc, add: autoload -Uz compinit && compinit",
|
|
705
|
+
),
|
|
706
|
+
);
|
|
707
|
+
}
|
|
618
708
|
return zshrc;
|
|
619
709
|
}
|
|
620
710
|
|
|
@@ -735,12 +825,17 @@ async function cmdInstallExtension() {
|
|
|
735
825
|
"..",
|
|
736
826
|
);
|
|
737
827
|
const src = path.join(repoRoot, "extension");
|
|
738
|
-
|
|
828
|
+
const extPkgPath = path.join(src, "package.json");
|
|
829
|
+
if (!fs.existsSync(extPkgPath)) {
|
|
739
830
|
throw new Error(`Extension source missing: ${src}`);
|
|
740
831
|
}
|
|
832
|
+
const extPkg = JSON.parse(fs.readFileSync(extPkgPath, "utf8"));
|
|
741
833
|
const extDir = path.join(os.homedir(), ".cursor", "extensions");
|
|
742
834
|
fs.mkdirSync(extDir, { recursive: true });
|
|
743
|
-
const target = path.join(
|
|
835
|
+
const target = path.join(
|
|
836
|
+
extDir,
|
|
837
|
+
`${extPkg.publisher || "lubu"}.${extPkg.name}-${extPkg.version}`,
|
|
838
|
+
);
|
|
744
839
|
fs.rmSync(target, { recursive: true, force: true });
|
|
745
840
|
fs.symlinkSync(src, target, "dir");
|
|
746
841
|
console.log(ok(`Extension linked: ${target}`));
|
|
@@ -828,8 +923,9 @@ async function cmdRm(args) {
|
|
|
828
923
|
if (!config.projects[name]) throw new Error(`Unknown project: ${name}`);
|
|
829
924
|
|
|
830
925
|
const p = getProject(config, name);
|
|
831
|
-
|
|
832
|
-
|
|
926
|
+
updateConfig((c) => {
|
|
927
|
+
delete c.projects[name];
|
|
928
|
+
});
|
|
833
929
|
console.log(ok(`Removed ${name} from config`));
|
|
834
930
|
|
|
835
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
|
*/
|