locker-cli 0.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/dist/auth/client.d.ts +18 -0
- package/dist/auth/client.js +66 -0
- package/dist/auth/client.js.map +1 -0
- package/dist/auth/config.d.ts +23 -0
- package/dist/auth/config.js +69 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/config.test.d.ts +1 -0
- package/dist/auth/config.test.js +73 -0
- package/dist/auth/config.test.js.map +1 -0
- package/dist/commands/api.test.d.ts +1 -0
- package/dist/commands/api.test.js +116 -0
- package/dist/commands/api.test.js.map +1 -0
- package/dist/commands/commands.test.d.ts +1 -0
- package/dist/commands/commands.test.js +104 -0
- package/dist/commands/commands.test.js.map +1 -0
- package/dist/commands/get.d.ts +3 -0
- package/dist/commands/get.js +25 -0
- package/dist/commands/get.js.map +1 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +27 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +87 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +15 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +83 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/revoke.d.ts +1 -0
- package/dist/commands/revoke.js +19 -0
- package/dist/commands/revoke.js.map +1 -0
- package/dist/commands/set.d.ts +1 -0
- package/dist/commands/set.js +15 -0
- package/dist/commands/set.js.map +1 -0
- package/dist/commands/whoami.d.ts +1 -0
- package/dist/commands/whoami.js +9 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/package.json +26 -0
- package/src/auth/client.ts +86 -0
- package/src/auth/config.test.ts +78 -0
- package/src/auth/config.ts +66 -0
- package/src/commands/api.test.ts +143 -0
- package/src/commands/commands.test.ts +87 -0
- package/src/commands/get.ts +32 -0
- package/src/commands/list.ts +38 -0
- package/src/commands/login.ts +93 -0
- package/src/commands/logout.ts +12 -0
- package/src/commands/mcp.ts +87 -0
- package/src/commands/revoke.ts +23 -0
- package/src/commands/set.ts +20 -0
- package/src/commands/whoami.ts +6 -0
- package/src/index.ts +72 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loginCommand = loginCommand;
|
|
7
|
+
const node_readline_1 = __importDefault(require("node:readline"));
|
|
8
|
+
const config_1 = require("../auth/config");
|
|
9
|
+
const client_1 = require("../auth/client");
|
|
10
|
+
function prompt(question, hidden = false) {
|
|
11
|
+
const rl = node_readline_1.default.createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout,
|
|
14
|
+
});
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
if (hidden && process.stdin.isTTY) {
|
|
17
|
+
// Hide password input
|
|
18
|
+
process.stdout.write(question);
|
|
19
|
+
const stdin = process.stdin;
|
|
20
|
+
stdin.setRawMode(true);
|
|
21
|
+
stdin.resume();
|
|
22
|
+
stdin.setEncoding("utf8");
|
|
23
|
+
let input = "";
|
|
24
|
+
const onData = (ch) => {
|
|
25
|
+
if (ch === "\n" || ch === "\r" || ch === "\u0004") {
|
|
26
|
+
stdin.setRawMode(false);
|
|
27
|
+
stdin.removeListener("data", onData);
|
|
28
|
+
stdin.pause();
|
|
29
|
+
rl.close();
|
|
30
|
+
process.stdout.write("\n");
|
|
31
|
+
resolve(input);
|
|
32
|
+
}
|
|
33
|
+
else if (ch === "\u0003") {
|
|
34
|
+
// Ctrl-C
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
else if (ch === "\u007F" || ch === "\b") {
|
|
38
|
+
// Backspace
|
|
39
|
+
if (input.length > 0) {
|
|
40
|
+
input = input.slice(0, -1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
input += ch;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
stdin.on("data", onData);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
rl.question(question, (answer) => {
|
|
51
|
+
rl.close();
|
|
52
|
+
resolve(answer);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function loginCommand(options) {
|
|
58
|
+
const apiUrl = options.api || (0, client_1.getDefaultApiUrl)();
|
|
59
|
+
const isRegister = options.register || false;
|
|
60
|
+
console.log(isRegister ? "Create a new Locker account" : "Log in to Locker");
|
|
61
|
+
console.log();
|
|
62
|
+
const email = await prompt("Email: ");
|
|
63
|
+
const password = await prompt("Password: ", true);
|
|
64
|
+
if (!email || !password) {
|
|
65
|
+
console.error("Email and password are required.");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
if (isRegister && password.length < 8) {
|
|
69
|
+
console.error("Password must be at least 8 characters.");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const endpoint = isRegister ? "/auth/register" : "/auth/login";
|
|
73
|
+
const res = await (0, client_1.authRequest)("POST", endpoint, apiUrl, { email, password });
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
console.error(res.data.error || "Authentication failed.");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
(0, config_1.writeConfig)({
|
|
79
|
+
token: res.data.token,
|
|
80
|
+
email: res.data.user.email,
|
|
81
|
+
apiUrl,
|
|
82
|
+
});
|
|
83
|
+
console.log();
|
|
84
|
+
console.log(`Logged in as ${res.data.user.email}`);
|
|
85
|
+
console.log("Token stored in ~/.locker/config");
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function logoutCommand(): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logoutCommand = logoutCommand;
|
|
4
|
+
const config_1 = require("../auth/config");
|
|
5
|
+
function logoutCommand() {
|
|
6
|
+
const config = (0, config_1.readConfig)();
|
|
7
|
+
(0, config_1.clearConfig)();
|
|
8
|
+
if (config) {
|
|
9
|
+
console.log(`Logged out (${config.email}). Token cleared.`);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
console.log("Already logged out.");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=logout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logout.js","sourceRoot":"","sources":["../../src/commands/logout.ts"],"names":[],"mappings":";;AAEA,sCASC;AAXD,2CAAyD;AAEzD,SAAgB,aAAa;IAC3B,MAAM,MAAM,GAAG,IAAA,mBAAU,GAAE,CAAC;IAC5B,IAAA,oBAAW,GAAE,CAAC;IAEd,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,CAAC,GAAG,CAAC,eAAe,MAAM,CAAC,KAAK,mBAAmB,CAAC,CAAC;IAC9D,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IACrC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.mcpInstallCommand = mcpInstallCommand;
|
|
7
|
+
exports.mcpUninstallCommand = mcpUninstallCommand;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const TARGETS = {
|
|
12
|
+
claude: node_path_1.default.join(node_os_1.default.homedir(), ".claude", "claude_desktop_config.json"),
|
|
13
|
+
cursor: node_path_1.default.join(node_os_1.default.homedir(), ".cursor", "mcp.json"),
|
|
14
|
+
};
|
|
15
|
+
function getMcpEntry() {
|
|
16
|
+
// Use the published npm package — npx resolves it globally
|
|
17
|
+
return { command: "npx", args: ["locker-mcp-server"] };
|
|
18
|
+
}
|
|
19
|
+
function installToTarget(name, configPath) {
|
|
20
|
+
const dir = node_path_1.default.dirname(configPath);
|
|
21
|
+
if (!node_fs_1.default.existsSync(dir)) {
|
|
22
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
let config = {};
|
|
25
|
+
if (node_fs_1.default.existsSync(configPath)) {
|
|
26
|
+
try {
|
|
27
|
+
config = JSON.parse(node_fs_1.default.readFileSync(configPath, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
console.error(` Could not parse ${configPath}`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!config.mcpServers) {
|
|
35
|
+
config.mcpServers = {};
|
|
36
|
+
}
|
|
37
|
+
const entry = getMcpEntry();
|
|
38
|
+
config.mcpServers.locker = entry;
|
|
39
|
+
node_fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
40
|
+
console.log(` ✓ ${name} — ${configPath}`);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
function mcpInstallCommand() {
|
|
44
|
+
console.log("Installing Locker MCP server...\n");
|
|
45
|
+
let installed = 0;
|
|
46
|
+
for (const [name, configPath] of Object.entries(TARGETS)) {
|
|
47
|
+
const dir = node_path_1.default.dirname(configPath);
|
|
48
|
+
// Only install if the tool's config directory exists (tool is installed)
|
|
49
|
+
if (node_fs_1.default.existsSync(dir) || name === "claude") {
|
|
50
|
+
if (installToTarget(name, configPath))
|
|
51
|
+
installed++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (installed === 0) {
|
|
55
|
+
console.log("No supported AI tools detected.");
|
|
56
|
+
console.log("Manually add to your tool's MCP config:");
|
|
57
|
+
const entry = getMcpEntry();
|
|
58
|
+
console.log(JSON.stringify({ locker: entry }, null, 2));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log(`\nDone. Restart your AI tool to activate.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function mcpUninstallCommand() {
|
|
65
|
+
console.log("Removing Locker MCP server...\n");
|
|
66
|
+
for (const [name, configPath] of Object.entries(TARGETS)) {
|
|
67
|
+
if (!node_fs_1.default.existsSync(configPath))
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
const config = JSON.parse(node_fs_1.default.readFileSync(configPath, "utf8"));
|
|
71
|
+
if (config.mcpServers?.locker) {
|
|
72
|
+
delete config.mcpServers.locker;
|
|
73
|
+
node_fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
74
|
+
console.log(` ✓ Removed from ${name}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Skip
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
console.log("\nDone.");
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=mcp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function revokeCommand(service: string): Promise<void>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.revokeCommand = revokeCommand;
|
|
4
|
+
const config_1 = require("../auth/config");
|
|
5
|
+
const client_1 = require("../auth/client");
|
|
6
|
+
async function revokeCommand(service) {
|
|
7
|
+
const config = (0, config_1.requireAuth)();
|
|
8
|
+
const res = await (0, client_1.apiRequest)("DELETE", `/keys/${encodeURIComponent(service)}`, config);
|
|
9
|
+
if (!res.ok) {
|
|
10
|
+
if (res.status === 404) {
|
|
11
|
+
console.error(`No key found for service: ${service}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
console.error(res.data.error || "Failed to revoke key.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
console.log(`Key revoked for ${service}`);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=revoke.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revoke.js","sourceRoot":"","sources":["../../src/commands/revoke.ts"],"names":[],"mappings":";;AAGA,sCAmBC;AAtBD,2CAA6C;AAC7C,2CAA4C;AAErC,KAAK,UAAU,aAAa,CAAC,OAAe;IACjD,MAAM,MAAM,GAAG,IAAA,oBAAW,GAAE,CAAC;IAE7B,MAAM,GAAG,GAAG,MAAM,IAAA,mBAAU,EAC1B,QAAQ,EACR,SAAS,kBAAkB,CAAC,OAAO,CAAC,EAAE,EACtC,MAAM,CACP,CAAC;IAEF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,OAAO,CAAC,KAAK,CAAC,6BAA6B,OAAO,EAAE,CAAC,CAAC;YACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,uBAAuB,CAAC,CAAC;QACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,mBAAmB,OAAO,EAAE,CAAC,CAAC;AAC5C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function setCommand(service: string, key: string): Promise<void>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setCommand = setCommand;
|
|
4
|
+
const config_1 = require("../auth/config");
|
|
5
|
+
const client_1 = require("../auth/client");
|
|
6
|
+
async function setCommand(service, key) {
|
|
7
|
+
const config = (0, config_1.requireAuth)();
|
|
8
|
+
const res = await (0, client_1.apiRequest)("POST", "/keys", config, { service, key });
|
|
9
|
+
if (!res.ok) {
|
|
10
|
+
console.error(res.data.error || "Failed to store key.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
console.log(`Key stored for ${service}`);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=set.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set.js","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":";;AAGA,gCAgBC;AAnBD,2CAA6C;AAC7C,2CAA4C;AAErC,KAAK,UAAU,UAAU,CAAC,OAAe,EAAE,GAAW;IAC3D,MAAM,MAAM,GAAG,IAAA,oBAAW,GAAE,CAAC;IAE7B,MAAM,GAAG,GAAG,MAAM,IAAA,mBAAU,EAC1B,MAAM,EACN,OAAO,EACP,MAAM,EACN,EAAE,OAAO,EAAE,GAAG,EAAE,CACjB,CAAC;IAEF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,sBAAsB,CAAC,CAAC;QACxD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,kBAAkB,OAAO,EAAE,CAAC,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function whoamiCommand(): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.whoamiCommand = whoamiCommand;
|
|
4
|
+
const config_1 = require("../auth/config");
|
|
5
|
+
function whoamiCommand() {
|
|
6
|
+
const config = (0, config_1.requireAuth)();
|
|
7
|
+
console.log(config.email);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=whoami.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whoami.js","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":";;AAEA,sCAGC;AALD,2CAA6C;AAE7C,SAAgB,aAAa;IAC3B,MAAM,MAAM,GAAG,IAAA,oBAAW,GAAE,CAAC;IAC7B,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const login_1 = require("./commands/login");
|
|
6
|
+
const logout_1 = require("./commands/logout");
|
|
7
|
+
const whoami_1 = require("./commands/whoami");
|
|
8
|
+
const get_1 = require("./commands/get");
|
|
9
|
+
const set_1 = require("./commands/set");
|
|
10
|
+
const list_1 = require("./commands/list");
|
|
11
|
+
const revoke_1 = require("./commands/revoke");
|
|
12
|
+
const mcp_1 = require("./commands/mcp");
|
|
13
|
+
const program = new commander_1.Command();
|
|
14
|
+
program
|
|
15
|
+
.name("locker")
|
|
16
|
+
.description("Secure API credential manager for AI agents")
|
|
17
|
+
.version("0.1.0");
|
|
18
|
+
program
|
|
19
|
+
.command("login")
|
|
20
|
+
.description("Log in to Locker")
|
|
21
|
+
.option("--register", "Create a new account")
|
|
22
|
+
.option("--api <url>", "API server URL")
|
|
23
|
+
.action(login_1.loginCommand);
|
|
24
|
+
program
|
|
25
|
+
.command("logout")
|
|
26
|
+
.description("Log out and clear stored token")
|
|
27
|
+
.action(logout_1.logoutCommand);
|
|
28
|
+
program
|
|
29
|
+
.command("whoami")
|
|
30
|
+
.description("Show the currently logged-in user")
|
|
31
|
+
.action(whoami_1.whoamiCommand);
|
|
32
|
+
program
|
|
33
|
+
.command("get <service>")
|
|
34
|
+
.description("Retrieve an API key (prints to stdout)")
|
|
35
|
+
.option("--agent <name>", "Agent identifier for audit log")
|
|
36
|
+
.action(get_1.getCommand);
|
|
37
|
+
program
|
|
38
|
+
.command("set <service> <key>")
|
|
39
|
+
.description("Store an API key")
|
|
40
|
+
.action(set_1.setCommand);
|
|
41
|
+
program
|
|
42
|
+
.command("list")
|
|
43
|
+
.description("List all stored services")
|
|
44
|
+
.action(list_1.listCommand);
|
|
45
|
+
program
|
|
46
|
+
.command("revoke <service>")
|
|
47
|
+
.description("Delete a stored API key")
|
|
48
|
+
.action(revoke_1.revokeCommand);
|
|
49
|
+
const mcp = program
|
|
50
|
+
.command("mcp")
|
|
51
|
+
.description("Manage the Locker MCP server");
|
|
52
|
+
mcp
|
|
53
|
+
.command("install")
|
|
54
|
+
.description("Install Locker MCP server into Claude Code, Cursor, etc.")
|
|
55
|
+
.action(mcp_1.mcpInstallCommand);
|
|
56
|
+
mcp
|
|
57
|
+
.command("uninstall")
|
|
58
|
+
.description("Remove Locker MCP server from AI tools")
|
|
59
|
+
.action(mcp_1.mcpUninstallCommand);
|
|
60
|
+
program.parse();
|
|
61
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAEA,yCAAoC;AACpC,4CAAgD;AAChD,8CAAkD;AAClD,8CAAkD;AAClD,wCAA4C;AAC5C,wCAA4C;AAC5C,0CAA8C;AAC9C,8CAAkD;AAClD,wCAAwE;AAExE,MAAM,OAAO,GAAG,IAAI,mBAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,QAAQ,CAAC;KACd,WAAW,CAAC,6CAA6C,CAAC;KAC1D,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,OAAO,CAAC;KAChB,WAAW,CAAC,kBAAkB,CAAC;KAC/B,MAAM,CAAC,YAAY,EAAE,sBAAsB,CAAC;KAC5C,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC;KACvC,MAAM,CAAC,oBAAY,CAAC,CAAC;AAExB,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,gCAAgC,CAAC;KAC7C,MAAM,CAAC,sBAAa,CAAC,CAAC;AAEzB,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,mCAAmC,CAAC;KAChD,MAAM,CAAC,sBAAa,CAAC,CAAC;AAEzB,OAAO;KACJ,OAAO,CAAC,eAAe,CAAC;KACxB,WAAW,CAAC,wCAAwC,CAAC;KACrD,MAAM,CAAC,gBAAgB,EAAE,gCAAgC,CAAC;KAC1D,MAAM,CAAC,gBAAU,CAAC,CAAC;AAEtB,OAAO;KACJ,OAAO,CAAC,qBAAqB,CAAC;KAC9B,WAAW,CAAC,kBAAkB,CAAC;KAC/B,MAAM,CAAC,gBAAU,CAAC,CAAC;AAEtB,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CAAC,0BAA0B,CAAC;KACvC,MAAM,CAAC,kBAAW,CAAC,CAAC;AAEvB,OAAO;KACJ,OAAO,CAAC,kBAAkB,CAAC;KAC3B,WAAW,CAAC,yBAAyB,CAAC;KACtC,MAAM,CAAC,sBAAa,CAAC,CAAC;AAEzB,MAAM,GAAG,GAAG,OAAO;KAChB,OAAO,CAAC,KAAK,CAAC;KACd,WAAW,CAAC,8BAA8B,CAAC,CAAC;AAE/C,GAAG;KACA,OAAO,CAAC,SAAS,CAAC;KAClB,WAAW,CAAC,0DAA0D,CAAC;KACvE,MAAM,CAAC,uBAAiB,CAAC,CAAC;AAE7B,GAAG;KACA,OAAO,CAAC,WAAW,CAAC;KACpB,WAAW,CAAC,wCAAwC,CAAC;KACrD,MAAM,CAAC,yBAAmB,CAAC,CAAC;AAE/B,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "locker-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for Locker — secure API credential manager for AI agents",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"locker": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsx src/index.ts",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"lint": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^12.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"tsx": "^4.19.0",
|
|
23
|
+
"typescript": "^5.6.0",
|
|
24
|
+
"vitest": "^2.1.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { LockerConfig } from "./config";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_API_URL = "http://localhost:3001";
|
|
4
|
+
|
|
5
|
+
export interface ApiResponse<T = any> {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
status: number;
|
|
8
|
+
data: T & { error?: string };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Makes an authenticated request to the Locker API.
|
|
13
|
+
* Handles common error cases (expired token, network, not found).
|
|
14
|
+
*/
|
|
15
|
+
export async function apiRequest<T = any>(
|
|
16
|
+
method: string,
|
|
17
|
+
path: string,
|
|
18
|
+
config: LockerConfig,
|
|
19
|
+
body?: Record<string, any>,
|
|
20
|
+
headers?: Record<string, string>
|
|
21
|
+
): Promise<ApiResponse<T>> {
|
|
22
|
+
const url = `${config.apiUrl}${path}`;
|
|
23
|
+
const reqHeaders: Record<string, string> = {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
Authorization: `Bearer ${config.token}`,
|
|
26
|
+
...headers,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(url, {
|
|
31
|
+
method,
|
|
32
|
+
headers: reqHeaders,
|
|
33
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const data = await res.json().catch(() => ({}));
|
|
37
|
+
|
|
38
|
+
if (res.status === 401) {
|
|
39
|
+
console.error("Session expired. Run: locker login");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { ok: res.ok, status: res.status, data: data as T & { error?: string } };
|
|
44
|
+
} catch (err: any) {
|
|
45
|
+
if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("fetch failed")) {
|
|
46
|
+
console.error(`Cannot connect to Locker API at ${config.apiUrl}`);
|
|
47
|
+
console.error("Is the server running?");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Makes an unauthenticated request (for login/register).
|
|
56
|
+
*/
|
|
57
|
+
export async function authRequest<T = any>(
|
|
58
|
+
method: string,
|
|
59
|
+
path: string,
|
|
60
|
+
apiUrl: string,
|
|
61
|
+
body: Record<string, any>
|
|
62
|
+
): Promise<ApiResponse<T>> {
|
|
63
|
+
const url = `${apiUrl}${path}`;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(url, {
|
|
67
|
+
method,
|
|
68
|
+
headers: { "Content-Type": "application/json" },
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const data = await res.json().catch(() => ({}));
|
|
73
|
+
return { ok: res.ok, status: res.status, data: data as T & { error?: string } };
|
|
74
|
+
} catch (err: any) {
|
|
75
|
+
if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("fetch failed")) {
|
|
76
|
+
console.error(`Cannot connect to Locker API at ${apiUrl}`);
|
|
77
|
+
console.error("Is the server running?");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getDefaultApiUrl(): string {
|
|
85
|
+
return process.env.LOCKER_API_URL || DEFAULT_API_URL;
|
|
86
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { readConfig, writeConfig, clearConfig, LockerConfig } from "./config";
|
|
6
|
+
|
|
7
|
+
// Use a temp directory to avoid touching the real ~/.locker
|
|
8
|
+
const ORIGINAL_HOME = os.homedir();
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "locker-test-"));
|
|
13
|
+
// Override HOME so ~/.locker resolves to tmpDir/.locker
|
|
14
|
+
process.env.HOME = tmpDir;
|
|
15
|
+
// The config module uses os.homedir() which caches, so we need to
|
|
16
|
+
// re-import or monkey-patch. Instead, we test the functions with
|
|
17
|
+
// a direct approach by creating the expected paths.
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
process.env.HOME = ORIGINAL_HOME;
|
|
22
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Since os.homedir() caches the HOME env at import time, we test
|
|
26
|
+
// the config read/write logic by directly operating on files in the
|
|
27
|
+
// expected location. The actual config module is tested via the CLI
|
|
28
|
+
// integration tests. Here we test the serialization logic.
|
|
29
|
+
|
|
30
|
+
describe("config serialization", () => {
|
|
31
|
+
it("writeConfig creates a valid JSON file", () => {
|
|
32
|
+
const configDir = path.join(tmpDir, ".locker");
|
|
33
|
+
const configFile = path.join(configDir, "config");
|
|
34
|
+
|
|
35
|
+
// Manually create what writeConfig does
|
|
36
|
+
fs.mkdirSync(configDir, { mode: 0o700 });
|
|
37
|
+
const config: LockerConfig = {
|
|
38
|
+
token: "jwt-token-123",
|
|
39
|
+
email: "test@example.com",
|
|
40
|
+
apiUrl: "http://localhost:3001",
|
|
41
|
+
};
|
|
42
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), {
|
|
43
|
+
mode: 0o600,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Verify
|
|
47
|
+
const raw = fs.readFileSync(configFile, "utf8");
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
expect(parsed.token).toBe("jwt-token-123");
|
|
50
|
+
expect(parsed.email).toBe("test@example.com");
|
|
51
|
+
expect(parsed.apiUrl).toBe("http://localhost:3001");
|
|
52
|
+
|
|
53
|
+
// Verify permissions (owner-only)
|
|
54
|
+
const stat = fs.statSync(configFile);
|
|
55
|
+
const mode = stat.mode & 0o777;
|
|
56
|
+
expect(mode).toBe(0o600);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("config file is valid JSON roundtrip", () => {
|
|
60
|
+
const config: LockerConfig = {
|
|
61
|
+
token: "eyJhbGciOiJIUzI1NiJ9.test",
|
|
62
|
+
email: "user@locker.dev",
|
|
63
|
+
apiUrl: "https://api.locker.dev",
|
|
64
|
+
};
|
|
65
|
+
const json = JSON.stringify(config, null, 2);
|
|
66
|
+
const parsed = JSON.parse(json) as LockerConfig;
|
|
67
|
+
expect(parsed).toEqual(config);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("handles missing fields gracefully", () => {
|
|
71
|
+
const incomplete = { token: "abc" };
|
|
72
|
+
const json = JSON.stringify(incomplete);
|
|
73
|
+
const parsed = JSON.parse(json);
|
|
74
|
+
// readConfig checks for all three fields
|
|
75
|
+
const valid = parsed.token && parsed.email && parsed.apiUrl;
|
|
76
|
+
expect(valid).toBeFalsy();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), ".locker");
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config");
|
|
7
|
+
|
|
8
|
+
export interface LockerConfig {
|
|
9
|
+
token: string;
|
|
10
|
+
email: string;
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reads the stored config from ~/.locker/config.
|
|
16
|
+
* Returns null if no config exists.
|
|
17
|
+
*/
|
|
18
|
+
export function readConfig(): LockerConfig | null {
|
|
19
|
+
try {
|
|
20
|
+
if (!fs.existsSync(CONFIG_FILE)) return null;
|
|
21
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf8");
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
if (!parsed.token || !parsed.email || !parsed.apiUrl) return null;
|
|
24
|
+
return parsed as LockerConfig;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Writes config to ~/.locker/config with chmod 600 (owner-only read/write).
|
|
32
|
+
* Creates ~/.locker directory if it doesn't exist.
|
|
33
|
+
*/
|
|
34
|
+
export function writeConfig(config: LockerConfig): void {
|
|
35
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
36
|
+
fs.mkdirSync(CONFIG_DIR, { mode: 0o700 });
|
|
37
|
+
}
|
|
38
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
|
39
|
+
mode: 0o600,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Deletes ~/.locker/config (logout).
|
|
45
|
+
*/
|
|
46
|
+
export function clearConfig(): void {
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
49
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Ignore — file may not exist
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns the config or exits with a helpful message if not logged in.
|
|
58
|
+
*/
|
|
59
|
+
export function requireAuth(): LockerConfig {
|
|
60
|
+
const config = readConfig();
|
|
61
|
+
if (!config) {
|
|
62
|
+
console.error("Not logged in. Run: locker login");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
return config;
|
|
66
|
+
}
|