cursor-keys 0.0.0-stage → 1.0.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 +62 -0
- package/extension/cursor-keys-1.0.0.vsix +0 -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 +779 -0
- package/src/config.mjs +167 -0
- package/src/detect.mjs +192 -0
- package/src/jsonc.mjs +109 -0
- package/src/keychain.mjs +136 -0
- package/src/profile.mjs +184 -0
package/LICENSE
ADDED
|
@@ -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/README.md
CHANGED
|
@@ -1,3 +1,130 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cursor-keys
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Per-project Cursor profiles so every window automatically uses the right BYOK API key. Two windows, two projects, two keys, zero switching.
|
|
4
|
+
|
|
5
|
+
Cursor stores your OpenAI-compatible API key (Bonzai, LiteLLM, AI Gateway, ...) **once per profile**, not per repo. If you bill five client projects, you either babysit Settings all day or bill the wrong project. `cursor-keys` gives each project its own isolated profile and makes opening, switching, and checking keys automatic.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
╔═╗┬ ┬┬─┐┌─┐┌─┐┬─┐ ┬┌─┌─┐┬ ┬┌─┐
|
|
9
|
+
│ │ │├┬┘└─┐│ │├┬┘ ├┴┐├┤ └┬┘└─┐
|
|
10
|
+
└─┘└─┘┴└─└─┘└─┘┴└─ ┴ ┴└─┘ ┴ └─┘ per-project Cursor API keys
|
|
11
|
+
|
|
12
|
+
$ cursor-keys status
|
|
13
|
+
|
|
14
|
+
Project | Color | Windows | Key | Repo
|
|
15
|
+
--------------------+----------+---------+-----+-------------------------
|
|
16
|
+
famework-platform-1 | #339af0 | 1 | yes | ~/Projects/famework-platform-1
|
|
17
|
+
jlr_mss | #4f8cff | 0 | no | ~/Projects/jlr_mss
|
|
18
|
+
nokia | #845ef7 | 1 | yes | ~/Projects/nokia-main-cs
|
|
19
|
+
|
|
20
|
+
$ cursor-keys doctor nokia
|
|
21
|
+
|
|
22
|
+
✓ OpenAI/Bonzai API key row present
|
|
23
|
+
✓ Explore subagent overrides present
|
|
24
|
+
✓ Base URL (connect.iobonzai.com/cursor) in profile state
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g cursor-keys
|
|
31
|
+
# or from source: git clone https://github.com/L-ubu/cursor-keys && cd cursor-keys && npm link
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# one-time, only if your gateway is not the Bonzai default
|
|
40
|
+
cursor-keys config baseUrl https://your-gateway.example.com/v1
|
|
41
|
+
|
|
42
|
+
# one-time per project
|
|
43
|
+
cursor-keys add acme --path ~/Projects/acme # register + create profile
|
|
44
|
+
cursor-keys clone acme # inherit sign-in, settings, model config from main Cursor
|
|
45
|
+
cursor-keys set-key acme # hidden prompt, key written encrypted (Cursor's own safeStorage)
|
|
46
|
+
|
|
47
|
+
# daily use (pick your style)
|
|
48
|
+
cursor . # transparent wrapper injects the right profile
|
|
49
|
+
acme # shell alias
|
|
50
|
+
# or Cmd+Shift+P -> "cursor-keys: Open Project..." inside Cursor
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
One-time extras for the full experience:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cursor-keys wrap --install # plain `cursor <path>` auto-picks the profile
|
|
57
|
+
cursor-keys aliases --install # per-project shell aliases
|
|
58
|
+
cursor-keys install-extension # status bar indicator + wrong-window guard
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## How it works
|
|
62
|
+
|
|
63
|
+
One window, one profile, one key. Each project gets an isolated `--user-data-dir` under `~/.cursor-profiles/<name>/`, which is the only way Cursor can hold two API keys at once. Extensions, MCP config, keybindings, and snippets stay shared (symlinked); secrets and sign-in isolate, which is exactly the split you want.
|
|
64
|
+
|
|
65
|
+
Settings flow one way, main to profiles: `sync` re-copies your main `settings.json` into every profile and re-applies that profile's title bar color on top. `open` does this automatically before every launch, and open windows hot-reload the file, so tweaks in your main Cursor show up everywhere. Changes made inside a profile window are overwritten on the next sync.
|
|
66
|
+
|
|
67
|
+
`clone` copies your main Cursor state (SQLite backup) so profiles open signed-in with your theme and model config. `set-key` encrypts your project key with Cursor's own safeStorage (Chromium OSCrypt, key from the "Cursor Safe Storage" Keychain item) and writes it into the profile's secret store, round-trip verified. macOS asks for Keychain permission once; click Always Allow.
|
|
68
|
+
|
|
69
|
+
## "Which key am I on?" in three layers
|
|
70
|
+
|
|
71
|
+
1. **Title bar color** per project, injected into each profile's settings
|
|
72
|
+
2. **Status bar item** from the companion extension: `$(key) acme` when correct, warning + one-click relaunch when you open a repo in the wrong window
|
|
73
|
+
3. **`cursor-keys status` / `current`** in the terminal
|
|
74
|
+
|
|
75
|
+
## Commands
|
|
76
|
+
|
|
77
|
+
| Command | What it does |
|
|
78
|
+
| ---------------------------------------------- | ------------------------------------------------------------ |
|
|
79
|
+
| `init [--root DIR] [--all]` | Discover git repos and register them |
|
|
80
|
+
| `add <name> [--path] [--color] [--clone main]` | Create a project profile |
|
|
81
|
+
| `clone <name> [--from main]` | Copy sign-in + settings + model config from a donor profile |
|
|
82
|
+
| `set-key <name> [--key sk-...]` | Inject the project API key (encrypted, verified) |
|
|
83
|
+
| `open [name]` | Launch Cursor with the profile (auto-detect from cwd) |
|
|
84
|
+
| `status` | Projects, colors, running windows, key configured |
|
|
85
|
+
| `current` | Mapping for the current directory |
|
|
86
|
+
| `config [baseUrl [URL]]` | Show or set the gateway base URL |
|
|
87
|
+
| `sync [name]` | Re-copy main Cursor settings into profiles |
|
|
88
|
+
| `doctor [name]` | Health checks: key row, base URL, subagent overrides |
|
|
89
|
+
| `wrap [--install]` | zsh function so plain `cursor <path>` auto-picks the profile |
|
|
90
|
+
| `aliases [--install]` | Per-project shell aliases |
|
|
91
|
+
| `install-extension` | Link the Cursor extension into `~/.cursor/extensions` |
|
|
92
|
+
| `rm <name> [--delete]` | Unregister (optionally delete the profile) |
|
|
93
|
+
|
|
94
|
+
## Config
|
|
95
|
+
|
|
96
|
+
`~/.config/cursor-keys/config.json`:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"profilesRoot": "~/.cursor-profiles",
|
|
101
|
+
"baseUrl": "https://connect.iobonzai.com/cursor",
|
|
102
|
+
"projects": {
|
|
103
|
+
"acme": { "path": "~/Projects/acme", "color": "#4f8cff" }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`baseUrl` is your gateway's OpenAI-compatible endpoint (Bonzai default; point it at LiteLLM, Vercel AI Gateway, ...). `doctor` verifies it per profile.
|
|
109
|
+
|
|
110
|
+
## Security
|
|
111
|
+
|
|
112
|
+
- Keys go **straight into Cursor's own encrypted store** (`secret://cursorAuth/openAIKey`), encrypted with the same safeStorage mechanism Cursor uses. Never in config files, logs, or stdout.
|
|
113
|
+
- `set-key` reads the "Cursor Safe Storage" Keychain item to encrypt, the same key Cursor itself uses. Nothing leaves your machine.
|
|
114
|
+
- `clone` copies `state.vscdb` locally via SQLite backup. The donor can keep running; the target profile window must be closed.
|
|
115
|
+
|
|
116
|
+
## Known limitations
|
|
117
|
+
|
|
118
|
+
- A window opened for project A keeps A's key if you open repo B inside it. The extension detects this and offers a one-click relaunch; title bar colors make it visible at a glance.
|
|
119
|
+
- `set-key` is macOS-only (Keychain + OSCrypt). On Windows/Linux, paste the key once in Settings per profile; everything else still works.
|
|
120
|
+
- Explore subagents spawned ad-hoc still use Cursor's built-in model (per Cursor's own limitation), parent agents use your custom models.
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm test # node --test, zero deps
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT · [L-ubu](https://github.com/L-ubu)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runCommand } from "../src/commands.mjs";
|
|
4
|
+
import { bold, cyan, dim, reset } from "../src/ansi.mjs";
|
|
5
|
+
|
|
6
|
+
const BANNER = `
|
|
7
|
+
${cyan}╔═╗┬ ┬┬─┐┌─┐┌─┐┬─┐ ┬┌─┌─┐┬ ┬┌─┐
|
|
8
|
+
${cyan}│ │ │├┬┘└─┐│ │├┬┘ ├┴┐├┤ └┬┘└─┐
|
|
9
|
+
${cyan}└─┘└─┘┴└─└─┘└─┘┴└─ ┴ ┴└─┘ ┴ └─┘${dim} per-project Cursor API keys
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
function printHelp() {
|
|
13
|
+
console.log(BANNER);
|
|
14
|
+
console.log(`${bold}Usage:${reset} cursor-keys <command> [options]\n`);
|
|
15
|
+
console.log(`${bold}Commands:${reset}`);
|
|
16
|
+
console.log(
|
|
17
|
+
" init [--root DIR] [--all] Scan for git repos and register projects",
|
|
18
|
+
);
|
|
19
|
+
console.log(" add <name> [options] Create profile for a project");
|
|
20
|
+
console.log(
|
|
21
|
+
" open [name] Open Cursor with this project's profile",
|
|
22
|
+
);
|
|
23
|
+
console.log(
|
|
24
|
+
" status List projects, running windows, key status",
|
|
25
|
+
);
|
|
26
|
+
console.log(" current Show mapping for cwd");
|
|
27
|
+
console.log(
|
|
28
|
+
" config [baseUrl [URL]] Show or set config (gateway base URL)",
|
|
29
|
+
);
|
|
30
|
+
console.log(
|
|
31
|
+
" sync [name] Re-copy main Cursor settings into profiles",
|
|
32
|
+
);
|
|
33
|
+
console.log(
|
|
34
|
+
" doctor [name] Verify setup for one or all projects",
|
|
35
|
+
);
|
|
36
|
+
console.log(" aliases [--install] Print or install shell aliases");
|
|
37
|
+
console.log(
|
|
38
|
+
" wrap [--install] Make plain `cursor <path>` auto-pick profile",
|
|
39
|
+
);
|
|
40
|
+
console.log(
|
|
41
|
+
" install-extension Link the Cursor extension (status bar + warnings)",
|
|
42
|
+
);
|
|
43
|
+
console.log(
|
|
44
|
+
" rm <name> [--delete] Unregister (optionally delete profile dir)",
|
|
45
|
+
);
|
|
46
|
+
console.log(
|
|
47
|
+
"\nRun `cursor-keys <command> --help` for command-specific options.",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const args = process.argv.slice(2);
|
|
52
|
+
if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
|
|
53
|
+
printHelp();
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await runCommand(args);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error(e.message || String(e));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
const vscode = require("vscode");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawn } = require("child_process");
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILE = path.join(
|
|
8
|
+
os.homedir(),
|
|
9
|
+
".config",
|
|
10
|
+
"cursor-keys",
|
|
11
|
+
"config.json",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
function expandHome(p) {
|
|
15
|
+
if (!p) return p;
|
|
16
|
+
if (p === "~") return os.homedir();
|
|
17
|
+
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
18
|
+
return p;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function loadConfig() {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The profile root this window runs in, derived from globalStorageUri:
|
|
31
|
+
* <profileRoot>/User/globalStorage/<publisher>.<name>
|
|
32
|
+
*/
|
|
33
|
+
function currentProfileRoot(context) {
|
|
34
|
+
const fsPath = context.globalStorageUri.fsPath;
|
|
35
|
+
const marker = path.sep + "User" + path.sep + "globalStorage" + path.sep;
|
|
36
|
+
const idx = fsPath.indexOf(marker);
|
|
37
|
+
return idx === -1 ? null : fsPath.slice(0, idx);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function projectForFolder(config, folder) {
|
|
41
|
+
let best = null;
|
|
42
|
+
let bestLen = 0;
|
|
43
|
+
for (const [name, entry] of Object.entries(config.projects || {})) {
|
|
44
|
+
if (!entry || typeof entry.path !== "string") continue;
|
|
45
|
+
const repo = path.resolve(expandHome(entry.path));
|
|
46
|
+
if (folder === repo || folder.startsWith(repo + path.sep)) {
|
|
47
|
+
if (repo.length > bestLen) {
|
|
48
|
+
best = name;
|
|
49
|
+
bestLen = repo.length;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return best;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function profileDirFor(config, name) {
|
|
57
|
+
return path.join(expandHome(config.profilesRoot), name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function findCursorBinary() {
|
|
61
|
+
// App-bundle CLI first: ~/.local/bin/cursor is a PATH-dependent shim that
|
|
62
|
+
// fails in the extension host's minimal GUI environment.
|
|
63
|
+
const candidates = [
|
|
64
|
+
"/Applications/Cursor.app/Contents/Resources/app/bin/cursor",
|
|
65
|
+
"/usr/local/bin/cursor",
|
|
66
|
+
"/opt/homebrew/bin/cursor",
|
|
67
|
+
path.join(os.homedir(), ".local", "bin", "cursor"),
|
|
68
|
+
];
|
|
69
|
+
for (const c of candidates) {
|
|
70
|
+
if (fs.existsSync(c)) return c;
|
|
71
|
+
}
|
|
72
|
+
return "cursor";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function launch(profileDir, folder) {
|
|
76
|
+
const bin = findCursorBinary();
|
|
77
|
+
const child = spawn(
|
|
78
|
+
bin,
|
|
79
|
+
["--user-data-dir", profileDir, "--new-window", folder],
|
|
80
|
+
{ detached: true, stdio: "ignore" },
|
|
81
|
+
);
|
|
82
|
+
child.unref();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @param {vscode.ExtensionContext} context */
|
|
86
|
+
function activate(context) {
|
|
87
|
+
const statusBar = vscode.window.createStatusBarItem(
|
|
88
|
+
vscode.StatusBarAlignment.Left,
|
|
89
|
+
100,
|
|
90
|
+
);
|
|
91
|
+
let warned = false;
|
|
92
|
+
|
|
93
|
+
function relaunch(projectName, folder) {
|
|
94
|
+
const config = loadConfig();
|
|
95
|
+
if (!config) return;
|
|
96
|
+
launch(profileDirFor(config, projectName), folder);
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
vscode.commands.executeCommand("workbench.action.closeWindow");
|
|
99
|
+
}, 1500);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function update() {
|
|
103
|
+
const config = loadConfig();
|
|
104
|
+
if (!config) {
|
|
105
|
+
statusBar.hide();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const profileRoot = currentProfileRoot(context);
|
|
109
|
+
if (!profileRoot) {
|
|
110
|
+
statusBar.hide();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const folders =
|
|
115
|
+
vscode.workspace.workspaceFolders?.map((f) => f.uri.fsPath) || [];
|
|
116
|
+
let expected = null;
|
|
117
|
+
let expectedFolder = null;
|
|
118
|
+
for (const f of folders) {
|
|
119
|
+
const p = projectForFolder(config, f);
|
|
120
|
+
if (p) {
|
|
121
|
+
expected = p;
|
|
122
|
+
expectedFolder = f;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const profilesRoot = path.resolve(expandHome(config.profilesRoot));
|
|
128
|
+
const resolvedRoot = path.resolve(profileRoot);
|
|
129
|
+
const inManagedProfile =
|
|
130
|
+
resolvedRoot.startsWith(profilesRoot + path.sep) ||
|
|
131
|
+
resolvedRoot === profilesRoot;
|
|
132
|
+
const activeProfileName = inManagedProfile
|
|
133
|
+
? path.basename(resolvedRoot)
|
|
134
|
+
: null;
|
|
135
|
+
|
|
136
|
+
if (expected) {
|
|
137
|
+
const expectedDir = path.resolve(profileDirFor(config, expected));
|
|
138
|
+
if (resolvedRoot === expectedDir) {
|
|
139
|
+
statusBar.text = `$(key) ${expected}`;
|
|
140
|
+
statusBar.tooltip = `cursor-keys: this window bills "${expected}"`;
|
|
141
|
+
statusBar.backgroundColor = undefined;
|
|
142
|
+
statusBar.command = "cursor-keys.openProject";
|
|
143
|
+
statusBar.show();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
statusBar.text = `$(warning) wrong key: expected ${expected}`;
|
|
148
|
+
statusBar.backgroundColor = new vscode.ThemeColor(
|
|
149
|
+
"statusBarItem.warningBackground",
|
|
150
|
+
);
|
|
151
|
+
statusBar.tooltip = `"${path.basename(expectedFolder)}" belongs to "${expected}", but this window uses ${
|
|
152
|
+
activeProfileName ? `"${activeProfileName}"` : "the default profile"
|
|
153
|
+
}. Click to relaunch.`;
|
|
154
|
+
statusBar.command = "cursor-keys.relaunchHere";
|
|
155
|
+
statusBar.show();
|
|
156
|
+
|
|
157
|
+
if (!warned) {
|
|
158
|
+
warned = true;
|
|
159
|
+
vscode.window
|
|
160
|
+
.showWarningMessage(
|
|
161
|
+
`cursor-keys: this folder belongs to project "${expected}", but this window uses ${
|
|
162
|
+
activeProfileName
|
|
163
|
+
? `the "${activeProfileName}" profile`
|
|
164
|
+
: "the default profile"
|
|
165
|
+
}. API calls would bill the wrong project.`,
|
|
166
|
+
"Relaunch correctly",
|
|
167
|
+
"Ignore",
|
|
168
|
+
)
|
|
169
|
+
.then((choice) => {
|
|
170
|
+
if (choice === "Relaunch correctly") {
|
|
171
|
+
relaunch(expected, expectedFolder);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (activeProfileName) {
|
|
179
|
+
statusBar.text = `$(key) ${activeProfileName}`;
|
|
180
|
+
statusBar.tooltip = `cursor-keys: window profile "${activeProfileName}" (this folder is not registered)`;
|
|
181
|
+
statusBar.backgroundColor = undefined;
|
|
182
|
+
statusBar.command = "cursor-keys.openProject";
|
|
183
|
+
statusBar.show();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
statusBar.hide();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
context.subscriptions.push(
|
|
191
|
+
statusBar,
|
|
192
|
+
vscode.workspace.onDidChangeWorkspaceFolders(update),
|
|
193
|
+
vscode.commands.registerCommand("cursor-keys.relaunchHere", () => {
|
|
194
|
+
const config = loadConfig();
|
|
195
|
+
if (!config) return;
|
|
196
|
+
const folders =
|
|
197
|
+
vscode.workspace.workspaceFolders?.map((f) => f.uri.fsPath) || [];
|
|
198
|
+
for (const f of folders) {
|
|
199
|
+
const p = projectForFolder(config, f);
|
|
200
|
+
if (p) {
|
|
201
|
+
relaunch(p, f);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}),
|
|
206
|
+
vscode.commands.registerCommand("cursor-keys.openProject", async () => {
|
|
207
|
+
const config = loadConfig();
|
|
208
|
+
if (!config || !config.projects) {
|
|
209
|
+
vscode.window.showInformationMessage(
|
|
210
|
+
"cursor-keys: no config found. Run `cursor-keys init` in a terminal.",
|
|
211
|
+
);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const names = Object.keys(config.projects).sort();
|
|
215
|
+
if (!names.length) {
|
|
216
|
+
vscode.window.showInformationMessage(
|
|
217
|
+
"cursor-keys: no projects registered yet.",
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const picked = await vscode.window.showQuickPick(
|
|
222
|
+
names.map((n) => ({
|
|
223
|
+
label: n,
|
|
224
|
+
description: config.projects[n].path,
|
|
225
|
+
})),
|
|
226
|
+
{ placeHolder: "Open project in its own Cursor profile" },
|
|
227
|
+
);
|
|
228
|
+
if (!picked) return;
|
|
229
|
+
launch(
|
|
230
|
+
profileDirFor(config, picked.label),
|
|
231
|
+
path.resolve(expandHome(config.projects[picked.label].path)),
|
|
232
|
+
);
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const watcher = vscode.workspace.createFileSystemWatcher(CONFIG_FILE);
|
|
237
|
+
context.subscriptions.push(
|
|
238
|
+
watcher,
|
|
239
|
+
watcher.onDidChange(update),
|
|
240
|
+
watcher.onDidCreate(update),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
update();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function deactivate() {}
|
|
247
|
+
|
|
248
|
+
module.exports = { activate, deactivate };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cursor-keys",
|
|
3
|
+
"displayName": "cursor-keys",
|
|
4
|
+
"description": "Shows which cursor-keys profile (API key context) this window uses, and warns when a folder is opened in the wrong profile.",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"publisher": "lubu",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/L-ubu/cursor-keys",
|
|
11
|
+
"directory": "extension"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"vscode": "^1.80.0"
|
|
15
|
+
},
|
|
16
|
+
"categories": [
|
|
17
|
+
"Other"
|
|
18
|
+
],
|
|
19
|
+
"activationEvents": [
|
|
20
|
+
"onStartupFinished"
|
|
21
|
+
],
|
|
22
|
+
"main": "./extension.js",
|
|
23
|
+
"contributes": {
|
|
24
|
+
"commands": [
|
|
25
|
+
{
|
|
26
|
+
"command": "cursor-keys.openProject",
|
|
27
|
+
"title": "cursor-keys: Open Project..."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"command": "cursor-keys.relaunchHere",
|
|
31
|
+
"title": "cursor-keys: Relaunch This Folder in Its Project Profile"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-keys",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Per-project Cursor profiles so each window uses the correct BYOK API key. Zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cursor-keys": "bin/cursor-keys.mjs",
|
|
8
|
+
"ckeys": "bin/cursor-keys.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"src",
|
|
16
|
+
"extension"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"cursor",
|
|
20
|
+
"cli",
|
|
21
|
+
"api-key",
|
|
22
|
+
"bonzai",
|
|
23
|
+
"byok",
|
|
24
|
+
"developer-tools",
|
|
25
|
+
"productivity"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"author": "L-ubu (https://github.com/L-ubu)",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/L-ubu/cursor-keys.git"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "node --test",
|
|
35
|
+
"prepublishOnly": "npm test"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/ansi.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const esc = (n) => `\u001b[${n}m`;
|
|
2
|
+
|
|
3
|
+
export const reset = esc(0);
|
|
4
|
+
export const bold = esc(1);
|
|
5
|
+
export const dim = esc(2);
|
|
6
|
+
export const red = esc(31);
|
|
7
|
+
export const green = esc(32);
|
|
8
|
+
export const yellow = esc(33);
|
|
9
|
+
export const blue = esc(34);
|
|
10
|
+
export const magenta = esc(35);
|
|
11
|
+
export const cyan = esc(36);
|
|
12
|
+
|
|
13
|
+
export function colorize(text, code) {
|
|
14
|
+
return `${code}${text}${reset}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ok(text) {
|
|
18
|
+
return colorize(text, green);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function warn(text) {
|
|
22
|
+
return colorize(text, yellow);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function err(text) {
|
|
26
|
+
return colorize(text, red);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function padEnd(str, len) {
|
|
30
|
+
const s = String(str);
|
|
31
|
+
if (s.length >= len) return s.slice(0, len);
|
|
32
|
+
return s + " ".repeat(len - s.length);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function printTable(headers, rows) {
|
|
36
|
+
const widths = headers.map((h, i) =>
|
|
37
|
+
Math.max(h.length, ...rows.map((r) => String(r[i] ?? "").length)),
|
|
38
|
+
);
|
|
39
|
+
const line = widths.map((w) => "-".repeat(w)).join("-+-");
|
|
40
|
+
console.log(widths.map((w, i) => padEnd(headers[i], w)).join(" | "));
|
|
41
|
+
console.log(line);
|
|
42
|
+
for (const row of rows) {
|
|
43
|
+
console.log(
|
|
44
|
+
widths.map((w, i) => padEnd(String(row[i] ?? ""), w)).join(" | "),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function blockSwatch(hex) {
|
|
50
|
+
if (!hex || !/^#[0-9a-fA-F]{6}$/.test(hex)) return " ";
|
|
51
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
52
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
53
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
54
|
+
return `${esc(48)};2;${r};${g};${b}m ${reset}`;
|
|
55
|
+
}
|