@ao_zorin/zocket 1.1.0 → 1.3.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/README.md +15 -5
- package/dist/zocket.js +624 -41
- package/docs/AI_AUTODEPLOY.md +9 -13
- package/docs/GIT_NPM_RELEASE.md +4 -13
- package/docs/INSTALL.md +50 -118
- package/package.json +3 -1
- package/scripts/install-zocket.ps1 +34 -70
- package/scripts/install-zocket.sh +82 -143
package/dist/zocket.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { createServer } from "http";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { randomBytes as randomBytes6 } from "crypto";
|
|
6
|
+
import { mkdirSync as mkdirSync5 } from "fs";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
9
8
|
import { serve } from "@hono/node-server";
|
|
10
9
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
10
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
11
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
11
12
|
|
|
12
13
|
// src/vault.ts
|
|
13
14
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
@@ -15,11 +16,26 @@ import { dirname } from "path";
|
|
|
15
16
|
import { lock } from "proper-lockfile";
|
|
16
17
|
|
|
17
18
|
// src/crypto.ts
|
|
18
|
-
import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
|
|
19
|
+
import { randomBytes, createCipheriv, createDecipheriv, createHmac, timingSafeEqual } from "crypto";
|
|
19
20
|
var VERSION = 1;
|
|
20
21
|
var ALGORITHM = "aes-256-gcm";
|
|
21
22
|
var IV_BYTES = 12;
|
|
22
23
|
var TAG_BYTES = 16;
|
|
24
|
+
var FERNET_VERSION = 128;
|
|
25
|
+
function base64UrlToBuffer(raw) {
|
|
26
|
+
const normalized = raw.replace(/-/g, "+").replace(/_/g, "/");
|
|
27
|
+
const pad = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
28
|
+
return Buffer.from(normalized + pad, "base64");
|
|
29
|
+
}
|
|
30
|
+
function pkcs7Unpad(buf) {
|
|
31
|
+
if (!buf.length) throw new Error("Invalid PKCS7 padding");
|
|
32
|
+
const pad = buf[buf.length - 1];
|
|
33
|
+
if (pad < 1 || pad > 16) throw new Error("Invalid PKCS7 padding");
|
|
34
|
+
for (let i = buf.length - pad; i < buf.length; i += 1) {
|
|
35
|
+
if (buf[i] !== pad) throw new Error("Invalid PKCS7 padding");
|
|
36
|
+
}
|
|
37
|
+
return buf.subarray(0, buf.length - pad);
|
|
38
|
+
}
|
|
23
39
|
function encrypt(plaintext, key) {
|
|
24
40
|
const iv = randomBytes(IV_BYTES);
|
|
25
41
|
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
@@ -39,25 +55,86 @@ function decrypt(data, key) {
|
|
|
39
55
|
decipher.setAuthTag(tag);
|
|
40
56
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
41
57
|
}
|
|
58
|
+
function decryptLegacyFernet(tokenData, keyBase64) {
|
|
59
|
+
const token = tokenData.toString("utf8").trim();
|
|
60
|
+
const raw = base64UrlToBuffer(token);
|
|
61
|
+
if (raw.length < 1 + 8 + 16 + 32) throw new Error("Invalid legacy token length");
|
|
62
|
+
if (raw[0] !== FERNET_VERSION) throw new Error("Invalid legacy token version");
|
|
63
|
+
const key = base64UrlToBuffer(keyBase64);
|
|
64
|
+
if (key.length !== 32) throw new Error("Invalid legacy key length");
|
|
65
|
+
const signingKey = key.subarray(0, 16);
|
|
66
|
+
const encryptionKey = key.subarray(16, 32);
|
|
67
|
+
const hmacStart = raw.length - 32;
|
|
68
|
+
const msg = raw.subarray(0, hmacStart);
|
|
69
|
+
const sig = raw.subarray(hmacStart);
|
|
70
|
+
const mac = createHmac("sha256", signingKey).update(msg).digest();
|
|
71
|
+
if (mac.length !== sig.length || !timingSafeEqual(mac, sig)) {
|
|
72
|
+
throw new Error("Legacy token HMAC verification failed");
|
|
73
|
+
}
|
|
74
|
+
const iv = raw.subarray(1 + 8, 1 + 8 + 16);
|
|
75
|
+
const ciphertext = raw.subarray(1 + 8 + 16, hmacStart);
|
|
76
|
+
const decipher = createDecipheriv("aes-128-cbc", encryptionKey, iv);
|
|
77
|
+
decipher.setAutoPadding(false);
|
|
78
|
+
const padded = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
79
|
+
return pkcs7Unpad(padded);
|
|
80
|
+
}
|
|
42
81
|
|
|
43
82
|
// src/vault.ts
|
|
44
83
|
var PROJECT_RE = /^[a-zA-Z0-9._-]+$/;
|
|
45
84
|
var SECRET_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
46
85
|
var VaultService = class {
|
|
47
|
-
constructor(vaultPath2, lockFile, key) {
|
|
86
|
+
constructor(vaultPath2, lockFile, key, legacyKeyBase64, keyFilePath) {
|
|
48
87
|
this.vaultPath = vaultPath2;
|
|
49
88
|
this.lockFile = lockFile;
|
|
50
89
|
this.key = key;
|
|
90
|
+
this.legacyKeyBase64 = legacyKeyBase64;
|
|
91
|
+
this.keyFilePath = keyFilePath;
|
|
51
92
|
}
|
|
52
93
|
load() {
|
|
53
94
|
if (!existsSync(this.vaultPath)) return { version: 1, projects: {} };
|
|
54
95
|
const raw = readFileSync(this.vaultPath);
|
|
55
|
-
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(decrypt(raw, this.key).toString("utf8"));
|
|
98
|
+
} catch (err2) {
|
|
99
|
+
const message = String(err2);
|
|
100
|
+
if (this.legacyKeyBase64 && message.includes("Unsupported vault version")) {
|
|
101
|
+
return this.migrateLegacy(raw);
|
|
102
|
+
}
|
|
103
|
+
throw err2;
|
|
104
|
+
}
|
|
56
105
|
}
|
|
57
106
|
save(data) {
|
|
58
107
|
mkdirSync(dirname(this.vaultPath), { recursive: true });
|
|
59
108
|
writeFileSync(this.vaultPath, encrypt(Buffer.from(JSON.stringify(data)), this.key));
|
|
60
109
|
}
|
|
110
|
+
ensureExists() {
|
|
111
|
+
if (!existsSync(this.vaultPath)) {
|
|
112
|
+
this.save({ version: 1, projects: {} });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
migrateLegacy(raw) {
|
|
116
|
+
const plaintext = decryptLegacyFernet(raw, this.legacyKeyBase64);
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = JSON.parse(plaintext.toString("utf8"));
|
|
120
|
+
} catch {
|
|
121
|
+
throw new Error("Legacy vault JSON parse failed");
|
|
122
|
+
}
|
|
123
|
+
const data = {
|
|
124
|
+
version: typeof parsed.version === "number" ? parsed.version : 1,
|
|
125
|
+
projects: parsed.projects && typeof parsed.projects === "object" ? parsed.projects : {}
|
|
126
|
+
};
|
|
127
|
+
if (!data.version || data.version !== 1) data.version = 1;
|
|
128
|
+
if (!data.projects) data.projects = {};
|
|
129
|
+
this.save(data);
|
|
130
|
+
if (this.keyFilePath) {
|
|
131
|
+
try {
|
|
132
|
+
writeFileSync(this.keyFilePath, this.key.toString("hex"), { mode: 384 });
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return data;
|
|
137
|
+
}
|
|
61
138
|
async withLock(fn) {
|
|
62
139
|
mkdirSync(dirname(this.lockFile), { recursive: true });
|
|
63
140
|
if (!existsSync(this.lockFile)) writeFileSync(this.lockFile, "");
|
|
@@ -302,12 +379,12 @@ function runCommand(command, env, policy, maxChars = 500) {
|
|
|
302
379
|
r.exit_code = proc.status ?? 1;
|
|
303
380
|
return r;
|
|
304
381
|
}
|
|
305
|
-
function runScript(
|
|
306
|
-
const ext =
|
|
382
|
+
function runScript(_lang, code, env, maxChars = 500) {
|
|
383
|
+
const ext = ".mjs";
|
|
307
384
|
const tmpFile = join(tmpdir(), `zkt-${randomBytes3(8).toString("hex")}${ext}`);
|
|
308
385
|
try {
|
|
309
386
|
writeFileSync3(tmpFile, code, "utf8");
|
|
310
|
-
const bin =
|
|
387
|
+
const bin = "node";
|
|
311
388
|
const proc = spawnSync(bin, [tmpFile], {
|
|
312
389
|
env: { ...process.env, ...env },
|
|
313
390
|
encoding: "utf8",
|
|
@@ -483,8 +560,8 @@ function envRefsIn(text) {
|
|
|
483
560
|
const matches = [...text.matchAll(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g)].map((m) => m[1]);
|
|
484
561
|
return [...new Set(matches)];
|
|
485
562
|
}
|
|
486
|
-
function buildCtx(text
|
|
487
|
-
return { text, envRefs: envRefsIn(text), hasNetwork: NETWORK_RE.test(text)
|
|
563
|
+
function buildCtx(text) {
|
|
564
|
+
return { text, envRefs: envRefsIn(text), hasNetwork: NETWORK_RE.test(text) };
|
|
488
565
|
}
|
|
489
566
|
var RULES = [
|
|
490
567
|
// ── Critical ────────────────────────────────────────────────────────────
|
|
@@ -577,13 +654,6 @@ var RULES = [
|
|
|
577
654
|
weight: W.MEDIUM,
|
|
578
655
|
test: (c) => /\b(printenv|env)\b[^|]*>/.test(c.text)
|
|
579
656
|
},
|
|
580
|
-
{
|
|
581
|
-
id: "PYTHON_SUBPROCESS_EXFIL",
|
|
582
|
-
description: "Python subprocess call invoking network tool (curl/wget/nc)",
|
|
583
|
-
severity: "medium",
|
|
584
|
-
weight: W.MEDIUM,
|
|
585
|
-
test: (c) => c.lang === "python" && /subprocess\.(run|Popen|call|check_output)\([^)]*\b(curl|wget|nc\b|netcat)\b/.test(c.text)
|
|
586
|
-
},
|
|
587
657
|
// ── Low ──────────────────────────────────────────────────────────────────
|
|
588
658
|
{
|
|
589
659
|
id: "SINGLE_ENV_NETWORK",
|
|
@@ -648,9 +718,9 @@ var SecurityAnalyzer = class {
|
|
|
648
718
|
const text = isBashC ? command[2] : command.join(" ");
|
|
649
719
|
return this.analyze(buildCtx(text));
|
|
650
720
|
}
|
|
651
|
-
analyzeScript(
|
|
721
|
+
analyzeScript(_lang, code) {
|
|
652
722
|
if (this.cfg.mode === "off") return allow();
|
|
653
|
-
const ctx = buildCtx(code
|
|
723
|
+
const ctx = buildCtx(code);
|
|
654
724
|
return this.analyze(ctx);
|
|
655
725
|
}
|
|
656
726
|
analyze(ctx) {
|
|
@@ -870,18 +940,18 @@ function buildCatalog(services) {
|
|
|
870
940
|
},
|
|
871
941
|
{
|
|
872
942
|
name: "run_script",
|
|
873
|
-
summary: "Run an inline node
|
|
943
|
+
summary: "Run an inline node script with project secrets injected as env vars. Prefer over multiple run_with_project_env calls.",
|
|
874
944
|
register: (server) => {
|
|
875
945
|
server.tool(
|
|
876
946
|
"run_script",
|
|
877
947
|
[
|
|
878
|
-
"Run an inline script with project secrets available as environment variables.",
|
|
948
|
+
"Run an inline node script with project secrets available as environment variables.",
|
|
879
949
|
"Use this instead of multiple run_with_project_env calls \u2014 write the full logic in one script.",
|
|
880
950
|
"Filesystem is NOT shared between calls. Secret values never appear in this conversation."
|
|
881
951
|
].join(" "),
|
|
882
952
|
{
|
|
883
953
|
project: z.string().describe("Project name"),
|
|
884
|
-
lang: z.enum(["node"
|
|
954
|
+
lang: z.enum(["node"]).describe("Script language"),
|
|
885
955
|
code: z.string().min(1).describe("Full script source code"),
|
|
886
956
|
max_chars: z.number().int().min(1).max(32e3).optional().describe("Max output chars (default ~500)"),
|
|
887
957
|
confirm: z.boolean().optional().describe("Set to true to confirm execution of a medium-risk script after reviewing the warning")
|
|
@@ -1118,12 +1188,12 @@ function createMcpServer(services, options = {}) {
|
|
|
1118
1188
|
// src/web.ts
|
|
1119
1189
|
import { Hono } from "hono";
|
|
1120
1190
|
import { getCookie, setCookie, deleteCookie } from "hono/cookie";
|
|
1121
|
-
import { createHmac, randomBytes as randomBytes5 } from "crypto";
|
|
1191
|
+
import { createHmac as createHmac2, randomBytes as randomBytes5 } from "crypto";
|
|
1122
1192
|
import { readdirSync, statSync, existsSync as existsSync4 } from "fs";
|
|
1123
1193
|
import { join as join2, resolve as resolvePath, dirname as dirname4 } from "path";
|
|
1124
1194
|
|
|
1125
1195
|
// src/auth.ts
|
|
1126
|
-
import { randomBytes as randomBytes4, pbkdf2Sync, timingSafeEqual } from "crypto";
|
|
1196
|
+
import { randomBytes as randomBytes4, pbkdf2Sync, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1127
1197
|
var ITERATIONS = 6e5;
|
|
1128
1198
|
var KEY_LEN = 32;
|
|
1129
1199
|
var DIGEST = "sha256";
|
|
@@ -1136,7 +1206,7 @@ function verifyPassword(password, hash, salt) {
|
|
|
1136
1206
|
const derived = pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN, DIGEST);
|
|
1137
1207
|
const expected = Buffer.from(hash, "hex");
|
|
1138
1208
|
if (derived.length !== expected.length) return false;
|
|
1139
|
-
return
|
|
1209
|
+
return timingSafeEqual2(derived, expected);
|
|
1140
1210
|
}
|
|
1141
1211
|
|
|
1142
1212
|
// src/i18n.ts
|
|
@@ -1338,7 +1408,7 @@ function normalizeLang(lang) {
|
|
|
1338
1408
|
var COOKIE = "zs";
|
|
1339
1409
|
function signSession(data, secret) {
|
|
1340
1410
|
const payload = Buffer.from(JSON.stringify(data)).toString("base64url");
|
|
1341
|
-
const sig =
|
|
1411
|
+
const sig = createHmac2("sha256", secret).update(payload).digest("hex").slice(0, 20);
|
|
1342
1412
|
return `${payload}.${sig}`;
|
|
1343
1413
|
}
|
|
1344
1414
|
function parseSession(value, secret) {
|
|
@@ -1346,7 +1416,7 @@ function parseSession(value, secret) {
|
|
|
1346
1416
|
if (dot === -1) return null;
|
|
1347
1417
|
const payload = value.slice(0, dot);
|
|
1348
1418
|
const sig = value.slice(dot + 1);
|
|
1349
|
-
const expected =
|
|
1419
|
+
const expected = createHmac2("sha256", secret).update(payload).digest("hex").slice(0, 20);
|
|
1350
1420
|
if (sig !== expected) return null;
|
|
1351
1421
|
try {
|
|
1352
1422
|
return JSON.parse(Buffer.from(payload, "base64url").toString());
|
|
@@ -1922,31 +1992,383 @@ function lockPath(home = zocketHome()) {
|
|
|
1922
1992
|
return join3(home, "vault.lock");
|
|
1923
1993
|
}
|
|
1924
1994
|
|
|
1925
|
-
// src/
|
|
1995
|
+
// src/keys.ts
|
|
1996
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
|
|
1997
|
+
import { dirname as dirname5 } from "path";
|
|
1998
|
+
import { randomBytes as randomBytes6 } from "crypto";
|
|
1999
|
+
function isHexKey(raw) {
|
|
2000
|
+
return /^[0-9a-fA-F]{64}$/.test(raw);
|
|
2001
|
+
}
|
|
2002
|
+
function isBase64UrlKey(raw) {
|
|
2003
|
+
return /^[A-Za-z0-9_-]{43,44}=?$/.test(raw);
|
|
2004
|
+
}
|
|
2005
|
+
function base64UrlToBuffer2(raw) {
|
|
2006
|
+
const normalized = raw.replace(/-/g, "+").replace(/_/g, "/");
|
|
2007
|
+
const pad = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
2008
|
+
return Buffer.from(normalized + pad, "base64");
|
|
2009
|
+
}
|
|
1926
2010
|
function loadOrCreateKey(keyFile) {
|
|
1927
2011
|
if (existsSync5(keyFile)) {
|
|
1928
2012
|
const raw = readFileSync5(keyFile, "utf8").trim();
|
|
1929
|
-
|
|
2013
|
+
if (isHexKey(raw)) {
|
|
2014
|
+
return { key: Buffer.from(raw, "hex"), source: "hex" };
|
|
2015
|
+
}
|
|
2016
|
+
if (isBase64UrlKey(raw)) {
|
|
2017
|
+
const key2 = base64UrlToBuffer2(raw);
|
|
2018
|
+
if (key2.length !== 32) {
|
|
2019
|
+
throw new Error("Legacy base64 key is invalid length");
|
|
2020
|
+
}
|
|
2021
|
+
return { key: key2, source: "base64", legacyBase64: raw };
|
|
2022
|
+
}
|
|
2023
|
+
throw new Error("Unsupported key format: expected 64-char hex or base64url");
|
|
1930
2024
|
}
|
|
1931
2025
|
mkdirSync4(dirname5(keyFile), { recursive: true });
|
|
1932
2026
|
const key = randomBytes6(32);
|
|
1933
2027
|
writeFileSync5(keyFile, key.toString("hex"), { mode: 384 });
|
|
1934
|
-
return key;
|
|
2028
|
+
return { key, source: "generated" };
|
|
1935
2029
|
}
|
|
1936
|
-
|
|
2030
|
+
|
|
2031
|
+
// src/tui.ts
|
|
2032
|
+
import blessed from "blessed";
|
|
2033
|
+
function safeText(value) {
|
|
2034
|
+
if (!value) return "";
|
|
2035
|
+
return String(value);
|
|
2036
|
+
}
|
|
2037
|
+
function buildProjectLabel(row) {
|
|
2038
|
+
const count = row.secret_count ?? 0;
|
|
2039
|
+
return `${row.name} (${count})`;
|
|
2040
|
+
}
|
|
2041
|
+
async function runTui(services) {
|
|
2042
|
+
const { vault } = services;
|
|
2043
|
+
const screen = blessed.screen({
|
|
2044
|
+
smartCSR: true,
|
|
2045
|
+
title: "zocket"
|
|
2046
|
+
});
|
|
2047
|
+
const header = blessed.box({
|
|
2048
|
+
parent: screen,
|
|
2049
|
+
top: 0,
|
|
2050
|
+
height: 1,
|
|
2051
|
+
width: "100%",
|
|
2052
|
+
style: { fg: "white", bg: "blue" },
|
|
2053
|
+
content: " zocket \u2022 TUI | q:quit tab:switch r:refresh v:values n:new project d:delete s:set secret e:edit x:delete"
|
|
2054
|
+
});
|
|
2055
|
+
const footer = blessed.box({
|
|
2056
|
+
parent: screen,
|
|
2057
|
+
bottom: 0,
|
|
2058
|
+
height: 1,
|
|
2059
|
+
width: "100%",
|
|
2060
|
+
style: { fg: "black", bg: "white" },
|
|
2061
|
+
content: " Ready"
|
|
2062
|
+
});
|
|
2063
|
+
const projectList = blessed.list({
|
|
2064
|
+
parent: screen,
|
|
2065
|
+
label: " Projects ",
|
|
2066
|
+
top: 1,
|
|
2067
|
+
left: 0,
|
|
2068
|
+
bottom: 1,
|
|
2069
|
+
width: "30%",
|
|
2070
|
+
border: "line",
|
|
2071
|
+
keys: true,
|
|
2072
|
+
mouse: true,
|
|
2073
|
+
style: {
|
|
2074
|
+
selected: { bg: "blue", fg: "white" },
|
|
2075
|
+
border: { fg: "cyan" }
|
|
2076
|
+
},
|
|
2077
|
+
scrollbar: {
|
|
2078
|
+
ch: " ",
|
|
2079
|
+
inverse: true
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
const secretTable = blessed.listtable({
|
|
2083
|
+
parent: screen,
|
|
2084
|
+
label: " Secrets ",
|
|
2085
|
+
top: 1,
|
|
2086
|
+
left: "30%",
|
|
2087
|
+
bottom: 1,
|
|
2088
|
+
width: "70%",
|
|
2089
|
+
border: "line",
|
|
2090
|
+
keys: true,
|
|
2091
|
+
mouse: true,
|
|
2092
|
+
align: "left",
|
|
2093
|
+
noCellBorders: true,
|
|
2094
|
+
style: {
|
|
2095
|
+
header: { fg: "yellow", bold: true },
|
|
2096
|
+
cell: { fg: "white" },
|
|
2097
|
+
selected: { bg: "blue", fg: "white" },
|
|
2098
|
+
border: { fg: "cyan" }
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
const prompt = blessed.prompt({
|
|
2102
|
+
parent: screen,
|
|
2103
|
+
border: "line",
|
|
2104
|
+
height: 9,
|
|
2105
|
+
width: "60%",
|
|
2106
|
+
top: "center",
|
|
2107
|
+
left: "center",
|
|
2108
|
+
label: " Input ",
|
|
2109
|
+
keys: true,
|
|
2110
|
+
vi: true
|
|
2111
|
+
});
|
|
2112
|
+
const confirm = blessed.question({
|
|
2113
|
+
parent: screen,
|
|
2114
|
+
border: "line",
|
|
2115
|
+
height: 7,
|
|
2116
|
+
width: "60%",
|
|
2117
|
+
top: "center",
|
|
2118
|
+
left: "center",
|
|
2119
|
+
label: " Confirm ",
|
|
2120
|
+
keys: true,
|
|
2121
|
+
vi: true
|
|
2122
|
+
});
|
|
2123
|
+
let projects = [];
|
|
2124
|
+
let secrets = [];
|
|
2125
|
+
let currentProject = null;
|
|
2126
|
+
let showValues = false;
|
|
2127
|
+
function setStatus(message) {
|
|
2128
|
+
footer.setContent(` ${message}`);
|
|
2129
|
+
screen.render();
|
|
2130
|
+
}
|
|
2131
|
+
function setFocusPane(pane) {
|
|
2132
|
+
if (pane === "projects") {
|
|
2133
|
+
projectList.style.border.fg = "green";
|
|
2134
|
+
secretTable.style.border.fg = "cyan";
|
|
2135
|
+
projectList.setLabel(" Projects ");
|
|
2136
|
+
secretTable.setLabel(" Secrets ");
|
|
2137
|
+
projectList.focus();
|
|
2138
|
+
} else {
|
|
2139
|
+
projectList.style.border.fg = "cyan";
|
|
2140
|
+
secretTable.style.border.fg = "green";
|
|
2141
|
+
projectList.setLabel(" Projects ");
|
|
2142
|
+
secretTable.setLabel(" Secrets ");
|
|
2143
|
+
secretTable.focus();
|
|
2144
|
+
}
|
|
2145
|
+
screen.render();
|
|
2146
|
+
}
|
|
2147
|
+
async function askInput(label, initial = "") {
|
|
2148
|
+
return await new Promise((resolve) => {
|
|
2149
|
+
prompt.input(label, initial, (err2, value) => {
|
|
2150
|
+
if (err2) return resolve(null);
|
|
2151
|
+
resolve(value ?? "");
|
|
2152
|
+
});
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
async function askConfirm(label) {
|
|
2156
|
+
return await new Promise((resolve) => {
|
|
2157
|
+
confirm.ask(label, (err2, answer) => {
|
|
2158
|
+
if (err2) return resolve(false);
|
|
2159
|
+
resolve(Boolean(answer));
|
|
2160
|
+
});
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
async function refreshProjects(selectName) {
|
|
2164
|
+
projects = await vault.listProjects();
|
|
2165
|
+
if (!projects.length) {
|
|
2166
|
+
projectList.setItems(["(no projects)"]);
|
|
2167
|
+
projectList.select(0);
|
|
2168
|
+
currentProject = null;
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
const labels = projects.map(buildProjectLabel);
|
|
2172
|
+
projectList.setItems(labels);
|
|
2173
|
+
let idx = 0;
|
|
2174
|
+
if (selectName) {
|
|
2175
|
+
const found = projects.findIndex((p) => p.name === selectName);
|
|
2176
|
+
if (found >= 0) idx = found;
|
|
2177
|
+
} else if (currentProject) {
|
|
2178
|
+
const found = projects.findIndex((p) => p.name === currentProject?.name);
|
|
2179
|
+
if (found >= 0) idx = found;
|
|
2180
|
+
}
|
|
2181
|
+
projectList.select(idx);
|
|
2182
|
+
currentProject = projects[idx];
|
|
2183
|
+
}
|
|
2184
|
+
async function refreshSecrets() {
|
|
2185
|
+
if (!currentProject) {
|
|
2186
|
+
secretTable.setData([["Key", "Value", "Description", "Updated"]]);
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
secrets = await vault.listSecrets(currentProject.name);
|
|
2190
|
+
let rows;
|
|
2191
|
+
if (!secrets.length) {
|
|
2192
|
+
rows = [[showValues ? "Key" : "Key", showValues ? "Value" : "Description", "Updated"]];
|
|
2193
|
+
} else {
|
|
2194
|
+
if (showValues) {
|
|
2195
|
+
const values = await Promise.all(secrets.map((s) => vault.getSecretValue(currentProject.name, s.key)));
|
|
2196
|
+
rows = [
|
|
2197
|
+
["Key", "Value", "Description", "Updated"],
|
|
2198
|
+
...secrets.map((s, i) => [
|
|
2199
|
+
s.key,
|
|
2200
|
+
safeText(values[i]),
|
|
2201
|
+
safeText(s.description),
|
|
2202
|
+
safeText(s.updated_at)
|
|
2203
|
+
])
|
|
2204
|
+
];
|
|
2205
|
+
} else {
|
|
2206
|
+
rows = [
|
|
2207
|
+
["Key", "Description", "Updated"],
|
|
2208
|
+
...secrets.map((s) => [s.key, safeText(s.description), safeText(s.updated_at)])
|
|
2209
|
+
];
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
secretTable.setData(rows);
|
|
2213
|
+
}
|
|
2214
|
+
async function refreshAll() {
|
|
2215
|
+
await refreshProjects();
|
|
2216
|
+
await refreshSecrets();
|
|
2217
|
+
screen.render();
|
|
2218
|
+
}
|
|
2219
|
+
async function createProject() {
|
|
2220
|
+
const name = await askInput("Project name");
|
|
2221
|
+
if (!name) return;
|
|
2222
|
+
const desc = await askInput("Description (optional)", "");
|
|
2223
|
+
const folder = await askInput("Folder path (optional)", "");
|
|
2224
|
+
await vault.createProject(name, desc ?? "");
|
|
2225
|
+
if (folder) await vault.setFolder(name, folder);
|
|
2226
|
+
setStatus(`Project created: ${name}`);
|
|
2227
|
+
await refreshProjects(name);
|
|
2228
|
+
await refreshSecrets();
|
|
2229
|
+
}
|
|
2230
|
+
async function deleteProject() {
|
|
2231
|
+
if (!currentProject) return;
|
|
2232
|
+
const ok2 = await askConfirm(`Delete project ${currentProject.name}?`);
|
|
2233
|
+
if (!ok2) return;
|
|
2234
|
+
await vault.deleteProject(currentProject.name);
|
|
2235
|
+
setStatus(`Project deleted: ${currentProject.name}`);
|
|
2236
|
+
await refreshProjects();
|
|
2237
|
+
await refreshSecrets();
|
|
2238
|
+
}
|
|
2239
|
+
async function setProjectFolder() {
|
|
2240
|
+
if (!currentProject) return;
|
|
2241
|
+
const path = await askInput("Folder path (empty to clear)", currentProject.folder_path ?? "");
|
|
2242
|
+
if (path === null) return;
|
|
2243
|
+
await vault.setFolder(currentProject.name, path.trim() || void 0);
|
|
2244
|
+
setStatus("Folder updated");
|
|
2245
|
+
await refreshProjects(currentProject.name);
|
|
2246
|
+
}
|
|
2247
|
+
async function setAllowedDomains() {
|
|
2248
|
+
if (!currentProject) return;
|
|
2249
|
+
const initial = (await vault.getAllowedDomains(currentProject.name))?.join(", ") ?? "";
|
|
2250
|
+
const domains = await askInput("Allowed domains (comma-separated)", initial);
|
|
2251
|
+
if (domains === null) return;
|
|
2252
|
+
const value = domains.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2253
|
+
await vault.setAllowedDomains(currentProject.name, value.length ? value : null);
|
|
2254
|
+
setStatus("Allowed domains updated");
|
|
2255
|
+
}
|
|
2256
|
+
function selectedSecretKey() {
|
|
2257
|
+
if (!secrets.length) return null;
|
|
2258
|
+
const idx = secretTable.selected - 1;
|
|
2259
|
+
if (idx < 0 || idx >= secrets.length) return null;
|
|
2260
|
+
return secrets[idx].key;
|
|
2261
|
+
}
|
|
2262
|
+
async function upsertSecret(edit = false) {
|
|
2263
|
+
if (!currentProject) return;
|
|
2264
|
+
let key = "";
|
|
2265
|
+
let value = "";
|
|
2266
|
+
let desc = "";
|
|
2267
|
+
if (edit) {
|
|
2268
|
+
const selectedKey = selectedSecretKey();
|
|
2269
|
+
if (!selectedKey) return;
|
|
2270
|
+
key = selectedKey;
|
|
2271
|
+
desc = secrets.find((s) => s.key === selectedKey)?.description ?? "";
|
|
2272
|
+
value = showValues ? await vault.getSecretValue(currentProject.name, selectedKey) : "";
|
|
2273
|
+
}
|
|
2274
|
+
const keyInput = await askInput("Key (UPPERCASE)", key);
|
|
2275
|
+
if (!keyInput) return;
|
|
2276
|
+
const valueInput = await askInput("Value", value);
|
|
2277
|
+
if (valueInput === null) return;
|
|
2278
|
+
const descInput = await askInput("Description (optional)", desc);
|
|
2279
|
+
await vault.setSecret(currentProject.name, keyInput, valueInput, descInput ?? "");
|
|
2280
|
+
setStatus("Secret saved");
|
|
2281
|
+
await refreshSecrets();
|
|
2282
|
+
}
|
|
2283
|
+
async function deleteSecret() {
|
|
2284
|
+
if (!currentProject) return;
|
|
2285
|
+
const key = selectedSecretKey();
|
|
2286
|
+
if (!key) return;
|
|
2287
|
+
const ok2 = await askConfirm(`Delete secret ${key}?`);
|
|
2288
|
+
if (!ok2) return;
|
|
2289
|
+
await vault.deleteSecret(currentProject.name, key);
|
|
2290
|
+
setStatus(`Secret deleted: ${key}`);
|
|
2291
|
+
await refreshSecrets();
|
|
2292
|
+
}
|
|
2293
|
+
projectList.on("select", async (_, idx) => {
|
|
2294
|
+
currentProject = projects[idx] ?? null;
|
|
2295
|
+
await refreshSecrets();
|
|
2296
|
+
screen.render();
|
|
2297
|
+
});
|
|
2298
|
+
screen.key(["q", "C-c"], () => process.exit(0));
|
|
2299
|
+
screen.key(["tab"], () => {
|
|
2300
|
+
if (screen.focused === projectList) setFocusPane("secrets");
|
|
2301
|
+
else setFocusPane("projects");
|
|
2302
|
+
});
|
|
2303
|
+
screen.key(["r"], async () => {
|
|
2304
|
+
setStatus("Refreshing...");
|
|
2305
|
+
await refreshAll();
|
|
2306
|
+
setStatus("Ready");
|
|
2307
|
+
});
|
|
2308
|
+
screen.key(["v"], async () => {
|
|
2309
|
+
showValues = !showValues;
|
|
2310
|
+
setStatus(showValues ? "Values: visible" : "Values: hidden");
|
|
2311
|
+
await refreshSecrets();
|
|
2312
|
+
screen.render();
|
|
2313
|
+
});
|
|
2314
|
+
screen.key(["n"], async () => {
|
|
2315
|
+
await createProject();
|
|
2316
|
+
screen.render();
|
|
2317
|
+
});
|
|
2318
|
+
screen.key(["d"], async () => {
|
|
2319
|
+
await deleteProject();
|
|
2320
|
+
screen.render();
|
|
2321
|
+
});
|
|
2322
|
+
screen.key(["f"], async () => {
|
|
2323
|
+
await setProjectFolder();
|
|
2324
|
+
screen.render();
|
|
2325
|
+
});
|
|
2326
|
+
screen.key(["a"], async () => {
|
|
2327
|
+
await setAllowedDomains();
|
|
2328
|
+
screen.render();
|
|
2329
|
+
});
|
|
2330
|
+
screen.key(["s"], async () => {
|
|
2331
|
+
await upsertSecret(false);
|
|
2332
|
+
screen.render();
|
|
2333
|
+
});
|
|
2334
|
+
screen.key(["e"], async () => {
|
|
2335
|
+
await upsertSecret(true);
|
|
2336
|
+
screen.render();
|
|
2337
|
+
});
|
|
2338
|
+
screen.key(["x"], async () => {
|
|
2339
|
+
await deleteSecret();
|
|
2340
|
+
screen.render();
|
|
2341
|
+
});
|
|
2342
|
+
await refreshAll();
|
|
2343
|
+
setFocusPane("projects");
|
|
2344
|
+
screen.render();
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// src/cli.ts
|
|
2348
|
+
function createServices() {
|
|
1937
2349
|
const home = zocketHome();
|
|
1938
|
-
|
|
1939
|
-
const
|
|
1940
|
-
const vault = new VaultService(vaultPath(), lockPath(), key);
|
|
2350
|
+
mkdirSync5(home, { recursive: true });
|
|
2351
|
+
const keyMaterial = loadOrCreateKey(keyPath());
|
|
2352
|
+
const vault = new VaultService(vaultPath(), lockPath(), keyMaterial.key, keyMaterial.legacyBase64, keyPath());
|
|
2353
|
+
vault.ensureExists();
|
|
1941
2354
|
const config = new ConfigStore(configPath());
|
|
1942
2355
|
const audit = new AuditLogger(auditPath());
|
|
2356
|
+
config.ensureExists();
|
|
2357
|
+
return { vault, config, audit };
|
|
2358
|
+
}
|
|
2359
|
+
async function cmdStart(opts) {
|
|
2360
|
+
const { vault, config, audit } = createServices();
|
|
1943
2361
|
const cfg = config.ensureExists();
|
|
1944
2362
|
const mode = opts.mode === "admin" ? "admin" : "metadata";
|
|
1945
2363
|
const services = { vault, config, audit, mode };
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2364
|
+
if (opts.webEnabled) {
|
|
2365
|
+
const webApp = createWebApp({ vault, config, audit });
|
|
2366
|
+
serve({ fetch: webApp.fetch, hostname: opts.host, port: opts.webPort }, () => {
|
|
2367
|
+
console.log(`[zocket] web http://${opts.host}:${opts.webPort}`);
|
|
2368
|
+
});
|
|
2369
|
+
} else {
|
|
2370
|
+
console.log("[zocket] web disabled");
|
|
2371
|
+
}
|
|
1950
2372
|
const sessions = /* @__PURE__ */ new Map();
|
|
1951
2373
|
const mcpHttp = createServer((req, res) => {
|
|
1952
2374
|
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
@@ -1976,19 +2398,180 @@ async function cmdStart(opts) {
|
|
|
1976
2398
|
});
|
|
1977
2399
|
mcpHttp.listen(opts.mcpPort, opts.host, () => {
|
|
1978
2400
|
console.log(`[zocket] mcp http://${opts.host}:${opts.mcpPort}/sse (mode: ${mode}, loading: ${cfg.mcp_loading})`);
|
|
2401
|
+
});
|
|
2402
|
+
const streamableSessions = /* @__PURE__ */ new Map();
|
|
2403
|
+
const streamableHttp = createServer(async (req, res) => {
|
|
2404
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
2405
|
+
if (url.pathname !== "/mcp") {
|
|
2406
|
+
res.writeHead(404).end("Not found");
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
2410
|
+
let parsedBody = void 0;
|
|
2411
|
+
if (method === "POST") {
|
|
2412
|
+
let raw = "";
|
|
2413
|
+
for await (const chunk of req) {
|
|
2414
|
+
raw += chunk;
|
|
2415
|
+
}
|
|
2416
|
+
if (raw.trim().length > 0) {
|
|
2417
|
+
try {
|
|
2418
|
+
parsedBody = JSON.parse(raw);
|
|
2419
|
+
} catch {
|
|
2420
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2421
|
+
res.end(JSON.stringify({
|
|
2422
|
+
jsonrpc: "2.0",
|
|
2423
|
+
error: { code: -32700, message: "Parse error" },
|
|
2424
|
+
id: null
|
|
2425
|
+
}));
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const header = req.headers["mcp-session-id"];
|
|
2431
|
+
const sessionId = Array.isArray(header) ? header[0] : header;
|
|
2432
|
+
let transport;
|
|
2433
|
+
if (sessionId && streamableSessions.has(sessionId)) {
|
|
2434
|
+
transport = streamableSessions.get(sessionId);
|
|
2435
|
+
} else if (!sessionId && method === "POST" && parsedBody && isInitializeRequest(parsedBody)) {
|
|
2436
|
+
transport = new StreamableHTTPServerTransport({
|
|
2437
|
+
sessionIdGenerator: () => randomUUID(),
|
|
2438
|
+
onsessioninitialized: (sid) => {
|
|
2439
|
+
if (transport) streamableSessions.set(sid, transport);
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
transport.onclose = () => {
|
|
2443
|
+
const sid = transport?.sessionId;
|
|
2444
|
+
if (sid && streamableSessions.has(sid)) streamableSessions.delete(sid);
|
|
2445
|
+
};
|
|
2446
|
+
const mcpServer = createMcpServer(services, { loading: cfg.mcp_loading });
|
|
2447
|
+
mcpServer.connect(transport).catch((e) => {
|
|
2448
|
+
console.error("[zocket] MCP streamable connect error:", e);
|
|
2449
|
+
});
|
|
2450
|
+
} else {
|
|
2451
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2452
|
+
res.end(JSON.stringify({
|
|
2453
|
+
jsonrpc: "2.0",
|
|
2454
|
+
error: { code: -32e3, message: "Bad Request: No valid session ID provided" },
|
|
2455
|
+
id: null
|
|
2456
|
+
}));
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
try {
|
|
2460
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
2461
|
+
} catch (e) {
|
|
2462
|
+
console.error("[zocket] MCP streamable request error:", e);
|
|
2463
|
+
if (!res.headersSent) {
|
|
2464
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2465
|
+
res.end(JSON.stringify({
|
|
2466
|
+
jsonrpc: "2.0",
|
|
2467
|
+
error: { code: -32603, message: "Internal server error" },
|
|
2468
|
+
id: null
|
|
2469
|
+
}));
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
streamableHttp.listen(opts.mcpStreamPort, opts.host, () => {
|
|
2474
|
+
console.log(`[zocket] mcp http://${opts.host}:${opts.mcpStreamPort}/mcp (streamable-http)`);
|
|
1979
2475
|
console.log(`[zocket] ready \u2014 vault: ${vaultPath()}`);
|
|
1980
2476
|
});
|
|
1981
2477
|
}
|
|
1982
2478
|
function buildCli() {
|
|
1983
2479
|
const program = new Command("zocket").description("Local encrypted vault + MCP server for AI agent workflows").version("1.0.0");
|
|
1984
|
-
program.command("
|
|
2480
|
+
program.command("init").description("Initialize vault and config").action(async () => {
|
|
2481
|
+
createServices();
|
|
2482
|
+
console.log(`[zocket] initialized \u2014 vault: ${vaultPath()}`);
|
|
2483
|
+
});
|
|
2484
|
+
program.command("start").description("Start web panel + MCP SSE + MCP Streamable HTTP servers (use --no-web for MCP-only)").option("--host <host>", "Bind host", "127.0.0.1").option("--web-port <port>", "Web panel port", "18001").option("--mcp-port <port>", "MCP SSE port", "18002").option("--mcp-stream-port <port>", "MCP Streamable HTTP port", "18003").option("--mode <mode>", "MCP mode (metadata|admin)", "admin").option("--no-web", "Disable web panel").action(async (opts) => {
|
|
1985
2485
|
await cmdStart({
|
|
1986
2486
|
host: opts.host,
|
|
1987
2487
|
webPort: parseInt(opts.webPort, 10),
|
|
1988
2488
|
mcpPort: parseInt(opts.mcpPort, 10),
|
|
1989
|
-
|
|
2489
|
+
mcpStreamPort: parseInt(opts.mcpStreamPort, 10),
|
|
2490
|
+
mode: opts.mode,
|
|
2491
|
+
webEnabled: opts.web
|
|
2492
|
+
});
|
|
2493
|
+
});
|
|
2494
|
+
program.command("server").description("Start MCP servers without web panel").option("--host <host>", "Bind host", "127.0.0.1").option("--mcp-port <port>", "MCP SSE port", "18002").option("--mcp-stream-port <port>", "MCP Streamable HTTP port", "18003").option("--mode <mode>", "MCP mode (metadata|admin)", "admin").action(async (opts) => {
|
|
2495
|
+
await cmdStart({
|
|
2496
|
+
host: opts.host,
|
|
2497
|
+
webPort: 18001,
|
|
2498
|
+
mcpPort: parseInt(opts.mcpPort, 10),
|
|
2499
|
+
mcpStreamPort: parseInt(opts.mcpStreamPort, 10),
|
|
2500
|
+
mode: opts.mode,
|
|
2501
|
+
webEnabled: false
|
|
1990
2502
|
});
|
|
1991
2503
|
});
|
|
2504
|
+
const projects = program.command("projects").description("Manage projects");
|
|
2505
|
+
projects.command("list").action(async () => {
|
|
2506
|
+
const { vault } = createServices();
|
|
2507
|
+
const rows = await vault.listProjects();
|
|
2508
|
+
if (!rows.length) {
|
|
2509
|
+
console.log("No projects");
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
for (const r of rows) {
|
|
2513
|
+
console.log(`${r.name} ${r.secret_count} ${r.folder_path ?? ""}`);
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
projects.command("create <name>").option("--description <text>", "Description", "").option("--folder <path>", "Folder path", "").action(async (name, opts) => {
|
|
2517
|
+
const { vault } = createServices();
|
|
2518
|
+
await vault.createProject(name, opts.description ?? "");
|
|
2519
|
+
if (opts.folder) await vault.setFolder(name, opts.folder);
|
|
2520
|
+
console.log("Project created:", name);
|
|
2521
|
+
});
|
|
2522
|
+
projects.command("delete <name>").action(async (name) => {
|
|
2523
|
+
const { vault } = createServices();
|
|
2524
|
+
await vault.deleteProject(name);
|
|
2525
|
+
console.log("Project deleted:", name);
|
|
2526
|
+
});
|
|
2527
|
+
projects.command("set-folder <name> [path]").description('Set or clear folder path (use "-" to clear)').action(async (name, path) => {
|
|
2528
|
+
const { vault } = createServices();
|
|
2529
|
+
const value = path && path !== "-" ? path : void 0;
|
|
2530
|
+
await vault.setFolder(name, value);
|
|
2531
|
+
console.log("Folder updated:", name);
|
|
2532
|
+
});
|
|
2533
|
+
projects.command("set-domains <name> [domains]").description('Set or clear allowed domains (comma-separated, use "-" to clear)').action(async (name, domains) => {
|
|
2534
|
+
const { vault } = createServices();
|
|
2535
|
+
const value = domains && domains !== "-" ? domains.split(",").map((s) => s.trim()).filter(Boolean) : null;
|
|
2536
|
+
await vault.setAllowedDomains(name, value);
|
|
2537
|
+
console.log("Domains updated:", name);
|
|
2538
|
+
});
|
|
2539
|
+
const secrets = program.command("secrets").description("Manage secrets");
|
|
2540
|
+
secrets.command("list <project>").option("--show-values", "Include secret values", false).action(async (project, opts) => {
|
|
2541
|
+
const { vault } = createServices();
|
|
2542
|
+
const rows = await vault.listSecrets(project);
|
|
2543
|
+
if (!rows.length) {
|
|
2544
|
+
console.log("No secrets");
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
for (const r of rows) {
|
|
2548
|
+
if (opts.showValues) {
|
|
2549
|
+
const v = await vault.getSecretValue(project, r.key);
|
|
2550
|
+
console.log(`${r.key} ${v} ${r.description ?? ""}`);
|
|
2551
|
+
} else {
|
|
2552
|
+
console.log(`${r.key} ${r.description ?? ""}`);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
});
|
|
2556
|
+
secrets.command("get <project> <key>").action(async (project, key) => {
|
|
2557
|
+
const { vault } = createServices();
|
|
2558
|
+
const v = await vault.getSecretValue(project, key);
|
|
2559
|
+
console.log(v);
|
|
2560
|
+
});
|
|
2561
|
+
secrets.command("set <project> <key> <value>").option("--description <text>", "Description", "").action(async (project, key, value, opts) => {
|
|
2562
|
+
const { vault } = createServices();
|
|
2563
|
+
await vault.setSecret(project, key, value, opts.description ?? "");
|
|
2564
|
+
console.log("Secret saved:", key);
|
|
2565
|
+
});
|
|
2566
|
+
secrets.command("delete <project> <key>").action(async (project, key) => {
|
|
2567
|
+
const { vault } = createServices();
|
|
2568
|
+
await vault.deleteSecret(project, key);
|
|
2569
|
+
console.log("Secret deleted:", key);
|
|
2570
|
+
});
|
|
2571
|
+
program.command("tui").description("Interactive terminal UI for full management").action(async () => {
|
|
2572
|
+
const { vault, config, audit } = createServices();
|
|
2573
|
+
await runTui({ vault, config, audit });
|
|
2574
|
+
});
|
|
1992
2575
|
return program;
|
|
1993
2576
|
}
|
|
1994
2577
|
|