@suronai/cli 0.1.30 → 0.1.31

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@suronai/cli",
3
- "version": "0.1.30",
4
- "description": "CLI for Suron — suron login, init, whoami, rotate",
3
+ "version": "0.1.31",
4
+ "description": "CLI for Suron — suron login, init, whoami, recover",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "suron": "./src/index.js"
@@ -10,8 +10,8 @@
10
10
  "src"
11
11
  ],
12
12
  "scripts": {
13
- "build": "node --check src/index.js src/commands/login.js src/commands/init.js src/commands/rotate.js src/commands/whoami.js src/utils/config.js src/utils/dotenvx.js src/utils/colors.js",
14
- "lint": "node --check src/index.js src/commands/login.js src/commands/init.js src/commands/rotate.js src/commands/whoami.js src/utils/config.js src/utils/dotenvx.js src/utils/colors.js"
13
+ "build": "node --check src/index.js src/commands/login.js src/commands/init.js src/commands/whoami.js src/commands/recover.js src/utils/config.js src/utils/dotenvx.js src/utils/colors.js",
14
+ "lint": "node --check src/index.js src/commands/login.js src/commands/init.js src/commands/whoami.js src/commands/recover.js src/utils/config.js src/utils/dotenvx.js src/utils/colors.js"
15
15
  },
16
16
  "dependencies": {
17
17
  "@dotenvx/dotenvx": "latest",
@@ -67,17 +67,23 @@ export const initCommand = new Command("init")
67
67
  }
68
68
 
69
69
  if (!res.ok) {
70
- let errMsg = `register-app failed (${res.status})`;
71
- try {
72
- const body = await res.json();
73
- if (body?.error) errMsg = body.error;
74
- } catch { /* ignore */ }
75
- console.error("\n " + c.red("") + ` ${errMsg}\n`);
70
+ let body = {};
71
+ try { body = await res.json(); } catch { /* ignore */ }
72
+
73
+ if (res.status === 409) {
74
+ console.error("\n " + c.red("✗") + ` An app named "${c.cyan(appName)}" is already registered.`);
75
+ console.error(" If you lost .suron.json, run: " + c.bold("suron recover") + "\n");
76
+ } else {
77
+ console.error("\n " + c.red("✗") + ` ${body?.error ?? `register-app failed (${res.status})`}\n`);
78
+ }
76
79
  process.exit(1);
77
80
  }
78
81
 
79
82
  const { app_id } = await res.json();
80
83
 
84
+ // ── .gitignore ─────────────────────────────────────────────────────────────
85
+ ensureGitignore(cwd);
86
+
81
87
  // ── Write .suron.json ─────────────────────────────────────────────────────
82
88
  writeFileSync(
83
89
  join(cwd, ".suron.json"),
@@ -105,6 +111,8 @@ export const initCommand = new Command("init")
105
111
  } catch {
106
112
  console.error(" " + c.yellow("⚠") + " SDK install failed — run manually: npm install @suronai/sdk");
107
113
  }
114
+ } else {
115
+ console.log(" " + c.dim("@suronai/sdk already installed — skipped."));
108
116
  }
109
117
  }
110
118
 
@@ -114,12 +122,41 @@ export const initCommand = new Command("init")
114
122
  // ── Done ──────────────────────────────────────────────────────────────────
115
123
  console.log();
116
124
  console.log(" " + c.green("✓") + " .env encrypted " + c.dim("(safe to commit)"));
117
- console.log(" " + c.green("✓") + " .env.keys kept " + c.dim("(add to .gitignore)"));
125
+ console.log(" " + c.green("✓") + " .env.keys kept " + c.dim("(gitignored)") );
118
126
  console.log(" " + c.green("✓") + " .suron.json written " + c.dim("(safe to commit)"));
119
127
  console.log(" " + c.green("✓") + " @suronai/sdk installed");
120
128
  console.log();
121
129
  });
122
130
 
131
+ /**
132
+ * Ensures .env.keys is in .gitignore.
133
+ * Creates .gitignore if it doesn't exist.
134
+ * Appends the entry if it's not already present (exact line match).
135
+ * @param {string} cwd
136
+ */
137
+ const GITIGNORE_ENTRIES = [
138
+ "node_modules/",
139
+ "package-lock.json",
140
+ ".env.keys",
141
+ ];
142
+
143
+ function ensureGitignore(cwd) {
144
+ const gitignorePath = join(cwd, ".gitignore");
145
+
146
+ if (!existsSync(gitignorePath)) {
147
+ writeFileSync(gitignorePath, GITIGNORE_ENTRIES.join("\n") + "\n", "utf-8");
148
+ return;
149
+ }
150
+
151
+ const content = readFileSync(gitignorePath, "utf-8");
152
+ const existing = new Set(content.split("\n").map(l => l.trim()));
153
+ const missing = GITIGNORE_ENTRIES.filter(e => !existing.has(e));
154
+ if (missing.length === 0) return;
155
+
156
+ const separator = content.endsWith("\n") ? "" : "\n";
157
+ writeFileSync(gitignorePath, content + separator + missing.join("\n") + "\n", "utf-8");
158
+ }
159
+
123
160
  /** @param {string} cwd @returns {string} */
124
161
  function detectPackageManager(cwd) {
125
162
  if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
@@ -0,0 +1,82 @@
1
+ import { Command } from "commander";
2
+ import { existsSync, writeFileSync } from "fs";
3
+ import { join, basename } from "path";
4
+ import { requireApiUrl, prompt } from "../utils/config.js";
5
+ import c from "../utils/colors.js";
6
+
7
+ export const recoverCommand = new Command("recover")
8
+ .description("Restore a lost .suron.json by looking up your app name")
9
+ .option("--name <n>", "App name to recover (skips interactive prompt)")
10
+ .action(async (opts) => {
11
+ const cwd = process.cwd();
12
+ const apiUrl = requireApiUrl();
13
+
14
+ console.log("\n" + c.bold(" suron recover") + " — " + c.dim(cwd) + "\n");
15
+
16
+ if (existsSync(join(cwd, ".suron.json"))) {
17
+ console.log(" " + c.yellow("⚠") + " .suron.json already exists here.");
18
+ const answer = await prompt(" Overwrite it? [y/N] › ");
19
+ if (!/^y(es)?$/i.test(answer)) {
20
+ console.log(" " + c.dim("Cancelled.\n"));
21
+ process.exit(0);
22
+ }
23
+ }
24
+
25
+ // ── App name ──────────────────────────────────────────────────────────────
26
+ let appName;
27
+ if (opts.name) {
28
+ appName = opts.name.toLowerCase().replace(/[^a-z0-9]/g, "");
29
+ console.log(" App name " + c.cyan(appName));
30
+ } else {
31
+ const suggested = basename(cwd) || "my-project";
32
+ const raw = await prompt(` App name [${c.dim(suggested)}] › `);
33
+ appName = (raw || suggested).toLowerCase().replace(/[^a-z0-9]/g, "");
34
+ }
35
+
36
+ if (!appName) {
37
+ console.error(" " + c.red("✗") + " App name is required.\n");
38
+ process.exit(1);
39
+ }
40
+
41
+ // ── Look up app ───────────────────────────────────────────────────────────
42
+ console.log("\n " + c.dim("Looking up app..."));
43
+
44
+ let res;
45
+ try {
46
+ res = await fetch(`${apiUrl}/cli/recover-app`, {
47
+ method: "POST",
48
+ headers: { "Content-Type": "application/json" },
49
+ body: JSON.stringify({ name: appName }),
50
+ });
51
+ } catch (err) {
52
+ console.error("\n " + c.red("✗") + ` Could not reach Suron API: ${err.message}\n`);
53
+ process.exit(1);
54
+ }
55
+
56
+ if (!res.ok) {
57
+ let body = {};
58
+ try { body = await res.json(); } catch { /* ignore */ }
59
+ if (res.status === 404) {
60
+ console.error("\n " + c.red("✗") + ` No app named "${c.cyan(appName)}" found.`);
61
+ console.error(" Check the name carefully — names are lowercase and alphanumeric only.\n");
62
+ } else {
63
+ console.error("\n " + c.red("✗") + ` ${body?.error ?? `recover-app failed (${res.status})`}\n`);
64
+ }
65
+ process.exit(1);
66
+ }
67
+
68
+ const { app_id, name } = await res.json();
69
+
70
+ // ── Write .suron.json ─────────────────────────────────────────────────────
71
+ writeFileSync(
72
+ join(cwd, ".suron.json"),
73
+ JSON.stringify({ app: name, id: app_id, api_url: apiUrl }, null, 2) + "\n",
74
+ "utf-8"
75
+ );
76
+
77
+ console.log();
78
+ console.log(" " + c.green("✓") + " .suron.json restored");
79
+ console.log(" " + c.dim(" app: ") + c.cyan(name));
80
+ console.log(" " + c.dim(" id: ") + c.cyan(app_id));
81
+ console.log();
82
+ });
package/src/index.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { Command } from "commander";
4
4
  import { createRequire } from "module";
5
- import { loginCommand } from "./commands/login.js";
6
- import { initCommand } from "./commands/init.js";
7
- import { whoamiCommand } from "./commands/whoami.js";
8
- import { rotateCommand } from "./commands/rotate.js";
5
+ import { loginCommand } from "./commands/login.js";
6
+ import { initCommand } from "./commands/init.js";
7
+ import { whoamiCommand } from "./commands/whoami.js";
8
+ import { recoverCommand } from "./commands/recover.js";
9
9
 
10
10
  const require = createRequire(import.meta.url);
11
11
  const { version } = require("../package.json");
@@ -20,6 +20,6 @@ program
20
20
  program.addCommand(loginCommand);
21
21
  program.addCommand(initCommand);
22
22
  program.addCommand(whoamiCommand);
23
- program.addCommand(rotateCommand);
23
+ program.addCommand(recoverCommand);
24
24
 
25
25
  program.parse(process.argv);
@@ -1,80 +0,0 @@
1
- import { Command } from "commander";
2
- import { existsSync, readFileSync, unlinkSync } from "fs";
3
- import { join } from "path";
4
- import { requireApiUrl } from "../utils/config.js";
5
- import { encryptDotenv, readPrivateKey } from "../utils/dotenvx.js";
6
- import c from "../utils/colors.js";
7
-
8
- export const rotateCommand = new Command("rotate")
9
- .description("Re-encrypt .env with a new private key and update Suron")
10
- .action(async () => {
11
- const cwd = process.cwd();
12
- const apiUrl = requireApiUrl();
13
-
14
- console.log("\n" + c.bold(" suron rotate") + " — " + c.dim(cwd) + "\n");
15
-
16
- const suronJsonPath = join(cwd, ".suron.json");
17
- if (!existsSync(suronJsonPath)) {
18
- console.error(" " + c.red("✗") + " .suron.json not found — run: suron init\n");
19
- process.exit(1);
20
- }
21
-
22
- let suronConfig;
23
- try {
24
- suronConfig = JSON.parse(readFileSync(suronJsonPath, "utf-8"));
25
- } catch {
26
- console.error(" " + c.red("✗") + " .suron.json is malformed\n");
27
- process.exit(1);
28
- }
29
-
30
- if (!existsSync(join(cwd, ".env"))) {
31
- console.error(" " + c.red("✗") + " .env not found\n");
32
- process.exit(1);
33
- }
34
-
35
- console.log(" " + c.dim("Re-encrypting .env with a new private key..."));
36
-
37
- try {
38
- encryptDotenv(cwd);
39
- } catch (err) {
40
- console.error("\n " + c.red("✗") + " " + err.message + "\n");
41
- process.exit(1);
42
- }
43
-
44
- let newPrivateKey;
45
- try {
46
- newPrivateKey = readPrivateKey(cwd);
47
- } catch (err) {
48
- console.error("\n " + c.red("✗") + " " + err.message + "\n");
49
- process.exit(1);
50
- }
51
-
52
- console.log(" " + c.dim("Updating private key in Suron..."));
53
-
54
- let res;
55
- try {
56
- res = await fetch(`${apiUrl}/cli/rotate-key`, {
57
- method: "POST",
58
- headers: { "Content-Type": "application/json" },
59
- body: JSON.stringify({ app_id: suronConfig.id, private_key: newPrivateKey }),
60
- });
61
- } catch (err) {
62
- console.error("\n " + c.red("✗") + ` Could not reach Suron API: ${err.message}\n`);
63
- process.exit(1);
64
- }
65
-
66
- if (!res.ok) {
67
- const text = await res.text().catch(() => "");
68
- console.error("\n " + c.red("✗") + ` rotate-key failed (${res.status}): ${text}\n`);
69
- process.exit(1);
70
- }
71
-
72
- const keysPath = join(cwd, ".env.keys");
73
- if (existsSync(keysPath)) unlinkSync(keysPath);
74
-
75
- console.log();
76
- console.log(" " + c.green("✓") + " Key rotated");
77
- console.log(" " + c.green("✓") + " .env re-encrypted " + c.dim("(safe to commit)"));
78
- console.log(" " + c.green("✓") + " .env.keys deleted");
79
- console.log();
80
- });