@suronai/cli 0.1.35 → 0.1.37
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 +23 -23
- package/src/commands.js +237 -0
- package/src/index.js +4 -7
- package/src/utils/api.js +20 -0
- package/src/utils/colors.js +179 -9
- package/src/utils/config.js +14 -34
- package/src/utils/crypto.js +7 -0
- package/README.md +0 -134
- package/src/commands/init.js +0 -394
- package/src/commands/login.js +0 -56
- package/src/commands/recover.js +0 -95
- package/src/commands/whoami.js +0 -13
- package/src/utils/dotenvx.js +0 -61
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@suronai/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"suron": "./src/index.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"src"
|
|
11
|
-
],
|
|
12
|
-
"scripts": {
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"@
|
|
18
|
-
"commander": "
|
|
19
|
-
},
|
|
20
|
-
"engines": {
|
|
21
|
-
"
|
|
22
|
-
}
|
|
23
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@suronai/cli",
|
|
3
|
+
"version": "0.1.37",
|
|
4
|
+
"description": "Suron CLI — secrets delivery",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"suron": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "bun src/index.js",
|
|
14
|
+
"dev": "bun --watch src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@clack/prompts": "^1.3.0",
|
|
18
|
+
"commander": "^14.0.3"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"bun": ">=1.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/commands.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { intro, outro, spinner, select, text, isCancel } from "@clack/prompts";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { exec } from "child_process";
|
|
6
|
+
import ui from "./utils/colors.js";
|
|
7
|
+
import { getToken, requireToken, saveConfig, clearToken } from "./utils/config.js";
|
|
8
|
+
import { api, BASE_URL } from "./utils/api.js";
|
|
9
|
+
|
|
10
|
+
function cancel(msg) {
|
|
11
|
+
console.log(ui.blank());
|
|
12
|
+
console.log(ui.errBlock(msg));
|
|
13
|
+
console.log(ui.blank());
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const loginCommand = new Command("login")
|
|
18
|
+
.description("Authenticate via browser")
|
|
19
|
+
.option("--force", "Re-authenticate even if already logged in")
|
|
20
|
+
.action(async (opts) => {
|
|
21
|
+
const existing = getToken();
|
|
22
|
+
if (existing && !opts.force) {
|
|
23
|
+
console.log(ui.blank());
|
|
24
|
+
console.log(ui.errBlock("already logged in", "use --force to re-authenticate"));
|
|
25
|
+
console.log(ui.blank());
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(ui.logo());
|
|
30
|
+
console.log(ui.blank());
|
|
31
|
+
console.log(ui.hr());
|
|
32
|
+
console.log(ui.blank());
|
|
33
|
+
|
|
34
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
35
|
+
let code = "";
|
|
36
|
+
for (let i = 0; i < 6; i++) code += chars[Math.floor(Math.random() * chars.length)];
|
|
37
|
+
|
|
38
|
+
await api("/cli/auth-code", {
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: JSON.stringify({ code }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const loginUrl = `${BASE_URL}/clilogin?code=${code}`;
|
|
44
|
+
|
|
45
|
+
console.log(ui.infoBlock("Opening browser to complete login..."));
|
|
46
|
+
console.log(ui.blank());
|
|
47
|
+
console.log(ui.kv("URL", loginUrl, 6));
|
|
48
|
+
console.log(ui.blank());
|
|
49
|
+
|
|
50
|
+
exec(`open "${loginUrl}" 2>/dev/null || xdg-open "${loginUrl}" 2>/dev/null || cmd /c start "" "${loginUrl}" 2>/dev/null`);
|
|
51
|
+
|
|
52
|
+
const s = spinner();
|
|
53
|
+
s.start("Waiting for approval");
|
|
54
|
+
|
|
55
|
+
const deadline = Date.now() + 300_000;
|
|
56
|
+
let token = null;
|
|
57
|
+
|
|
58
|
+
while (Date.now() < deadline) {
|
|
59
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
60
|
+
try {
|
|
61
|
+
const result = await api(`/cli/auth-code/status?code=${code}`);
|
|
62
|
+
if (result.status === "approved" && result.token) {
|
|
63
|
+
token = result.token;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
s.stop(token ? "Approved" : "Timed out");
|
|
70
|
+
|
|
71
|
+
if (!token) {
|
|
72
|
+
console.log(ui.blank());
|
|
73
|
+
console.log(ui.errBlock("login timed out", "try again"));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
saveConfig({ token });
|
|
78
|
+
|
|
79
|
+
console.log(ui.blank());
|
|
80
|
+
console.log(ui.step("done", "logged in"));
|
|
81
|
+
console.log(ui.step("done", "saved ~/.suron-config"));
|
|
82
|
+
console.log(ui.blank());
|
|
83
|
+
console.log(ui.hr());
|
|
84
|
+
console.log(ui.blank());
|
|
85
|
+
console.log(ui.infoBlock("next › cd your-project && " + ui.code("suron init")));
|
|
86
|
+
console.log(ui.blank());
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export const logoutCommand = new Command("logout")
|
|
90
|
+
.description("Clear local session token")
|
|
91
|
+
.action(async () => {
|
|
92
|
+
clearToken();
|
|
93
|
+
try {
|
|
94
|
+
await api("/auth/logout", { method: "POST" });
|
|
95
|
+
} catch {}
|
|
96
|
+
console.log(ui.blank());
|
|
97
|
+
console.log(ui.okBlock("logged out"));
|
|
98
|
+
console.log(ui.blank());
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export const initCommand = new Command("init")
|
|
102
|
+
.description("Initialize Suron in this project")
|
|
103
|
+
.action(async () => {
|
|
104
|
+
const token = requireToken();
|
|
105
|
+
|
|
106
|
+
if (existsSync(".suron.json")) {
|
|
107
|
+
console.log(ui.blank());
|
|
108
|
+
console.log(ui.errBlock(".suron.json already exists", "use suron up to push a new version"));
|
|
109
|
+
console.log(ui.blank());
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!existsSync(".env")) {
|
|
114
|
+
console.log(ui.blank());
|
|
115
|
+
console.log(ui.errBlock(".env not found", "create a .env file first"));
|
|
116
|
+
console.log(ui.blank());
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(ui.logo());
|
|
121
|
+
console.log(ui.blank());
|
|
122
|
+
console.log(ui.hr());
|
|
123
|
+
console.log(ui.blank());
|
|
124
|
+
|
|
125
|
+
const mode = await select({
|
|
126
|
+
message: "How do you want to link this project?",
|
|
127
|
+
options: [
|
|
128
|
+
{ value: "new", label: "Create new app" },
|
|
129
|
+
{ value: "existing", label: "Link to existing app" },
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (isCancel(mode)) cancel("cancelled");
|
|
134
|
+
|
|
135
|
+
let app_id, app_name;
|
|
136
|
+
|
|
137
|
+
if (mode === "new") {
|
|
138
|
+
const name = await text({
|
|
139
|
+
message: "App name (alphanumeric)",
|
|
140
|
+
validate: v => /^[a-zA-Z0-9]+$/.test(v) ? undefined : "Alphanumeric only",
|
|
141
|
+
});
|
|
142
|
+
if (isCancel(name)) cancel("cancelled");
|
|
143
|
+
|
|
144
|
+
const result = await api("/apps", {
|
|
145
|
+
method: "POST",
|
|
146
|
+
body: JSON.stringify({ name }),
|
|
147
|
+
}).catch(err => cancel(err.message));
|
|
148
|
+
|
|
149
|
+
app_id = result.app_id;
|
|
150
|
+
app_name = result.name;
|
|
151
|
+
} else {
|
|
152
|
+
const apps = await api("/apps").catch(err => cancel(err.message));
|
|
153
|
+
|
|
154
|
+
if (!apps.length) {
|
|
155
|
+
console.log(ui.blank());
|
|
156
|
+
console.log(ui.errBlock("no apps found", "use 'Create new app' instead"));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const chosen = await select({
|
|
161
|
+
message: "Select an app",
|
|
162
|
+
options: apps.map(a => ({ value: a.app_id, label: a.name })),
|
|
163
|
+
});
|
|
164
|
+
if (isCancel(chosen)) cancel("cancelled");
|
|
165
|
+
|
|
166
|
+
const found = apps.find(a => a.app_id === chosen);
|
|
167
|
+
app_id = found.app_id;
|
|
168
|
+
app_name = found.name;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const env_plaintext = readFileSync(".env", "utf-8");
|
|
172
|
+
|
|
173
|
+
const s = spinner();
|
|
174
|
+
s.start("Pushing v1");
|
|
175
|
+
|
|
176
|
+
const result = await api(`/apps/${app_id}/versions`, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body: JSON.stringify({ env_plaintext }),
|
|
179
|
+
}).catch(err => { s.stop("failed"); cancel(err.message); });
|
|
180
|
+
|
|
181
|
+
s.stop("Done");
|
|
182
|
+
|
|
183
|
+
const suronJson = { app_name, app_id, version: result.version };
|
|
184
|
+
writeFileSync(".suron.json", JSON.stringify(suronJson, null, 2) + "\n");
|
|
185
|
+
|
|
186
|
+
console.log(ui.blank());
|
|
187
|
+
console.log(ui.step("done", "encrypted and uploaded .env"));
|
|
188
|
+
console.log(ui.step("done", `version ${result.version}`));
|
|
189
|
+
console.log(ui.step("done", "wrote .suron.json"));
|
|
190
|
+
console.log(ui.blank());
|
|
191
|
+
console.log(ui.hr());
|
|
192
|
+
console.log(ui.blank());
|
|
193
|
+
console.log(ui.infoBlock("add .suron.json to git · add .env to .gitignore"));
|
|
194
|
+
console.log(ui.blank());
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
export const upCommand = new Command("up")
|
|
198
|
+
.description("Push a new version of .env")
|
|
199
|
+
.action(async () => {
|
|
200
|
+
requireToken();
|
|
201
|
+
|
|
202
|
+
if (!existsSync(".suron.json")) {
|
|
203
|
+
console.log(ui.blank());
|
|
204
|
+
console.log(ui.errBlock(".suron.json not found", "run: suron init"));
|
|
205
|
+
console.log(ui.blank());
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!existsSync(".env")) {
|
|
210
|
+
console.log(ui.blank());
|
|
211
|
+
console.log(ui.errBlock(".env not found"));
|
|
212
|
+
console.log(ui.blank());
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const suronJson = JSON.parse(readFileSync(".suron.json", "utf-8"));
|
|
217
|
+
const { app_id, app_name } = suronJson;
|
|
218
|
+
|
|
219
|
+
const env_plaintext = readFileSync(".env", "utf-8");
|
|
220
|
+
|
|
221
|
+
const s = spinner();
|
|
222
|
+
s.start("Pushing new version");
|
|
223
|
+
|
|
224
|
+
const result = await api(`/apps/${app_id}/versions`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
body: JSON.stringify({ env_plaintext }),
|
|
227
|
+
}).catch(err => { s.stop("failed"); cancel(err.message); });
|
|
228
|
+
|
|
229
|
+
s.stop("Done");
|
|
230
|
+
|
|
231
|
+
suronJson.version = result.version;
|
|
232
|
+
writeFileSync(".suron.json", JSON.stringify(suronJson, null, 2) + "\n");
|
|
233
|
+
|
|
234
|
+
console.log(ui.blank());
|
|
235
|
+
console.log(ui.step("done", `version ${result.version} pushed`));
|
|
236
|
+
console.log(ui.blank());
|
|
237
|
+
});
|
package/src/index.js
CHANGED
|
@@ -2,12 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { createRequire } from "module";
|
|
5
|
-
import { loginCommand }
|
|
6
|
-
import { initCommand } from "./commands/init.js";
|
|
7
|
-
import { whoamiCommand } from "./commands/whoami.js";
|
|
8
|
-
import { recoverCommand } from "./commands/recover.js";
|
|
5
|
+
import { loginCommand, logoutCommand, initCommand, upCommand } from "./commands.js";
|
|
9
6
|
|
|
10
|
-
const require
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
11
8
|
const { version } = require("../package.json");
|
|
12
9
|
|
|
13
10
|
const program = new Command();
|
|
@@ -18,8 +15,8 @@ program
|
|
|
18
15
|
.version(version);
|
|
19
16
|
|
|
20
17
|
program.addCommand(loginCommand);
|
|
18
|
+
program.addCommand(logoutCommand);
|
|
21
19
|
program.addCommand(initCommand);
|
|
22
|
-
program.addCommand(
|
|
23
|
-
program.addCommand(recoverCommand);
|
|
20
|
+
program.addCommand(upCommand);
|
|
24
21
|
|
|
25
22
|
program.parse(process.argv);
|
package/src/utils/api.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getToken } from "./config.js";
|
|
2
|
+
|
|
3
|
+
const BASE_URL = "https://suronai.com";
|
|
4
|
+
|
|
5
|
+
export async function api(path, options = {}) {
|
|
6
|
+
const token = getToken();
|
|
7
|
+
const headers = { "Content-Type": "application/json", ...options.headers };
|
|
8
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
9
|
+
|
|
10
|
+
const res = await fetch(`${BASE_URL}${path}`, { ...options, headers });
|
|
11
|
+
|
|
12
|
+
if (!res.ok) {
|
|
13
|
+
const data = await res.json().catch(() => ({}));
|
|
14
|
+
throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return res.json();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { BASE_URL };
|
package/src/utils/colors.js
CHANGED
|
@@ -1,11 +1,181 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SURON — Terminal Design System
|
|
3
|
+
*
|
|
4
|
+
* Aesthetic: Dieter Rams × Teenage Engineering
|
|
5
|
+
* Principle: Less, but better. Every character earns its place.
|
|
6
|
+
*
|
|
7
|
+
* Palette:
|
|
8
|
+
* Amber — primary signal, values, focus #FFB347 → \x1b[38;5;214m
|
|
9
|
+
* Warm white — labels, structural text \x1b[97m
|
|
10
|
+
* Slate — secondary / dim \x1b[2m
|
|
11
|
+
* Green — success / done \x1b[38;5;71m
|
|
12
|
+
* Red — error / fail \x1b[38;5;167m
|
|
13
|
+
* No blue. No purple. No cyan.
|
|
14
|
+
*
|
|
15
|
+
* Grid: all content lives at a 2-space left indent.
|
|
16
|
+
* Rule width: 58 chars of content (60 total with indent).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const ESC = "\x1b[";
|
|
20
|
+
const R = "\x1b[0m";
|
|
21
|
+
|
|
22
|
+
// Core palette
|
|
23
|
+
const amber = (s) => `${ESC}38;5;214m${s}${R}`;
|
|
24
|
+
const warm = (s) => `${ESC}97m${s}${R}`;
|
|
25
|
+
const slate = (s) => `${ESC}2m${s}${R}`;
|
|
26
|
+
const green = (s) => `${ESC}38;5;71m${s}${R}`;
|
|
27
|
+
const red = (s) => `${ESC}38;5;167m${s}${R}`;
|
|
28
|
+
const bold = (s) => `${ESC}1m${s}${R}`;
|
|
29
|
+
const italic = (s) => `${ESC}3m${s}${R}`;
|
|
30
|
+
|
|
31
|
+
// Semantic aliases
|
|
32
|
+
const value = amber; // user-supplied values, app names, URLs
|
|
33
|
+
const label = slate; // column headers, static labels
|
|
34
|
+
const ok = green; // success states
|
|
35
|
+
const fail = red; // error states
|
|
36
|
+
const em = warm; // emphasis within a line
|
|
37
|
+
|
|
38
|
+
// ── Structural primitives ────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const INDENT = " ";
|
|
41
|
+
const COL_W = 58;
|
|
42
|
+
|
|
43
|
+
/** Horizontal rule — thin, full-width */
|
|
44
|
+
const hr = () => INDENT + slate("─".repeat(COL_W));
|
|
45
|
+
|
|
46
|
+
/** Thick rule — section separator */
|
|
47
|
+
const hrThick = () => INDENT + slate("━".repeat(COL_W));
|
|
48
|
+
|
|
49
|
+
/** Blank line */
|
|
50
|
+
const blank = () => "";
|
|
51
|
+
|
|
52
|
+
/** Section header — sparse, uppercase, no decoration */
|
|
53
|
+
const header = (text) =>
|
|
54
|
+
INDENT + bold(warm(text.toUpperCase()));
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Key-value row — strict two-column layout
|
|
58
|
+
* key is left-padded to `keyWidth` (default 14), value is amber
|
|
59
|
+
* @param {string} key
|
|
60
|
+
* @param {string} val
|
|
61
|
+
* @param {number} [keyWidth=14]
|
|
62
|
+
*/
|
|
63
|
+
const kv = (key, val, keyWidth = 14) =>
|
|
64
|
+
INDENT + label(key.padEnd(keyWidth)) + value(val);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Status row — used in step lists
|
|
68
|
+
* States: "pending" | "run" | "done" | "skip" | "fail"
|
|
69
|
+
* @param {"pending"|"run"|"done"|"skip"|"fail"} state
|
|
70
|
+
* @param {string} text
|
|
71
|
+
* @param {string} [note]
|
|
72
|
+
*/
|
|
73
|
+
const STATUS_GLYPHS = {
|
|
74
|
+
pending: slate("·"),
|
|
75
|
+
run: amber("▶"),
|
|
76
|
+
done: green("✔"),
|
|
77
|
+
skip: slate("○"),
|
|
78
|
+
fail: red("✘"),
|
|
79
|
+
};
|
|
80
|
+
const STATUS_LABELS = {
|
|
81
|
+
pending: slate("···"),
|
|
82
|
+
run: amber("RUN"),
|
|
83
|
+
done: green("OK "),
|
|
84
|
+
skip: slate("SKP"),
|
|
85
|
+
fail: red("ERR"),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const step = (state, text, note) => {
|
|
89
|
+
const glyph = STATUS_GLYPHS[state] ?? STATUS_GLYPHS.pending;
|
|
90
|
+
const status = STATUS_LABELS[state] ?? STATUS_LABELS.pending;
|
|
91
|
+
const body = state === "fail" ? red(text)
|
|
92
|
+
: state === "done" ? slate(text)
|
|
93
|
+
: state === "skip" ? slate(text)
|
|
94
|
+
: state === "run" ? warm(text)
|
|
95
|
+
: slate(text);
|
|
96
|
+
const tail = note ? " " + slate(italic(note)) : "";
|
|
97
|
+
return INDENT + glyph + " " + status + " " + body + tail;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Inline prompt indicator — shown before user input
|
|
102
|
+
* @param {string} question
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
const promptLine = (question) =>
|
|
106
|
+
INDENT + amber("▸") + " " + warm(question) + " " + slate("›") + " ";
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Error block — tight, no wasted space
|
|
110
|
+
* @param {string} msg
|
|
111
|
+
* @param {string} [hint]
|
|
112
|
+
*/
|
|
113
|
+
const errBlock = (msg, hint) => {
|
|
114
|
+
const lines = [INDENT + red("✘") + " " + red(msg)];
|
|
115
|
+
if (hint) lines.push(INDENT + " " + slate(hint));
|
|
116
|
+
return lines.join("\n");
|
|
9
117
|
};
|
|
10
118
|
|
|
11
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Success block
|
|
121
|
+
* @param {string} msg
|
|
122
|
+
*/
|
|
123
|
+
const okBlock = (msg) =>
|
|
124
|
+
INDENT + green("✔") + " " + ok(msg);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Info block (neutral)
|
|
128
|
+
* @param {string} msg
|
|
129
|
+
*/
|
|
130
|
+
const infoBlock = (msg) =>
|
|
131
|
+
INDENT + slate("·") + " " + slate(msg);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Code / path snippet — amber, stencil feel
|
|
135
|
+
* @param {string} text
|
|
136
|
+
*/
|
|
137
|
+
const code = (text) =>
|
|
138
|
+
amber(text);
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Diff line — used in entry-point patching
|
|
142
|
+
* @param {"remove"|"add"|"unknown"} type
|
|
143
|
+
* @param {string} text
|
|
144
|
+
*/
|
|
145
|
+
const diff = (type, text) => {
|
|
146
|
+
if (type === "remove") return INDENT + " " + red("−") + " " + slate(text);
|
|
147
|
+
if (type === "add") return INDENT + " " + green("+") + " " + amber(text);
|
|
148
|
+
return INDENT + " " + slate("?") + " " + slate("(manual update required)");
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Logo / wordmark — sparse, dot-matrix feel
|
|
153
|
+
* Only shown once per session (login/init headers).
|
|
154
|
+
*/
|
|
155
|
+
const logo = () => [
|
|
156
|
+
blank(),
|
|
157
|
+
INDENT + bold(warm("SURON")) + " " + slate("· secrets delivery"),
|
|
158
|
+
].join("\n");
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Final "ready" block shown at end of init/encrypt
|
|
162
|
+
* @param {Array<{label:string, note:string}>} facts
|
|
163
|
+
*/
|
|
164
|
+
const readyBlock = (facts) => {
|
|
165
|
+
const lines = facts.map(f =>
|
|
166
|
+
INDENT + green("·") + " " + label(f.label.padEnd(16)) + slate(f.note)
|
|
167
|
+
);
|
|
168
|
+
lines.push(blank());
|
|
169
|
+
lines.push(INDENT + green("▶") + " " + bold(ok("READY")));
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export default {
|
|
174
|
+
// palette
|
|
175
|
+
amber, warm, slate, green, red, bold, italic, value, label, ok, fail, em,
|
|
176
|
+
// layout
|
|
177
|
+
hr, hrThick, blank, header, kv, step, promptLine, errBlock, okBlock,
|
|
178
|
+
infoBlock, code, diff, logo, readyBlock,
|
|
179
|
+
// constants
|
|
180
|
+
INDENT, COL_W,
|
|
181
|
+
};
|
package/src/utils/config.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { homedir } from "os";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
4
|
-
import
|
|
5
|
-
import c from "./colors.js";
|
|
4
|
+
import ui from "./colors.js";
|
|
6
5
|
|
|
7
6
|
const CONFIG_PATH = join(homedir(), ".suron-config");
|
|
8
7
|
|
|
9
|
-
/**
|
|
10
|
-
* @returns {{ apiUrl?: string }}
|
|
11
|
-
*/
|
|
12
8
|
export function readConfig() {
|
|
13
9
|
if (!existsSync(CONFIG_PATH)) return {};
|
|
14
10
|
try {
|
|
@@ -19,7 +15,7 @@ export function readConfig() {
|
|
|
19
15
|
const key = line.slice(0, eq).trim();
|
|
20
16
|
const val = line.slice(eq + 1).trim();
|
|
21
17
|
if (!val) continue;
|
|
22
|
-
if (key === "
|
|
18
|
+
if (key === "SURON_TOKEN") config.token = val;
|
|
23
19
|
}
|
|
24
20
|
return config;
|
|
25
21
|
} catch {
|
|
@@ -27,44 +23,28 @@ export function readConfig() {
|
|
|
27
23
|
}
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
/**
|
|
31
|
-
* @param {{ apiUrl?: string }} values
|
|
32
|
-
*/
|
|
33
26
|
export function saveConfig(values) {
|
|
34
27
|
const merged = { ...readConfig(), ...values };
|
|
35
28
|
const lines = [];
|
|
36
|
-
if (merged.
|
|
29
|
+
if (merged.token) lines.push(`SURON_TOKEN=${merged.token}`);
|
|
37
30
|
writeFileSync(CONFIG_PATH, lines.join("\n") + "\n", { mode: 0o600 });
|
|
38
31
|
}
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Strip trailing slash from both sources so callers always get a clean URL.
|
|
43
|
-
const fromEnv = process.env.SURON_API_URL?.replace(/\/$/, "");
|
|
44
|
-
const fromConfig = readConfig().apiUrl?.replace(/\/$/, "");
|
|
45
|
-
return fromEnv ?? fromConfig ?? null;
|
|
33
|
+
export function getToken() {
|
|
34
|
+
return readConfig().token ?? null;
|
|
46
35
|
}
|
|
47
36
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
console.error(
|
|
37
|
+
export function requireToken() {
|
|
38
|
+
const token = getToken();
|
|
39
|
+
if (!token) {
|
|
40
|
+
console.error(ui.blank());
|
|
41
|
+
console.error(ui.errBlock("not logged in", "run: suron login"));
|
|
42
|
+
console.error(ui.blank());
|
|
53
43
|
process.exit(1);
|
|
54
44
|
}
|
|
55
|
-
return
|
|
45
|
+
return token;
|
|
56
46
|
}
|
|
57
47
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* @returns {Promise<string>}
|
|
61
|
-
*/
|
|
62
|
-
export function prompt(question) {
|
|
63
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
64
|
-
return new Promise((resolve) => {
|
|
65
|
-
rl.question(question, (answer) => {
|
|
66
|
-
rl.close();
|
|
67
|
-
resolve(answer.trim());
|
|
68
|
-
});
|
|
69
|
-
});
|
|
48
|
+
export function clearToken() {
|
|
49
|
+
writeFileSync(CONFIG_PATH, "", { mode: 0o600 });
|
|
70
50
|
}
|