onedeploy-cli 0.1.8 → 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 +60 -25
- 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,27 +101,25 @@ 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`,
|
|
75
108
|
]);
|
|
109
|
+
const cert = JSON.parse(stdout);
|
|
76
110
|
// in case our local time isn't correct, wait for the certificate to become valid
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
throw new Error("
|
|
111
|
+
const validFromDate = new Date(cert.details.notBefore);
|
|
112
|
+
if (validFromDate.toString() === "Invalid Date") {
|
|
113
|
+
throw new Error("Nebula certificate has invalid 'notBefore' date");
|
|
80
114
|
}
|
|
81
|
-
const validFromDate = new Date(validFrom[1]);
|
|
82
115
|
if (validFromDate > new Date()) {
|
|
83
116
|
const waitTime = validFromDate.getTime() - Date.now();
|
|
84
117
|
console.log(`Your system clock is running slow! Waiting ${Math.ceil(waitTime / 1000)} seconds for Nebula certificate to become valid...`);
|
|
85
118
|
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
86
119
|
}
|
|
87
|
-
process.stdout.write(stdout);
|
|
88
120
|
};
|
|
89
121
|
export let isNebulaRunning = false;
|
|
90
122
|
const getSession = (instance) => {
|
|
91
|
-
const verificationCode = randomBytes(8).toString("base64url");
|
|
92
123
|
return new Promise((resolve) => {
|
|
93
124
|
const getServerAddr = () => {
|
|
94
125
|
const addr = server.address();
|
|
@@ -106,8 +137,10 @@ const getSession = (instance) => {
|
|
|
106
137
|
throw new Error("Missing token in callback URL");
|
|
107
138
|
}
|
|
108
139
|
// authentication complete
|
|
109
|
-
res.writeHead(200
|
|
110
|
-
|
|
140
|
+
res.writeHead(200, "OK", {
|
|
141
|
+
"content-type": "text/html",
|
|
142
|
+
});
|
|
143
|
+
res.write(successPage);
|
|
111
144
|
res.end();
|
|
112
145
|
server.close();
|
|
113
146
|
resolve(token);
|
|
@@ -115,17 +148,19 @@ const getSession = (instance) => {
|
|
|
115
148
|
else {
|
|
116
149
|
const target = `${getServerAddr()}/authenticated`;
|
|
117
150
|
res.writeHead(302, {
|
|
118
|
-
location: `${instance.url}/auth/nebula?redirect=${encodeURIComponent(target)}
|
|
151
|
+
location: `${instance.url}/auth/nebula?redirect=${encodeURIComponent(target)}`,
|
|
119
152
|
});
|
|
120
153
|
res.end();
|
|
121
154
|
}
|
|
122
155
|
});
|
|
123
|
-
server.on("listening", () => {
|
|
156
|
+
server.on("listening", async () => {
|
|
124
157
|
const addr = server.address();
|
|
125
158
|
if (addr === null || typeof addr === "string") {
|
|
126
159
|
throw new Error("Unexpected address type");
|
|
127
160
|
}
|
|
128
|
-
|
|
161
|
+
const authUrl = getServerAddr();
|
|
162
|
+
console.log(`Visit ${authUrl} to authenticate (opened in browser)`);
|
|
163
|
+
await openBrowser(authUrl);
|
|
129
164
|
});
|
|
130
165
|
server.listen();
|
|
131
166
|
});
|
|
@@ -134,7 +169,7 @@ export const connectNebula = async (instance) => {
|
|
|
134
169
|
const session = await getSession(instance);
|
|
135
170
|
await getNebulaCert(instance, session);
|
|
136
171
|
isNebulaRunning = true;
|
|
137
|
-
const nebula = spawn(`${
|
|
172
|
+
const nebula = spawn(`${nebulaDir}/nebula`, ["-config", `${tmpDir}/nebula.yml`], { stdio: "inherit" });
|
|
138
173
|
const interval = setInterval(async () => {
|
|
139
174
|
// refresh Nebula cert every 10 minutes (lifetime is 15 minutes)
|
|
140
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
|
|