onedeploy-cli 0.1.9 → 0.1.10
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/README.md +6 -7
- package/dist/browser.js +46 -0
- package/dist/config.js +2 -10
- package/dist/index.js +62 -45
- package/dist/nebula.js +56 -20
- package/dist/success.js +201 -0
- package/package.json +4 -5
- package/src/index.ts +73 -43
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# oneDeploy CLI
|
|
2
2
|
|
|
3
|
-
This CLI allows you to connect to a
|
|
3
|
+
This CLI allows you to connect to a oneDeploy cluster via
|
|
4
4
|
[Nebula](https://github.com/slackhq/nebula).
|
|
5
5
|
|
|
6
6
|
## Getting Started
|
|
@@ -17,12 +17,11 @@ or from an administrator prompt on Windows:
|
|
|
17
17
|
npx onedeploy-cli
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Then, you need to create an API key on your
|
|
21
|
-
`
|
|
22
|
-
|
|
23
|
-
per computer (for the root user), not individually per user.
|
|
20
|
+
Then, you need to create an API key on your oneDeploy dashboard. You can
|
|
21
|
+
`Enter new URL` to enter your oneDeploy URL. After the first connection, this
|
|
22
|
+
URL will be remembered. You will need to authenticate using your passkey.
|
|
24
23
|
|
|
25
24
|
## License
|
|
26
25
|
|
|
27
|
-
The
|
|
26
|
+
The oneDeploy CLI licensed under MIT, see [here](./LICENSE). It uses the Nebula
|
|
28
27
|
overlay network, which is also MIT licensed.
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
export const openBrowser = async (url) => {
|
|
4
|
+
try {
|
|
5
|
+
// If running as root via sudo, run browser as the original user
|
|
6
|
+
const isRoot = process.getuid?.() === 0;
|
|
7
|
+
const sudoUser = process.env.SUDO_USER;
|
|
8
|
+
switch (process.platform) {
|
|
9
|
+
case "linux": {
|
|
10
|
+
if (isRoot && sudoUser) {
|
|
11
|
+
// Run as the original user to avoid browser sandbox issues
|
|
12
|
+
await promisify(execFile)("sudo", [
|
|
13
|
+
"-u",
|
|
14
|
+
sudoUser,
|
|
15
|
+
"xdg-open",
|
|
16
|
+
url,
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
await promisify(execFile)("xdg-open", [url]);
|
|
21
|
+
}
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
case "darwin": {
|
|
25
|
+
if (isRoot && sudoUser) {
|
|
26
|
+
// Run as the original user to avoid browser issues
|
|
27
|
+
await promisify(execFile)("sudo", ["-u", sudoUser, "open", url]);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
await promisify(execFile)("open", [url]);
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case "win32":
|
|
35
|
+
await promisify(execFile)("cmd", ["/c", "start", '""', url]);
|
|
36
|
+
break;
|
|
37
|
+
default:
|
|
38
|
+
console.error(`Unsupported platform: ${process.platform}.`);
|
|
39
|
+
console.error("Please open the URL manually.");
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
console.error("Could not open browser automatically. Please open the URL manually.");
|
|
45
|
+
}
|
|
46
|
+
};
|
package/dist/config.js
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
|
|
4
|
-
const configDir = `${homedir()}/.config/onedeploy`;
|
|
3
|
+
export const configDir = `${homedir()}/.config/onedeploy`;
|
|
5
4
|
await mkdir(configDir, { recursive: true });
|
|
6
|
-
const Instance = y.object({
|
|
7
|
-
url: y.string(),
|
|
8
|
-
});
|
|
9
|
-
const Config = y.object({
|
|
10
|
-
instances: Instance.array(),
|
|
11
|
-
});
|
|
12
5
|
export const loadConfig = async () => {
|
|
13
6
|
try {
|
|
14
7
|
const result = await readFile(`${configDir}/config.json`, "utf-8");
|
|
15
|
-
|
|
16
|
-
return Config.parse(config);
|
|
8
|
+
return JSON.parse(result);
|
|
17
9
|
}
|
|
18
10
|
catch (_error) {
|
|
19
11
|
// no config yet
|
package/dist/index.js
CHANGED
|
@@ -7,21 +7,33 @@ if (process.argv.includes("--version")) {
|
|
|
7
7
|
process.exit();
|
|
8
8
|
}
|
|
9
9
|
const config = await loadConfig();
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (
|
|
10
|
+
const NEW_URL_VALUE = "__NEW_URL__";
|
|
11
|
+
const normalizeUrl = (url) => {
|
|
12
|
+
const trimmedUrl = url.trim();
|
|
13
|
+
// Convert http:// to https://
|
|
14
|
+
if (trimmedUrl.startsWith("http://")) {
|
|
15
|
+
return trimmedUrl.replace("http://", "https://");
|
|
16
|
+
}
|
|
17
|
+
// Add https:// if no protocol specified
|
|
18
|
+
if (!trimmedUrl.startsWith("https://")) {
|
|
19
|
+
return `https://${trimmedUrl}`;
|
|
20
|
+
}
|
|
21
|
+
return trimmedUrl;
|
|
22
|
+
};
|
|
23
|
+
const addInstance = async (url) => {
|
|
24
|
+
// Check for duplicates
|
|
25
|
+
if (config.instances.some((instance) => instance.url === url)) {
|
|
19
26
|
return;
|
|
20
27
|
}
|
|
21
|
-
config.instances.push({ url
|
|
28
|
+
config.instances.push({ url });
|
|
22
29
|
await saveConfig(config);
|
|
23
30
|
};
|
|
31
|
+
const REMOVE_URL_VALUE = "__REMOVE_URL__";
|
|
24
32
|
const removeConnection = async () => {
|
|
33
|
+
if (config.instances.length === 0) {
|
|
34
|
+
console.log("No connections to remove.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
25
37
|
const { connection } = await prompts([
|
|
26
38
|
{
|
|
27
39
|
type: "select",
|
|
@@ -50,53 +62,58 @@ const removeConnection = async () => {
|
|
|
50
62
|
}
|
|
51
63
|
};
|
|
52
64
|
const mainMenu = async () => {
|
|
53
|
-
const
|
|
65
|
+
const choices = [
|
|
66
|
+
...config.instances.map((instance) => ({
|
|
67
|
+
title: instance.url,
|
|
68
|
+
value: instance.url,
|
|
69
|
+
})),
|
|
70
|
+
{
|
|
71
|
+
title: "Enter new URL",
|
|
72
|
+
value: NEW_URL_VALUE,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
title: "Remove a URL",
|
|
76
|
+
value: REMOVE_URL_VALUE,
|
|
77
|
+
disabled: config.instances.length === 0,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
const { selection } = await prompts([
|
|
54
81
|
{
|
|
55
82
|
type: "select",
|
|
56
|
-
name: "
|
|
57
|
-
message: "
|
|
58
|
-
choices
|
|
59
|
-
{
|
|
60
|
-
title: "Connect",
|
|
61
|
-
value: "connect",
|
|
62
|
-
disabled: config.instances.length === 0,
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
title: "Setup new connection",
|
|
66
|
-
value: "setup",
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
title: "Remove connection",
|
|
70
|
-
value: "remove",
|
|
71
|
-
},
|
|
72
|
-
],
|
|
83
|
+
name: "selection",
|
|
84
|
+
message: "Select oneDeploy instance",
|
|
85
|
+
choices,
|
|
73
86
|
},
|
|
74
87
|
]);
|
|
75
|
-
if (
|
|
88
|
+
if (selection === undefined) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (selection === REMOVE_URL_VALUE) {
|
|
92
|
+
await removeConnection();
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
let urlToConnect;
|
|
96
|
+
if (selection === NEW_URL_VALUE) {
|
|
76
97
|
const result = await prompts([
|
|
77
98
|
{
|
|
78
|
-
type: "
|
|
79
|
-
name: "
|
|
80
|
-
message: "
|
|
81
|
-
choices: config.instances.map((instance) => ({
|
|
82
|
-
title: instance.url,
|
|
83
|
-
value: instance,
|
|
84
|
-
})),
|
|
99
|
+
type: "text",
|
|
100
|
+
name: "url",
|
|
101
|
+
message: "Enter oneDeploy URL",
|
|
85
102
|
},
|
|
86
103
|
]);
|
|
87
|
-
if (result.
|
|
88
|
-
|
|
104
|
+
if (result.url === undefined) {
|
|
105
|
+
return true;
|
|
89
106
|
}
|
|
90
|
-
|
|
91
|
-
else if (action === "setup") {
|
|
92
|
-
await setupConnection();
|
|
93
|
-
}
|
|
94
|
-
else if (action === "remove") {
|
|
95
|
-
await removeConnection();
|
|
107
|
+
urlToConnect = normalizeUrl(result.url);
|
|
96
108
|
}
|
|
97
109
|
else {
|
|
98
|
-
|
|
110
|
+
urlToConnect = selection;
|
|
99
111
|
}
|
|
112
|
+
// Connect first
|
|
113
|
+
const instance = { url: urlToConnect, apiKey: "" };
|
|
114
|
+
await connectNebula(instance);
|
|
115
|
+
// Only save if connection was successful (didn't throw)
|
|
116
|
+
await addInstance(urlToConnect);
|
|
100
117
|
return true;
|
|
101
118
|
};
|
|
102
119
|
process.on("SIGINT", () => {
|
package/dist/nebula.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { chmod, mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { access, chmod, mkdir, readFile, rm, writeFile, } from "node:fs/promises";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import { parse } from "yaml";
|
|
10
|
+
import { openBrowser } from "./browser.js";
|
|
11
|
+
import { configDir } from "./config.js";
|
|
12
|
+
import { successPage } from "./success.js";
|
|
10
13
|
const getNebulaPackageName = () => {
|
|
11
14
|
switch (process.platform) {
|
|
12
15
|
case "linux":
|
|
@@ -33,22 +36,52 @@ const getNebulaPackageName = () => {
|
|
|
33
36
|
const nebulaVersion = "v1.10.0";
|
|
34
37
|
const packageName = getNebulaPackageName();
|
|
35
38
|
const nebulaUrl = `https://github.com/slackhq/nebula/releases/download/${nebulaVersion}/${packageName}`;
|
|
39
|
+
const nebulaDir = path.join(configDir, "nebula");
|
|
36
40
|
const tmpDir = path.join(tmpdir(), `onedeploy-${randomUUID()}`);
|
|
37
41
|
await mkdir(tmpDir, { recursive: true });
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
const isCacheValid = async () => {
|
|
43
|
+
try {
|
|
44
|
+
const versionFile = path.join(nebulaDir, "version");
|
|
45
|
+
const cachedVersion = await readFile(versionFile, "utf-8");
|
|
46
|
+
if (cachedVersion.trim() !== nebulaVersion) {
|
|
47
|
+
// Version mismatch - delete old version
|
|
48
|
+
await rm(nebulaDir, { recursive: true, force: true });
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
// Check if binaries exist
|
|
52
|
+
await access(path.join(nebulaDir, "nebula"));
|
|
53
|
+
await access(path.join(nebulaDir, "nebula-cert"));
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// If any error occurs (file doesn't exist, etc.), invalidate cache
|
|
58
|
+
await rm(nebulaDir, { recursive: true, force: true });
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const downloadNebula = async () => {
|
|
63
|
+
console.log(`Downloading Nebula ${nebulaVersion}...`);
|
|
40
64
|
const file = await fetch(nebulaUrl);
|
|
41
65
|
if (file.body === null) {
|
|
42
66
|
throw new Error("Could not download Nebula executable.");
|
|
43
67
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
const downloadPath = path.join(tmpDir, packageName);
|
|
69
|
+
await writeFile(downloadPath, Readable.from(file.body));
|
|
70
|
+
console.log(`Extracting Nebula...`);
|
|
71
|
+
// Extract directly to nebulaDir
|
|
72
|
+
await mkdir(nebulaDir, { recursive: true });
|
|
73
|
+
await promisify(execFile)("tar", ["-xf", downloadPath, "-C", nebulaDir]);
|
|
74
|
+
// Store version info
|
|
75
|
+
await writeFile(path.join(nebulaDir, "version"), nebulaVersion);
|
|
76
|
+
// Clean up downloaded archive
|
|
77
|
+
await rm(downloadPath, { force: true });
|
|
78
|
+
console.log(`Stored Nebula binaries to ${nebulaDir}`);
|
|
79
|
+
};
|
|
80
|
+
export const installNebula = async () => {
|
|
81
|
+
const cached = await isCacheValid();
|
|
82
|
+
if (!cached) {
|
|
83
|
+
await downloadNebula();
|
|
84
|
+
}
|
|
52
85
|
};
|
|
53
86
|
export const cleanupNebula = async () => {
|
|
54
87
|
await rm(tmpDir, { recursive: true, force: true });
|
|
@@ -68,7 +101,7 @@ export const getNebulaCert = async (instance, session) => {
|
|
|
68
101
|
console.log(`Wrote Nebula configuration to ${tmpDir}/nebula.yml`);
|
|
69
102
|
const parsedConfig = parse(nebulaConfig);
|
|
70
103
|
await writeFile(`${tmpDir}/nebula-debug.crt`, parsedConfig.pki.cert);
|
|
71
|
-
const { stdout } = await promisify(execFile)(`${
|
|
104
|
+
const { stdout } = await promisify(execFile)(`${nebulaDir}/nebula-cert`, [
|
|
72
105
|
"print",
|
|
73
106
|
"-path",
|
|
74
107
|
`${tmpDir}/nebula-debug.crt`,
|
|
@@ -87,7 +120,6 @@ export const getNebulaCert = async (instance, session) => {
|
|
|
87
120
|
};
|
|
88
121
|
export let isNebulaRunning = false;
|
|
89
122
|
const getSession = (instance) => {
|
|
90
|
-
const verificationCode = randomBytes(8).toString("base64url");
|
|
91
123
|
return new Promise((resolve) => {
|
|
92
124
|
const getServerAddr = () => {
|
|
93
125
|
const addr = server.address();
|
|
@@ -105,8 +137,10 @@ const getSession = (instance) => {
|
|
|
105
137
|
throw new Error("Missing token in callback URL");
|
|
106
138
|
}
|
|
107
139
|
// authentication complete
|
|
108
|
-
res.writeHead(200
|
|
109
|
-
|
|
140
|
+
res.writeHead(200, "OK", {
|
|
141
|
+
"content-type": "text/html",
|
|
142
|
+
});
|
|
143
|
+
res.write(successPage);
|
|
110
144
|
res.end();
|
|
111
145
|
server.close();
|
|
112
146
|
resolve(token);
|
|
@@ -114,17 +148,19 @@ const getSession = (instance) => {
|
|
|
114
148
|
else {
|
|
115
149
|
const target = `${getServerAddr()}/authenticated`;
|
|
116
150
|
res.writeHead(302, {
|
|
117
|
-
location: `${instance.url}/auth/nebula?redirect=${encodeURIComponent(target)}
|
|
151
|
+
location: `${instance.url}/auth/nebula?redirect=${encodeURIComponent(target)}`,
|
|
118
152
|
});
|
|
119
153
|
res.end();
|
|
120
154
|
}
|
|
121
155
|
});
|
|
122
|
-
server.on("listening", () => {
|
|
156
|
+
server.on("listening", async () => {
|
|
123
157
|
const addr = server.address();
|
|
124
158
|
if (addr === null || typeof addr === "string") {
|
|
125
159
|
throw new Error("Unexpected address type");
|
|
126
160
|
}
|
|
127
|
-
|
|
161
|
+
const authUrl = getServerAddr();
|
|
162
|
+
console.log(`Visit ${authUrl} to authenticate (opened in browser)`);
|
|
163
|
+
await openBrowser(authUrl);
|
|
128
164
|
});
|
|
129
165
|
server.listen();
|
|
130
166
|
});
|
|
@@ -133,7 +169,7 @@ export const connectNebula = async (instance) => {
|
|
|
133
169
|
const session = await getSession(instance);
|
|
134
170
|
await getNebulaCert(instance, session);
|
|
135
171
|
isNebulaRunning = true;
|
|
136
|
-
const nebula = spawn(`${
|
|
172
|
+
const nebula = spawn(`${nebulaDir}/nebula`, ["-config", `${tmpDir}/nebula.yml`], { stdio: "inherit" });
|
|
137
173
|
const interval = setInterval(async () => {
|
|
138
174
|
// refresh Nebula cert every 10 minutes (lifetime is 15 minutes)
|
|
139
175
|
await getNebulaCert(instance, session);
|
package/dist/success.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
export const successPage = `<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>oneDeploy</title>
|
|
8
|
+
<link rel="icon" type="image/svg+xml"
|
|
9
|
+
href="data:image/svg+xml,%3Csvg width='48' height='48' viewBox='0 0 48 48' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='14' cy='27' r='14' fill='%2322C55E'/%3E%3Ccircle cx='38' cy='10' r='10' fill='%234ADE80'/%3E%3Ccircle cx='40' cy='40' r='8' fill='%2316A34A'/%3E%3Cline x1='26' y1='19' x2='30' y2='14' stroke='%2386EFAC' stroke-width='3' stroke-linecap='round'/%3E%3Cline x1='26' y1='36' x2='34' y2='38' stroke='%2386EFAC' stroke-width='3' stroke-linecap='round'/%3E%3Cpath d='M9 20V34L21 27L9 20Z' fill='white'/%3E%3C/svg%3E">
|
|
10
|
+
<style>
|
|
11
|
+
* {
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 0;
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.page-container {
|
|
24
|
+
min-height: 100vh;
|
|
25
|
+
background-color: #0b0b0f;
|
|
26
|
+
color: #f3f4f6;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.navbar {
|
|
30
|
+
padding: 1rem;
|
|
31
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
32
|
+
background-color: #0e0e14;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.logo-container {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: 0.75rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.logo-container img {
|
|
42
|
+
width: 2.5rem;
|
|
43
|
+
height: 2.5rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.logo-text {
|
|
47
|
+
font-size: 1.5rem;
|
|
48
|
+
font-weight: 700;
|
|
49
|
+
letter-spacing: -0.025em;
|
|
50
|
+
line-height: 1.2;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@media (min-width: 768px) {
|
|
54
|
+
.logo-text {
|
|
55
|
+
font-size: 1.875rem;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.logo-one {
|
|
60
|
+
color: #4ade80;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.logo-deploy {
|
|
64
|
+
color: #f3f4f6;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.content-wrapper {
|
|
68
|
+
padding: 1rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.container {
|
|
72
|
+
min-height: 80vh;
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
padding-left: 1rem;
|
|
77
|
+
padding-right: 1rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.card {
|
|
81
|
+
width: 100%;
|
|
82
|
+
max-width: 28rem;
|
|
83
|
+
background-color: #14141c;
|
|
84
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
85
|
+
border-radius: 1rem;
|
|
86
|
+
padding: 2rem;
|
|
87
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.header {
|
|
91
|
+
text-align: center;
|
|
92
|
+
margin-bottom: 2rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.icon-container {
|
|
96
|
+
display: inline-flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
width: 4rem;
|
|
100
|
+
height: 4rem;
|
|
101
|
+
border-radius: 1rem;
|
|
102
|
+
background-color: rgba(139, 92, 246, 0.1);
|
|
103
|
+
color: #a78bfa;
|
|
104
|
+
margin-bottom: 1.5rem;
|
|
105
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.icon-container svg {
|
|
109
|
+
width: 2rem;
|
|
110
|
+
height: 2rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
h1 {
|
|
114
|
+
font-size: 1.875rem;
|
|
115
|
+
font-weight: 700;
|
|
116
|
+
color: #f3f4f6;
|
|
117
|
+
letter-spacing: -0.025em;
|
|
118
|
+
margin: 0 0 0.5rem 0;
|
|
119
|
+
line-height: 2.25rem;
|
|
120
|
+
text-align: center;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.description {
|
|
124
|
+
color: #9ca3af;
|
|
125
|
+
font-size: 1rem;
|
|
126
|
+
line-height: 1.5rem;
|
|
127
|
+
text-align: center;
|
|
128
|
+
margin: 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.status-container {
|
|
132
|
+
min-height: 60px;
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: center;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.success-box {
|
|
139
|
+
width: 100%;
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
justify-content: center;
|
|
143
|
+
padding-top: 0.75rem;
|
|
144
|
+
padding-bottom: 0.75rem;
|
|
145
|
+
font-size: 1rem;
|
|
146
|
+
font-weight: 500;
|
|
147
|
+
background-color: rgba(34, 197, 94, 0.1);
|
|
148
|
+
border: 1px solid rgba(34, 197, 94, 0.2);
|
|
149
|
+
border-radius: 0.5rem;
|
|
150
|
+
color: #4ade80;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.success-box svg {
|
|
154
|
+
width: 1.25rem;
|
|
155
|
+
height: 1.25rem;
|
|
156
|
+
}
|
|
157
|
+
</style>
|
|
158
|
+
</head>
|
|
159
|
+
|
|
160
|
+
<body>
|
|
161
|
+
<div class="page-container">
|
|
162
|
+
<div class="navbar">
|
|
163
|
+
<div class="logo-container">
|
|
164
|
+
<img alt="oneDeploy Logo"
|
|
165
|
+
src="data:image/svg+xml,%3Csvg width='48' height='48' viewBox='0 0 48 48' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='14' cy='27' r='14' fill='%2322C55E'/%3E%3Ccircle cx='38' cy='10' r='10' fill='%234ADE80'/%3E%3Ccircle cx='40' cy='40' r='8' fill='%2316A34A'/%3E%3Cline x1='26' y1='19' x2='30' y2='14' stroke='%2386EFAC' stroke-width='3' stroke-linecap='round'/%3E%3Cline x1='26' y1='36' x2='34' y2='38' stroke='%2386EFAC' stroke-width='3' stroke-linecap='round'/%3E%3Cpath d='M9 20V34L21 27L9 20Z' fill='white'/%3E%3C/svg%3E">
|
|
166
|
+
<div class="logo-text">
|
|
167
|
+
<span class="logo-one">one</span><span class="logo-deploy">Deploy</span>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="content-wrapper">
|
|
172
|
+
<div class="container">
|
|
173
|
+
<div class="card">
|
|
174
|
+
<div class="header">
|
|
175
|
+
<div class="icon-container">
|
|
176
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
|
|
177
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
178
|
+
<circle cx="12" cy="12" r="10" />
|
|
179
|
+
<line x1="2" y1="12" x2="22" y2="12" />
|
|
180
|
+
<path
|
|
181
|
+
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
|
182
|
+
</svg>
|
|
183
|
+
</div>
|
|
184
|
+
<h1>Network Access Request</h1>
|
|
185
|
+
<p class="description">oneDeploy CLI is requesting access to the oneDeploy network</p>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="status-container">
|
|
189
|
+
<div class="success-box">
|
|
190
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
|
|
191
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
192
|
+
<path d="M20 6 9 17l-5-5" />
|
|
193
|
+
</svg>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</body>
|
|
201
|
+
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onedeploy-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "CLI tool for connecting to OneDeploy via Nebula",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"onedeploy",
|
|
@@ -32,14 +32,13 @@
|
|
|
32
32
|
"fix": "biome check src/* --fix"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@biomejs/biome": "^2.3.
|
|
36
|
-
"@types/node": "^25.0.
|
|
35
|
+
"@biomejs/biome": "^2.3.10",
|
|
36
|
+
"@types/node": "^25.0.3",
|
|
37
37
|
"@types/prompts": "^2.4.9",
|
|
38
38
|
"typescript": "^5.9.3"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"prompts": "^2.4.2",
|
|
42
|
-
"yaml": "^2.8.2"
|
|
43
|
-
"yedra": "^0.19.0"
|
|
42
|
+
"yaml": "^2.8.2"
|
|
44
43
|
}
|
|
45
44
|
}
|
package/src/index.ts
CHANGED
|
@@ -15,22 +15,37 @@ if (process.argv.includes("--version")) {
|
|
|
15
15
|
|
|
16
16
|
const config = await loadConfig();
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if
|
|
18
|
+
const NEW_URL_VALUE = "__NEW_URL__";
|
|
19
|
+
|
|
20
|
+
const normalizeUrl = (url: string): string => {
|
|
21
|
+
const trimmedUrl = url.trim();
|
|
22
|
+
// Convert http:// to https://
|
|
23
|
+
if (trimmedUrl.startsWith("http://")) {
|
|
24
|
+
return trimmedUrl.replace("http://", "https://");
|
|
25
|
+
}
|
|
26
|
+
// Add https:// if no protocol specified
|
|
27
|
+
if (!trimmedUrl.startsWith("https://")) {
|
|
28
|
+
return `https://${trimmedUrl}`;
|
|
29
|
+
}
|
|
30
|
+
return trimmedUrl;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const addInstance = async (url: string) => {
|
|
34
|
+
// Check for duplicates
|
|
35
|
+
if (config.instances.some((instance) => instance.url === url)) {
|
|
27
36
|
return;
|
|
28
37
|
}
|
|
29
|
-
config.instances.push({ url
|
|
38
|
+
config.instances.push({ url });
|
|
30
39
|
await saveConfig(config);
|
|
31
40
|
};
|
|
32
41
|
|
|
42
|
+
const REMOVE_URL_VALUE = "__REMOVE_URL__";
|
|
43
|
+
|
|
33
44
|
const removeConnection = async () => {
|
|
45
|
+
if (config.instances.length === 0) {
|
|
46
|
+
console.log("No connections to remove.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
34
49
|
const { connection } = await prompts([
|
|
35
50
|
{
|
|
36
51
|
type: "select",
|
|
@@ -60,50 +75,65 @@ const removeConnection = async () => {
|
|
|
60
75
|
};
|
|
61
76
|
|
|
62
77
|
const mainMenu = async (): Promise<boolean> => {
|
|
63
|
-
const
|
|
78
|
+
const choices = [
|
|
79
|
+
...config.instances.map((instance) => ({
|
|
80
|
+
title: instance.url,
|
|
81
|
+
value: instance.url,
|
|
82
|
+
})),
|
|
83
|
+
{
|
|
84
|
+
title: "Enter new URL",
|
|
85
|
+
value: NEW_URL_VALUE,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
title: "Remove a URL",
|
|
89
|
+
value: REMOVE_URL_VALUE,
|
|
90
|
+
disabled: config.instances.length === 0,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const { selection } = await prompts([
|
|
64
95
|
{
|
|
65
96
|
type: "select",
|
|
66
|
-
name: "
|
|
67
|
-
message: "
|
|
68
|
-
choices
|
|
69
|
-
{
|
|
70
|
-
title: "Connect",
|
|
71
|
-
value: "connect",
|
|
72
|
-
disabled: config.instances.length === 0,
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
title: "Setup new connection",
|
|
76
|
-
value: "setup",
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
title: "Remove connection",
|
|
80
|
-
value: "remove",
|
|
81
|
-
},
|
|
82
|
-
],
|
|
97
|
+
name: "selection",
|
|
98
|
+
message: "Select oneDeploy instance",
|
|
99
|
+
choices,
|
|
83
100
|
},
|
|
84
101
|
]);
|
|
85
|
-
|
|
102
|
+
|
|
103
|
+
if (selection === undefined) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (selection === REMOVE_URL_VALUE) {
|
|
108
|
+
await removeConnection();
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let urlToConnect: string;
|
|
113
|
+
|
|
114
|
+
if (selection === NEW_URL_VALUE) {
|
|
86
115
|
const result = await prompts([
|
|
87
116
|
{
|
|
88
|
-
type: "
|
|
89
|
-
name: "
|
|
90
|
-
message: "
|
|
91
|
-
choices: config.instances.map((instance) => ({
|
|
92
|
-
title: instance.url,
|
|
93
|
-
value: instance,
|
|
94
|
-
})),
|
|
117
|
+
type: "text",
|
|
118
|
+
name: "url",
|
|
119
|
+
message: "Enter oneDeploy URL",
|
|
95
120
|
},
|
|
96
121
|
]);
|
|
97
|
-
if (result.
|
|
98
|
-
|
|
122
|
+
if (result.url === undefined) {
|
|
123
|
+
return true;
|
|
99
124
|
}
|
|
100
|
-
|
|
101
|
-
await setupConnection();
|
|
102
|
-
} else if (action === "remove") {
|
|
103
|
-
await removeConnection();
|
|
125
|
+
urlToConnect = normalizeUrl(result.url);
|
|
104
126
|
} else {
|
|
105
|
-
|
|
127
|
+
urlToConnect = selection;
|
|
106
128
|
}
|
|
129
|
+
|
|
130
|
+
// Connect first
|
|
131
|
+
const instance = { url: urlToConnect, apiKey: "" };
|
|
132
|
+
await connectNebula(instance);
|
|
133
|
+
|
|
134
|
+
// Only save if connection was successful (didn't throw)
|
|
135
|
+
await addInstance(urlToConnect);
|
|
136
|
+
|
|
107
137
|
return true;
|
|
108
138
|
};
|
|
109
139
|
|