@suronai/cli 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/cli.js +51 -40
  2. package/package.json +6 -5
  3. package/suron +776 -0
package/cli.js CHANGED
@@ -46,8 +46,8 @@ const c = {
46
46
 
47
47
  // ── Logo ──────────────────────────────────────────────────────────────────────
48
48
  const logo = `
49
- ${c.orange("▄▀ █ █ █▀▄ █▀█ █▄ █")}
50
- ${c.orange("▀▄█ ▀▄█ █▀▄ █▄█ █ ▀█")} ${c.gray("secrets manager · v2")}
49
+ ${c.orange("▄▀▀ █ █ █▀▄ █▀█ █▄ █")}
50
+ ${c.orange("▀▀▄ ▀▄█ █▀▄ █▄█ █ ▀█")} ${c.gray("secrets manager · v2")}
51
51
  `;
52
52
 
53
53
  // ── Config ────────────────────────────────────────────────────────────────────
@@ -75,7 +75,7 @@ function getConfig() {
75
75
  function requireConfig() {
76
76
  const cfg = getConfig();
77
77
  if (!cfg.url) {
78
- console.error(c.red("✗ SURON_URL not set — run: suron init"));
78
+ console.error(c.red("✗ SURON_URL not set — run: suron login"));
79
79
  process.exit(1);
80
80
  }
81
81
  return cfg;
@@ -154,10 +154,10 @@ function printHelp() {
154
154
  console.log(logo);
155
155
  const sections = [
156
156
  { heading: "Setup", cmds: [
157
- ["suron init", "", "Interactive setup wizard saves ~/.suron/config"],
158
- ["suron login", "", "Authenticate CLI via browser (opens dashboard)"],
157
+ ["suron login", "", "Authenticate CLIasks for URLs + opens approval page in browser"],
159
158
  ["suron logout", "", "Remove saved credentials"],
160
159
  ["suron whoami", "", "Show current auth status"],
160
+ ["suron init", "", "(legacy) — just runs: suron login"],
161
161
  ]},
162
162
  { heading: "Scaffold", cmds: [
163
163
  ["suron new <directory>", "", "Scaffold a new Node.js project with SURON pre-wired"],
@@ -196,57 +196,65 @@ function printHelp() {
196
196
  const cfg = getConfig();
197
197
  console.log(` ${c.gray("Config:")} ${cfg.url ? c.cyan(cfg.url) : c.red("not set — run: suron init")}`);
198
198
  console.log(` ${c.gray("Version:")} ${c.gray(VERSION)}`);
199
- console.log(` ${c.gray("Docs: ")} ${c.cyan("https://github.com/your-org/suron")}\n`);
199
+ console.log(` ${c.gray("Docs: ")} ${c.cyan("https://github.com/Cryptoistaken/Suron")}\n`);
200
200
  }
201
201
 
202
202
  // ── INIT ──────────────────────────────────────────────────────────────────────
203
203
  async function cmdInit() {
204
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`);
205
+ console.log(`${c.bold("Setup Wizard")}\n`);
206
+ console.log(c.gray(" suron init is no longer needed for URL setup."));
207
+ console.log(c.gray(" Run ") + c.cyan("suron login") + c.gray(" — it will ask for your Convex URL and Dashboard URL automatically.\n"));
208
+ console.log(c.dim(" Or run: suron whoami to check current status\n"));
221
209
  }
222
210
 
223
211
  // ── LOGIN ─────────────────────────────────────────────────────────────────────
224
212
  async function cmdLogin() {
225
- const { url } = requireConfig();
226
- const client = getClient();
227
213
  console.log(logo);
228
214
  console.log(`${c.bold("CLI Authentication")}\n`);
229
215
 
216
+ // ── Step 1: ensure Convex URL is set
217
+ let cfg = loadConfig();
218
+ let convexUrl = process.env.SURON_URL ?? cfg.SURON_URL;
219
+ if (!convexUrl) {
220
+ console.log(c.gray(" No Convex URL found. Let's set it up.\n"));
221
+ convexUrl = await prompt(` Convex URL ${c.gray("(e.g. https://xxx.eu-west-1.convex.cloud)")}: `);
222
+ if (!convexUrl) { console.error(c.red("✗ Convex URL is required")); await new Promise(r => setTimeout(r, 50)); process.exit(1); }
223
+ cfg = { ...cfg, SURON_URL: convexUrl };
224
+ saveConfig(cfg);
225
+ console.log(c.green(" ✓ Convex URL saved\n"));
226
+ }
227
+
228
+ // ── Step 2: ensure Dashboard URL is set
229
+ let dashUrl = process.env.SURON_DASHBOARD_URL ?? cfg.SURON_DASHBOARD_URL;
230
+ if (!dashUrl) {
231
+ console.log(c.gray(" No Dashboard URL found. Where is your SURON dashboard deployed?\n"));
232
+ dashUrl = await prompt(` Dashboard URL ${c.gray("(e.g. https://suron.vercel.app)")}: `);
233
+ if (!dashUrl) { console.error(c.red("✗ Dashboard URL is required")); await new Promise(r => setTimeout(r, 50)); process.exit(1); }
234
+ cfg = { ...cfg, SURON_DASHBOARD_URL: dashUrl };
235
+ saveConfig(cfg);
236
+ console.log(c.green(" ✓ Dashboard URL saved\n"));
237
+ }
238
+
239
+ // ── Step 3: create session and open browser
240
+ const client = new ConvexHttpClient(convexUrl);
230
241
  let sessionId, code;
231
242
  try {
232
243
  const r = await client.mutation("auth:createCliSession", {});
233
244
  sessionId = r.sessionId; code = r.code;
234
- } catch(e) { console.error(c.red(`✗ ${e.message}`)); process.exit(1); }
245
+ } catch(e) { console.error(c.red(`✗ ${e.message}`)); await new Promise(r => setTimeout(r, 50)); process.exit(1); }
235
246
 
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
- }
247
+ const approvalUrl = `${dashUrl.replace(/\/$/, "")}/approve?code=${code}`;
241
248
 
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"));
249
+ console.log(` ${c.bold("Your code")}: ${c.orange(c.bold(code))}\n`);
250
+ console.log(` ${c.gray("Opening approval page in browser…")}`);
251
+ console.log(` ${c.dim(approvalUrl)}\n`);
252
+ console.log(c.dim(" Press Enter to open in browser, or visit the URL above manually."));
246
253
  await prompt("");
247
254
  openBrowser(approvalUrl);
248
- console.log(c.gray("\n Waiting for dashboard approval… (Ctrl+C to cancel)\n"));
255
+ console.log(c.gray("\n Waiting for approval… (Ctrl+C to cancel)\n"));
249
256
 
257
+ // ── Step 4: poll
250
258
  const deadline = Date.now() + 10 * 60 * 1000;
251
259
  let dots = 0;
252
260
  while (Date.now() < deadline) {
@@ -257,20 +265,23 @@ async function cmdLogin() {
257
265
  process.stdout.write(`\r ${c.gray("Waiting" + ".".repeat(dots).padEnd(3))}`);
258
266
  if (result.status === "approved" && result.adminToken) {
259
267
  process.stdout.write("\n");
260
- const cfg = loadConfig();
261
268
  cfg.SURON_ADMIN_TOKEN = result.adminToken;
262
269
  saveConfig(cfg);
263
270
  console.log(`\n${c.green("✓ Authenticated!")} Token saved to ${c.cyan(CONFIG_FILE)}\n`);
271
+ await new Promise(r => setTimeout(r, 50));
264
272
  process.exit(0);
265
273
  }
266
274
  if (result.status === "denied") {
267
- console.log("\n" + c.red("✗ Denied by dashboard.")); process.exit(1);
275
+ process.stdout.write("\n"); console.log(c.red("✗ Denied."));
276
+ await new Promise(r => setTimeout(r, 50)); process.exit(1);
268
277
  }
269
278
  if (["expired","not_found"].includes(result.status)) {
270
- console.log("\n" + c.red("✗ Session expired. Run: suron login")); process.exit(1);
279
+ process.stdout.write("\n"); console.log(c.red("✗ Session expired. Run: suron login"));
280
+ await new Promise(r => setTimeout(r, 50)); process.exit(1);
271
281
  }
272
282
  }
273
- console.log("\n" + c.red("✗ Timed out.")); process.exit(1);
283
+ process.stdout.write("\n"); console.log(c.red("✗ Timed out."));
284
+ await new Promise(r => setTimeout(r, 50)); process.exit(1);
274
285
  }
275
286
 
276
287
  // ── WHOAMI ────────────────────────────────────────────────────────────────────
@@ -731,7 +742,7 @@ try {
731
742
  case "--help":
732
743
  case "-h": printHelp(); break;
733
744
  case "version": console.log(`suron v${VERSION}`); break;
734
- case "init": await cmdInit(); break;
745
+ case "init": await cmdLogin(); break; // init now just runs login
735
746
  case "login": await cmdLogin(); break;
736
747
  case "logout": cmdLogout(); break;
737
748
  case "whoami": await cmdWhoami(); break;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@suronai/cli",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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": "./cli.js"
8
+ "suron": "suron"
9
9
  },
10
10
  "engines": {
11
11
  "node": ">=18.0.0"
@@ -26,16 +26,17 @@
26
26
  ],
27
27
  "license": "MIT",
28
28
  "author": "SuronAI",
29
- "homepage": "https://github.com/your-org/suron",
29
+ "homepage": "https://github.com/Cryptoistaken/Suron",
30
30
  "repository": {
31
31
  "type": "git",
32
- "url": "git+https://github.com/your-org/suron.git",
32
+ "url": "git+https://github.com/Cryptoistaken/Suron.git",
33
33
  "directory": "SDK/CLI"
34
34
  },
35
35
  "bugs": {
36
- "url": "https://github.com/your-org/suron/issues"
36
+ "url": "https://github.com/Cryptoistaken/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,776 @@
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
+ console.log(c.dim(" Tip: suron login will also ask for these URLs if not set.\n"));
207
+ const ex = loadConfig();
208
+
209
+ const url = await prompt(`Convex URL ${ex.SURON_URL ? c.gray("("+ex.SURON_URL+")") : c.gray("e.g. https://xxx.convex.cloud")}: `);
210
+
211
+ const cfg = {
212
+ ...ex,
213
+ ...(url ? { SURON_URL: url } : {}),
214
+ };
215
+ saveConfig(cfg);
216
+ console.log(c.green(`\n✓ Config saved`));
217
+ console.log(c.gray(` SURON_URL = ${cfg.SURON_URL ?? c.red("(not set)")}`));
218
+ console.log(`\n${c.dim(" Next: suron login")}\n`);
219
+ }
220
+
221
+ // ── LOGIN ─────────────────────────────────────────────────────────────────────
222
+ async function cmdLogin() {
223
+ console.log(logo);
224
+ console.log(`${c.bold("CLI Authentication")}\n`);
225
+
226
+ // ── Step 1: ensure Convex URL is set ────────────────────────────────────────
227
+ let cfg = loadConfig();
228
+ let convexUrl = process.env.SURON_URL ?? cfg.SURON_URL;
229
+ if (!convexUrl) {
230
+ console.log(c.gray(" No Convex URL found. Let's set it up.\n"));
231
+ convexUrl = await prompt(` Convex URL ${c.gray("(e.g. https://xxx.eu-west-1.convex.cloud)")}: `);
232
+ if (!convexUrl) { console.error(c.red("✗ Convex URL is required")); await new Promise(r => setTimeout(r, 50)); process.exit(1); }
233
+ cfg = { ...cfg, SURON_URL: convexUrl };
234
+ saveConfig(cfg);
235
+ console.log(c.green(" ✓ Convex URL saved\n"));
236
+ }
237
+
238
+ // ── Step 2: ensure Dashboard URL is set ─────────────────────────────────────
239
+ let dashUrl = process.env.SURON_DASHBOARD_URL ?? cfg.SURON_DASHBOARD_URL;
240
+ if (!dashUrl) {
241
+ console.log(c.gray(" No Dashboard URL found. Where is your SURON dashboard deployed?\n"));
242
+ dashUrl = await prompt(` Dashboard URL ${c.gray("(e.g. https://suron.vercel.app)")}: `);
243
+ if (!dashUrl) { console.error(c.red("✗ Dashboard URL is required")); await new Promise(r => setTimeout(r, 50)); process.exit(1); }
244
+ cfg = { ...cfg, SURON_DASHBOARD_URL: dashUrl };
245
+ saveConfig(cfg);
246
+ console.log(c.green(" ✓ Dashboard URL saved\n"));
247
+ }
248
+
249
+ // ── Step 3: create session and open browser ──────────────────────────────────
250
+ const client = new ConvexHttpClient(convexUrl);
251
+ let sessionId, code;
252
+ try {
253
+ const r = await client.mutation("auth:createCliSession", {});
254
+ sessionId = r.sessionId; code = r.code;
255
+ } catch(e) { console.error(c.red(`✗ ${e.message}`)); await new Promise(r => setTimeout(r, 50)); process.exit(1); }
256
+
257
+ // Open a clean standalone approval page (not the full dashboard)
258
+ const approvalUrl = `${dashUrl.replace(/\/$/, "")}/approve?code=${code}`;
259
+
260
+ console.log(` ${c.bold("Your code")}: ${c.orange(c.bold(code))}\n`);
261
+ console.log(` ${c.gray("Opening approval page in browser…")}`);
262
+ console.log(` ${c.dim(approvalUrl)}\n`);
263
+ console.log(c.dim(" Press Enter to open in browser, or visit the URL above manually."));
264
+ await prompt("");
265
+ openBrowser(approvalUrl);
266
+ console.log(c.gray("\n Waiting for approval… (Ctrl+C to cancel)\n"));
267
+
268
+ // ── Step 4: poll until approved/denied/expired ───────────────────────────────
269
+ const deadline = Date.now() + 10 * 60 * 1000;
270
+ let dots = 0;
271
+ while (Date.now() < deadline) {
272
+ await new Promise(r => setTimeout(r, 1500));
273
+ let result;
274
+ try { result = await client.query("auth:pollCliSession", { sessionId }); } catch { continue; }
275
+ dots = (dots + 1) % 4;
276
+ process.stdout.write(`\r ${c.gray("Waiting" + ".".repeat(dots).padEnd(3))}`);
277
+ if (result.status === "approved" && result.adminToken) {
278
+ process.stdout.write("\n");
279
+ cfg.SURON_ADMIN_TOKEN = result.adminToken;
280
+ saveConfig(cfg);
281
+ console.log(`\n${c.green("✓ Authenticated!")} Token saved to ${c.cyan(CONFIG_FILE)}\n`);
282
+ await new Promise(r => setTimeout(r, 50));
283
+ process.exit(0);
284
+ }
285
+ if (result.status === "denied") {
286
+ process.stdout.write("\n");
287
+ console.log(c.red("✗ Denied."));
288
+ await new Promise(r => setTimeout(r, 50)); process.exit(1);
289
+ }
290
+ if (["expired","not_found"].includes(result.status)) {
291
+ process.stdout.write("\n");
292
+ console.log(c.red("✗ Session expired. Run: suron login"));
293
+ await new Promise(r => setTimeout(r, 50)); process.exit(1);
294
+ }
295
+ }
296
+ process.stdout.write("\n");
297
+ console.log(c.red("✗ Timed out."));
298
+ await new Promise(r => setTimeout(r, 50)); process.exit(1);
299
+ }
300
+
301
+ // ── WHOAMI ────────────────────────────────────────────────────────────────────
302
+ async function cmdWhoami() {
303
+ const cfg = loadConfig();
304
+ console.log(logo);
305
+ if (cfg.SURON_ADMIN_TOKEN) {
306
+ console.log(c.green("✓ Authenticated"));
307
+ console.log(` ${c.gray("Token:")} ${c.cyan("cli_..."+cfg.SURON_ADMIN_TOKEN.slice(-8))}`);
308
+ console.log(` ${c.gray("Server:")} ${cfg.SURON_URL ?? c.red("not set")}`);
309
+ if (cfg.SURON_DASHBOARD_URL) console.log(` ${c.gray("Dashboard:")} ${cfg.SURON_DASHBOARD_URL}`);
310
+ } else {
311
+ console.log(c.red("✗ Not authenticated"));
312
+ console.log(` ${c.gray("Run: suron login")}`);
313
+ }
314
+ }
315
+
316
+ // ── LOGOUT ────────────────────────────────────────────────────────────────────
317
+ function cmdLogout() {
318
+ const cfg = loadConfig();
319
+ if (!cfg.SURON_ADMIN_TOKEN) { console.log(c.gray("Not logged in.")); return; }
320
+ delete cfg.SURON_ADMIN_TOKEN;
321
+ saveConfig(cfg);
322
+ console.log(c.green("✓ Logged out"));
323
+ }
324
+
325
+ // ── APPS ──────────────────────────────────────────────────────────────────────
326
+ async function cmdApps() {
327
+ const sub = arg(1);
328
+ const client = getClient();
329
+
330
+ if (sub === "create") {
331
+ const name = flag("name") ?? flag("n");
332
+ if (!name) { console.error(c.red("✗ --name required")); process.exit(1); }
333
+ const { token } = await client.mutation("apps:createApp", {
334
+ name, displayName: flag("display") ?? name, ownerId: flag("owner") ?? "admin"
335
+ });
336
+ console.log(logo);
337
+ console.log(c.green(`✓ App '${name}' created\n`));
338
+ console.log(` ${c.bold("Token:")} ${c.orange(token)}\n`);
339
+ console.log(c.gray(" Add to your project .env:"));
340
+ console.log(c.cyan(` SURON_URL=${requireConfig().url}`));
341
+ console.log(c.cyan(` SURON_TOKEN=${token}\n`));
342
+ console.log(c.dim(" This token is not shown again in the dashboard."));
343
+ return;
344
+ }
345
+
346
+ const apps = await client.query("apps:listApps");
347
+ console.log(logo); console.log(`${c.bold("Apps")}\n`);
348
+ if (!apps.length) { console.log(c.gray(" No apps. Run: suron apps create --name my-app\n")); return; }
349
+ printTable(
350
+ apps.map(a => ({
351
+ name: c.white(a.name),
352
+ status: a.isActive ? c.green("● active") : c.gray("○ offline"),
353
+ host: c.gray(a.hostname ?? "—"),
354
+ pid: c.gray(String(a.pid ?? "—")),
355
+ seen: c.gray(a.lastSeen ? new Date(a.lastSeen).toLocaleTimeString() : "never"),
356
+ })),
357
+ ["name","status","host","pid","seen"]
358
+ );
359
+ console.log();
360
+ }
361
+
362
+ // ── PUSH ──────────────────────────────────────────────────────────────────────
363
+ async function cmdPush() {
364
+ const envFile = flag("env") ?? flag("f") ?? ".env";
365
+ const appName = flag("app") ?? flag("a");
366
+ if (!appName) { console.error(c.red("✗ --app required")); process.exit(1); }
367
+ const envPath = resolve(process.cwd(), envFile);
368
+ if (!existsSync(envPath)) { console.error(c.red(`✗ Not found: ${envPath}`)); process.exit(1); }
369
+ const parsed = parse(readFileSync(envPath, "utf8"));
370
+ const SKIP = new Set(["SURON_URL","SURON_TOKEN"]);
371
+ const entries = Object.entries(parsed).filter(([k]) => !SKIP.has(k));
372
+ console.log(logo);
373
+ console.log(`${c.bold("Push")} ${c.orange(envFile)} → ${c.bold(appName)}`);
374
+ console.log(c.gray(` ${entries.length} secrets to push\n`));
375
+ const client = getClient();
376
+ let ok = 0;
377
+ for (const [key, value] of entries) {
378
+ try {
379
+ await client.mutation("secrets:upsertSecret", { appName, key, value });
380
+ console.log(` ${c.green("✓")} ${c.white(key)}`);
381
+ ok++;
382
+ } catch(e) { console.log(` ${c.red("✗")} ${key}: ${c.gray(e.message)}`); }
383
+ }
384
+ console.log(`\n${c.green(`✓ ${ok}/${entries.length} secrets pushed to '${appName}'`)}\n`);
385
+ }
386
+
387
+ // ── SECRETS ───────────────────────────────────────────────────────────────────
388
+ async function cmdSecrets() {
389
+ const sub = arg(1);
390
+ const appName = flag("app") ?? flag("a");
391
+ if (!appName) { console.error(c.red("✗ --app required (--app my-app)")); process.exit(1); }
392
+ const client = getClient();
393
+
394
+ // ── LIST (masked)
395
+ if (!sub || sub === "list") {
396
+ const secrets = await client.query("secrets:listSecrets", { appName });
397
+ console.log(logo);
398
+ console.log(`${c.bold("Secrets")} · ${c.orange(appName)}\n`);
399
+ if (!secrets.length) {
400
+ console.log(c.gray(` No secrets. Run: suron secrets set --app ${appName} --key KEY\n`));
401
+ return;
402
+ }
403
+ printTable(
404
+ secrets.map(s => ({
405
+ key: c.white(s.key),
406
+ value: c.gray("••••••••"),
407
+ updated: c.gray(new Date(s.updatedAt).toLocaleString()),
408
+ })),
409
+ ["key","value","updated"],
410
+ { footer: `To reveal values: suron secrets reveal --app ${appName}` }
411
+ );
412
+ console.log();
413
+ return;
414
+ }
415
+
416
+ // ── REVEAL (cleartext table — for debugging)
417
+ if (sub === "reveal") {
418
+ const secrets = await client.query("secrets:listSecrets", { appName });
419
+ console.log(logo);
420
+ console.log(`${c.yellow("⚠ CLEARTEXT")} · ${c.orange(appName)}\n`);
421
+ if (!secrets.length) { console.log(c.gray(" No secrets.\n")); return; }
422
+ // Box-style output — easier for AI to parse
423
+ console.log(c.gray(" ┌─ ENV TABLE " + "─".repeat(Math.max(0, 60 - 12)) + "┐"));
424
+ secrets.forEach(s => {
425
+ const val = s.value ?? "(encrypted)";
426
+ const line = ` ${c.orange(s.key.padEnd(28))}= ${c.cyan(val)}`;
427
+ console.log(line);
428
+ });
429
+ console.log(c.gray(" └" + "─".repeat(62) + "┘"));
430
+ console.log(c.gray(`\n ${secrets.length} secrets · ${appName}\n`));
431
+ return;
432
+ }
433
+
434
+ // ── SET
435
+ if (sub === "set") {
436
+ const key = flag("key") ?? flag("k");
437
+ if (!key) { console.error(c.red("✗ --key required")); process.exit(1); }
438
+ const value = await promptSecret(`Value for ${c.orange(key)}: `);
439
+ if (!value) { console.error(c.red("✗ Empty value — aborted")); process.exit(1); }
440
+ await client.mutation("secrets:upsertSecret", { appName, key, value });
441
+ console.log(c.green(`✓ '${key}' saved for '${appName}'`));
442
+ return;
443
+ }
444
+
445
+ // ── DELETE
446
+ if (sub === "delete") {
447
+ const key = flag("key") ?? flag("k");
448
+ if (!key) { console.error(c.red("✗ --key required")); process.exit(1); }
449
+ const confirm = await prompt(`Delete '${key}' from '${appName}'? ${c.gray("[y/N]")} `);
450
+ if (confirm.toLowerCase() !== "y") { console.log(c.gray("Aborted.")); return; }
451
+ await client.mutation("secrets:deleteSecret", { appName, key });
452
+ console.log(c.green(`✓ Deleted '${key}' from '${appName}'`));
453
+ return;
454
+ }
455
+
456
+ console.error(c.red(`✗ Unknown subcommand: ${sub}`));
457
+ console.log(c.gray(" Usage: suron secrets [list|reveal|set|delete] --app <name>"));
458
+ }
459
+
460
+ // ── REQUESTS ──────────────────────────────────────────────────────────────────
461
+ async function cmdRequests() {
462
+ const client = getClient();
463
+ const reqs = await client.query("secrets:listPendingRequests");
464
+ console.log(logo); console.log(`${c.bold("Pending Requests")}\n`);
465
+ if (!reqs.length) { console.log(c.green(" ✓ No pending requests\n")); return; }
466
+ printTable(
467
+ reqs.map(r => ({
468
+ id: c.gray(r._id),
469
+ app: c.white(r.appName),
470
+ key: c.orange(r.secretKey),
471
+ requested: c.gray(new Date(r.requestedAt).toLocaleString()),
472
+ })),
473
+ ["id","app","key","requested"],
474
+ { footer: "suron approve --id <id> or suron deny --id <id>" }
475
+ );
476
+ console.log();
477
+ }
478
+
479
+ // ── APPROVE / DENY ────────────────────────────────────────────────────────────
480
+ async function cmdResolve(deny = false) {
481
+ const id = flag("id");
482
+ if (!id) { console.error(c.red("✗ --id required")); process.exit(1); }
483
+ const client = getClient();
484
+ await client.mutation("secrets:resolveRequest", {
485
+ requestId: id, status: deny ? "denied" : "approved", resolvedBy: "cli"
486
+ });
487
+ console.log(deny ? c.red(`✗ Denied ${id}`) : c.green(`✓ Approved ${id}`));
488
+ }
489
+
490
+ // ── SURON NEW — scaffold a complete Node.js project ───────────────────────────
491
+ async function cmdNew() {
492
+ const dirName = arg(1);
493
+ if (!dirName) {
494
+ console.error(c.red("✗ Directory name required (e.g. suron new my-project)"));
495
+ process.exit(1);
496
+ }
497
+
498
+ const projectDir = resolve(process.cwd(), dirName);
499
+ const projectName = basename(projectDir);
500
+
501
+ console.log(logo);
502
+ console.log(`${c.bold("Scaffold")} → ${c.orange(projectDir)}\n`);
503
+
504
+ if (existsSync(projectDir)) {
505
+ const ans = await prompt(c.yellow(` '${dirName}' already exists. Continue? [y/N] `));
506
+ if (ans.toLowerCase() !== "y") { console.log(c.gray(" Aborted.")); return; }
507
+ } else {
508
+ mkdirSync(projectDir, { recursive: true });
509
+ }
510
+
511
+ // Prompt for app name
512
+ const appName = await prompt(` App name in SURON ${c.gray(`(${projectName})`)}: `) || projectName;
513
+
514
+ const cfg = loadConfig();
515
+ const suronUrl = cfg.SURON_URL ?? "https://your-deployment.convex.cloud";
516
+ const suronDash = cfg.SURON_DASHBOARD_URL ?? "https://your-dashboard.vercel.app";
517
+
518
+ // ── Files to create ───────────────────────────────────────────────────────
519
+ const files = {
520
+
521
+ // package.json
522
+ "package.json": JSON.stringify({
523
+ name: projectName,
524
+ version: "1.0.0",
525
+ type: "module",
526
+ description: "Node.js app powered by SURON secrets manager",
527
+ main: "src/index.js",
528
+ scripts: {
529
+ start: "node src/index.js",
530
+ dev: "node --watch src/index.js",
531
+ "secrets:list": `suron secrets --app ${appName}`,
532
+ "secrets:reveal": `suron secrets reveal --app ${appName}`,
533
+ "secrets:push": `suron push --app ${appName} --env .env.secrets`,
534
+ },
535
+ dependencies: {
536
+ "@suronai/sdk": "^2.0.0",
537
+ "dotenv": "^16.0.0",
538
+ },
539
+ engines: { node: ">=18.0.0" },
540
+ author: "",
541
+ license: "MIT",
542
+ }, null, 2),
543
+
544
+ // src/index.js — the entry point
545
+ "src/index.js": `/**
546
+ * ${projectName}
547
+ *
548
+ * Entry point — SURON handles all secrets.
549
+ * Run: npm start
550
+ * For dev (auto-restart): npm run dev
551
+ */
552
+
553
+ import "dotenv/config"; // loads SURON_URL + SURON_TOKEN
554
+ import { SURON } from "@suronai/sdk";
555
+
556
+ // ── Bootstrap ─────────────────────────────────────────────────────────────────
557
+ const env = await SURON.connect();
558
+
559
+ // ── Load secrets (triggers Telegram approval on first run) ─────────────────────
560
+ // Add your own keys here:
561
+ const secrets = await env.getAll([
562
+ "EXAMPLE_API_KEY",
563
+ // "DATABASE_URL",
564
+ // "OPENAI_API_KEY",
565
+ ]);
566
+
567
+ // ── Your app code starts here ──────────────────────────────────────────────────
568
+ console.log("✓ Secrets loaded:", Object.keys(secrets).join(", "));
569
+ console.log(" Example key:", secrets.EXAMPLE_API_KEY);
570
+
571
+ // TODO: replace with your actual app logic
572
+ `,
573
+
574
+ // .env — SURON credentials only
575
+ ".env": `# SURON connection — the ONLY vars needed here.
576
+ # Your actual secrets (API keys, DB URLs) live on the SURON server.
577
+ SURON_URL=${suronUrl}
578
+ SURON_TOKEN=sk_YOUR_APP_TOKEN_HERE
579
+ `,
580
+
581
+ // .env.example — safe to commit
582
+ ".env.example": `# SURON credentials — copy to .env and fill in your values.
583
+ # Get your token: suron apps create --name ${appName}
584
+ SURON_URL=${suronUrl}
585
+ SURON_TOKEN=sk_...
586
+ `,
587
+
588
+ // .env.secrets — for first-time push, then delete this file
589
+ ".env.secrets": `# Put your ACTUAL secrets here for the initial push.
590
+ # Run: npm run secrets:push
591
+ # Then DELETE this file — it is NOT committed.
592
+ EXAMPLE_API_KEY=replace_me_with_your_api_key
593
+ # DATABASE_URL=postgres://...
594
+ # OPENAI_API_KEY=sk-...
595
+ `,
596
+
597
+ // .gitignore
598
+ ".gitignore": `.env
599
+ .env.secrets
600
+ node_modules/
601
+ dist/
602
+ *.log
603
+ .DS_Store
604
+ `,
605
+
606
+ // README.md
607
+ "README.md": `# ${projectName}
608
+
609
+ Node.js application using [SURON](${suronDash}) for secrets management.
610
+
611
+ ## Quick Start
612
+
613
+ \`\`\`bash
614
+ # 1. Install dependencies
615
+ npm install
616
+
617
+ # 2. Set your SURON token in .env
618
+ # Get one: suron apps create --name ${appName}
619
+ nano .env
620
+
621
+ # 3. Push initial secrets (edit .env.secrets first)
622
+ npm run secrets:push
623
+
624
+ # 4. Delete .env.secrets (never commit it)
625
+ rm .env.secrets
626
+
627
+ # 5. Run the app (will prompt Telegram approval on first secret access)
628
+ npm start
629
+ \`\`\`
630
+
631
+ ## Secrets Management
632
+
633
+ \`\`\`bash
634
+ # List secrets (masked)
635
+ npm run secrets:list
636
+
637
+ # Reveal secrets in terminal (for debugging)
638
+ npm run secrets:reveal
639
+
640
+ # Set a single secret
641
+ suron secrets set --app ${appName} --key MY_KEY
642
+
643
+ # Push entire .env file
644
+ npm run secrets:push
645
+ \`\`\`
646
+
647
+ ## How SURON Works
648
+
649
+ 1. App calls \`env.get('MY_KEY')\` at runtime
650
+ 2. SURON checks if already approved for this session → returns cached value
651
+ 3. If not → Telegram notification sent to owner
652
+ 4. Owner approves → secret returned and cached until app restarts
653
+
654
+ See [llm.md](${suronDash}/llm.md) for AI integration guide.
655
+ `,
656
+
657
+ // llm.md — AI integration hints specific to this project
658
+ "llm.md": `# AI Integration Notes — ${projectName}
659
+
660
+ > This file is for AI assistants (Claude, Cursor, Copilot, etc.)
661
+ > Read this before touching any secrets, .env files, or startup code.
662
+
663
+ ## Project Type
664
+ Node.js ESM app using SURON for secrets management.
665
+
666
+ ## Key Rules
667
+
668
+ 1. **Never hardcode secrets** — they live on the SURON server.
669
+ 2. **Never use process.env.* for actual secrets** — use \`await env.get('KEY')\`.
670
+ 3. **SURON_URL and SURON_TOKEN** are the only vars in .env — they are not secrets.
671
+ 4. **.env.secrets** is for one-time push only — delete after use, never commit.
672
+
673
+ ## Adding a New Secret
674
+
675
+ \`\`\`bash
676
+ # Terminal — adds secret to SURON server
677
+ suron secrets set --app ${appName} --key NEW_KEY
678
+
679
+ # Then in code:
680
+ const value = await env.get('NEW_KEY')
681
+ \`\`\`
682
+
683
+ ## Revealing All Secrets (for debugging)
684
+
685
+ \`\`\`bash
686
+ suron secrets reveal --app ${appName}
687
+ \`\`\`
688
+
689
+ This shows a full key=value table for inspection.
690
+
691
+ ## Startup Pattern
692
+
693
+ Always follow this pattern at the top of entry files:
694
+
695
+ \`\`\`javascript
696
+ import "dotenv/config"; // loads SURON_URL + SURON_TOKEN
697
+ import { SURON } from "@suronai/sdk";
698
+
699
+ const env = await SURON.connect(); // connects and registers app
700
+
701
+ // Bulk-load all secrets at startup (one Telegram prompt per key)
702
+ const secrets = await env.getAll(["KEY_A", "KEY_B", "KEY_C"]);
703
+ \`\`\`
704
+
705
+ ## App Info
706
+
707
+ - **App name in SURON**: \`${appName}\`
708
+ - **Dashboard**: ${suronDash}
709
+ - **Convex URL**: ${suronUrl}
710
+ `,
711
+ };
712
+
713
+ // ── Write files ────────────────────────────────────────────────────────────
714
+ mkdirSync(join(projectDir, "src"), { recursive: true });
715
+ let written = 0;
716
+ for (const [filePath, content] of Object.entries(files)) {
717
+ const full = join(projectDir, filePath);
718
+ const exists = existsSync(full);
719
+ writeFileSync(full, content, "utf8");
720
+ console.log(` ${c.green("✓")} ${c.white(filePath)} ${exists ? c.gray("(overwritten)") : ""}`);
721
+ written++;
722
+ }
723
+
724
+ console.log(`\n${c.green(`✓ ${written} files created in '${dirName}'`)}\n`);
725
+
726
+ // ── Next steps ─────────────────────────────────────────────────────────────
727
+ const nextSteps = [
728
+ [`cd ${dirName}`, "Enter project directory"],
729
+ ["npm install", "Install dependencies"],
730
+ [`suron apps create --name ${appName}`, "Register app in SURON, get token"],
731
+ ["nano .env", "Set SURON_TOKEN from above"],
732
+ ["nano .env.secrets", "Add your actual secrets"],
733
+ ["npm run secrets:push", "Upload secrets to SURON"],
734
+ ["rm .env.secrets", "Delete secrets file (never commit it)"],
735
+ ["npm start", "Run the app"],
736
+ ];
737
+ console.log(c.gray(" Next steps:"));
738
+ nextSteps.forEach(([cmd, desc], i) => {
739
+ console.log(` ${c.gray(String(i+1)+".")} ${c.cyan(cmd.padEnd(42))} ${c.dim(desc)}`);
740
+ });
741
+ console.log();
742
+ }
743
+
744
+ // ── ROUTER ────────────────────────────────────────────────────────────────────
745
+ const cmd = arg(0);
746
+
747
+ // Handle --version / -v flag at any position
748
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
749
+ console.log(`suron v${VERSION}`);
750
+ process.exit(0);
751
+ }
752
+
753
+ try {
754
+ switch (cmd) {
755
+ case "help":
756
+ case "--help":
757
+ case "-h": printHelp(); break;
758
+ case "version": console.log(`suron v${VERSION}`); break;
759
+ case "init": await cmdInit(); break;
760
+ case "login": await cmdLogin(); break;
761
+ case "logout": cmdLogout(); break;
762
+ case "whoami": await cmdWhoami(); break;
763
+ case "new": await cmdNew(); break;
764
+ case "push": await cmdPush(); break;
765
+ case "apps": await cmdApps(); break;
766
+ case "secrets": await cmdSecrets(); break;
767
+ case "requests": await cmdRequests(); break;
768
+ case "approve": await cmdResolve(false); break;
769
+ case "deny": await cmdResolve(true); break;
770
+ default: printHelp();
771
+ }
772
+ } catch(err) {
773
+ console.error(c.red(`\n✗ ${err.message}`));
774
+ if (hasFlag("debug")) console.error(err.stack);
775
+ process.exit(1);
776
+ }