@suronai/cli 0.1.29 → 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 +4 -4
- package/src/commands/init.js +44 -7
- package/src/commands/recover.js +82 -0
- package/src/index.js +10 -6
- package/src/commands/rotate.js +0 -80
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suronai/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI for Suron — suron login, init, whoami,
|
|
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/
|
|
14
|
-
"lint": "node --check src/index.js src/commands/login.js src/commands/init.js src/commands/
|
|
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",
|
package/src/commands/init.js
CHANGED
|
@@ -67,17 +67,23 @@ export const initCommand = new Command("init")
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
if (!res.ok) {
|
|
70
|
-
let
|
|
71
|
-
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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("(
|
|
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
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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 { recoverCommand } from "./commands/recover.js";
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const { version } = require("../package.json");
|
|
8
12
|
|
|
9
13
|
const program = new Command();
|
|
10
14
|
|
|
11
15
|
program
|
|
12
16
|
.name("suron")
|
|
13
17
|
.description("Secrets delivery with Telegram approval")
|
|
14
|
-
.version(
|
|
18
|
+
.version(version);
|
|
15
19
|
|
|
16
20
|
program.addCommand(loginCommand);
|
|
17
21
|
program.addCommand(initCommand);
|
|
18
22
|
program.addCommand(whoamiCommand);
|
|
19
|
-
program.addCommand(
|
|
23
|
+
program.addCommand(recoverCommand);
|
|
20
24
|
|
|
21
25
|
program.parse(process.argv);
|
package/src/commands/rotate.js
DELETED
|
@@ -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
|
-
});
|