@suronai/cli 2.0.1 → 2.0.2
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/cli.js +0 -0
- package/package.json +3 -2
- package/suron +751 -0
package/cli.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suronai/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "SURON CLI — manage secrets, push .env files, scaffold Node.js projects with Telegram approval",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"suron": "
|
|
8
|
+
"suron": "suron"
|
|
9
9
|
},
|
|
10
10
|
"engines": {
|
|
11
11
|
"node": ">=18.0.0"
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"url": "https://github.com/your-org/suron/issues"
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
|
+
"suron",
|
|
39
40
|
"cli.js",
|
|
40
41
|
"README.md",
|
|
41
42
|
"LICENSE"
|
package/suron
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SURON CLI v2
|
|
4
|
+
*
|
|
5
|
+
* suron help Full command reference
|
|
6
|
+
* suron init Interactive setup wizard
|
|
7
|
+
* suron login Browser-based auth
|
|
8
|
+
* suron logout Remove credentials
|
|
9
|
+
* suron whoami Show auth status
|
|
10
|
+
* suron new <dir> Scaffold a new Node.js project with SURON
|
|
11
|
+
* suron push --app <n> [--env .env] Push .env to SURON
|
|
12
|
+
* suron apps List all apps
|
|
13
|
+
* suron apps create --name <n> Register app, get token
|
|
14
|
+
* suron secrets [list] --app <n> List secrets (key + masked preview)
|
|
15
|
+
* suron secrets reveal --app <n> Show key=value table (cleartext)
|
|
16
|
+
* suron secrets set --app <n> -k K Set a secret (prompted securely)
|
|
17
|
+
* suron secrets delete --app <n> -k K Delete a secret
|
|
18
|
+
* suron requests List pending approval requests
|
|
19
|
+
* suron approve --id <id> Approve a request
|
|
20
|
+
* suron deny --id <id> Deny a request
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
24
|
+
import {
|
|
25
|
+
readFileSync, writeFileSync, existsSync,
|
|
26
|
+
mkdirSync, readdirSync
|
|
27
|
+
} from "fs";
|
|
28
|
+
import { resolve, join, basename } from "path";
|
|
29
|
+
import { homedir } from "os";
|
|
30
|
+
import { createInterface } from "readline";
|
|
31
|
+
import { parse } from "dotenv";
|
|
32
|
+
import { exec } from "child_process";
|
|
33
|
+
|
|
34
|
+
// ── Terminal colors ───────────────────────────────────────────────────────────
|
|
35
|
+
const c = {
|
|
36
|
+
orange: s => `\x1b[38;5;208m${s}\x1b[0m`,
|
|
37
|
+
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
38
|
+
red: s => `\x1b[31m${s}\x1b[0m`,
|
|
39
|
+
gray: s => `\x1b[90m${s}\x1b[0m`,
|
|
40
|
+
bold: s => `\x1b[1m${s}\x1b[0m`,
|
|
41
|
+
cyan: s => `\x1b[36m${s}\x1b[0m`,
|
|
42
|
+
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
43
|
+
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
44
|
+
white: s => `\x1b[97m${s}\x1b[0m`,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ── Logo ──────────────────────────────────────────────────────────────────────
|
|
48
|
+
const logo = `
|
|
49
|
+
${c.orange("▄▀ █ █ █▀▄ █▀█ █▄ █")}
|
|
50
|
+
${c.orange("▀▄█ ▀▄█ █▀▄ █▄█ █ ▀█")} ${c.gray("secrets manager · v2")}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
54
|
+
const CONFIG_DIR = join(homedir(), ".suron");
|
|
55
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config");
|
|
56
|
+
|
|
57
|
+
function loadConfig() {
|
|
58
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
59
|
+
try {
|
|
60
|
+
return Object.fromEntries(
|
|
61
|
+
readFileSync(CONFIG_FILE, "utf8")
|
|
62
|
+
.split("\n").filter(Boolean)
|
|
63
|
+
.map(l => { const i = l.indexOf("="); return [l.slice(0,i).trim(), l.slice(i+1).trim()]; })
|
|
64
|
+
);
|
|
65
|
+
} catch { return {}; }
|
|
66
|
+
}
|
|
67
|
+
function saveConfig(obj) {
|
|
68
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
69
|
+
writeFileSync(CONFIG_FILE, Object.entries(obj).map(([k,v]) => `${k}=${v}`).join("\n"), "utf8");
|
|
70
|
+
}
|
|
71
|
+
function getConfig() {
|
|
72
|
+
const f = loadConfig();
|
|
73
|
+
return { url: process.env.SURON_URL ?? f.SURON_URL, token: process.env.SURON_TOKEN ?? f.SURON_TOKEN };
|
|
74
|
+
}
|
|
75
|
+
function requireConfig() {
|
|
76
|
+
const cfg = getConfig();
|
|
77
|
+
if (!cfg.url) {
|
|
78
|
+
console.error(c.red("✗ SURON_URL not set — run: suron init"));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
return cfg;
|
|
82
|
+
}
|
|
83
|
+
function getClient() {
|
|
84
|
+
const { url } = requireConfig();
|
|
85
|
+
return new ConvexHttpClient(url);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Prompt helpers ────────────────────────────────────────────────────────────
|
|
89
|
+
function prompt(q) {
|
|
90
|
+
return new Promise(res => {
|
|
91
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
92
|
+
rl.question(q, a => { rl.close(); res(a.trim()); });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function promptSecret(q) {
|
|
96
|
+
return new Promise(res => {
|
|
97
|
+
process.stdout.write(q);
|
|
98
|
+
const restore = () => { try { process.stdin.setRawMode(false); } catch {} };
|
|
99
|
+
try { process.stdin.setRawMode(true); } catch {}
|
|
100
|
+
process.stdin.resume(); process.stdin.setEncoding("utf8");
|
|
101
|
+
let val = "";
|
|
102
|
+
process.stdin.on("data", function handler(ch) {
|
|
103
|
+
if (ch === "\r" || ch === "\n") {
|
|
104
|
+
restore(); process.stdin.pause(); process.stdin.removeListener("data", handler);
|
|
105
|
+
process.stdout.write("\n"); res(val);
|
|
106
|
+
} else if (ch === "\u0003") { restore(); process.exit(); }
|
|
107
|
+
else if (ch === "\u007f") { if (val.length) { val = val.slice(0,-1); process.stdout.write("\b \b"); } }
|
|
108
|
+
else { val += ch; process.stdout.write("*"); }
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Version ───────────────────────────────────────────────────────────────────
|
|
114
|
+
const VERSION = "2.0.0";
|
|
115
|
+
|
|
116
|
+
// ── Arg helpers ───────────────────────────────────────────────────────────────
|
|
117
|
+
const arg = i => process.argv[i + 2];
|
|
118
|
+
const flag = n => { const i = process.argv.findIndex(a => a === `--${n}` || a === `-${n[0]}`); return i !== -1 ? process.argv[i+1] : undefined; };
|
|
119
|
+
const hasFlag = n => process.argv.includes(`--${n}`) || process.argv.includes(`-${n[0]}`);
|
|
120
|
+
|
|
121
|
+
// ── Table printer ─────────────────────────────────────────────────────────────
|
|
122
|
+
function printTable(rows, cols, opts = {}) {
|
|
123
|
+
if (!rows.length) { console.log(c.gray(" (empty)")); return; }
|
|
124
|
+
// strip ANSI for width calculation
|
|
125
|
+
const strip = s => String(s ?? "").replace(/\x1b\[[0-9;]*m/g, "");
|
|
126
|
+
const widths = cols.map(col =>
|
|
127
|
+
Math.max(col.label?.length ?? col.length, ...rows.map(r => strip(r[col.key ?? col] ?? "").length))
|
|
128
|
+
);
|
|
129
|
+
const labels = cols.map((col, i) => (col.label ?? col).toUpperCase().padEnd(widths[i]));
|
|
130
|
+
console.log(c.gray(" " + labels.join(" ")));
|
|
131
|
+
console.log(c.gray(" " + widths.map(w => "─".repeat(w)).join(" ")));
|
|
132
|
+
rows.forEach(row => {
|
|
133
|
+
const cells = cols.map((col, i) => {
|
|
134
|
+
const key = col.key ?? col;
|
|
135
|
+
const val = String(row[key] ?? "");
|
|
136
|
+
const raw = strip(val);
|
|
137
|
+
return val + " ".repeat(Math.max(0, widths[i] - raw.length));
|
|
138
|
+
});
|
|
139
|
+
console.log(" " + cells.join(" "));
|
|
140
|
+
});
|
|
141
|
+
if (opts.footer) console.log(c.gray("\n " + opts.footer));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Browser opener ─────────────────────────────────────────────────────────────
|
|
145
|
+
function openBrowser(url) {
|
|
146
|
+
const cmd = process.platform === "win32" ? `start "" "${url}"`
|
|
147
|
+
: process.platform === "darwin" ? `open "${url}"`
|
|
148
|
+
: `xdg-open "${url}"`;
|
|
149
|
+
exec(cmd, () => {});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── HELP ──────────────────────────────────────────────────────────────────────
|
|
153
|
+
function printHelp() {
|
|
154
|
+
console.log(logo);
|
|
155
|
+
const sections = [
|
|
156
|
+
{ heading: "Setup", cmds: [
|
|
157
|
+
["suron init", "", "Interactive setup wizard — saves ~/.suron/config"],
|
|
158
|
+
["suron login", "", "Authenticate CLI via browser (opens dashboard)"],
|
|
159
|
+
["suron logout", "", "Remove saved credentials"],
|
|
160
|
+
["suron whoami", "", "Show current auth status"],
|
|
161
|
+
]},
|
|
162
|
+
{ heading: "Scaffold", cmds: [
|
|
163
|
+
["suron new <directory>", "", "Scaffold a new Node.js project with SURON pre-wired"],
|
|
164
|
+
]},
|
|
165
|
+
{ heading: "Apps", cmds: [
|
|
166
|
+
["suron apps", "", "List all registered apps"],
|
|
167
|
+
["suron apps create", "--name <n>", "Register a new app and get its token"],
|
|
168
|
+
]},
|
|
169
|
+
{ heading: "Secrets", cmds: [
|
|
170
|
+
["suron secrets", "--app <n>", "List secrets (key + masked preview, no values)"],
|
|
171
|
+
["suron secrets reveal", "--app <n>", "Show full key=value table in terminal"],
|
|
172
|
+
["suron secrets set", "--app <n> --key K", "Set a secret (prompted securely, no shell history)"],
|
|
173
|
+
["suron secrets delete", "--app <n> --key K", "Delete a secret"],
|
|
174
|
+
["suron push", "--app <n> [--env .env]", "Bulk-push .env file (skips SURON_* vars)"],
|
|
175
|
+
]},
|
|
176
|
+
{ heading: "Requests", cmds: [
|
|
177
|
+
["suron requests", "", "List pending access-approval requests"],
|
|
178
|
+
["suron approve", "--id <id>", "Approve a request"],
|
|
179
|
+
["suron deny", "--id <id>", "Deny a request"],
|
|
180
|
+
]},
|
|
181
|
+
{ heading: "Info", cmds: [
|
|
182
|
+
["suron version", "", "Show CLI version"],
|
|
183
|
+
["suron help", "", "Show this help message"],
|
|
184
|
+
]},
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
for (const { heading, cmds } of sections) {
|
|
188
|
+
console.log(` ${c.gray("── " + heading)}`);
|
|
189
|
+
for (const [cmd, opts, desc] of cmds) {
|
|
190
|
+
const cmdPad = (c.orange(cmd) + " " + c.dim(opts)).padEnd(70);
|
|
191
|
+
console.log(` ${cmdPad} ${c.gray(desc)}`);
|
|
192
|
+
}
|
|
193
|
+
console.log();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const cfg = getConfig();
|
|
197
|
+
console.log(` ${c.gray("Config:")} ${cfg.url ? c.cyan(cfg.url) : c.red("not set — run: suron init")}`);
|
|
198
|
+
console.log(` ${c.gray("Version:")} ${c.gray(VERSION)}`);
|
|
199
|
+
console.log(` ${c.gray("Docs: ")} ${c.cyan("https://github.com/your-org/suron")}\n`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── INIT ──────────────────────────────────────────────────────────────────────
|
|
203
|
+
async function cmdInit() {
|
|
204
|
+
console.log(logo);
|
|
205
|
+
console.log(`${c.bold("Setup Wizard")} — saves config to ${c.cyan(CONFIG_FILE)}\n`);
|
|
206
|
+
const ex = loadConfig();
|
|
207
|
+
|
|
208
|
+
const url = await prompt(`Convex URL ${ex.SURON_URL ? c.gray("("+ex.SURON_URL+")") : c.gray("e.g. https://xxx.convex.cloud")}: `);
|
|
209
|
+
const dashUrl = await prompt(`Dashboard URL ${ex.SURON_DASHBOARD_URL ? c.gray("("+ex.SURON_DASHBOARD_URL+")") : c.gray("e.g. https://suron.vercel.app")}: `);
|
|
210
|
+
|
|
211
|
+
const cfg = {
|
|
212
|
+
...ex,
|
|
213
|
+
...(url ? { SURON_URL: url } : {}),
|
|
214
|
+
...(dashUrl ? { SURON_DASHBOARD_URL: dashUrl } : {}),
|
|
215
|
+
};
|
|
216
|
+
saveConfig(cfg);
|
|
217
|
+
console.log(c.green(`\n✓ Config saved`));
|
|
218
|
+
console.log(c.gray(` SURON_URL = ${cfg.SURON_URL}`));
|
|
219
|
+
if (cfg.SURON_DASHBOARD_URL) console.log(c.gray(` SURON_DASHBOARD_URL = ${cfg.SURON_DASHBOARD_URL}`));
|
|
220
|
+
console.log(`\n${c.dim(" Next: suron login or suron apps create --name my-app")}\n`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── LOGIN ─────────────────────────────────────────────────────────────────────
|
|
224
|
+
async function cmdLogin() {
|
|
225
|
+
const { url } = requireConfig();
|
|
226
|
+
const client = getClient();
|
|
227
|
+
console.log(logo);
|
|
228
|
+
console.log(`${c.bold("CLI Authentication")}\n`);
|
|
229
|
+
|
|
230
|
+
let sessionId, code;
|
|
231
|
+
try {
|
|
232
|
+
const r = await client.mutation("auth:createCliSession", {});
|
|
233
|
+
sessionId = r.sessionId; code = r.code;
|
|
234
|
+
} catch(e) { console.error(c.red(`✗ ${e.message}`)); process.exit(1); }
|
|
235
|
+
|
|
236
|
+
const dashUrl = process.env.SURON_DASHBOARD_URL ?? loadConfig().SURON_DASHBOARD_URL;
|
|
237
|
+
if (!dashUrl) {
|
|
238
|
+
console.error(c.red("✗ SURON_DASHBOARD_URL not set — run: suron init"));
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const approvalUrl = `${dashUrl}/cli-auth?code=${code}`;
|
|
243
|
+
console.log(` ${c.bold("Login code")}: ${c.orange(c.bold(code))}\n`);
|
|
244
|
+
console.log(` ${c.gray("Approval URL")}: ${c.cyan(approvalUrl)}\n`);
|
|
245
|
+
console.log(c.dim(" Press Enter to open in browser…"));
|
|
246
|
+
await prompt("");
|
|
247
|
+
openBrowser(approvalUrl);
|
|
248
|
+
console.log(c.gray("\n Waiting for dashboard approval… (Ctrl+C to cancel)\n"));
|
|
249
|
+
|
|
250
|
+
const deadline = Date.now() + 10 * 60 * 1000;
|
|
251
|
+
let dots = 0;
|
|
252
|
+
while (Date.now() < deadline) {
|
|
253
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
254
|
+
let result;
|
|
255
|
+
try { result = await client.query("auth:pollCliSession", { sessionId }); } catch { continue; }
|
|
256
|
+
dots = (dots + 1) % 4;
|
|
257
|
+
process.stdout.write(`\r ${c.gray("Waiting" + ".".repeat(dots).padEnd(3))}`);
|
|
258
|
+
if (result.status === "approved" && result.adminToken) {
|
|
259
|
+
process.stdout.write("\n");
|
|
260
|
+
const cfg = loadConfig();
|
|
261
|
+
cfg.SURON_ADMIN_TOKEN = result.adminToken;
|
|
262
|
+
saveConfig(cfg);
|
|
263
|
+
console.log(`\n${c.green("✓ Authenticated!")} Token saved to ${c.cyan(CONFIG_FILE)}\n`);
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
266
|
+
if (result.status === "denied") {
|
|
267
|
+
console.log("\n" + c.red("✗ Denied by dashboard.")); process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
if (["expired","not_found"].includes(result.status)) {
|
|
270
|
+
console.log("\n" + c.red("✗ Session expired. Run: suron login")); process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
console.log("\n" + c.red("✗ Timed out.")); process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── WHOAMI ────────────────────────────────────────────────────────────────────
|
|
277
|
+
async function cmdWhoami() {
|
|
278
|
+
const cfg = loadConfig();
|
|
279
|
+
console.log(logo);
|
|
280
|
+
if (cfg.SURON_ADMIN_TOKEN) {
|
|
281
|
+
console.log(c.green("✓ Authenticated"));
|
|
282
|
+
console.log(` ${c.gray("Token:")} ${c.cyan("cli_..."+cfg.SURON_ADMIN_TOKEN.slice(-8))}`);
|
|
283
|
+
console.log(` ${c.gray("Server:")} ${cfg.SURON_URL ?? c.red("not set")}`);
|
|
284
|
+
if (cfg.SURON_DASHBOARD_URL) console.log(` ${c.gray("Dashboard:")} ${cfg.SURON_DASHBOARD_URL}`);
|
|
285
|
+
} else {
|
|
286
|
+
console.log(c.red("✗ Not authenticated"));
|
|
287
|
+
console.log(` ${c.gray("Run: suron login")}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── LOGOUT ────────────────────────────────────────────────────────────────────
|
|
292
|
+
function cmdLogout() {
|
|
293
|
+
const cfg = loadConfig();
|
|
294
|
+
if (!cfg.SURON_ADMIN_TOKEN) { console.log(c.gray("Not logged in.")); return; }
|
|
295
|
+
delete cfg.SURON_ADMIN_TOKEN;
|
|
296
|
+
saveConfig(cfg);
|
|
297
|
+
console.log(c.green("✓ Logged out"));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── APPS ──────────────────────────────────────────────────────────────────────
|
|
301
|
+
async function cmdApps() {
|
|
302
|
+
const sub = arg(1);
|
|
303
|
+
const client = getClient();
|
|
304
|
+
|
|
305
|
+
if (sub === "create") {
|
|
306
|
+
const name = flag("name") ?? flag("n");
|
|
307
|
+
if (!name) { console.error(c.red("✗ --name required")); process.exit(1); }
|
|
308
|
+
const { token } = await client.mutation("apps:createApp", {
|
|
309
|
+
name, displayName: flag("display") ?? name, ownerId: flag("owner") ?? "admin"
|
|
310
|
+
});
|
|
311
|
+
console.log(logo);
|
|
312
|
+
console.log(c.green(`✓ App '${name}' created\n`));
|
|
313
|
+
console.log(` ${c.bold("Token:")} ${c.orange(token)}\n`);
|
|
314
|
+
console.log(c.gray(" Add to your project .env:"));
|
|
315
|
+
console.log(c.cyan(` SURON_URL=${requireConfig().url}`));
|
|
316
|
+
console.log(c.cyan(` SURON_TOKEN=${token}\n`));
|
|
317
|
+
console.log(c.dim(" This token is not shown again in the dashboard."));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const apps = await client.query("apps:listApps");
|
|
322
|
+
console.log(logo); console.log(`${c.bold("Apps")}\n`);
|
|
323
|
+
if (!apps.length) { console.log(c.gray(" No apps. Run: suron apps create --name my-app\n")); return; }
|
|
324
|
+
printTable(
|
|
325
|
+
apps.map(a => ({
|
|
326
|
+
name: c.white(a.name),
|
|
327
|
+
status: a.isActive ? c.green("● active") : c.gray("○ offline"),
|
|
328
|
+
host: c.gray(a.hostname ?? "—"),
|
|
329
|
+
pid: c.gray(String(a.pid ?? "—")),
|
|
330
|
+
seen: c.gray(a.lastSeen ? new Date(a.lastSeen).toLocaleTimeString() : "never"),
|
|
331
|
+
})),
|
|
332
|
+
["name","status","host","pid","seen"]
|
|
333
|
+
);
|
|
334
|
+
console.log();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── PUSH ──────────────────────────────────────────────────────────────────────
|
|
338
|
+
async function cmdPush() {
|
|
339
|
+
const envFile = flag("env") ?? flag("f") ?? ".env";
|
|
340
|
+
const appName = flag("app") ?? flag("a");
|
|
341
|
+
if (!appName) { console.error(c.red("✗ --app required")); process.exit(1); }
|
|
342
|
+
const envPath = resolve(process.cwd(), envFile);
|
|
343
|
+
if (!existsSync(envPath)) { console.error(c.red(`✗ Not found: ${envPath}`)); process.exit(1); }
|
|
344
|
+
const parsed = parse(readFileSync(envPath, "utf8"));
|
|
345
|
+
const SKIP = new Set(["SURON_URL","SURON_TOKEN"]);
|
|
346
|
+
const entries = Object.entries(parsed).filter(([k]) => !SKIP.has(k));
|
|
347
|
+
console.log(logo);
|
|
348
|
+
console.log(`${c.bold("Push")} ${c.orange(envFile)} → ${c.bold(appName)}`);
|
|
349
|
+
console.log(c.gray(` ${entries.length} secrets to push\n`));
|
|
350
|
+
const client = getClient();
|
|
351
|
+
let ok = 0;
|
|
352
|
+
for (const [key, value] of entries) {
|
|
353
|
+
try {
|
|
354
|
+
await client.mutation("secrets:upsertSecret", { appName, key, value });
|
|
355
|
+
console.log(` ${c.green("✓")} ${c.white(key)}`);
|
|
356
|
+
ok++;
|
|
357
|
+
} catch(e) { console.log(` ${c.red("✗")} ${key}: ${c.gray(e.message)}`); }
|
|
358
|
+
}
|
|
359
|
+
console.log(`\n${c.green(`✓ ${ok}/${entries.length} secrets pushed to '${appName}'`)}\n`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── SECRETS ───────────────────────────────────────────────────────────────────
|
|
363
|
+
async function cmdSecrets() {
|
|
364
|
+
const sub = arg(1);
|
|
365
|
+
const appName = flag("app") ?? flag("a");
|
|
366
|
+
if (!appName) { console.error(c.red("✗ --app required (--app my-app)")); process.exit(1); }
|
|
367
|
+
const client = getClient();
|
|
368
|
+
|
|
369
|
+
// ── LIST (masked)
|
|
370
|
+
if (!sub || sub === "list") {
|
|
371
|
+
const secrets = await client.query("secrets:listSecrets", { appName });
|
|
372
|
+
console.log(logo);
|
|
373
|
+
console.log(`${c.bold("Secrets")} · ${c.orange(appName)}\n`);
|
|
374
|
+
if (!secrets.length) {
|
|
375
|
+
console.log(c.gray(` No secrets. Run: suron secrets set --app ${appName} --key KEY\n`));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
printTable(
|
|
379
|
+
secrets.map(s => ({
|
|
380
|
+
key: c.white(s.key),
|
|
381
|
+
value: c.gray("••••••••"),
|
|
382
|
+
updated: c.gray(new Date(s.updatedAt).toLocaleString()),
|
|
383
|
+
})),
|
|
384
|
+
["key","value","updated"],
|
|
385
|
+
{ footer: `To reveal values: suron secrets reveal --app ${appName}` }
|
|
386
|
+
);
|
|
387
|
+
console.log();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── REVEAL (cleartext table — for debugging)
|
|
392
|
+
if (sub === "reveal") {
|
|
393
|
+
const secrets = await client.query("secrets:listSecrets", { appName });
|
|
394
|
+
console.log(logo);
|
|
395
|
+
console.log(`${c.yellow("⚠ CLEARTEXT")} · ${c.orange(appName)}\n`);
|
|
396
|
+
if (!secrets.length) { console.log(c.gray(" No secrets.\n")); return; }
|
|
397
|
+
// Box-style output — easier for AI to parse
|
|
398
|
+
console.log(c.gray(" ┌─ ENV TABLE " + "─".repeat(Math.max(0, 60 - 12)) + "┐"));
|
|
399
|
+
secrets.forEach(s => {
|
|
400
|
+
const val = s.value ?? "(encrypted)";
|
|
401
|
+
const line = ` ${c.orange(s.key.padEnd(28))}= ${c.cyan(val)}`;
|
|
402
|
+
console.log(line);
|
|
403
|
+
});
|
|
404
|
+
console.log(c.gray(" └" + "─".repeat(62) + "┘"));
|
|
405
|
+
console.log(c.gray(`\n ${secrets.length} secrets · ${appName}\n`));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ── SET
|
|
410
|
+
if (sub === "set") {
|
|
411
|
+
const key = flag("key") ?? flag("k");
|
|
412
|
+
if (!key) { console.error(c.red("✗ --key required")); process.exit(1); }
|
|
413
|
+
const value = await promptSecret(`Value for ${c.orange(key)}: `);
|
|
414
|
+
if (!value) { console.error(c.red("✗ Empty value — aborted")); process.exit(1); }
|
|
415
|
+
await client.mutation("secrets:upsertSecret", { appName, key, value });
|
|
416
|
+
console.log(c.green(`✓ '${key}' saved for '${appName}'`));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── DELETE
|
|
421
|
+
if (sub === "delete") {
|
|
422
|
+
const key = flag("key") ?? flag("k");
|
|
423
|
+
if (!key) { console.error(c.red("✗ --key required")); process.exit(1); }
|
|
424
|
+
const confirm = await prompt(`Delete '${key}' from '${appName}'? ${c.gray("[y/N]")} `);
|
|
425
|
+
if (confirm.toLowerCase() !== "y") { console.log(c.gray("Aborted.")); return; }
|
|
426
|
+
await client.mutation("secrets:deleteSecret", { appName, key });
|
|
427
|
+
console.log(c.green(`✓ Deleted '${key}' from '${appName}'`));
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
console.error(c.red(`✗ Unknown subcommand: ${sub}`));
|
|
432
|
+
console.log(c.gray(" Usage: suron secrets [list|reveal|set|delete] --app <name>"));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── REQUESTS ──────────────────────────────────────────────────────────────────
|
|
436
|
+
async function cmdRequests() {
|
|
437
|
+
const client = getClient();
|
|
438
|
+
const reqs = await client.query("secrets:listPendingRequests");
|
|
439
|
+
console.log(logo); console.log(`${c.bold("Pending Requests")}\n`);
|
|
440
|
+
if (!reqs.length) { console.log(c.green(" ✓ No pending requests\n")); return; }
|
|
441
|
+
printTable(
|
|
442
|
+
reqs.map(r => ({
|
|
443
|
+
id: c.gray(r._id),
|
|
444
|
+
app: c.white(r.appName),
|
|
445
|
+
key: c.orange(r.secretKey),
|
|
446
|
+
requested: c.gray(new Date(r.requestedAt).toLocaleString()),
|
|
447
|
+
})),
|
|
448
|
+
["id","app","key","requested"],
|
|
449
|
+
{ footer: "suron approve --id <id> or suron deny --id <id>" }
|
|
450
|
+
);
|
|
451
|
+
console.log();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── APPROVE / DENY ────────────────────────────────────────────────────────────
|
|
455
|
+
async function cmdResolve(deny = false) {
|
|
456
|
+
const id = flag("id");
|
|
457
|
+
if (!id) { console.error(c.red("✗ --id required")); process.exit(1); }
|
|
458
|
+
const client = getClient();
|
|
459
|
+
await client.mutation("secrets:resolveRequest", {
|
|
460
|
+
requestId: id, status: deny ? "denied" : "approved", resolvedBy: "cli"
|
|
461
|
+
});
|
|
462
|
+
console.log(deny ? c.red(`✗ Denied ${id}`) : c.green(`✓ Approved ${id}`));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── SURON NEW — scaffold a complete Node.js project ───────────────────────────
|
|
466
|
+
async function cmdNew() {
|
|
467
|
+
const dirName = arg(1);
|
|
468
|
+
if (!dirName) {
|
|
469
|
+
console.error(c.red("✗ Directory name required (e.g. suron new my-project)"));
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const projectDir = resolve(process.cwd(), dirName);
|
|
474
|
+
const projectName = basename(projectDir);
|
|
475
|
+
|
|
476
|
+
console.log(logo);
|
|
477
|
+
console.log(`${c.bold("Scaffold")} → ${c.orange(projectDir)}\n`);
|
|
478
|
+
|
|
479
|
+
if (existsSync(projectDir)) {
|
|
480
|
+
const ans = await prompt(c.yellow(` '${dirName}' already exists. Continue? [y/N] `));
|
|
481
|
+
if (ans.toLowerCase() !== "y") { console.log(c.gray(" Aborted.")); return; }
|
|
482
|
+
} else {
|
|
483
|
+
mkdirSync(projectDir, { recursive: true });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Prompt for app name
|
|
487
|
+
const appName = await prompt(` App name in SURON ${c.gray(`(${projectName})`)}: `) || projectName;
|
|
488
|
+
|
|
489
|
+
const cfg = loadConfig();
|
|
490
|
+
const suronUrl = cfg.SURON_URL ?? "https://your-deployment.convex.cloud";
|
|
491
|
+
const suronDash = cfg.SURON_DASHBOARD_URL ?? "https://your-dashboard.vercel.app";
|
|
492
|
+
|
|
493
|
+
// ── Files to create ───────────────────────────────────────────────────────
|
|
494
|
+
const files = {
|
|
495
|
+
|
|
496
|
+
// package.json
|
|
497
|
+
"package.json": JSON.stringify({
|
|
498
|
+
name: projectName,
|
|
499
|
+
version: "1.0.0",
|
|
500
|
+
type: "module",
|
|
501
|
+
description: "Node.js app powered by SURON secrets manager",
|
|
502
|
+
main: "src/index.js",
|
|
503
|
+
scripts: {
|
|
504
|
+
start: "node src/index.js",
|
|
505
|
+
dev: "node --watch src/index.js",
|
|
506
|
+
"secrets:list": `suron secrets --app ${appName}`,
|
|
507
|
+
"secrets:reveal": `suron secrets reveal --app ${appName}`,
|
|
508
|
+
"secrets:push": `suron push --app ${appName} --env .env.secrets`,
|
|
509
|
+
},
|
|
510
|
+
dependencies: {
|
|
511
|
+
"@suronai/sdk": "^2.0.0",
|
|
512
|
+
"dotenv": "^16.0.0",
|
|
513
|
+
},
|
|
514
|
+
engines: { node: ">=18.0.0" },
|
|
515
|
+
author: "",
|
|
516
|
+
license: "MIT",
|
|
517
|
+
}, null, 2),
|
|
518
|
+
|
|
519
|
+
// src/index.js — the entry point
|
|
520
|
+
"src/index.js": `/**
|
|
521
|
+
* ${projectName}
|
|
522
|
+
*
|
|
523
|
+
* Entry point — SURON handles all secrets.
|
|
524
|
+
* Run: npm start
|
|
525
|
+
* For dev (auto-restart): npm run dev
|
|
526
|
+
*/
|
|
527
|
+
|
|
528
|
+
import "dotenv/config"; // loads SURON_URL + SURON_TOKEN
|
|
529
|
+
import { SURON } from "@suronai/sdk";
|
|
530
|
+
|
|
531
|
+
// ── Bootstrap ─────────────────────────────────────────────────────────────────
|
|
532
|
+
const env = await SURON.connect();
|
|
533
|
+
|
|
534
|
+
// ── Load secrets (triggers Telegram approval on first run) ─────────────────────
|
|
535
|
+
// Add your own keys here:
|
|
536
|
+
const secrets = await env.getAll([
|
|
537
|
+
"EXAMPLE_API_KEY",
|
|
538
|
+
// "DATABASE_URL",
|
|
539
|
+
// "OPENAI_API_KEY",
|
|
540
|
+
]);
|
|
541
|
+
|
|
542
|
+
// ── Your app code starts here ──────────────────────────────────────────────────
|
|
543
|
+
console.log("✓ Secrets loaded:", Object.keys(secrets).join(", "));
|
|
544
|
+
console.log(" Example key:", secrets.EXAMPLE_API_KEY);
|
|
545
|
+
|
|
546
|
+
// TODO: replace with your actual app logic
|
|
547
|
+
`,
|
|
548
|
+
|
|
549
|
+
// .env — SURON credentials only
|
|
550
|
+
".env": `# SURON connection — the ONLY vars needed here.
|
|
551
|
+
# Your actual secrets (API keys, DB URLs) live on the SURON server.
|
|
552
|
+
SURON_URL=${suronUrl}
|
|
553
|
+
SURON_TOKEN=sk_YOUR_APP_TOKEN_HERE
|
|
554
|
+
`,
|
|
555
|
+
|
|
556
|
+
// .env.example — safe to commit
|
|
557
|
+
".env.example": `# SURON credentials — copy to .env and fill in your values.
|
|
558
|
+
# Get your token: suron apps create --name ${appName}
|
|
559
|
+
SURON_URL=${suronUrl}
|
|
560
|
+
SURON_TOKEN=sk_...
|
|
561
|
+
`,
|
|
562
|
+
|
|
563
|
+
// .env.secrets — for first-time push, then delete this file
|
|
564
|
+
".env.secrets": `# Put your ACTUAL secrets here for the initial push.
|
|
565
|
+
# Run: npm run secrets:push
|
|
566
|
+
# Then DELETE this file — it is NOT committed.
|
|
567
|
+
EXAMPLE_API_KEY=replace_me_with_your_api_key
|
|
568
|
+
# DATABASE_URL=postgres://...
|
|
569
|
+
# OPENAI_API_KEY=sk-...
|
|
570
|
+
`,
|
|
571
|
+
|
|
572
|
+
// .gitignore
|
|
573
|
+
".gitignore": `.env
|
|
574
|
+
.env.secrets
|
|
575
|
+
node_modules/
|
|
576
|
+
dist/
|
|
577
|
+
*.log
|
|
578
|
+
.DS_Store
|
|
579
|
+
`,
|
|
580
|
+
|
|
581
|
+
// README.md
|
|
582
|
+
"README.md": `# ${projectName}
|
|
583
|
+
|
|
584
|
+
Node.js application using [SURON](${suronDash}) for secrets management.
|
|
585
|
+
|
|
586
|
+
## Quick Start
|
|
587
|
+
|
|
588
|
+
\`\`\`bash
|
|
589
|
+
# 1. Install dependencies
|
|
590
|
+
npm install
|
|
591
|
+
|
|
592
|
+
# 2. Set your SURON token in .env
|
|
593
|
+
# Get one: suron apps create --name ${appName}
|
|
594
|
+
nano .env
|
|
595
|
+
|
|
596
|
+
# 3. Push initial secrets (edit .env.secrets first)
|
|
597
|
+
npm run secrets:push
|
|
598
|
+
|
|
599
|
+
# 4. Delete .env.secrets (never commit it)
|
|
600
|
+
rm .env.secrets
|
|
601
|
+
|
|
602
|
+
# 5. Run the app (will prompt Telegram approval on first secret access)
|
|
603
|
+
npm start
|
|
604
|
+
\`\`\`
|
|
605
|
+
|
|
606
|
+
## Secrets Management
|
|
607
|
+
|
|
608
|
+
\`\`\`bash
|
|
609
|
+
# List secrets (masked)
|
|
610
|
+
npm run secrets:list
|
|
611
|
+
|
|
612
|
+
# Reveal secrets in terminal (for debugging)
|
|
613
|
+
npm run secrets:reveal
|
|
614
|
+
|
|
615
|
+
# Set a single secret
|
|
616
|
+
suron secrets set --app ${appName} --key MY_KEY
|
|
617
|
+
|
|
618
|
+
# Push entire .env file
|
|
619
|
+
npm run secrets:push
|
|
620
|
+
\`\`\`
|
|
621
|
+
|
|
622
|
+
## How SURON Works
|
|
623
|
+
|
|
624
|
+
1. App calls \`env.get('MY_KEY')\` at runtime
|
|
625
|
+
2. SURON checks if already approved for this session → returns cached value
|
|
626
|
+
3. If not → Telegram notification sent to owner
|
|
627
|
+
4. Owner approves → secret returned and cached until app restarts
|
|
628
|
+
|
|
629
|
+
See [llm.md](${suronDash}/llm.md) for AI integration guide.
|
|
630
|
+
`,
|
|
631
|
+
|
|
632
|
+
// llm.md — AI integration hints specific to this project
|
|
633
|
+
"llm.md": `# AI Integration Notes — ${projectName}
|
|
634
|
+
|
|
635
|
+
> This file is for AI assistants (Claude, Cursor, Copilot, etc.)
|
|
636
|
+
> Read this before touching any secrets, .env files, or startup code.
|
|
637
|
+
|
|
638
|
+
## Project Type
|
|
639
|
+
Node.js ESM app using SURON for secrets management.
|
|
640
|
+
|
|
641
|
+
## Key Rules
|
|
642
|
+
|
|
643
|
+
1. **Never hardcode secrets** — they live on the SURON server.
|
|
644
|
+
2. **Never use process.env.* for actual secrets** — use \`await env.get('KEY')\`.
|
|
645
|
+
3. **SURON_URL and SURON_TOKEN** are the only vars in .env — they are not secrets.
|
|
646
|
+
4. **.env.secrets** is for one-time push only — delete after use, never commit.
|
|
647
|
+
|
|
648
|
+
## Adding a New Secret
|
|
649
|
+
|
|
650
|
+
\`\`\`bash
|
|
651
|
+
# Terminal — adds secret to SURON server
|
|
652
|
+
suron secrets set --app ${appName} --key NEW_KEY
|
|
653
|
+
|
|
654
|
+
# Then in code:
|
|
655
|
+
const value = await env.get('NEW_KEY')
|
|
656
|
+
\`\`\`
|
|
657
|
+
|
|
658
|
+
## Revealing All Secrets (for debugging)
|
|
659
|
+
|
|
660
|
+
\`\`\`bash
|
|
661
|
+
suron secrets reveal --app ${appName}
|
|
662
|
+
\`\`\`
|
|
663
|
+
|
|
664
|
+
This shows a full key=value table for inspection.
|
|
665
|
+
|
|
666
|
+
## Startup Pattern
|
|
667
|
+
|
|
668
|
+
Always follow this pattern at the top of entry files:
|
|
669
|
+
|
|
670
|
+
\`\`\`javascript
|
|
671
|
+
import "dotenv/config"; // loads SURON_URL + SURON_TOKEN
|
|
672
|
+
import { SURON } from "@suronai/sdk";
|
|
673
|
+
|
|
674
|
+
const env = await SURON.connect(); // connects and registers app
|
|
675
|
+
|
|
676
|
+
// Bulk-load all secrets at startup (one Telegram prompt per key)
|
|
677
|
+
const secrets = await env.getAll(["KEY_A", "KEY_B", "KEY_C"]);
|
|
678
|
+
\`\`\`
|
|
679
|
+
|
|
680
|
+
## App Info
|
|
681
|
+
|
|
682
|
+
- **App name in SURON**: \`${appName}\`
|
|
683
|
+
- **Dashboard**: ${suronDash}
|
|
684
|
+
- **Convex URL**: ${suronUrl}
|
|
685
|
+
`,
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// ── Write files ────────────────────────────────────────────────────────────
|
|
689
|
+
mkdirSync(join(projectDir, "src"), { recursive: true });
|
|
690
|
+
let written = 0;
|
|
691
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
692
|
+
const full = join(projectDir, filePath);
|
|
693
|
+
const exists = existsSync(full);
|
|
694
|
+
writeFileSync(full, content, "utf8");
|
|
695
|
+
console.log(` ${c.green("✓")} ${c.white(filePath)} ${exists ? c.gray("(overwritten)") : ""}`);
|
|
696
|
+
written++;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
console.log(`\n${c.green(`✓ ${written} files created in '${dirName}'`)}\n`);
|
|
700
|
+
|
|
701
|
+
// ── Next steps ─────────────────────────────────────────────────────────────
|
|
702
|
+
const nextSteps = [
|
|
703
|
+
[`cd ${dirName}`, "Enter project directory"],
|
|
704
|
+
["npm install", "Install dependencies"],
|
|
705
|
+
[`suron apps create --name ${appName}`, "Register app in SURON, get token"],
|
|
706
|
+
["nano .env", "Set SURON_TOKEN from above"],
|
|
707
|
+
["nano .env.secrets", "Add your actual secrets"],
|
|
708
|
+
["npm run secrets:push", "Upload secrets to SURON"],
|
|
709
|
+
["rm .env.secrets", "Delete secrets file (never commit it)"],
|
|
710
|
+
["npm start", "Run the app"],
|
|
711
|
+
];
|
|
712
|
+
console.log(c.gray(" Next steps:"));
|
|
713
|
+
nextSteps.forEach(([cmd, desc], i) => {
|
|
714
|
+
console.log(` ${c.gray(String(i+1)+".")} ${c.cyan(cmd.padEnd(42))} ${c.dim(desc)}`);
|
|
715
|
+
});
|
|
716
|
+
console.log();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ── ROUTER ────────────────────────────────────────────────────────────────────
|
|
720
|
+
const cmd = arg(0);
|
|
721
|
+
|
|
722
|
+
// Handle --version / -v flag at any position
|
|
723
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
724
|
+
console.log(`suron v${VERSION}`);
|
|
725
|
+
process.exit(0);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
try {
|
|
729
|
+
switch (cmd) {
|
|
730
|
+
case "help":
|
|
731
|
+
case "--help":
|
|
732
|
+
case "-h": printHelp(); break;
|
|
733
|
+
case "version": console.log(`suron v${VERSION}`); break;
|
|
734
|
+
case "init": await cmdInit(); break;
|
|
735
|
+
case "login": await cmdLogin(); break;
|
|
736
|
+
case "logout": cmdLogout(); break;
|
|
737
|
+
case "whoami": await cmdWhoami(); break;
|
|
738
|
+
case "new": await cmdNew(); break;
|
|
739
|
+
case "push": await cmdPush(); break;
|
|
740
|
+
case "apps": await cmdApps(); break;
|
|
741
|
+
case "secrets": await cmdSecrets(); break;
|
|
742
|
+
case "requests": await cmdRequests(); break;
|
|
743
|
+
case "approve": await cmdResolve(false); break;
|
|
744
|
+
case "deny": await cmdResolve(true); break;
|
|
745
|
+
default: printHelp();
|
|
746
|
+
}
|
|
747
|
+
} catch(err) {
|
|
748
|
+
console.error(c.red(`\n✗ ${err.message}`));
|
|
749
|
+
if (hasFlag("debug")) console.error(err.stack);
|
|
750
|
+
process.exit(1);
|
|
751
|
+
}
|