@it-club/provisor 0.1.0
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 +317 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2359 -0
- package/dist/cli.js.map +1 -0
- package/package.json +55 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.tsx
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
import { render } from "ink";
|
|
6
|
+
|
|
7
|
+
// src/commands/init.tsx
|
|
8
|
+
import { useState as useState2, useEffect } from "react";
|
|
9
|
+
import { Box as Box4, Text as Text4, useApp } from "ink";
|
|
10
|
+
|
|
11
|
+
// src/components/Task.tsx
|
|
12
|
+
import { Box, Text } from "ink";
|
|
13
|
+
import Spinner from "ink-spinner";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
var statusIcons = {
|
|
16
|
+
pending: /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u25CB" }),
|
|
17
|
+
running: /* @__PURE__ */ jsx(Text, { color: "cyan", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }),
|
|
18
|
+
success: /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713" }),
|
|
19
|
+
error: /* @__PURE__ */ jsx(Text, { color: "red", children: "\u2717" }),
|
|
20
|
+
skipped: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "-" })
|
|
21
|
+
};
|
|
22
|
+
function Task({ label, status, message }) {
|
|
23
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
24
|
+
/* @__PURE__ */ jsx(Box, { width: 3, children: statusIcons[status] }),
|
|
25
|
+
/* @__PURE__ */ jsxs(Text, { color: status === "error" ? "red" : void 0, children: [
|
|
26
|
+
label,
|
|
27
|
+
message && /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
28
|
+
" ",
|
|
29
|
+
message
|
|
30
|
+
] })
|
|
31
|
+
] })
|
|
32
|
+
] });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/components/Header.tsx
|
|
36
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
37
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
38
|
+
function Header({ title, subtitle }) {
|
|
39
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
40
|
+
/* @__PURE__ */ jsxs2(Text2, { bold: true, color: "cyan", children: [
|
|
41
|
+
"\u25C6 ",
|
|
42
|
+
title
|
|
43
|
+
] }),
|
|
44
|
+
subtitle && /* @__PURE__ */ jsx2(Text2, { color: "gray", children: subtitle })
|
|
45
|
+
] });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/components/Confirm.tsx
|
|
49
|
+
import { useState } from "react";
|
|
50
|
+
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
51
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
52
|
+
function Confirm({ message, onConfirm }) {
|
|
53
|
+
const [answered, setAnswered] = useState(false);
|
|
54
|
+
useInput((input, key) => {
|
|
55
|
+
if (answered) return;
|
|
56
|
+
if (input.toLowerCase() === "y" || key.return) {
|
|
57
|
+
setAnswered(true);
|
|
58
|
+
onConfirm(true);
|
|
59
|
+
} else if (input.toLowerCase() === "n" || key.escape) {
|
|
60
|
+
setAnswered(true);
|
|
61
|
+
onConfirm(false);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
if (answered) return null;
|
|
65
|
+
return /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
66
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: message }),
|
|
67
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", children: " (y/n) " })
|
|
68
|
+
] });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/utils/ssh.ts
|
|
72
|
+
import { Client } from "ssh2";
|
|
73
|
+
import { readFileSync, existsSync } from "fs";
|
|
74
|
+
import { homedir } from "os";
|
|
75
|
+
import { join } from "path";
|
|
76
|
+
function findDefaultKey() {
|
|
77
|
+
const sshDir = join(homedir(), ".ssh");
|
|
78
|
+
const keyNames = ["id_ed25519", "id_rsa", "id_ecdsa"];
|
|
79
|
+
for (const name of keyNames) {
|
|
80
|
+
const keyPath = join(sshDir, name);
|
|
81
|
+
if (existsSync(keyPath)) {
|
|
82
|
+
return keyPath;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function createSSHConfig(options) {
|
|
88
|
+
const keyPath = options.key || findDefaultKey();
|
|
89
|
+
if (!keyPath) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
"No SSH key found. Specify with --key or ensure ~/.ssh/id_ed25519 exists"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (!existsSync(keyPath)) {
|
|
95
|
+
throw new Error(`SSH key not found: ${keyPath}`);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
host: options.host,
|
|
99
|
+
port: parseInt(String(options.port || 22), 10),
|
|
100
|
+
username: options.user || "root",
|
|
101
|
+
privateKey: readFileSync(keyPath)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function connect(options) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const client = new Client();
|
|
107
|
+
const config = createSSHConfig(options);
|
|
108
|
+
client.on("ready", () => resolve(client));
|
|
109
|
+
client.on("error", reject);
|
|
110
|
+
client.connect(config);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function exec(client, command) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
client.exec(command, (err, stream) => {
|
|
116
|
+
if (err) {
|
|
117
|
+
reject(err);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
let stdout = "";
|
|
121
|
+
let stderr = "";
|
|
122
|
+
stream.on("data", (data) => {
|
|
123
|
+
stdout += data.toString();
|
|
124
|
+
});
|
|
125
|
+
stream.stderr.on("data", (data) => {
|
|
126
|
+
stderr += data.toString();
|
|
127
|
+
});
|
|
128
|
+
stream.on("close", (code) => {
|
|
129
|
+
resolve({ stdout, stderr, code: code ?? 0 });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function execScript(client, script, useSudo = false) {
|
|
135
|
+
const escapedScript = script.replace(/'/g, "'\\''");
|
|
136
|
+
const command = useSudo ? `sudo bash -c '${escapedScript}'` : `bash -c '${escapedScript}'`;
|
|
137
|
+
return exec(client, command);
|
|
138
|
+
}
|
|
139
|
+
function disconnect(client) {
|
|
140
|
+
client.end();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/commands/init.tsx
|
|
144
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
145
|
+
var SCRIPTS = {
|
|
146
|
+
checkUser: (user) => `id ${user} &>/dev/null && echo "exists" || echo "not_found"`,
|
|
147
|
+
createUser: (user) => `
|
|
148
|
+
adduser --gecos "" --disabled-password ${user}
|
|
149
|
+
usermod -aG sudo ${user}
|
|
150
|
+
echo "${user} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${user}
|
|
151
|
+
chmod 440 /etc/sudoers.d/${user}
|
|
152
|
+
`,
|
|
153
|
+
copyRootKeys: (user) => `
|
|
154
|
+
mkdir -p /home/${user}/.ssh
|
|
155
|
+
cp /root/.ssh/authorized_keys /home/${user}/.ssh/authorized_keys
|
|
156
|
+
chown -R ${user}:${user} /home/${user}/.ssh
|
|
157
|
+
chmod 700 /home/${user}/.ssh
|
|
158
|
+
chmod 600 /home/${user}/.ssh/authorized_keys
|
|
159
|
+
`,
|
|
160
|
+
setupFirewall: () => `
|
|
161
|
+
apt install -y ufw
|
|
162
|
+
ufw allow OpenSSH
|
|
163
|
+
ufw allow 80
|
|
164
|
+
ufw allow 443
|
|
165
|
+
echo "y" | ufw enable
|
|
166
|
+
`,
|
|
167
|
+
hardenSsh: () => `
|
|
168
|
+
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
|
|
169
|
+
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
|
|
170
|
+
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
|
|
171
|
+
systemctl restart ssh || systemctl restart sshd
|
|
172
|
+
`
|
|
173
|
+
};
|
|
174
|
+
function InitCommand(props) {
|
|
175
|
+
const { exit } = useApp();
|
|
176
|
+
const [client, setClient] = useState2(null);
|
|
177
|
+
const [tasks, setTasks] = useState2({
|
|
178
|
+
connect: "pending",
|
|
179
|
+
update: "pending",
|
|
180
|
+
createUser: "pending",
|
|
181
|
+
setupSsh: "pending",
|
|
182
|
+
firewall: "pending",
|
|
183
|
+
hardenSsh: "pending"
|
|
184
|
+
});
|
|
185
|
+
const [error, setError] = useState2(null);
|
|
186
|
+
const [waitingConfirm, setWaitingConfirm] = useState2(false);
|
|
187
|
+
const [summary, setSummary] = useState2([]);
|
|
188
|
+
const updateTask = (task, status) => {
|
|
189
|
+
setTasks((prev) => ({ ...prev, [task]: status }));
|
|
190
|
+
};
|
|
191
|
+
const addSummary = (msg) => {
|
|
192
|
+
setSummary((prev) => [...prev, msg]);
|
|
193
|
+
};
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
const run = async () => {
|
|
196
|
+
updateTask("connect", "running");
|
|
197
|
+
try {
|
|
198
|
+
const sshClient = await connect({ ...props, user: "root" });
|
|
199
|
+
setClient(sshClient);
|
|
200
|
+
updateTask("connect", "success");
|
|
201
|
+
} catch (err) {
|
|
202
|
+
updateTask("connect", "error");
|
|
203
|
+
setError(`Connection failed: ${err instanceof Error ? err.message : err}`);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
run();
|
|
207
|
+
}, []);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (!client || tasks.connect !== "success" || tasks.update !== "pending") return;
|
|
210
|
+
const run = async () => {
|
|
211
|
+
updateTask("update", "running");
|
|
212
|
+
try {
|
|
213
|
+
await exec(client, "apt update && apt upgrade -y");
|
|
214
|
+
await exec(client, "apt install -y curl git");
|
|
215
|
+
updateTask("update", "success");
|
|
216
|
+
} catch (err) {
|
|
217
|
+
updateTask("update", "error");
|
|
218
|
+
setError(`Update failed: ${err instanceof Error ? err.message : err}`);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
run();
|
|
222
|
+
}, [client, tasks.connect]);
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (!client || tasks.update !== "success" || tasks.createUser !== "pending") return;
|
|
225
|
+
const run = async () => {
|
|
226
|
+
updateTask("createUser", "running");
|
|
227
|
+
try {
|
|
228
|
+
const result = await exec(client, SCRIPTS.checkUser(props.user));
|
|
229
|
+
if (result.stdout.trim() === "exists") {
|
|
230
|
+
updateTask("createUser", "skipped");
|
|
231
|
+
addSummary(`User '${props.user}' already exists`);
|
|
232
|
+
} else {
|
|
233
|
+
await execScript(client, SCRIPTS.createUser(props.user));
|
|
234
|
+
updateTask("createUser", "success");
|
|
235
|
+
addSummary(`User '${props.user}' created with sudo access`);
|
|
236
|
+
}
|
|
237
|
+
} catch (err) {
|
|
238
|
+
updateTask("createUser", "error");
|
|
239
|
+
setError(`User creation failed: ${err instanceof Error ? err.message : err}`);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
run();
|
|
243
|
+
}, [client, tasks.update]);
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (!client || !["success", "skipped"].includes(tasks.createUser) || tasks.setupSsh !== "pending") return;
|
|
246
|
+
const run = async () => {
|
|
247
|
+
updateTask("setupSsh", "running");
|
|
248
|
+
try {
|
|
249
|
+
await execScript(client, SCRIPTS.copyRootKeys(props.user));
|
|
250
|
+
updateTask("setupSsh", "success");
|
|
251
|
+
addSummary(`SSH keys copied to '${props.user}'`);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
updateTask("setupSsh", "error");
|
|
254
|
+
setError(`SSH setup failed: ${err instanceof Error ? err.message : err}`);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
run();
|
|
258
|
+
}, [client, tasks.createUser]);
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (!client || tasks.setupSsh !== "success" || tasks.firewall !== "pending") return;
|
|
261
|
+
const run = async () => {
|
|
262
|
+
updateTask("firewall", "running");
|
|
263
|
+
try {
|
|
264
|
+
await execScript(client, SCRIPTS.setupFirewall());
|
|
265
|
+
updateTask("firewall", "success");
|
|
266
|
+
addSummary("Firewall configured (SSH, HTTP, HTTPS)");
|
|
267
|
+
} catch (err) {
|
|
268
|
+
updateTask("firewall", "error");
|
|
269
|
+
setError(`Firewall setup failed: ${err instanceof Error ? err.message : err}`);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
run();
|
|
273
|
+
}, [client, tasks.setupSsh]);
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (tasks.firewall !== "success" || waitingConfirm || tasks.hardenSsh !== "pending") return;
|
|
276
|
+
setWaitingConfirm(true);
|
|
277
|
+
}, [tasks.firewall]);
|
|
278
|
+
const handleConfirm = async (confirmed) => {
|
|
279
|
+
if (!client) return;
|
|
280
|
+
if (!confirmed) {
|
|
281
|
+
updateTask("hardenSsh", "skipped");
|
|
282
|
+
addSummary("SSH hardening skipped (root login still enabled)");
|
|
283
|
+
disconnect(client);
|
|
284
|
+
setTimeout(() => exit(), 100);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
updateTask("hardenSsh", "running");
|
|
288
|
+
try {
|
|
289
|
+
await execScript(client, SCRIPTS.hardenSsh());
|
|
290
|
+
updateTask("hardenSsh", "success");
|
|
291
|
+
addSummary("SSH hardened (root login disabled, password auth disabled)");
|
|
292
|
+
disconnect(client);
|
|
293
|
+
setTimeout(() => exit(), 100);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
updateTask("hardenSsh", "error");
|
|
296
|
+
setError(`SSH hardening failed: ${err instanceof Error ? err.message : err}`);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
if (error && client) {
|
|
301
|
+
disconnect(client);
|
|
302
|
+
setTimeout(() => exit(), 100);
|
|
303
|
+
}
|
|
304
|
+
}, [error]);
|
|
305
|
+
const allDone = tasks.hardenSsh === "success" || tasks.hardenSsh === "skipped";
|
|
306
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
307
|
+
/* @__PURE__ */ jsx4(Header, { title: "Server Initialization", subtitle: `Host: ${props.host}` }),
|
|
308
|
+
/* @__PURE__ */ jsx4(Task, { label: "Connect to server", status: tasks.connect }),
|
|
309
|
+
/* @__PURE__ */ jsx4(Task, { label: "Update system packages", status: tasks.update }),
|
|
310
|
+
/* @__PURE__ */ jsx4(Task, { label: `Create user '${props.user}'`, status: tasks.createUser }),
|
|
311
|
+
/* @__PURE__ */ jsx4(Task, { label: "Setup SSH keys", status: tasks.setupSsh }),
|
|
312
|
+
/* @__PURE__ */ jsx4(Task, { label: "Configure firewall", status: tasks.firewall }),
|
|
313
|
+
/* @__PURE__ */ jsx4(Task, { label: "Harden SSH", status: tasks.hardenSsh }),
|
|
314
|
+
waitingConfirm && tasks.hardenSsh === "pending" && /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
|
|
315
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "yellow", bold: true, children: [
|
|
316
|
+
"\u26A0 Before proceeding, verify SSH access as '",
|
|
317
|
+
props.user,
|
|
318
|
+
"':"
|
|
319
|
+
] }),
|
|
320
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "gray", children: [
|
|
321
|
+
" ssh ",
|
|
322
|
+
props.port !== "22" ? `-p ${props.port} ` : "",
|
|
323
|
+
props.user,
|
|
324
|
+
"@",
|
|
325
|
+
props.host
|
|
326
|
+
] }),
|
|
327
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(
|
|
328
|
+
Confirm,
|
|
329
|
+
{
|
|
330
|
+
message: "Have you verified SSH access works?",
|
|
331
|
+
onConfirm: handleConfirm
|
|
332
|
+
}
|
|
333
|
+
) })
|
|
334
|
+
] }),
|
|
335
|
+
error && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
|
|
336
|
+
"Error: ",
|
|
337
|
+
error
|
|
338
|
+
] }) }),
|
|
339
|
+
allDone && /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
|
|
340
|
+
/* @__PURE__ */ jsx4(Text4, { color: "green", bold: true, children: "\u2713 Initialization complete" }),
|
|
341
|
+
summary.map((msg, i) => /* @__PURE__ */ jsxs4(Text4, { color: "gray", children: [
|
|
342
|
+
" \u2022 ",
|
|
343
|
+
msg
|
|
344
|
+
] }, i)),
|
|
345
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
346
|
+
"Next: ",
|
|
347
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "cyan", children: [
|
|
348
|
+
"provisor app -h ",
|
|
349
|
+
props.host
|
|
350
|
+
] })
|
|
351
|
+
] }) })
|
|
352
|
+
] })
|
|
353
|
+
] });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/commands/app.tsx
|
|
357
|
+
import { useState as useState3, useEffect as useEffect2 } from "react";
|
|
358
|
+
import { Box as Box5, Text as Text5, useApp as useApp2, useInput as useInput2 } from "ink";
|
|
359
|
+
import SelectInput from "ink-select-input";
|
|
360
|
+
import Spinner2 from "ink-spinner";
|
|
361
|
+
import TextInput from "ink-text-input";
|
|
362
|
+
import crypto from "crypto";
|
|
363
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
364
|
+
var SCRIPTS2 = {
|
|
365
|
+
installCaddy: () => `
|
|
366
|
+
if ! command -v caddy &>/dev/null; then
|
|
367
|
+
apt install -y debian-keyring debian-archive-keyring apt-transport-https
|
|
368
|
+
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
|
369
|
+
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
|
370
|
+
apt update
|
|
371
|
+
apt install caddy -y
|
|
372
|
+
echo "installed"
|
|
373
|
+
else
|
|
374
|
+
echo "exists"
|
|
375
|
+
fi
|
|
376
|
+
`,
|
|
377
|
+
installNode: () => `
|
|
378
|
+
if ! command -v node &>/dev/null; then
|
|
379
|
+
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
|
380
|
+
apt install -y nodejs
|
|
381
|
+
echo "installed"
|
|
382
|
+
else
|
|
383
|
+
echo "exists"
|
|
384
|
+
fi
|
|
385
|
+
if ! command -v pm2 &>/dev/null; then
|
|
386
|
+
npm install -g pm2
|
|
387
|
+
fi
|
|
388
|
+
`,
|
|
389
|
+
// Check if app directory exists
|
|
390
|
+
checkAppExists: (name) => `
|
|
391
|
+
APP_DIR="/var/www/${name}"
|
|
392
|
+
if [ -d "$APP_DIR" ] && [ "$(ls -A $APP_DIR 2>/dev/null)" ]; then
|
|
393
|
+
echo "exists"
|
|
394
|
+
else
|
|
395
|
+
echo "empty"
|
|
396
|
+
fi
|
|
397
|
+
`,
|
|
398
|
+
// Generate deploy key for private repos
|
|
399
|
+
generateDeployKey: (name, user) => `
|
|
400
|
+
SSH_DIR="/home/${user}/.ssh"
|
|
401
|
+
KEY_FILE="$SSH_DIR/deploy_${name}"
|
|
402
|
+
|
|
403
|
+
mkdir -p "$SSH_DIR"
|
|
404
|
+
|
|
405
|
+
# Generate key if doesn't exist
|
|
406
|
+
if [ ! -f "$KEY_FILE" ]; then
|
|
407
|
+
ssh-keygen -t ed25519 -f "$KEY_FILE" -N "" -C "deploy-key-${name}"
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
# Configure SSH to use this key for github.com and gitlab.com
|
|
411
|
+
if ! grep -q "deploy_${name}" "$SSH_DIR/config" 2>/dev/null; then
|
|
412
|
+
cat >> "$SSH_DIR/config" << EOF
|
|
413
|
+
|
|
414
|
+
# Deploy key for ${name}
|
|
415
|
+
Host github.com gitlab.com bitbucket.org
|
|
416
|
+
IdentityFile $KEY_FILE
|
|
417
|
+
IdentitiesOnly yes
|
|
418
|
+
EOF
|
|
419
|
+
fi
|
|
420
|
+
|
|
421
|
+
chown -R ${user}:${user} "$SSH_DIR"
|
|
422
|
+
chmod 700 "$SSH_DIR"
|
|
423
|
+
chmod 600 "$KEY_FILE"
|
|
424
|
+
chmod 644 "$KEY_FILE.pub"
|
|
425
|
+
chmod 600 "$SSH_DIR/config" 2>/dev/null || true
|
|
426
|
+
|
|
427
|
+
# Output the public key
|
|
428
|
+
cat "$KEY_FILE.pub"
|
|
429
|
+
`,
|
|
430
|
+
// Test SSH connection to git host
|
|
431
|
+
testGitConnection: (host) => `
|
|
432
|
+
ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes -T git@${host} 2>&1 || true
|
|
433
|
+
`,
|
|
434
|
+
// Push-to-deploy: setup bare repo with hook
|
|
435
|
+
setupPushDeploy: (name, branch, user) => `
|
|
436
|
+
APP_DIR="/var/www/${name}"
|
|
437
|
+
REPO_DIR="/var/repo/${name}.git"
|
|
438
|
+
|
|
439
|
+
mkdir -p "$APP_DIR"
|
|
440
|
+
mkdir -p "$(dirname "$REPO_DIR")"
|
|
441
|
+
|
|
442
|
+
if [ ! -d "$REPO_DIR" ]; then
|
|
443
|
+
git init --bare "$REPO_DIR"
|
|
444
|
+
fi
|
|
445
|
+
|
|
446
|
+
# Create post-receive hook
|
|
447
|
+
cat << 'HOOK_EOF' > "$REPO_DIR/hooks/post-receive"
|
|
448
|
+
#!/bin/bash
|
|
449
|
+
set -e
|
|
450
|
+
|
|
451
|
+
TARGET="/var/www/${name}"
|
|
452
|
+
GIT_DIR="/var/repo/${name}.git"
|
|
453
|
+
BRANCH="${branch}"
|
|
454
|
+
|
|
455
|
+
echo "=== Deployment Started ==="
|
|
456
|
+
echo "Deploying branch '$BRANCH' to $TARGET..."
|
|
457
|
+
|
|
458
|
+
git --work-tree="$TARGET" --git-dir="$GIT_DIR" checkout -f "$BRANCH"
|
|
459
|
+
|
|
460
|
+
cd "$TARGET"
|
|
461
|
+
|
|
462
|
+
if [ -f "package.json" ]; then
|
|
463
|
+
echo "Node.js project detected."
|
|
464
|
+
|
|
465
|
+
NEED_INSTALL=false
|
|
466
|
+
|
|
467
|
+
if [ ! -d "node_modules" ]; then
|
|
468
|
+
NEED_INSTALL=true
|
|
469
|
+
elif [ -f "package-lock.json" ]; then
|
|
470
|
+
LOCK_HASH_FILE="$GIT_DIR/.package-lock-hash"
|
|
471
|
+
CURRENT_HASH=$(sha256sum package-lock.json | awk '{print $1}')
|
|
472
|
+
|
|
473
|
+
if [ -f "$LOCK_HASH_FILE" ]; then
|
|
474
|
+
PREV_HASH=$(cat "$LOCK_HASH_FILE")
|
|
475
|
+
if [ "$CURRENT_HASH" != "$PREV_HASH" ]; then
|
|
476
|
+
NEED_INSTALL=true
|
|
477
|
+
fi
|
|
478
|
+
else
|
|
479
|
+
NEED_INSTALL=true
|
|
480
|
+
fi
|
|
481
|
+
|
|
482
|
+
echo "$CURRENT_HASH" > "$LOCK_HASH_FILE"
|
|
483
|
+
fi
|
|
484
|
+
|
|
485
|
+
if [ "$NEED_INSTALL" = true ]; then
|
|
486
|
+
echo "Installing dependencies..."
|
|
487
|
+
npm ci --production 2>/dev/null || npm install --production
|
|
488
|
+
fi
|
|
489
|
+
|
|
490
|
+
if grep -q '"build"' "package.json"; then
|
|
491
|
+
echo "Building application..."
|
|
492
|
+
npm run build
|
|
493
|
+
fi
|
|
494
|
+
|
|
495
|
+
PM2_NAME="${name}"
|
|
496
|
+
if pm2 list 2>/dev/null | grep -q "$PM2_NAME"; then
|
|
497
|
+
pm2 restart "$PM2_NAME"
|
|
498
|
+
fi
|
|
499
|
+
else
|
|
500
|
+
echo "Static site detected."
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
echo "=== Deployment Complete ==="
|
|
504
|
+
HOOK_EOF
|
|
505
|
+
|
|
506
|
+
chmod +x "$REPO_DIR/hooks/post-receive"
|
|
507
|
+
chown -R ${user}:${user} "$REPO_DIR"
|
|
508
|
+
chown -R ${user}:${user} "$APP_DIR"
|
|
509
|
+
echo "push-deploy-ready"
|
|
510
|
+
`,
|
|
511
|
+
// Clone from repository - fresh clone (replace)
|
|
512
|
+
cloneRepoFresh: (name, repoUrl, branch, user) => `
|
|
513
|
+
APP_DIR="/var/www/${name}"
|
|
514
|
+
|
|
515
|
+
# Remove existing if present
|
|
516
|
+
rm -rf "$APP_DIR"
|
|
517
|
+
mkdir -p "$APP_DIR"
|
|
518
|
+
chown ${user}:${user} "$APP_DIR"
|
|
519
|
+
|
|
520
|
+
# Clone as the deploy user to use their SSH keys
|
|
521
|
+
sudo -u ${user} git clone --branch ${branch} --single-branch ${repoUrl} "$APP_DIR"
|
|
522
|
+
|
|
523
|
+
cd "$APP_DIR"
|
|
524
|
+
|
|
525
|
+
# Build if Node.js project
|
|
526
|
+
if [ -f "package.json" ]; then
|
|
527
|
+
echo "Node.js project detected."
|
|
528
|
+
sudo -u ${user} npm ci --production 2>/dev/null || sudo -u ${user} npm install --production
|
|
529
|
+
|
|
530
|
+
if grep -q '"build"' "package.json"; then
|
|
531
|
+
echo "Building application..."
|
|
532
|
+
sudo -u ${user} npm run build
|
|
533
|
+
fi
|
|
534
|
+
else
|
|
535
|
+
echo "Static site detected."
|
|
536
|
+
fi
|
|
537
|
+
|
|
538
|
+
chown -R ${user}:${user} "$APP_DIR"
|
|
539
|
+
echo "clone-complete"
|
|
540
|
+
`,
|
|
541
|
+
// Update existing cloned repo (git pull)
|
|
542
|
+
updateRepo: (name, branch, user) => `
|
|
543
|
+
APP_DIR="/var/www/${name}"
|
|
544
|
+
|
|
545
|
+
cd "$APP_DIR"
|
|
546
|
+
|
|
547
|
+
# Fetch and reset to latest
|
|
548
|
+
sudo -u ${user} git fetch origin
|
|
549
|
+
sudo -u ${user} git reset --hard origin/${branch}
|
|
550
|
+
|
|
551
|
+
# Build if Node.js project
|
|
552
|
+
if [ -f "package.json" ]; then
|
|
553
|
+
echo "Node.js project detected."
|
|
554
|
+
sudo -u ${user} npm ci --production 2>/dev/null || sudo -u ${user} npm install --production
|
|
555
|
+
|
|
556
|
+
if grep -q '"build"' "package.json"; then
|
|
557
|
+
echo "Building application..."
|
|
558
|
+
sudo -u ${user} npm run build
|
|
559
|
+
fi
|
|
560
|
+
|
|
561
|
+
PM2_NAME="${name}"
|
|
562
|
+
if pm2 list 2>/dev/null | grep -q "$PM2_NAME"; then
|
|
563
|
+
pm2 restart "$PM2_NAME"
|
|
564
|
+
fi
|
|
565
|
+
else
|
|
566
|
+
echo "Static site detected."
|
|
567
|
+
fi
|
|
568
|
+
|
|
569
|
+
echo "update-complete"
|
|
570
|
+
`,
|
|
571
|
+
// Create update script for cloned repos
|
|
572
|
+
createUpdateScript: (name, branch, user) => `
|
|
573
|
+
cat << 'SCRIPT_EOF' > /usr/local/bin/update-${name}
|
|
574
|
+
#!/bin/bash
|
|
575
|
+
set -e
|
|
576
|
+
|
|
577
|
+
APP_DIR="/var/www/${name}"
|
|
578
|
+
BRANCH="${branch}"
|
|
579
|
+
USER="${user}"
|
|
580
|
+
|
|
581
|
+
echo "=== Updating ${name} ==="
|
|
582
|
+
|
|
583
|
+
cd "$APP_DIR"
|
|
584
|
+
|
|
585
|
+
# Run git commands as deploy user to use their SSH keys
|
|
586
|
+
sudo -u $USER git fetch origin
|
|
587
|
+
sudo -u $USER git reset --hard origin/$BRANCH
|
|
588
|
+
|
|
589
|
+
if [ -f "package.json" ]; then
|
|
590
|
+
sudo -u $USER npm ci --production 2>/dev/null || sudo -u $USER npm install --production
|
|
591
|
+
|
|
592
|
+
if grep -q '"build"' "package.json"; then
|
|
593
|
+
sudo -u $USER npm run build
|
|
594
|
+
fi
|
|
595
|
+
|
|
596
|
+
PM2_NAME="${name}"
|
|
597
|
+
if pm2 list 2>/dev/null | grep -q "$PM2_NAME"; then
|
|
598
|
+
pm2 restart "$PM2_NAME"
|
|
599
|
+
fi
|
|
600
|
+
fi
|
|
601
|
+
|
|
602
|
+
echo "=== Update Complete ==="
|
|
603
|
+
SCRIPT_EOF
|
|
604
|
+
|
|
605
|
+
chmod +x /usr/local/bin/update-${name}
|
|
606
|
+
echo "update-script-created"
|
|
607
|
+
`,
|
|
608
|
+
caddyOnDemand: (appDir) => `
|
|
609
|
+
cat << 'EOF' > /etc/caddy/Caddyfile
|
|
610
|
+
{
|
|
611
|
+
on_demand_tls {
|
|
612
|
+
interval 2m
|
|
613
|
+
burst 5
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
https:// {
|
|
618
|
+
tls {
|
|
619
|
+
on_demand
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
root * ${appDir}
|
|
623
|
+
encode gzip
|
|
624
|
+
try_files {path} /index.html
|
|
625
|
+
file_server
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
http:// {
|
|
629
|
+
redir https://{host}{uri} permanent
|
|
630
|
+
}
|
|
631
|
+
EOF
|
|
632
|
+
caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy
|
|
633
|
+
`,
|
|
634
|
+
caddySpecific: (appDir, domains) => `
|
|
635
|
+
cat << EOF > /etc/caddy/Caddyfile
|
|
636
|
+
${domains} {
|
|
637
|
+
root * ${appDir}
|
|
638
|
+
encode gzip
|
|
639
|
+
try_files {path} /index.html
|
|
640
|
+
file_server
|
|
641
|
+
}
|
|
642
|
+
EOF
|
|
643
|
+
caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy
|
|
644
|
+
`,
|
|
645
|
+
caddyHttp: (appDir) => `
|
|
646
|
+
cat << 'EOF' > /etc/caddy/Caddyfile
|
|
647
|
+
:80 {
|
|
648
|
+
root * ${appDir}
|
|
649
|
+
encode gzip
|
|
650
|
+
try_files {path} /index.html
|
|
651
|
+
file_server
|
|
652
|
+
}
|
|
653
|
+
EOF
|
|
654
|
+
caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy
|
|
655
|
+
`,
|
|
656
|
+
// Setup webhook handler for automatic deployment
|
|
657
|
+
setupWebhook: (name, branch, port, secret) => `
|
|
658
|
+
# Create webhook handler script
|
|
659
|
+
cat << 'HANDLER_EOF' > /usr/local/bin/webhook-${name}.js
|
|
660
|
+
#!/usr/bin/env node
|
|
661
|
+
|
|
662
|
+
const http = require('http');
|
|
663
|
+
const crypto = require('crypto');
|
|
664
|
+
const { spawn } = require('child_process');
|
|
665
|
+
|
|
666
|
+
const config = {
|
|
667
|
+
port: ${port},
|
|
668
|
+
secret: '${secret}',
|
|
669
|
+
branch: '${branch}',
|
|
670
|
+
app: '${name}',
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const updateScript = '/usr/local/bin/update-' + config.app;
|
|
674
|
+
|
|
675
|
+
function log(message) {
|
|
676
|
+
const timestamp = new Date().toISOString();
|
|
677
|
+
console.log('[' + timestamp + '] ' + message);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function verifySignature(payload, signature, secret) {
|
|
681
|
+
if (!secret) return true;
|
|
682
|
+
if (signature.startsWith('sha256=')) {
|
|
683
|
+
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
684
|
+
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
685
|
+
}
|
|
686
|
+
if (signature.startsWith('sha1=')) {
|
|
687
|
+
const expected = 'sha1=' + crypto.createHmac('sha1', secret).update(payload).digest('hex');
|
|
688
|
+
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
689
|
+
}
|
|
690
|
+
return signature === secret;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function getBranchFromPayload(payload) {
|
|
694
|
+
try {
|
|
695
|
+
const data = JSON.parse(payload);
|
|
696
|
+
if (data.ref) return data.ref.replace('refs/heads/', '');
|
|
697
|
+
if (data.object_kind === 'push' && data.ref) return data.ref.replace('refs/heads/', '');
|
|
698
|
+
if (data.push && data.push.changes && data.push.changes[0]) {
|
|
699
|
+
const change = data.push.changes[0];
|
|
700
|
+
if (change.new && change.new.name) return change.new.name;
|
|
701
|
+
}
|
|
702
|
+
return null;
|
|
703
|
+
} catch (e) { return null; }
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function runUpdate() {
|
|
707
|
+
log('Running update script: ' + updateScript);
|
|
708
|
+
const child = spawn('sudo', [updateScript], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
709
|
+
child.stdout.on('data', (data) => log('[update] ' + data.toString().trim()));
|
|
710
|
+
child.stderr.on('data', (data) => log('[update:err] ' + data.toString().trim()));
|
|
711
|
+
child.on('close', (code) => log(code === 0 ? 'Update completed' : 'Update failed: ' + code));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const server = http.createServer((req, res) => {
|
|
715
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
716
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
717
|
+
res.end(JSON.stringify({ status: 'ok', app: config.app, branch: config.branch }));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (req.method !== 'POST' || (req.url !== '/webhook' && req.url !== '/')) {
|
|
721
|
+
res.writeHead(404); res.end('Not found'); return;
|
|
722
|
+
}
|
|
723
|
+
let body = '';
|
|
724
|
+
req.on('data', (chunk) => { body += chunk.toString(); });
|
|
725
|
+
req.on('end', () => {
|
|
726
|
+
const signature = req.headers['x-hub-signature-256'] || req.headers['x-hub-signature'] || req.headers['x-gitlab-token'] || '';
|
|
727
|
+
if (config.secret && !verifySignature(body, signature, config.secret)) {
|
|
728
|
+
log('Invalid signature'); res.writeHead(401); res.end('Unauthorized'); return;
|
|
729
|
+
}
|
|
730
|
+
const branch = getBranchFromPayload(body);
|
|
731
|
+
if (!branch) { res.writeHead(400); res.end('Invalid payload'); return; }
|
|
732
|
+
log('Push to branch: ' + branch);
|
|
733
|
+
if (branch !== config.branch) { res.writeHead(200); res.end('OK (ignored)'); return; }
|
|
734
|
+
log('Triggering deployment...');
|
|
735
|
+
runUpdate();
|
|
736
|
+
res.writeHead(200); res.end('Deployment triggered');
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
server.listen(config.port, '0.0.0.0', () => {
|
|
741
|
+
log('Webhook handler started for ' + config.app + ' on port ' + config.port);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
process.on('SIGTERM', () => { server.close(() => process.exit(0)); });
|
|
745
|
+
process.on('SIGINT', () => { server.close(() => process.exit(0)); });
|
|
746
|
+
HANDLER_EOF
|
|
747
|
+
|
|
748
|
+
chmod +x /usr/local/bin/webhook-${name}.js
|
|
749
|
+
|
|
750
|
+
# Create systemd service
|
|
751
|
+
cat << 'SERVICE_EOF' > /etc/systemd/system/webhook-${name}.service
|
|
752
|
+
[Unit]
|
|
753
|
+
Description=Webhook handler for ${name}
|
|
754
|
+
After=network.target
|
|
755
|
+
|
|
756
|
+
[Service]
|
|
757
|
+
Type=simple
|
|
758
|
+
ExecStart=/usr/bin/node /usr/local/bin/webhook-${name}.js
|
|
759
|
+
Restart=always
|
|
760
|
+
RestartSec=10
|
|
761
|
+
StandardOutput=journal
|
|
762
|
+
StandardError=journal
|
|
763
|
+
SyslogIdentifier=webhook-${name}
|
|
764
|
+
|
|
765
|
+
[Install]
|
|
766
|
+
WantedBy=multi-user.target
|
|
767
|
+
SERVICE_EOF
|
|
768
|
+
|
|
769
|
+
# Open firewall port
|
|
770
|
+
ufw allow ${port}/tcp comment "webhook-${name}"
|
|
771
|
+
|
|
772
|
+
# Start service
|
|
773
|
+
systemctl daemon-reload
|
|
774
|
+
systemctl enable webhook-${name}
|
|
775
|
+
systemctl start webhook-${name}
|
|
776
|
+
|
|
777
|
+
echo "webhook-ready:${port}"
|
|
778
|
+
`,
|
|
779
|
+
// Save app config
|
|
780
|
+
saveConfig: (name, config) => `
|
|
781
|
+
CONFIG_FILE="/var/www/${name}/.provisor.json"
|
|
782
|
+
mkdir -p "/var/www/${name}"
|
|
783
|
+
echo '${JSON.stringify(config)}' > "$CONFIG_FILE"
|
|
784
|
+
chmod 600 "$CONFIG_FILE"
|
|
785
|
+
`,
|
|
786
|
+
// Find available port for webhook
|
|
787
|
+
findAvailablePort: () => `
|
|
788
|
+
for port in $(seq 9000 9100); do
|
|
789
|
+
if ! ss -tuln | grep -q ":$port "; then
|
|
790
|
+
echo $port
|
|
791
|
+
exit 0
|
|
792
|
+
fi
|
|
793
|
+
done
|
|
794
|
+
echo "9000"
|
|
795
|
+
`,
|
|
796
|
+
// Setup git polling service for automatic deployment
|
|
797
|
+
setupPolling: (name, branch, user, interval) => `
|
|
798
|
+
# Create polling script (single poll)
|
|
799
|
+
cat << 'POLLING_EOF' > /usr/local/bin/poll-${name}.sh
|
|
800
|
+
#!/bin/bash
|
|
801
|
+
set -e
|
|
802
|
+
|
|
803
|
+
APP_DIR="/var/www/${name}"
|
|
804
|
+
BRANCH="${branch}"
|
|
805
|
+
USER="${user}"
|
|
806
|
+
LOCK_FILE="/tmp/poll-${name}.lock"
|
|
807
|
+
|
|
808
|
+
# Prevent concurrent runs
|
|
809
|
+
exec 200>"$LOCK_FILE"
|
|
810
|
+
flock -n 200 || exit 0
|
|
811
|
+
|
|
812
|
+
cd "$APP_DIR"
|
|
813
|
+
|
|
814
|
+
# Fetch latest changes
|
|
815
|
+
sudo -u $USER git fetch origin "$BRANCH" --quiet 2>/dev/null
|
|
816
|
+
|
|
817
|
+
# Get local and remote hashes
|
|
818
|
+
LOCAL=$(git rev-parse HEAD)
|
|
819
|
+
REMOTE=$(git rev-parse "origin/$BRANCH")
|
|
820
|
+
|
|
821
|
+
# If different, deploy
|
|
822
|
+
if [ "$LOCAL" != "$REMOTE" ]; then
|
|
823
|
+
echo "[$(date -Iseconds)] New commit detected: $REMOTE"
|
|
824
|
+
sudo /usr/local/bin/update-${name}
|
|
825
|
+
else
|
|
826
|
+
echo "[$(date -Iseconds)] No changes"
|
|
827
|
+
fi
|
|
828
|
+
POLLING_EOF
|
|
829
|
+
|
|
830
|
+
chmod +x /usr/local/bin/poll-${name}.sh
|
|
831
|
+
|
|
832
|
+
# Check if systemd is available (PID 1)
|
|
833
|
+
if pidof systemd > /dev/null 2>&1 || [ "$(cat /proc/1/comm 2>/dev/null)" = "systemd" ]; then
|
|
834
|
+
# SYSTEMD MODE: Use timer
|
|
835
|
+
cat << SERVICE_EOF > /etc/systemd/system/poll-${name}.service
|
|
836
|
+
[Unit]
|
|
837
|
+
Description=Git polling service for ${name}
|
|
838
|
+
After=network.target
|
|
839
|
+
|
|
840
|
+
[Service]
|
|
841
|
+
Type=oneshot
|
|
842
|
+
ExecStart=/usr/local/bin/poll-${name}.sh
|
|
843
|
+
StandardOutput=journal
|
|
844
|
+
StandardError=journal
|
|
845
|
+
SyslogIdentifier=poll-${name}
|
|
846
|
+
SERVICE_EOF
|
|
847
|
+
|
|
848
|
+
cat << TIMER_EOF > /etc/systemd/system/poll-${name}.timer
|
|
849
|
+
[Unit]
|
|
850
|
+
Description=Run git polling for ${name} every ${interval} seconds
|
|
851
|
+
|
|
852
|
+
[Timer]
|
|
853
|
+
OnBootSec=30
|
|
854
|
+
OnUnitActiveSec=${interval}s
|
|
855
|
+
AccuracySec=1s
|
|
856
|
+
|
|
857
|
+
[Install]
|
|
858
|
+
WantedBy=timers.target
|
|
859
|
+
TIMER_EOF
|
|
860
|
+
|
|
861
|
+
systemctl daemon-reload
|
|
862
|
+
systemctl enable poll-${name}.timer
|
|
863
|
+
systemctl start poll-${name}.timer
|
|
864
|
+
echo "polling-ready:${interval}:systemd"
|
|
865
|
+
else
|
|
866
|
+
# DAEMON MODE: Use background loop (for Docker/non-systemd)
|
|
867
|
+
cat << 'DAEMON_EOF' > /usr/local/bin/poll-${name}-daemon.sh
|
|
868
|
+
#!/bin/bash
|
|
869
|
+
LOG_FILE="/var/log/poll-${name}.log"
|
|
870
|
+
PID_FILE="/var/run/poll-${name}.pid"
|
|
871
|
+
INTERVAL=${interval}
|
|
872
|
+
|
|
873
|
+
# Write PID
|
|
874
|
+
echo $$ > "$PID_FILE"
|
|
875
|
+
|
|
876
|
+
echo "[$(date -Iseconds)] Polling daemon started (every ${interval}s)" >> "$LOG_FILE"
|
|
877
|
+
|
|
878
|
+
while true; do
|
|
879
|
+
/usr/local/bin/poll-${name}.sh >> "$LOG_FILE" 2>&1
|
|
880
|
+
sleep $INTERVAL
|
|
881
|
+
done
|
|
882
|
+
DAEMON_EOF
|
|
883
|
+
|
|
884
|
+
chmod +x /usr/local/bin/poll-${name}-daemon.sh
|
|
885
|
+
|
|
886
|
+
# Stop existing daemon if running
|
|
887
|
+
if [ -f /var/run/poll-${name}.pid ]; then
|
|
888
|
+
kill $(cat /var/run/poll-${name}.pid) 2>/dev/null || true
|
|
889
|
+
rm -f /var/run/poll-${name}.pid
|
|
890
|
+
fi
|
|
891
|
+
|
|
892
|
+
# Start daemon in background
|
|
893
|
+
mkdir -p /var/log
|
|
894
|
+
touch /var/log/poll-${name}.log
|
|
895
|
+
nohup /usr/local/bin/poll-${name}-daemon.sh > /dev/null 2>&1 &
|
|
896
|
+
|
|
897
|
+
echo "polling-ready:${interval}:daemon"
|
|
898
|
+
fi
|
|
899
|
+
`
|
|
900
|
+
};
|
|
901
|
+
var deployOptions = [
|
|
902
|
+
{ label: "Push-to-deploy (git push to server)", value: "push" },
|
|
903
|
+
{ label: "Clone from public repository", value: "clone-public" },
|
|
904
|
+
{ label: "Clone from private repository (with deploy key)", value: "clone-private" }
|
|
905
|
+
];
|
|
906
|
+
var existingAppOptions = [
|
|
907
|
+
{ label: "Replace (delete and clone fresh)", value: "replace" },
|
|
908
|
+
{ label: "Update (git pull latest changes)", value: "update" },
|
|
909
|
+
{ label: "Cancel", value: "cancel" }
|
|
910
|
+
];
|
|
911
|
+
var tlsOptions = [
|
|
912
|
+
{ label: "On-demand TLS (auto-cert for any domain)", value: "ondemand" },
|
|
913
|
+
{ label: "Specific domain(s)", value: "specific" },
|
|
914
|
+
{ label: "No TLS (HTTP only)", value: "none" }
|
|
915
|
+
];
|
|
916
|
+
var autoDeployOptions = [
|
|
917
|
+
{ label: "Git polling (checks every 10s, simpler setup)", value: "polling" },
|
|
918
|
+
{ label: "Webhook (instant, requires repo webhook setup)", value: "webhook" },
|
|
919
|
+
{ label: "Manual only (use provisor deploy command)", value: "none" }
|
|
920
|
+
];
|
|
921
|
+
function getGitHost(url) {
|
|
922
|
+
if (url.includes("github.com")) return "github.com";
|
|
923
|
+
if (url.includes("gitlab.com")) return "gitlab.com";
|
|
924
|
+
if (url.includes("bitbucket.org")) return "bitbucket.org";
|
|
925
|
+
const match = url.match(/git@([^:]+):/);
|
|
926
|
+
if (match) return match[1];
|
|
927
|
+
return "github.com";
|
|
928
|
+
}
|
|
929
|
+
function getDeployKeyInstructions(host) {
|
|
930
|
+
switch (host) {
|
|
931
|
+
case "github.com":
|
|
932
|
+
return {
|
|
933
|
+
url: "https://github.com/<owner>/<repo>/settings/keys",
|
|
934
|
+
steps: [
|
|
935
|
+
"1. Go to your repository on GitHub",
|
|
936
|
+
"2. Click Settings \u2192 Deploy keys \u2192 Add deploy key",
|
|
937
|
+
"3. Paste the public key above",
|
|
938
|
+
'4. Give it a title (e.g., "Server deploy key")',
|
|
939
|
+
'5. Click "Add key" (leave "Allow write access" unchecked)'
|
|
940
|
+
]
|
|
941
|
+
};
|
|
942
|
+
case "gitlab.com":
|
|
943
|
+
return {
|
|
944
|
+
url: "https://gitlab.com/<owner>/<repo>/-/settings/repository",
|
|
945
|
+
steps: [
|
|
946
|
+
"1. Go to your repository on GitLab",
|
|
947
|
+
"2. Click Settings \u2192 Repository \u2192 Deploy keys",
|
|
948
|
+
"3. Paste the public key above",
|
|
949
|
+
'4. Give it a title and click "Add key"'
|
|
950
|
+
]
|
|
951
|
+
};
|
|
952
|
+
case "bitbucket.org":
|
|
953
|
+
return {
|
|
954
|
+
url: "https://bitbucket.org/<owner>/<repo>/admin/access-keys/",
|
|
955
|
+
steps: [
|
|
956
|
+
"1. Go to your repository on Bitbucket",
|
|
957
|
+
"2. Click Repository settings \u2192 Access keys",
|
|
958
|
+
'3. Click "Add key" and paste the public key above'
|
|
959
|
+
]
|
|
960
|
+
};
|
|
961
|
+
default:
|
|
962
|
+
return {
|
|
963
|
+
url: "",
|
|
964
|
+
steps: [
|
|
965
|
+
"1. Go to your repository settings",
|
|
966
|
+
'2. Find "Deploy keys" or "Access keys" section',
|
|
967
|
+
"3. Add the public key shown above"
|
|
968
|
+
]
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
function AppCommand(props) {
|
|
973
|
+
const { exit } = useApp2();
|
|
974
|
+
const [client, setClient] = useState3(null);
|
|
975
|
+
const [tasks, setTasks] = useState3({
|
|
976
|
+
connect: "pending",
|
|
977
|
+
caddy: "pending",
|
|
978
|
+
node: "pending",
|
|
979
|
+
deployKey: "pending",
|
|
980
|
+
deploy: "pending",
|
|
981
|
+
webhook: "pending",
|
|
982
|
+
caddyConfig: "pending"
|
|
983
|
+
});
|
|
984
|
+
const [error, setError] = useState3(null);
|
|
985
|
+
const [selectingMethod, setSelectingMethod] = useState3(false);
|
|
986
|
+
const [deployMethod, setDeployMethod] = useState3(props.repo ? "clone-public" : null);
|
|
987
|
+
const [enteringRepo, setEnteringRepo] = useState3(false);
|
|
988
|
+
const [repoUrl, setRepoUrl] = useState3(props.repo || "");
|
|
989
|
+
const [selectingTls, setSelectingTls] = useState3(false);
|
|
990
|
+
const [tlsChoice, setTlsChoice] = useState3(null);
|
|
991
|
+
const [deployKey, setDeployKey] = useState3("");
|
|
992
|
+
const [waitingForKeySetup, setWaitingForKeySetup] = useState3(false);
|
|
993
|
+
const [keyConfirmed, setKeyConfirmed] = useState3(false);
|
|
994
|
+
const [keyVerifying, setKeyVerifying] = useState3(false);
|
|
995
|
+
const [keyVerified, setKeyVerified] = useState3(false);
|
|
996
|
+
const [keyVerifyError, setKeyVerifyError] = useState3(null);
|
|
997
|
+
const [appExists, setAppExists] = useState3(null);
|
|
998
|
+
const [selectingExistingAction, setSelectingExistingAction] = useState3(false);
|
|
999
|
+
const [existingAppAction, setExistingAppAction] = useState3(null);
|
|
1000
|
+
const [selectingAutoDeploy, setSelectingAutoDeploy] = useState3(false);
|
|
1001
|
+
const [autoDeployChoice, setAutoDeployChoice] = useState3(null);
|
|
1002
|
+
const [webhookPort, setWebhookPort] = useState3(0);
|
|
1003
|
+
const [webhookSecret, setWebhookSecret] = useState3("");
|
|
1004
|
+
const [pollingInterval, setPollingInterval] = useState3(10);
|
|
1005
|
+
const [serverIp, setServerIp] = useState3("");
|
|
1006
|
+
const appDir = `/var/www/${props.name}`;
|
|
1007
|
+
const repoDir = `/var/repo/${props.name}.git`;
|
|
1008
|
+
const gitHost = getGitHost(repoUrl);
|
|
1009
|
+
const keyInstructions = getDeployKeyInstructions(gitHost);
|
|
1010
|
+
const updateTask = (task, status) => {
|
|
1011
|
+
setTasks((prev) => ({ ...prev, [task]: status }));
|
|
1012
|
+
};
|
|
1013
|
+
useInput2((input, key) => {
|
|
1014
|
+
if (waitingForKeySetup && !keyConfirmed && !keyVerifying) {
|
|
1015
|
+
if (input.toLowerCase() === "y" || key.return) {
|
|
1016
|
+
setKeyConfirmed(true);
|
|
1017
|
+
setKeyVerifying(true);
|
|
1018
|
+
setKeyVerifyError(null);
|
|
1019
|
+
} else if (input.toLowerCase() === "n" || key.escape) {
|
|
1020
|
+
setError("Deployment cancelled. Please add the deploy key and try again.");
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (keyVerifyError && !keyVerifying) {
|
|
1024
|
+
if (input.toLowerCase() === "r") {
|
|
1025
|
+
setKeyVerifying(true);
|
|
1026
|
+
setKeyVerifyError(null);
|
|
1027
|
+
} else if (input.toLowerCase() === "q" || key.escape) {
|
|
1028
|
+
setError("Deployment cancelled. Please add the deploy key and try again.");
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
useEffect2(() => {
|
|
1033
|
+
const run = async () => {
|
|
1034
|
+
updateTask("connect", "running");
|
|
1035
|
+
try {
|
|
1036
|
+
const sshClient = await connect(props);
|
|
1037
|
+
setClient(sshClient);
|
|
1038
|
+
const ipResult = await exec(sshClient, "hostname -I | awk '{print $1}'");
|
|
1039
|
+
setServerIp(ipResult.stdout.trim());
|
|
1040
|
+
updateTask("connect", "success");
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
updateTask("connect", "error");
|
|
1043
|
+
setError(`Connection failed: ${err instanceof Error ? err.message : err}`);
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
run();
|
|
1047
|
+
}, []);
|
|
1048
|
+
useEffect2(() => {
|
|
1049
|
+
if (!client || tasks.connect !== "success" || tasks.caddy !== "pending") return;
|
|
1050
|
+
const run = async () => {
|
|
1051
|
+
updateTask("caddy", "running");
|
|
1052
|
+
try {
|
|
1053
|
+
await execScript(client, SCRIPTS2.installCaddy(), true);
|
|
1054
|
+
updateTask("caddy", "success");
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
updateTask("caddy", "error");
|
|
1057
|
+
setError(`Caddy installation failed: ${err instanceof Error ? err.message : err}`);
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
run();
|
|
1061
|
+
}, [client, tasks.connect]);
|
|
1062
|
+
useEffect2(() => {
|
|
1063
|
+
if (!client || tasks.caddy !== "success" || tasks.node !== "pending") return;
|
|
1064
|
+
const run = async () => {
|
|
1065
|
+
updateTask("node", "running");
|
|
1066
|
+
try {
|
|
1067
|
+
await execScript(client, SCRIPTS2.installNode(), true);
|
|
1068
|
+
updateTask("node", "success");
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
updateTask("node", "error");
|
|
1071
|
+
setError(`Node.js installation failed: ${err instanceof Error ? err.message : err}`);
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
run();
|
|
1075
|
+
}, [client, tasks.caddy]);
|
|
1076
|
+
useEffect2(() => {
|
|
1077
|
+
if (tasks.node !== "success" || selectingMethod || deployMethod !== null) return;
|
|
1078
|
+
setSelectingMethod(true);
|
|
1079
|
+
}, [tasks.node]);
|
|
1080
|
+
const handleMethodSelect = (item) => {
|
|
1081
|
+
setSelectingMethod(false);
|
|
1082
|
+
const method = item.value;
|
|
1083
|
+
setDeployMethod(method);
|
|
1084
|
+
if ((method === "clone-public" || method === "clone-private") && !repoUrl) {
|
|
1085
|
+
setEnteringRepo(true);
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
const handleRepoSubmit = (value) => {
|
|
1089
|
+
setRepoUrl(value);
|
|
1090
|
+
setEnteringRepo(false);
|
|
1091
|
+
};
|
|
1092
|
+
useEffect2(() => {
|
|
1093
|
+
if (!client || !deployMethod || enteringRepo) return;
|
|
1094
|
+
if (deployMethod === "push") {
|
|
1095
|
+
setAppExists(false);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (appExists !== null) return;
|
|
1099
|
+
const run = async () => {
|
|
1100
|
+
try {
|
|
1101
|
+
const result = await exec(client, SCRIPTS2.checkAppExists(props.name));
|
|
1102
|
+
setAppExists(result.stdout.trim() === "exists");
|
|
1103
|
+
} catch {
|
|
1104
|
+
setAppExists(false);
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
run();
|
|
1108
|
+
}, [client, deployMethod, enteringRepo, repoUrl]);
|
|
1109
|
+
useEffect2(() => {
|
|
1110
|
+
if (appExists === true && !selectingExistingAction && existingAppAction === null) {
|
|
1111
|
+
setSelectingExistingAction(true);
|
|
1112
|
+
}
|
|
1113
|
+
if (appExists === false && existingAppAction === null) {
|
|
1114
|
+
setExistingAppAction("replace");
|
|
1115
|
+
}
|
|
1116
|
+
}, [appExists]);
|
|
1117
|
+
const handleExistingAppSelect = (item) => {
|
|
1118
|
+
setSelectingExistingAction(false);
|
|
1119
|
+
const action = item.value;
|
|
1120
|
+
if (action === "cancel") {
|
|
1121
|
+
setError("Deployment cancelled.");
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
setExistingAppAction(action);
|
|
1125
|
+
};
|
|
1126
|
+
useEffect2(() => {
|
|
1127
|
+
if (!client || deployMethod !== "clone-private" || !repoUrl || tasks.deployKey !== "pending") return;
|
|
1128
|
+
if (existingAppAction === null) return;
|
|
1129
|
+
const run = async () => {
|
|
1130
|
+
updateTask("deployKey", "running");
|
|
1131
|
+
try {
|
|
1132
|
+
const result = await execScript(client, SCRIPTS2.generateDeployKey(props.name, props.user || "deploy"), true);
|
|
1133
|
+
setDeployKey(result.stdout.trim());
|
|
1134
|
+
updateTask("deployKey", "success");
|
|
1135
|
+
setWaitingForKeySetup(true);
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
updateTask("deployKey", "error");
|
|
1138
|
+
setError(`Deploy key generation failed: ${err instanceof Error ? err.message : err}`);
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
run();
|
|
1142
|
+
}, [client, deployMethod, repoUrl, existingAppAction]);
|
|
1143
|
+
useEffect2(() => {
|
|
1144
|
+
if (deployMethod && deployMethod !== "clone-private" && tasks.deployKey === "pending" && existingAppAction !== null) {
|
|
1145
|
+
updateTask("deployKey", "skipped");
|
|
1146
|
+
}
|
|
1147
|
+
}, [deployMethod, existingAppAction]);
|
|
1148
|
+
useEffect2(() => {
|
|
1149
|
+
if (!client || !keyVerifying || keyVerified) return;
|
|
1150
|
+
const run = async () => {
|
|
1151
|
+
try {
|
|
1152
|
+
const result = await execScript(client, SCRIPTS2.testGitConnection(gitHost), false);
|
|
1153
|
+
const output = result.stdout.toLowerCase();
|
|
1154
|
+
if (output.includes("permission denied") && !output.includes("successfully authenticated")) {
|
|
1155
|
+
setKeyVerifyError("Permission denied. The deploy key may not be added correctly or may not have read access.");
|
|
1156
|
+
setKeyVerifying(false);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (output.includes("successfully authenticated") || output.includes("welcome to gitlab") || output.includes("logged in as") || output.includes("hi ") && output.includes("!")) {
|
|
1160
|
+
setKeyVerified(true);
|
|
1161
|
+
setKeyVerifying(false);
|
|
1162
|
+
setWaitingForKeySetup(false);
|
|
1163
|
+
} else if (output.includes("could not resolve") || output.includes("connection refused")) {
|
|
1164
|
+
setKeyVerifyError(`Could not connect to ${gitHost}. Check your network connection.`);
|
|
1165
|
+
setKeyVerifying(false);
|
|
1166
|
+
} else {
|
|
1167
|
+
setKeyVerifyError(`Unexpected response from ${gitHost}: ${output.substring(0, 200)}`);
|
|
1168
|
+
setKeyVerifying(false);
|
|
1169
|
+
}
|
|
1170
|
+
} catch (err) {
|
|
1171
|
+
setKeyVerifyError(`Connection test failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1172
|
+
setKeyVerifying(false);
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
run();
|
|
1176
|
+
}, [client, keyVerifying, keyVerified, gitHost]);
|
|
1177
|
+
useEffect2(() => {
|
|
1178
|
+
if (!client || !deployMethod || tasks.deploy !== "pending") return;
|
|
1179
|
+
if (tasks.deployKey !== "success" && tasks.deployKey !== "skipped") return;
|
|
1180
|
+
if (deployMethod === "clone-private" && !keyVerified) return;
|
|
1181
|
+
if ((deployMethod === "clone-public" || deployMethod === "clone-private") && !repoUrl) return;
|
|
1182
|
+
if (existingAppAction === null) return;
|
|
1183
|
+
const run = async () => {
|
|
1184
|
+
updateTask("deploy", "running");
|
|
1185
|
+
try {
|
|
1186
|
+
if (deployMethod === "push") {
|
|
1187
|
+
await execScript(client, SCRIPTS2.setupPushDeploy(props.name, props.branch, props.user || "deploy"), true);
|
|
1188
|
+
} else {
|
|
1189
|
+
if (deployMethod === "clone-private") {
|
|
1190
|
+
await execScript(client, SCRIPTS2.testGitConnection(gitHost), true);
|
|
1191
|
+
}
|
|
1192
|
+
if (existingAppAction === "replace") {
|
|
1193
|
+
await execScript(client, SCRIPTS2.cloneRepoFresh(props.name, repoUrl, props.branch, props.user || "deploy"), true);
|
|
1194
|
+
} else {
|
|
1195
|
+
await execScript(client, SCRIPTS2.updateRepo(props.name, props.branch, props.user || "deploy"), true);
|
|
1196
|
+
}
|
|
1197
|
+
await execScript(client, SCRIPTS2.createUpdateScript(props.name, props.branch, props.user || "deploy"), true);
|
|
1198
|
+
}
|
|
1199
|
+
updateTask("deploy", "success");
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
updateTask("deploy", "error");
|
|
1202
|
+
setError(`Deployment setup failed: ${err instanceof Error ? err.message : err}`);
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
run();
|
|
1206
|
+
}, [client, deployMethod, repoUrl, keyVerified, tasks.deployKey, existingAppAction]);
|
|
1207
|
+
useEffect2(() => {
|
|
1208
|
+
if (tasks.deploy !== "success" || tasks.webhook !== "pending") return;
|
|
1209
|
+
if (deployMethod === "push") {
|
|
1210
|
+
updateTask("webhook", "skipped");
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (!selectingAutoDeploy && autoDeployChoice === null) {
|
|
1214
|
+
setSelectingAutoDeploy(true);
|
|
1215
|
+
}
|
|
1216
|
+
}, [tasks.deploy, deployMethod]);
|
|
1217
|
+
const handleAutoDeploySelect = async (item) => {
|
|
1218
|
+
setSelectingAutoDeploy(false);
|
|
1219
|
+
const choice = item.value;
|
|
1220
|
+
setAutoDeployChoice(choice);
|
|
1221
|
+
if (choice === "none") {
|
|
1222
|
+
updateTask("webhook", "skipped");
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
useEffect2(() => {
|
|
1226
|
+
if (!client || !autoDeployChoice || autoDeployChoice === "none" || tasks.webhook !== "pending") return;
|
|
1227
|
+
const run = async () => {
|
|
1228
|
+
updateTask("webhook", "running");
|
|
1229
|
+
try {
|
|
1230
|
+
const user = props.user || "deploy";
|
|
1231
|
+
if (autoDeployChoice === "polling") {
|
|
1232
|
+
await execScript(client, SCRIPTS2.setupPolling(props.name, props.branch, user, pollingInterval), true);
|
|
1233
|
+
const config = {
|
|
1234
|
+
repo: repoUrl,
|
|
1235
|
+
branch: props.branch,
|
|
1236
|
+
autoDeployType: "polling",
|
|
1237
|
+
pollingInterval
|
|
1238
|
+
};
|
|
1239
|
+
await execScript(client, SCRIPTS2.saveConfig(props.name, config), true);
|
|
1240
|
+
} else if (autoDeployChoice === "webhook") {
|
|
1241
|
+
const portResult = await exec(client, SCRIPTS2.findAvailablePort());
|
|
1242
|
+
const port = parseInt(portResult.stdout.trim(), 10) || 9e3;
|
|
1243
|
+
setWebhookPort(port);
|
|
1244
|
+
const secret = crypto.randomBytes(32).toString("hex");
|
|
1245
|
+
setWebhookSecret(secret);
|
|
1246
|
+
await execScript(client, SCRIPTS2.setupWebhook(props.name, props.branch, port, secret), true);
|
|
1247
|
+
const config = {
|
|
1248
|
+
repo: repoUrl,
|
|
1249
|
+
branch: props.branch,
|
|
1250
|
+
autoDeployType: "webhook",
|
|
1251
|
+
webhookPort: port
|
|
1252
|
+
};
|
|
1253
|
+
await execScript(client, SCRIPTS2.saveConfig(props.name, config), true);
|
|
1254
|
+
}
|
|
1255
|
+
updateTask("webhook", "success");
|
|
1256
|
+
} catch (err) {
|
|
1257
|
+
updateTask("webhook", "error");
|
|
1258
|
+
setError(`Auto-deploy setup failed: ${err instanceof Error ? err.message : err}`);
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
run();
|
|
1262
|
+
}, [client, autoDeployChoice]);
|
|
1263
|
+
useEffect2(() => {
|
|
1264
|
+
if (tasks.deploy !== "success") return;
|
|
1265
|
+
if (tasks.webhook !== "success" && tasks.webhook !== "skipped") return;
|
|
1266
|
+
if (selectingTls || tlsChoice !== null) return;
|
|
1267
|
+
setSelectingTls(true);
|
|
1268
|
+
}, [tasks.deploy, tasks.webhook]);
|
|
1269
|
+
useEffect2(() => {
|
|
1270
|
+
if (!client || !tlsChoice || tasks.caddyConfig !== "pending") return;
|
|
1271
|
+
const run = async () => {
|
|
1272
|
+
updateTask("caddyConfig", "running");
|
|
1273
|
+
try {
|
|
1274
|
+
let script;
|
|
1275
|
+
switch (tlsChoice) {
|
|
1276
|
+
case "ondemand":
|
|
1277
|
+
script = SCRIPTS2.caddyOnDemand(appDir);
|
|
1278
|
+
break;
|
|
1279
|
+
case "specific":
|
|
1280
|
+
script = SCRIPTS2.caddyOnDemand(appDir);
|
|
1281
|
+
break;
|
|
1282
|
+
case "none":
|
|
1283
|
+
script = SCRIPTS2.caddyHttp(appDir);
|
|
1284
|
+
break;
|
|
1285
|
+
}
|
|
1286
|
+
await execScript(client, script, true);
|
|
1287
|
+
updateTask("caddyConfig", "success");
|
|
1288
|
+
disconnect(client);
|
|
1289
|
+
setTimeout(() => exit(), 100);
|
|
1290
|
+
} catch (err) {
|
|
1291
|
+
updateTask("caddyConfig", "error");
|
|
1292
|
+
setError(`Caddy config failed: ${err instanceof Error ? err.message : err}`);
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
run();
|
|
1296
|
+
}, [client, tlsChoice]);
|
|
1297
|
+
const handleTlsSelect = (item) => {
|
|
1298
|
+
setSelectingTls(false);
|
|
1299
|
+
setTlsChoice(item.value);
|
|
1300
|
+
};
|
|
1301
|
+
useEffect2(() => {
|
|
1302
|
+
if (error && client) {
|
|
1303
|
+
disconnect(client);
|
|
1304
|
+
setTimeout(() => exit(), 100);
|
|
1305
|
+
}
|
|
1306
|
+
}, [error]);
|
|
1307
|
+
const allDone = tasks.caddyConfig === "success";
|
|
1308
|
+
const isClone = deployMethod === "clone-public" || deployMethod === "clone-private";
|
|
1309
|
+
const deployLabel = isClone ? existingAppAction === "update" ? `Update ${props.name}` : `Clone from ${repoUrl || "repository"}` : "Setup push-to-deploy";
|
|
1310
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
1311
|
+
/* @__PURE__ */ jsx5(Header, { title: "Application Provisioning", subtitle: `Host: ${props.host}` }),
|
|
1312
|
+
/* @__PURE__ */ jsx5(Task, { label: "Connect to server", status: tasks.connect }),
|
|
1313
|
+
/* @__PURE__ */ jsx5(Task, { label: "Install Caddy", status: tasks.caddy }),
|
|
1314
|
+
/* @__PURE__ */ jsx5(Task, { label: "Install Node.js & PM2", status: tasks.node }),
|
|
1315
|
+
deployMethod === "clone-private" && /* @__PURE__ */ jsx5(Task, { label: "Generate deploy key", status: tasks.deployKey }),
|
|
1316
|
+
/* @__PURE__ */ jsx5(Task, { label: deployLabel, status: tasks.deploy }),
|
|
1317
|
+
(deployMethod === "clone-public" || deployMethod === "clone-private") && autoDeployChoice && autoDeployChoice !== "none" && /* @__PURE__ */ jsx5(Task, { label: `Setup ${autoDeployChoice === "polling" ? "git polling" : "webhook"} for auto-deploy`, status: tasks.webhook }),
|
|
1318
|
+
/* @__PURE__ */ jsx5(Task, { label: "Configure Caddy", status: tasks.caddyConfig }),
|
|
1319
|
+
selectingMethod && deployMethod === null && /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1320
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Select deployment method:" }),
|
|
1321
|
+
/* @__PURE__ */ jsx5(SelectInput, { items: deployOptions, onSelect: handleMethodSelect })
|
|
1322
|
+
] }),
|
|
1323
|
+
enteringRepo && /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1324
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Enter repository URL:" }),
|
|
1325
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: deployMethod === "clone-private" ? "(Use SSH URL: git@github.com:user/repo.git)" : "(e.g., https://github.com/user/repo.git)" }),
|
|
1326
|
+
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
1327
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "> " }),
|
|
1328
|
+
/* @__PURE__ */ jsx5(TextInput, { value: repoUrl, onChange: setRepoUrl, onSubmit: handleRepoSubmit })
|
|
1329
|
+
] })
|
|
1330
|
+
] }),
|
|
1331
|
+
selectingExistingAction && existingAppAction === null && /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1332
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, color: "yellow", children: [
|
|
1333
|
+
"\u26A0 App directory ",
|
|
1334
|
+
appDir,
|
|
1335
|
+
" already exists"
|
|
1336
|
+
] }),
|
|
1337
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: "What would you like to do?" }),
|
|
1338
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(SelectInput, { items: existingAppOptions, onSelect: handleExistingAppSelect }) })
|
|
1339
|
+
] }),
|
|
1340
|
+
waitingForKeySetup && deployKey && !keyConfirmed && !keyVerifying && !keyVerified && !keyVerifyError && /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1341
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: "\u2501\u2501\u2501 Deploy Key Generated \u2501\u2501\u2501" }),
|
|
1342
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1343
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Public key (copy this):" }),
|
|
1344
|
+
/* @__PURE__ */ jsx5(Box5, { marginY: 1, paddingX: 1, borderStyle: "single", borderColor: "cyan", children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: deployKey }) })
|
|
1345
|
+
] }),
|
|
1346
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1347
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
|
|
1348
|
+
"Add this key to ",
|
|
1349
|
+
gitHost,
|
|
1350
|
+
":"
|
|
1351
|
+
] }),
|
|
1352
|
+
keyInstructions.steps.map((step, i) => /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
1353
|
+
" ",
|
|
1354
|
+
step
|
|
1355
|
+
] }, i))
|
|
1356
|
+
] }),
|
|
1357
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
|
|
1358
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "yellow", children: [
|
|
1359
|
+
"Have you added the deploy key to ",
|
|
1360
|
+
gitHost,
|
|
1361
|
+
"? "
|
|
1362
|
+
] }),
|
|
1363
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: "(y/n) " })
|
|
1364
|
+
] })
|
|
1365
|
+
] }),
|
|
1366
|
+
keyVerifying && /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1367
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: "\u2501\u2501\u2501 Verifying Deploy Key \u2501\u2501\u2501" }),
|
|
1368
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
|
|
1369
|
+
/* @__PURE__ */ jsx5(Spinner2, { type: "dots" }),
|
|
1370
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
1371
|
+
" Testing SSH connection to ",
|
|
1372
|
+
gitHost,
|
|
1373
|
+
"..."
|
|
1374
|
+
] })
|
|
1375
|
+
] })
|
|
1376
|
+
] }),
|
|
1377
|
+
keyVerified && !error && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "green", children: "\u2713 Deploy key verified successfully!" }) }),
|
|
1378
|
+
keyVerifyError && /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1379
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "red", children: "\u2501\u2501\u2501 Deploy Key Verification Failed \u2501\u2501\u2501" }),
|
|
1380
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
|
|
1381
|
+
"\u2717 ",
|
|
1382
|
+
keyVerifyError
|
|
1383
|
+
] }) }),
|
|
1384
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1385
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Please check:" }),
|
|
1386
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
1387
|
+
" 1. The deploy key is added to ",
|
|
1388
|
+
gitHost
|
|
1389
|
+
] }),
|
|
1390
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: " 2. The key has read access to the repository" }),
|
|
1391
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: " 3. The repository URL is correct" })
|
|
1392
|
+
] }),
|
|
1393
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1394
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Public key to add:" }),
|
|
1395
|
+
/* @__PURE__ */ jsx5(Box5, { marginY: 1, paddingX: 1, borderStyle: "single", borderColor: "cyan", children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: deployKey }) })
|
|
1396
|
+
] }),
|
|
1397
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Press (r) to retry or (q) to quit" }) })
|
|
1398
|
+
] }),
|
|
1399
|
+
selectingAutoDeploy && autoDeployChoice === null && /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1400
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Enable automatic deployment?" }),
|
|
1401
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
1402
|
+
"Auto-deploy when you push to ",
|
|
1403
|
+
props.branch,
|
|
1404
|
+
" branch."
|
|
1405
|
+
] }),
|
|
1406
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(SelectInput, { items: autoDeployOptions, onSelect: handleAutoDeploySelect }) })
|
|
1407
|
+
] }),
|
|
1408
|
+
selectingTls && tlsChoice === null && /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1409
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Select TLS configuration:" }),
|
|
1410
|
+
/* @__PURE__ */ jsx5(SelectInput, { items: tlsOptions, onSelect: handleTlsSelect })
|
|
1411
|
+
] }),
|
|
1412
|
+
error && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
|
|
1413
|
+
"Error: ",
|
|
1414
|
+
error
|
|
1415
|
+
] }) }),
|
|
1416
|
+
allDone && /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1417
|
+
/* @__PURE__ */ jsx5(Text5, { color: "green", bold: true, children: "\u2713 Application provisioning complete" }),
|
|
1418
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: deployMethod === "push" ? /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
1419
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Git remote (add to local project):" }),
|
|
1420
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
1421
|
+
" git remote add production ssh://",
|
|
1422
|
+
props.user,
|
|
1423
|
+
"@",
|
|
1424
|
+
serverIp,
|
|
1425
|
+
repoDir
|
|
1426
|
+
] }),
|
|
1427
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Deploy with:" }) }),
|
|
1428
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
1429
|
+
" git push production ",
|
|
1430
|
+
props.branch
|
|
1431
|
+
] })
|
|
1432
|
+
] }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
1433
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
|
|
1434
|
+
"App ",
|
|
1435
|
+
existingAppAction === "update" ? "updated" : "deployed",
|
|
1436
|
+
" from: ",
|
|
1437
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: repoUrl })
|
|
1438
|
+
] }),
|
|
1439
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Manual update:" }) }),
|
|
1440
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
1441
|
+
" ssh ",
|
|
1442
|
+
props.user,
|
|
1443
|
+
"@",
|
|
1444
|
+
serverIp,
|
|
1445
|
+
' "sudo update-',
|
|
1446
|
+
props.name,
|
|
1447
|
+
'"'
|
|
1448
|
+
] }),
|
|
1449
|
+
autoDeployChoice === "polling" && /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1450
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: "\u2501\u2501\u2501 Auto-Deploy (Git Polling) \u2501\u2501\u2501" }),
|
|
1451
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
1452
|
+
"Polling interval: ",
|
|
1453
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
1454
|
+
pollingInterval,
|
|
1455
|
+
" seconds"
|
|
1456
|
+
] })
|
|
1457
|
+
] }),
|
|
1458
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
1459
|
+
"Branch: ",
|
|
1460
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: props.branch })
|
|
1461
|
+
] }),
|
|
1462
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: "The server checks for new commits and deploys automatically." }),
|
|
1463
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { children: [
|
|
1464
|
+
"View logs: ",
|
|
1465
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
1466
|
+
"journalctl -u poll-",
|
|
1467
|
+
props.name,
|
|
1468
|
+
" -f"
|
|
1469
|
+
] })
|
|
1470
|
+
] }) })
|
|
1471
|
+
] }),
|
|
1472
|
+
autoDeployChoice === "webhook" && webhookPort > 0 && /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
1473
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1474
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: "\u2501\u2501\u2501 Auto-Deploy (Webhook) \u2501\u2501\u2501" }),
|
|
1475
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
1476
|
+
"Webhook URL: ",
|
|
1477
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
1478
|
+
"http://",
|
|
1479
|
+
serverIp,
|
|
1480
|
+
":",
|
|
1481
|
+
webhookPort,
|
|
1482
|
+
"/webhook"
|
|
1483
|
+
] })
|
|
1484
|
+
] }),
|
|
1485
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
1486
|
+
"Secret: ",
|
|
1487
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: webhookSecret })
|
|
1488
|
+
] }),
|
|
1489
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
1490
|
+
"Branch: ",
|
|
1491
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: props.branch })
|
|
1492
|
+
] })
|
|
1493
|
+
] }),
|
|
1494
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1495
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "Add webhook to your repository:" }),
|
|
1496
|
+
gitHost === "github.com" && /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
1497
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: " 1. Go to your repo \u2192 Settings \u2192 Webhooks \u2192 Add webhook" }),
|
|
1498
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
1499
|
+
" 2. Payload URL: http://",
|
|
1500
|
+
serverIp,
|
|
1501
|
+
":",
|
|
1502
|
+
webhookPort,
|
|
1503
|
+
"/webhook"
|
|
1504
|
+
] }),
|
|
1505
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: " 3. Content type: application/json" }),
|
|
1506
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
1507
|
+
" 4. Secret: ",
|
|
1508
|
+
webhookSecret
|
|
1509
|
+
] }),
|
|
1510
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: ' 5. Select "Just the push event"' })
|
|
1511
|
+
] }),
|
|
1512
|
+
gitHost === "gitlab.com" && /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
1513
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: " 1. Go to your repo \u2192 Settings \u2192 Webhooks" }),
|
|
1514
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
1515
|
+
" 2. URL: http://",
|
|
1516
|
+
serverIp,
|
|
1517
|
+
":",
|
|
1518
|
+
webhookPort,
|
|
1519
|
+
"/webhook"
|
|
1520
|
+
] }),
|
|
1521
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
1522
|
+
" 3. Secret token: ",
|
|
1523
|
+
webhookSecret
|
|
1524
|
+
] }),
|
|
1525
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: " 4. Trigger: Push events" })
|
|
1526
|
+
] }),
|
|
1527
|
+
gitHost === "bitbucket.org" && /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
1528
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: " 1. Go to your repo \u2192 Repository settings \u2192 Webhooks" }),
|
|
1529
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
1530
|
+
" 2. URL: http://",
|
|
1531
|
+
serverIp,
|
|
1532
|
+
":",
|
|
1533
|
+
webhookPort,
|
|
1534
|
+
"/webhook"
|
|
1535
|
+
] }),
|
|
1536
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: " 3. Triggers: Repository push" })
|
|
1537
|
+
] })
|
|
1538
|
+
] })
|
|
1539
|
+
] })
|
|
1540
|
+
] }) })
|
|
1541
|
+
] })
|
|
1542
|
+
] });
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// src/commands/ssh-key.tsx
|
|
1546
|
+
import { useState as useState4, useEffect as useEffect3 } from "react";
|
|
1547
|
+
import { Box as Box6, Text as Text6, useApp as useApp3 } from "ink";
|
|
1548
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1549
|
+
function SshKeyCommand(props) {
|
|
1550
|
+
const { exit } = useApp3();
|
|
1551
|
+
const [client, setClient] = useState4(null);
|
|
1552
|
+
const [status, setStatus] = useState4("pending");
|
|
1553
|
+
const [error, setError] = useState4(null);
|
|
1554
|
+
const [keys, setKeys] = useState4([]);
|
|
1555
|
+
const [message, setMessage] = useState4("");
|
|
1556
|
+
useEffect3(() => {
|
|
1557
|
+
const run = async () => {
|
|
1558
|
+
setStatus("running");
|
|
1559
|
+
try {
|
|
1560
|
+
const sshClient = await connect(props);
|
|
1561
|
+
setClient(sshClient);
|
|
1562
|
+
if (props.list) {
|
|
1563
|
+
const result = await exec(sshClient, 'cat ~/.ssh/authorized_keys 2>/dev/null || echo ""');
|
|
1564
|
+
const keyList = result.stdout.trim().split("\n").filter((k) => k.length > 0).map((k) => {
|
|
1565
|
+
const parts = k.split(" ");
|
|
1566
|
+
const type = parts[0] || "unknown";
|
|
1567
|
+
const comment = parts[2] || "no comment";
|
|
1568
|
+
const keyPreview = parts[1]?.slice(-12) || "";
|
|
1569
|
+
return `${type} ...${keyPreview} ${comment}`;
|
|
1570
|
+
});
|
|
1571
|
+
setKeys(keyList);
|
|
1572
|
+
setStatus("success");
|
|
1573
|
+
setMessage(`Found ${keyList.length} key(s)`);
|
|
1574
|
+
} else if (props.add) {
|
|
1575
|
+
const keyPattern = /^(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521)\s+\S+/;
|
|
1576
|
+
if (!keyPattern.test(props.add)) {
|
|
1577
|
+
throw new Error("Invalid SSH key format");
|
|
1578
|
+
}
|
|
1579
|
+
const existing = await exec(sshClient, 'cat ~/.ssh/authorized_keys 2>/dev/null || echo ""');
|
|
1580
|
+
if (existing.stdout.includes(props.add)) {
|
|
1581
|
+
setStatus("success");
|
|
1582
|
+
setMessage("Key already exists (skipped)");
|
|
1583
|
+
} else {
|
|
1584
|
+
const escapedKey = props.add.replace(/'/g, "'\\''");
|
|
1585
|
+
await exec(sshClient, `echo '${escapedKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`);
|
|
1586
|
+
setStatus("success");
|
|
1587
|
+
setMessage("Key added successfully");
|
|
1588
|
+
}
|
|
1589
|
+
} else {
|
|
1590
|
+
setStatus("error");
|
|
1591
|
+
setError("Specify --add <key> or --list");
|
|
1592
|
+
}
|
|
1593
|
+
disconnect(sshClient);
|
|
1594
|
+
setTimeout(() => exit(), 100);
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
setStatus("error");
|
|
1597
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1598
|
+
if (client) disconnect(client);
|
|
1599
|
+
setTimeout(() => exit(), 100);
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
run();
|
|
1603
|
+
}, []);
|
|
1604
|
+
const operation = props.list ? "List SSH keys" : "Add SSH key";
|
|
1605
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
1606
|
+
/* @__PURE__ */ jsx6(Header, { title: "SSH Key Management", subtitle: `Host: ${props.host}` }),
|
|
1607
|
+
/* @__PURE__ */ jsx6(Task, { label: operation, status, message }),
|
|
1608
|
+
props.list && keys.length > 0 && /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
1609
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, children: "Authorized keys:" }),
|
|
1610
|
+
keys.map((key, i) => /* @__PURE__ */ jsxs6(Text6, { color: "gray", children: [
|
|
1611
|
+
" ",
|
|
1612
|
+
i + 1,
|
|
1613
|
+
". ",
|
|
1614
|
+
key
|
|
1615
|
+
] }, i))
|
|
1616
|
+
] }),
|
|
1617
|
+
error && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
|
|
1618
|
+
"Error: ",
|
|
1619
|
+
error
|
|
1620
|
+
] }) })
|
|
1621
|
+
] });
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// src/commands/status.tsx
|
|
1625
|
+
import { useState as useState5, useEffect as useEffect4 } from "react";
|
|
1626
|
+
import { Box as Box7, Text as Text7, useApp as useApp4 } from "ink";
|
|
1627
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1628
|
+
function StatusCommand(props) {
|
|
1629
|
+
const { exit } = useApp4();
|
|
1630
|
+
const [taskStatus, setTaskStatus] = useState5("pending");
|
|
1631
|
+
const [error, setError] = useState5(null);
|
|
1632
|
+
const [services, setServices] = useState5([]);
|
|
1633
|
+
const [system, setSystem] = useState5(null);
|
|
1634
|
+
useEffect4(() => {
|
|
1635
|
+
const run = async () => {
|
|
1636
|
+
setTaskStatus("running");
|
|
1637
|
+
try {
|
|
1638
|
+
const client = await connect(props);
|
|
1639
|
+
const [hostnameRes, uptimeRes, loadRes, memRes, diskRes] = await Promise.all([
|
|
1640
|
+
exec(client, "hostname"),
|
|
1641
|
+
exec(client, "uptime -p 2>/dev/null || uptime | awk -F'up' '{print $2}' | awk -F',' '{print $1}'"),
|
|
1642
|
+
exec(client, "cat /proc/loadavg | awk '{print $1, $2, $3}'"),
|
|
1643
|
+
exec(client, `free -h | awk '/^Mem:/ {print $3 "/" $2}'`),
|
|
1644
|
+
exec(client, `df -h / | awk 'NR==2 {print $3 "/" $2 " (" $5 ")"}'`)
|
|
1645
|
+
]);
|
|
1646
|
+
setSystem({
|
|
1647
|
+
hostname: hostnameRes.stdout.trim(),
|
|
1648
|
+
uptime: uptimeRes.stdout.trim(),
|
|
1649
|
+
load: loadRes.stdout.trim(),
|
|
1650
|
+
memory: memRes.stdout.trim(),
|
|
1651
|
+
disk: diskRes.stdout.trim()
|
|
1652
|
+
});
|
|
1653
|
+
const serviceChecks = ["caddy", "ssh", "ufw"];
|
|
1654
|
+
const serviceResults = [];
|
|
1655
|
+
for (const svc of serviceChecks) {
|
|
1656
|
+
const result = await exec(client, `systemctl is-active ${svc} 2>/dev/null || echo "not-found"`);
|
|
1657
|
+
const status = result.stdout.trim();
|
|
1658
|
+
serviceResults.push({
|
|
1659
|
+
name: svc,
|
|
1660
|
+
running: status === "active",
|
|
1661
|
+
status
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
const pm2Result = await exec(client, 'pm2 list 2>/dev/null | grep -E "online|stopped|errored" | wc -l || echo "0"');
|
|
1665
|
+
const pm2Count = parseInt(pm2Result.stdout.trim(), 10);
|
|
1666
|
+
serviceResults.push({
|
|
1667
|
+
name: "pm2",
|
|
1668
|
+
running: pm2Count > 0,
|
|
1669
|
+
status: pm2Count > 0 ? `${pm2Count} process(es)` : "no processes"
|
|
1670
|
+
});
|
|
1671
|
+
setServices(serviceResults);
|
|
1672
|
+
setTaskStatus("success");
|
|
1673
|
+
disconnect(client);
|
|
1674
|
+
setTimeout(() => exit(), 100);
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
setTaskStatus("error");
|
|
1677
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1678
|
+
setTimeout(() => exit(), 100);
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
run();
|
|
1682
|
+
}, []);
|
|
1683
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1684
|
+
/* @__PURE__ */ jsx7(Header, { title: "Server Status", subtitle: `Host: ${props.host}` }),
|
|
1685
|
+
/* @__PURE__ */ jsx7(Task, { label: "Checking server status", status: taskStatus }),
|
|
1686
|
+
system && /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
|
|
1687
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, children: "System:" }),
|
|
1688
|
+
/* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
|
|
1689
|
+
" Hostname: ",
|
|
1690
|
+
/* @__PURE__ */ jsx7(Text7, { color: "white", children: system.hostname })
|
|
1691
|
+
] }),
|
|
1692
|
+
/* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
|
|
1693
|
+
" Uptime: ",
|
|
1694
|
+
/* @__PURE__ */ jsx7(Text7, { color: "white", children: system.uptime })
|
|
1695
|
+
] }),
|
|
1696
|
+
/* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
|
|
1697
|
+
" Load: ",
|
|
1698
|
+
/* @__PURE__ */ jsx7(Text7, { color: "white", children: system.load })
|
|
1699
|
+
] }),
|
|
1700
|
+
/* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
|
|
1701
|
+
" Memory: ",
|
|
1702
|
+
/* @__PURE__ */ jsx7(Text7, { color: "white", children: system.memory })
|
|
1703
|
+
] }),
|
|
1704
|
+
/* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
|
|
1705
|
+
" Disk: ",
|
|
1706
|
+
/* @__PURE__ */ jsx7(Text7, { color: "white", children: system.disk })
|
|
1707
|
+
] })
|
|
1708
|
+
] }),
|
|
1709
|
+
services.length > 0 && /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
|
|
1710
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, children: "Services:" }),
|
|
1711
|
+
services.map((svc) => /* @__PURE__ */ jsxs7(Box7, { children: [
|
|
1712
|
+
/* @__PURE__ */ jsx7(Text7, { color: "gray", children: " " }),
|
|
1713
|
+
/* @__PURE__ */ jsx7(Text7, { color: svc.running ? "green" : "red", children: svc.running ? "\u25CF" : "\u25CB" }),
|
|
1714
|
+
/* @__PURE__ */ jsxs7(Text7, { children: [
|
|
1715
|
+
" ",
|
|
1716
|
+
svc.name,
|
|
1717
|
+
": "
|
|
1718
|
+
] }),
|
|
1719
|
+
/* @__PURE__ */ jsx7(Text7, { color: svc.running ? "green" : "yellow", children: svc.status })
|
|
1720
|
+
] }, svc.name))
|
|
1721
|
+
] }),
|
|
1722
|
+
error && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
|
|
1723
|
+
"Error: ",
|
|
1724
|
+
error
|
|
1725
|
+
] }) })
|
|
1726
|
+
] });
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// src/commands/deploy.tsx
|
|
1730
|
+
import { useState as useState6, useEffect as useEffect5 } from "react";
|
|
1731
|
+
import { Box as Box8, Text as Text8, useApp as useApp5 } from "ink";
|
|
1732
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1733
|
+
function DeployCommand(props) {
|
|
1734
|
+
const { exit } = useApp5();
|
|
1735
|
+
const [status, setStatus] = useState6("pending");
|
|
1736
|
+
const [output, setOutput] = useState6([]);
|
|
1737
|
+
const [error, setError] = useState6(null);
|
|
1738
|
+
useEffect5(() => {
|
|
1739
|
+
const run = async () => {
|
|
1740
|
+
setStatus("running");
|
|
1741
|
+
try {
|
|
1742
|
+
const client = await connect(props);
|
|
1743
|
+
const checkResult = await exec(client, `test -f /usr/local/bin/update-${props.name} && echo "exists" || echo "not_found"`);
|
|
1744
|
+
if (checkResult.stdout.trim() !== "exists") {
|
|
1745
|
+
throw new Error(`Update script for '${props.name}' not found. Run 'provisor app' first.`);
|
|
1746
|
+
}
|
|
1747
|
+
const result = await exec(client, `sudo /usr/local/bin/update-${props.name} 2>&1`);
|
|
1748
|
+
setOutput(result.stdout.trim().split("\n"));
|
|
1749
|
+
setStatus("success");
|
|
1750
|
+
disconnect(client);
|
|
1751
|
+
setTimeout(() => exit(), 100);
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
setStatus("error");
|
|
1754
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1755
|
+
setTimeout(() => exit(), 100);
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
run();
|
|
1759
|
+
}, []);
|
|
1760
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
1761
|
+
/* @__PURE__ */ jsx8(Header, { title: "Deploy Application", subtitle: `App: ${props.name} | Host: ${props.host}` }),
|
|
1762
|
+
/* @__PURE__ */ jsx8(Task, { label: `Deploying ${props.name}`, status }),
|
|
1763
|
+
output.length > 0 && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, flexDirection: "column", children: output.map((line, i) => /* @__PURE__ */ jsx8(Text8, { color: "gray", children: line }, i)) }),
|
|
1764
|
+
error && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text8, { color: "red", children: [
|
|
1765
|
+
"Error: ",
|
|
1766
|
+
error
|
|
1767
|
+
] }) }),
|
|
1768
|
+
status === "success" && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "green", bold: true, children: "\u2713 Deployment complete" }) })
|
|
1769
|
+
] });
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// src/commands/config.tsx
|
|
1773
|
+
import { useState as useState7, useEffect as useEffect6 } from "react";
|
|
1774
|
+
import { Box as Box9, Text as Text9, useApp as useApp6 } from "ink";
|
|
1775
|
+
import SelectInput2 from "ink-select-input";
|
|
1776
|
+
import TextInput2 from "ink-text-input";
|
|
1777
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1778
|
+
var SCRIPTS3 = {
|
|
1779
|
+
// Read current config
|
|
1780
|
+
getConfig: (name) => `
|
|
1781
|
+
CONFIG_FILE="/var/www/${name}/.provisor.json"
|
|
1782
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
1783
|
+
sudo cat "$CONFIG_FILE"
|
|
1784
|
+
else
|
|
1785
|
+
# Try to construct from git info
|
|
1786
|
+
APP_DIR="/var/www/${name}"
|
|
1787
|
+
if [ -d "$APP_DIR/.git" ]; then
|
|
1788
|
+
cd "$APP_DIR"
|
|
1789
|
+
REPO=$(git remote get-url origin 2>/dev/null || echo "")
|
|
1790
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
|
|
1791
|
+
echo '{"repo":"'"$REPO"'","branch":"'"$BRANCH"'","webhookEnabled":false,"webhookPort":0}'
|
|
1792
|
+
else
|
|
1793
|
+
echo '{"error":"App not found or not a git repository"}'
|
|
1794
|
+
fi
|
|
1795
|
+
fi
|
|
1796
|
+
`,
|
|
1797
|
+
// Save config
|
|
1798
|
+
saveConfig: (name, config) => `
|
|
1799
|
+
CONFIG_FILE="/var/www/${name}/.provisor.json"
|
|
1800
|
+
echo '${config}' > "$CONFIG_FILE"
|
|
1801
|
+
chmod 600 "$CONFIG_FILE"
|
|
1802
|
+
`,
|
|
1803
|
+
// Update repository URL
|
|
1804
|
+
updateRepo: (name, repoUrl, user) => `
|
|
1805
|
+
APP_DIR="/var/www/${name}"
|
|
1806
|
+
cd "$APP_DIR"
|
|
1807
|
+
sudo -u ${user} git remote set-url origin "${repoUrl}"
|
|
1808
|
+
echo "repo-updated"
|
|
1809
|
+
`,
|
|
1810
|
+
// Update branch
|
|
1811
|
+
updateBranch: (name, branch, user) => `
|
|
1812
|
+
APP_DIR="/var/www/${name}"
|
|
1813
|
+
cd "$APP_DIR"
|
|
1814
|
+
|
|
1815
|
+
# Fetch all branches
|
|
1816
|
+
sudo -u ${user} git fetch origin
|
|
1817
|
+
|
|
1818
|
+
# Switch to new branch
|
|
1819
|
+
sudo -u ${user} git checkout ${branch} 2>/dev/null || sudo -u ${user} git checkout -b ${branch} origin/${branch}
|
|
1820
|
+
sudo -u ${user} git reset --hard origin/${branch}
|
|
1821
|
+
|
|
1822
|
+
# Update the update script with new branch
|
|
1823
|
+
UPDATE_SCRIPT="/usr/local/bin/update-${name}"
|
|
1824
|
+
if [ -f "$UPDATE_SCRIPT" ]; then
|
|
1825
|
+
sed -i 's/BRANCH=".*"/BRANCH="${branch}"/' "$UPDATE_SCRIPT"
|
|
1826
|
+
fi
|
|
1827
|
+
|
|
1828
|
+
# Update webhook service if exists
|
|
1829
|
+
WEBHOOK_SERVICE="/etc/systemd/system/webhook-${name}.service"
|
|
1830
|
+
if [ -f "$WEBHOOK_SERVICE" ]; then
|
|
1831
|
+
sed -i 's/--branch [^ ]*/--branch ${branch}/' "$WEBHOOK_SERVICE"
|
|
1832
|
+
systemctl daemon-reload
|
|
1833
|
+
systemctl restart webhook-${name} 2>/dev/null || true
|
|
1834
|
+
fi
|
|
1835
|
+
|
|
1836
|
+
echo "branch-updated"
|
|
1837
|
+
`,
|
|
1838
|
+
// Generate new deploy key
|
|
1839
|
+
generateNewKey: (name, user) => `
|
|
1840
|
+
SSH_DIR="/home/${user}/.ssh"
|
|
1841
|
+
KEY_FILE="$SSH_DIR/deploy_${name}"
|
|
1842
|
+
|
|
1843
|
+
# Backup old key if exists
|
|
1844
|
+
if [ -f "$KEY_FILE" ]; then
|
|
1845
|
+
mv "$KEY_FILE" "$KEY_FILE.old.$(date +%s)"
|
|
1846
|
+
mv "$KEY_FILE.pub" "$KEY_FILE.pub.old.$(date +%s)"
|
|
1847
|
+
fi
|
|
1848
|
+
|
|
1849
|
+
# Generate new key
|
|
1850
|
+
ssh-keygen -t ed25519 -f "$KEY_FILE" -N "" -C "deploy-key-${name}"
|
|
1851
|
+
|
|
1852
|
+
chown ${user}:${user} "$KEY_FILE" "$KEY_FILE.pub"
|
|
1853
|
+
chmod 600 "$KEY_FILE"
|
|
1854
|
+
chmod 644 "$KEY_FILE.pub"
|
|
1855
|
+
|
|
1856
|
+
cat "$KEY_FILE.pub"
|
|
1857
|
+
`,
|
|
1858
|
+
// Delete deploy key
|
|
1859
|
+
deleteDeployKey: (name, user) => `
|
|
1860
|
+
SSH_DIR="/home/${user}/.ssh"
|
|
1861
|
+
KEY_FILE="$SSH_DIR/deploy_${name}"
|
|
1862
|
+
|
|
1863
|
+
if [ -f "$KEY_FILE" ]; then
|
|
1864
|
+
rm -f "$KEY_FILE" "$KEY_FILE.pub"
|
|
1865
|
+
|
|
1866
|
+
# Remove from SSH config
|
|
1867
|
+
if [ -f "$SSH_DIR/config" ]; then
|
|
1868
|
+
sed -i "/# Deploy key for ${name}/,+4d" "$SSH_DIR/config"
|
|
1869
|
+
fi
|
|
1870
|
+
|
|
1871
|
+
echo "key-deleted"
|
|
1872
|
+
else
|
|
1873
|
+
echo "key-not-found"
|
|
1874
|
+
fi
|
|
1875
|
+
`,
|
|
1876
|
+
// Get current deploy key
|
|
1877
|
+
getDeployKey: (name, user) => `
|
|
1878
|
+
KEY_FILE="/home/${user}/.ssh/deploy_${name}.pub"
|
|
1879
|
+
if [ -f "$KEY_FILE" ]; then
|
|
1880
|
+
cat "$KEY_FILE"
|
|
1881
|
+
else
|
|
1882
|
+
echo ""
|
|
1883
|
+
fi
|
|
1884
|
+
`,
|
|
1885
|
+
// Update webhook secret
|
|
1886
|
+
updateWebhookSecret: (name, secret) => `
|
|
1887
|
+
WEBHOOK_SERVICE="/etc/systemd/system/webhook-${name}.service"
|
|
1888
|
+
if [ -f "$WEBHOOK_SERVICE" ]; then
|
|
1889
|
+
sed -i "s/--secret '[^']*'/--secret '${secret}'/" "$WEBHOOK_SERVICE"
|
|
1890
|
+
systemctl daemon-reload
|
|
1891
|
+
systemctl restart webhook-${name}
|
|
1892
|
+
echo "webhook-secret-updated"
|
|
1893
|
+
else
|
|
1894
|
+
echo "webhook-not-configured"
|
|
1895
|
+
fi
|
|
1896
|
+
`,
|
|
1897
|
+
// Disable webhook
|
|
1898
|
+
disableWebhook: (name) => `
|
|
1899
|
+
systemctl stop webhook-${name} 2>/dev/null || true
|
|
1900
|
+
systemctl disable webhook-${name} 2>/dev/null || true
|
|
1901
|
+
rm -f /etc/systemd/system/webhook-${name}.service
|
|
1902
|
+
systemctl daemon-reload
|
|
1903
|
+
echo "webhook-disabled"
|
|
1904
|
+
`,
|
|
1905
|
+
// Get webhook status
|
|
1906
|
+
getWebhookStatus: (name) => `
|
|
1907
|
+
if systemctl is-active --quiet webhook-${name} 2>/dev/null; then
|
|
1908
|
+
PORT=$(grep -oP '\\-\\-port \\K[0-9]+' /etc/systemd/system/webhook-${name}.service 2>/dev/null || echo "")
|
|
1909
|
+
echo "running:$PORT"
|
|
1910
|
+
else
|
|
1911
|
+
echo "stopped"
|
|
1912
|
+
fi
|
|
1913
|
+
`,
|
|
1914
|
+
// Get polling status (supports both systemd and daemon modes)
|
|
1915
|
+
getPollingStatus: (name) => `
|
|
1916
|
+
# Check systemd timer first
|
|
1917
|
+
if systemctl is-active --quiet poll-${name}.timer 2>/dev/null; then
|
|
1918
|
+
INTERVAL=$(grep -oP 'OnUnitActiveSec=\\\\K[0-9]+' /etc/systemd/system/poll-${name}.timer 2>/dev/null || echo "10")
|
|
1919
|
+
echo "running:$INTERVAL:systemd"
|
|
1920
|
+
# Check daemon mode (PID file) - use ps -p instead of kill -0 (no permission issues)
|
|
1921
|
+
elif [ -f /var/run/poll-${name}.pid ] && ps -p $(cat /var/run/poll-${name}.pid) > /dev/null 2>&1; then
|
|
1922
|
+
INTERVAL=$(grep -oP 'INTERVAL=\\K[0-9]+' /usr/local/bin/poll-${name}-daemon.sh 2>/dev/null || echo "10")
|
|
1923
|
+
echo "running:$INTERVAL:daemon"
|
|
1924
|
+
else
|
|
1925
|
+
echo "stopped"
|
|
1926
|
+
fi
|
|
1927
|
+
`,
|
|
1928
|
+
// Update polling interval (supports both systemd and daemon modes)
|
|
1929
|
+
updatePollingInterval: (name, interval) => `
|
|
1930
|
+
TIMER_FILE="/etc/systemd/system/poll-${name}.timer"
|
|
1931
|
+
DAEMON_FILE="/usr/local/bin/poll-${name}-daemon.sh"
|
|
1932
|
+
|
|
1933
|
+
if [ -f "$TIMER_FILE" ]; then
|
|
1934
|
+
# Systemd mode
|
|
1935
|
+
sed -i "s/OnUnitActiveSec=[0-9]*s/OnUnitActiveSec=${interval}s/" "$TIMER_FILE"
|
|
1936
|
+
systemctl daemon-reload
|
|
1937
|
+
systemctl restart poll-${name}.timer
|
|
1938
|
+
echo "polling-interval-updated:${interval}:systemd"
|
|
1939
|
+
elif [ -f "$DAEMON_FILE" ]; then
|
|
1940
|
+
# Daemon mode - update interval and restart (all as root)
|
|
1941
|
+
sudo sed -i "s/INTERVAL=[0-9]*/INTERVAL=${interval}/" "$DAEMON_FILE"
|
|
1942
|
+
|
|
1943
|
+
# Restart daemon (use sudo bash -c for proper backgrounding)
|
|
1944
|
+
if [ -f /var/run/poll-${name}.pid ]; then
|
|
1945
|
+
sudo kill $(cat /var/run/poll-${name}.pid) 2>/dev/null || true
|
|
1946
|
+
sleep 1
|
|
1947
|
+
fi
|
|
1948
|
+
sudo bash -c 'nohup /usr/local/bin/poll-${name}-daemon.sh > /dev/null 2>&1 &'
|
|
1949
|
+
echo "polling-interval-updated:${interval}:daemon"
|
|
1950
|
+
else
|
|
1951
|
+
echo "polling-not-configured"
|
|
1952
|
+
fi
|
|
1953
|
+
`,
|
|
1954
|
+
// Enable polling (supports both systemd and daemon modes)
|
|
1955
|
+
enablePolling: (name, branch, user, interval) => `
|
|
1956
|
+
# Check if systemd is available
|
|
1957
|
+
if pidof systemd > /dev/null 2>&1 || [ "$(cat /proc/1/comm 2>/dev/null)" = "systemd" ]; then
|
|
1958
|
+
# SYSTEMD MODE
|
|
1959
|
+
if [ -f "/etc/systemd/system/poll-${name}.timer" ]; then
|
|
1960
|
+
systemctl enable poll-${name}.timer
|
|
1961
|
+
systemctl start poll-${name}.timer
|
|
1962
|
+
echo "polling-enabled:systemd"
|
|
1963
|
+
else
|
|
1964
|
+
# Create from scratch (same as app.tsx)
|
|
1965
|
+
cat << 'POLLING_EOF' > /usr/local/bin/poll-${name}.sh
|
|
1966
|
+
#!/bin/bash
|
|
1967
|
+
set -e
|
|
1968
|
+
APP_DIR="/var/www/${name}"
|
|
1969
|
+
BRANCH="${branch}"
|
|
1970
|
+
USER="${user}"
|
|
1971
|
+
LOCK_FILE="/tmp/poll-${name}.lock"
|
|
1972
|
+
exec 200>"$LOCK_FILE"
|
|
1973
|
+
flock -n 200 || exit 0
|
|
1974
|
+
cd "$APP_DIR"
|
|
1975
|
+
sudo -u $USER git fetch origin "$BRANCH" --quiet 2>/dev/null
|
|
1976
|
+
LOCAL=$(git rev-parse HEAD)
|
|
1977
|
+
REMOTE=$(git rev-parse "origin/$BRANCH")
|
|
1978
|
+
if [ "$LOCAL" != "$REMOTE" ]; then
|
|
1979
|
+
echo "[$(date -Iseconds)] New commit detected: $REMOTE"
|
|
1980
|
+
sudo /usr/local/bin/update-${name}
|
|
1981
|
+
else
|
|
1982
|
+
echo "[$(date -Iseconds)] No changes"
|
|
1983
|
+
fi
|
|
1984
|
+
POLLING_EOF
|
|
1985
|
+
chmod +x /usr/local/bin/poll-${name}.sh
|
|
1986
|
+
|
|
1987
|
+
cat << SERVICE_EOF > /etc/systemd/system/poll-${name}.service
|
|
1988
|
+
[Unit]
|
|
1989
|
+
Description=Git polling service for ${name}
|
|
1990
|
+
After=network.target
|
|
1991
|
+
[Service]
|
|
1992
|
+
Type=oneshot
|
|
1993
|
+
ExecStart=/usr/local/bin/poll-${name}.sh
|
|
1994
|
+
StandardOutput=journal
|
|
1995
|
+
StandardError=journal
|
|
1996
|
+
SyslogIdentifier=poll-${name}
|
|
1997
|
+
SERVICE_EOF
|
|
1998
|
+
|
|
1999
|
+
cat << TIMER_EOF > /etc/systemd/system/poll-${name}.timer
|
|
2000
|
+
[Unit]
|
|
2001
|
+
Description=Run git polling for ${name} every ${interval} seconds
|
|
2002
|
+
[Timer]
|
|
2003
|
+
OnBootSec=30
|
|
2004
|
+
OnUnitActiveSec=${interval}s
|
|
2005
|
+
AccuracySec=1s
|
|
2006
|
+
[Install]
|
|
2007
|
+
WantedBy=timers.target
|
|
2008
|
+
TIMER_EOF
|
|
2009
|
+
|
|
2010
|
+
systemctl daemon-reload
|
|
2011
|
+
systemctl enable poll-${name}.timer
|
|
2012
|
+
systemctl start poll-${name}.timer
|
|
2013
|
+
echo "polling-created:systemd"
|
|
2014
|
+
fi
|
|
2015
|
+
else
|
|
2016
|
+
# DAEMON MODE (Docker/non-systemd)
|
|
2017
|
+
if [ -f /var/run/poll-${name}.pid ] && kill -0 $(cat /var/run/poll-${name}.pid) 2>/dev/null; then
|
|
2018
|
+
echo "polling-already-running:daemon"
|
|
2019
|
+
else
|
|
2020
|
+
# Create poll script if not exists
|
|
2021
|
+
if [ ! -f /usr/local/bin/poll-${name}.sh ]; then
|
|
2022
|
+
cat << 'POLLING_EOF' > /usr/local/bin/poll-${name}.sh
|
|
2023
|
+
#!/bin/bash
|
|
2024
|
+
set -e
|
|
2025
|
+
APP_DIR="/var/www/${name}"
|
|
2026
|
+
BRANCH="${branch}"
|
|
2027
|
+
USER="${user}"
|
|
2028
|
+
LOCK_FILE="/tmp/poll-${name}.lock"
|
|
2029
|
+
exec 200>"$LOCK_FILE"
|
|
2030
|
+
flock -n 200 || exit 0
|
|
2031
|
+
cd "$APP_DIR"
|
|
2032
|
+
sudo -u $USER git fetch origin "$BRANCH" --quiet 2>/dev/null
|
|
2033
|
+
LOCAL=$(git rev-parse HEAD)
|
|
2034
|
+
REMOTE=$(git rev-parse "origin/$BRANCH")
|
|
2035
|
+
if [ "$LOCAL" != "$REMOTE" ]; then
|
|
2036
|
+
echo "[$(date -Iseconds)] New commit detected: $REMOTE"
|
|
2037
|
+
sudo /usr/local/bin/update-${name}
|
|
2038
|
+
else
|
|
2039
|
+
echo "[$(date -Iseconds)] No changes"
|
|
2040
|
+
fi
|
|
2041
|
+
POLLING_EOF
|
|
2042
|
+
chmod +x /usr/local/bin/poll-${name}.sh
|
|
2043
|
+
fi
|
|
2044
|
+
|
|
2045
|
+
# Create daemon script
|
|
2046
|
+
cat << DAEMON_EOF > /usr/local/bin/poll-${name}-daemon.sh
|
|
2047
|
+
#!/bin/bash
|
|
2048
|
+
LOG_FILE="/var/log/poll-${name}.log"
|
|
2049
|
+
PID_FILE="/var/run/poll-${name}.pid"
|
|
2050
|
+
INTERVAL=${interval}
|
|
2051
|
+
echo $$ > "$PID_FILE"
|
|
2052
|
+
echo "[$(date -Iseconds)] Polling daemon started (every ${interval}s)" >> "$LOG_FILE"
|
|
2053
|
+
while true; do
|
|
2054
|
+
/usr/local/bin/poll-${name}.sh >> "$LOG_FILE" 2>&1
|
|
2055
|
+
sleep $INTERVAL
|
|
2056
|
+
done
|
|
2057
|
+
DAEMON_EOF
|
|
2058
|
+
chmod +x /usr/local/bin/poll-${name}-daemon.sh
|
|
2059
|
+
|
|
2060
|
+
mkdir -p /var/log
|
|
2061
|
+
touch /var/log/poll-${name}.log
|
|
2062
|
+
nohup /usr/local/bin/poll-${name}-daemon.sh > /dev/null 2>&1 &
|
|
2063
|
+
echo "polling-created:daemon"
|
|
2064
|
+
fi
|
|
2065
|
+
fi
|
|
2066
|
+
`,
|
|
2067
|
+
// Disable polling (supports both systemd and daemon modes)
|
|
2068
|
+
disablePolling: (name) => `
|
|
2069
|
+
# Stop systemd timer if exists
|
|
2070
|
+
systemctl stop poll-${name}.timer 2>/dev/null || true
|
|
2071
|
+
systemctl disable poll-${name}.timer 2>/dev/null || true
|
|
2072
|
+
rm -f /etc/systemd/system/poll-${name}.timer
|
|
2073
|
+
rm -f /etc/systemd/system/poll-${name}.service
|
|
2074
|
+
systemctl daemon-reload 2>/dev/null || true
|
|
2075
|
+
|
|
2076
|
+
# Stop daemon if running (use sudo since daemon runs as root)
|
|
2077
|
+
if [ -f /var/run/poll-${name}.pid ]; then
|
|
2078
|
+
sudo kill $(cat /var/run/poll-${name}.pid) 2>/dev/null || true
|
|
2079
|
+
rm -f /var/run/poll-${name}.pid
|
|
2080
|
+
fi
|
|
2081
|
+
|
|
2082
|
+
# Clean up all polling files
|
|
2083
|
+
rm -f /usr/local/bin/poll-${name}.sh
|
|
2084
|
+
rm -f /usr/local/bin/poll-${name}-daemon.sh
|
|
2085
|
+
|
|
2086
|
+
echo "polling-disabled"
|
|
2087
|
+
`
|
|
2088
|
+
};
|
|
2089
|
+
var configActions = [
|
|
2090
|
+
{ label: "Show current configuration", value: "show" },
|
|
2091
|
+
{ label: "Change repository URL", value: "repo" },
|
|
2092
|
+
{ label: "Change deploy branch", value: "branch" },
|
|
2093
|
+
{ label: "Generate new deploy key", value: "new-key" },
|
|
2094
|
+
{ label: "Delete deploy key", value: "delete-key" },
|
|
2095
|
+
{ label: "Update webhook secret", value: "webhook-secret" },
|
|
2096
|
+
{ label: "Disable webhook", value: "disable-webhook" },
|
|
2097
|
+
{ label: "Change polling interval", value: "polling-interval" },
|
|
2098
|
+
{ label: "Enable git polling", value: "enable-polling" },
|
|
2099
|
+
{ label: "Disable git polling", value: "disable-polling" }
|
|
2100
|
+
];
|
|
2101
|
+
function ConfigCommand(props) {
|
|
2102
|
+
const { exit } = useApp6();
|
|
2103
|
+
const [client, setClient] = useState7(null);
|
|
2104
|
+
const [status, setStatus] = useState7("pending");
|
|
2105
|
+
const [error, setError] = useState7(null);
|
|
2106
|
+
const [output, setOutput] = useState7([]);
|
|
2107
|
+
const [selectingAction, setSelectingAction] = useState7(false);
|
|
2108
|
+
const [action, setAction] = useState7(null);
|
|
2109
|
+
const [enteringValue, setEnteringValue] = useState7(false);
|
|
2110
|
+
const [inputValue, setInputValue] = useState7("");
|
|
2111
|
+
const [inputLabel, setInputLabel] = useState7("");
|
|
2112
|
+
const [config, setConfig] = useState7(null);
|
|
2113
|
+
const [deployKey, setDeployKey] = useState7(null);
|
|
2114
|
+
const [webhookStatus, setWebhookStatus] = useState7("");
|
|
2115
|
+
useEffect6(() => {
|
|
2116
|
+
if (props.show) setAction("show");
|
|
2117
|
+
else if (props.repo) {
|
|
2118
|
+
setAction("repo");
|
|
2119
|
+
setInputValue(props.repo);
|
|
2120
|
+
} else if (props.branch) {
|
|
2121
|
+
setAction("branch");
|
|
2122
|
+
setInputValue(props.branch);
|
|
2123
|
+
} else if (props.newKey) setAction("new-key");
|
|
2124
|
+
else if (props.deleteKey) setAction("delete-key");
|
|
2125
|
+
else if (props.webhookSecret) {
|
|
2126
|
+
setAction("webhook-secret");
|
|
2127
|
+
setInputValue(props.webhookSecret);
|
|
2128
|
+
} else if (props.disableWebhook) setAction("disable-webhook");
|
|
2129
|
+
else if (props.pollingInterval) {
|
|
2130
|
+
setAction("polling-interval");
|
|
2131
|
+
setInputValue(String(props.pollingInterval));
|
|
2132
|
+
} else if (props.enablePolling) setAction("enable-polling");
|
|
2133
|
+
else if (props.disablePolling) setAction("disable-polling");
|
|
2134
|
+
}, []);
|
|
2135
|
+
useEffect6(() => {
|
|
2136
|
+
const run = async () => {
|
|
2137
|
+
setStatus("running");
|
|
2138
|
+
try {
|
|
2139
|
+
const sshClient = await connect(props);
|
|
2140
|
+
setClient(sshClient);
|
|
2141
|
+
setStatus("success");
|
|
2142
|
+
} catch (err) {
|
|
2143
|
+
setStatus("error");
|
|
2144
|
+
setError(`Connection failed: ${err instanceof Error ? err.message : err}`);
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
run();
|
|
2148
|
+
}, []);
|
|
2149
|
+
useEffect6(() => {
|
|
2150
|
+
if (status === "success" && action === null && !selectingAction) {
|
|
2151
|
+
setSelectingAction(true);
|
|
2152
|
+
}
|
|
2153
|
+
}, [status, action]);
|
|
2154
|
+
const handleActionSelect = (item) => {
|
|
2155
|
+
setSelectingAction(false);
|
|
2156
|
+
const selectedAction = item.value;
|
|
2157
|
+
setAction(selectedAction);
|
|
2158
|
+
if (selectedAction === "repo") {
|
|
2159
|
+
setInputLabel("Enter new repository URL:");
|
|
2160
|
+
setEnteringValue(true);
|
|
2161
|
+
} else if (selectedAction === "branch") {
|
|
2162
|
+
setInputLabel("Enter new branch name:");
|
|
2163
|
+
setEnteringValue(true);
|
|
2164
|
+
} else if (selectedAction === "webhook-secret") {
|
|
2165
|
+
setInputLabel("Enter new webhook secret:");
|
|
2166
|
+
setEnteringValue(true);
|
|
2167
|
+
} else if (selectedAction === "polling-interval") {
|
|
2168
|
+
setInputLabel("Enter polling interval in seconds (e.g., 10, 30, 60):");
|
|
2169
|
+
setEnteringValue(true);
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
const handleInputSubmit = (value) => {
|
|
2173
|
+
setInputValue(value);
|
|
2174
|
+
setEnteringValue(false);
|
|
2175
|
+
};
|
|
2176
|
+
useEffect6(() => {
|
|
2177
|
+
if (!client || action === null || enteringValue) return;
|
|
2178
|
+
if ((action === "repo" || action === "branch" || action === "webhook-secret" || action === "polling-interval") && !inputValue) return;
|
|
2179
|
+
const run = async () => {
|
|
2180
|
+
try {
|
|
2181
|
+
const user = props.user || "deploy";
|
|
2182
|
+
const lines = [];
|
|
2183
|
+
switch (action) {
|
|
2184
|
+
case "show": {
|
|
2185
|
+
const configResult = await exec(client, SCRIPTS3.getConfig(props.name));
|
|
2186
|
+
const configData = JSON.parse(configResult.stdout.trim());
|
|
2187
|
+
if (configData.error) {
|
|
2188
|
+
setError(configData.error);
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
const keyResult = await exec(client, SCRIPTS3.getDeployKey(props.name, user));
|
|
2192
|
+
const webhookResult = await exec(client, SCRIPTS3.getWebhookStatus(props.name));
|
|
2193
|
+
const pollingResult = await exec(client, SCRIPTS3.getPollingStatus(props.name));
|
|
2194
|
+
lines.push(`Repository: ${configData.repo || "Not configured"}`);
|
|
2195
|
+
lines.push(`Branch: ${configData.branch || "main"}`);
|
|
2196
|
+
lines.push(`Deploy Key: ${keyResult.stdout.trim() ? "Configured" : "Not configured"}`);
|
|
2197
|
+
const webhookStatusStr = webhookResult.stdout.trim();
|
|
2198
|
+
if (webhookStatusStr.startsWith("running:")) {
|
|
2199
|
+
const port = webhookStatusStr.split(":")[1];
|
|
2200
|
+
lines.push(`Webhook: Running on port ${port}`);
|
|
2201
|
+
} else {
|
|
2202
|
+
lines.push("Webhook: Not configured");
|
|
2203
|
+
}
|
|
2204
|
+
const pollingStatusStr = pollingResult.stdout.trim();
|
|
2205
|
+
if (pollingStatusStr.startsWith("running:")) {
|
|
2206
|
+
const parts = pollingStatusStr.split(":");
|
|
2207
|
+
const interval = parts[1];
|
|
2208
|
+
const mode = parts[2] || "systemd";
|
|
2209
|
+
lines.push(`Git Polling: Running (every ${interval}s, ${mode} mode)`);
|
|
2210
|
+
} else {
|
|
2211
|
+
lines.push("Git Polling: Not configured");
|
|
2212
|
+
}
|
|
2213
|
+
if (keyResult.stdout.trim()) {
|
|
2214
|
+
lines.push("");
|
|
2215
|
+
lines.push("Public Key:");
|
|
2216
|
+
lines.push(keyResult.stdout.trim());
|
|
2217
|
+
}
|
|
2218
|
+
break;
|
|
2219
|
+
}
|
|
2220
|
+
case "repo": {
|
|
2221
|
+
await execScript(client, SCRIPTS3.updateRepo(props.name, inputValue, user), true);
|
|
2222
|
+
lines.push(`Repository updated to: ${inputValue}`);
|
|
2223
|
+
break;
|
|
2224
|
+
}
|
|
2225
|
+
case "branch": {
|
|
2226
|
+
await execScript(client, SCRIPTS3.updateBranch(props.name, inputValue, user), true);
|
|
2227
|
+
lines.push(`Branch updated to: ${inputValue}`);
|
|
2228
|
+
lines.push("Run update command to deploy from new branch.");
|
|
2229
|
+
break;
|
|
2230
|
+
}
|
|
2231
|
+
case "new-key": {
|
|
2232
|
+
const result = await execScript(client, SCRIPTS3.generateNewKey(props.name, user), true);
|
|
2233
|
+
lines.push("New deploy key generated:");
|
|
2234
|
+
lines.push("");
|
|
2235
|
+
lines.push(result.stdout.trim());
|
|
2236
|
+
lines.push("");
|
|
2237
|
+
lines.push("Add this key to your repository settings.");
|
|
2238
|
+
lines.push("Remember to remove the old key if it was configured.");
|
|
2239
|
+
break;
|
|
2240
|
+
}
|
|
2241
|
+
case "delete-key": {
|
|
2242
|
+
const result = await execScript(client, SCRIPTS3.deleteDeployKey(props.name, user), true);
|
|
2243
|
+
if (result.stdout.includes("key-deleted")) {
|
|
2244
|
+
lines.push("Deploy key deleted successfully.");
|
|
2245
|
+
lines.push("Remember to remove it from your repository settings too.");
|
|
2246
|
+
} else {
|
|
2247
|
+
lines.push("No deploy key found for this app.");
|
|
2248
|
+
}
|
|
2249
|
+
break;
|
|
2250
|
+
}
|
|
2251
|
+
case "webhook-secret": {
|
|
2252
|
+
const result = await execScript(client, SCRIPTS3.updateWebhookSecret(props.name, inputValue), true);
|
|
2253
|
+
if (result.stdout.includes("webhook-secret-updated")) {
|
|
2254
|
+
lines.push("Webhook secret updated.");
|
|
2255
|
+
lines.push("Update the secret in your repository webhook settings too.");
|
|
2256
|
+
} else {
|
|
2257
|
+
lines.push("Webhook not configured for this app.");
|
|
2258
|
+
}
|
|
2259
|
+
break;
|
|
2260
|
+
}
|
|
2261
|
+
case "disable-webhook": {
|
|
2262
|
+
await execScript(client, SCRIPTS3.disableWebhook(props.name), true);
|
|
2263
|
+
lines.push("Webhook disabled.");
|
|
2264
|
+
break;
|
|
2265
|
+
}
|
|
2266
|
+
case "polling-interval": {
|
|
2267
|
+
const interval = parseInt(inputValue, 10);
|
|
2268
|
+
if (isNaN(interval) || interval < 1) {
|
|
2269
|
+
setError("Invalid interval. Please enter a number >= 1.");
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
const result = await execScript(client, SCRIPTS3.updatePollingInterval(props.name, interval), true);
|
|
2273
|
+
if (result.stdout.includes("polling-interval-updated")) {
|
|
2274
|
+
lines.push(`Polling interval updated to ${interval} seconds.`);
|
|
2275
|
+
} else {
|
|
2276
|
+
lines.push("Git polling not configured for this app.");
|
|
2277
|
+
lines.push('Use "Enable git polling" to set it up first.');
|
|
2278
|
+
}
|
|
2279
|
+
break;
|
|
2280
|
+
}
|
|
2281
|
+
case "enable-polling": {
|
|
2282
|
+
const configResult = await exec(client, SCRIPTS3.getConfig(props.name));
|
|
2283
|
+
const configData = JSON.parse(configResult.stdout.trim());
|
|
2284
|
+
const branch = configData.branch || "main";
|
|
2285
|
+
const interval = 10;
|
|
2286
|
+
const result = await execScript(client, SCRIPTS3.enablePolling(props.name, branch, user, interval), true);
|
|
2287
|
+
if (result.stdout.includes("polling-enabled")) {
|
|
2288
|
+
lines.push("Git polling re-enabled.");
|
|
2289
|
+
lines.push(`Checking for updates every ${interval} seconds.`);
|
|
2290
|
+
} else if (result.stdout.includes("polling-created")) {
|
|
2291
|
+
lines.push("Git polling enabled.");
|
|
2292
|
+
lines.push(`Checking for updates every ${interval} seconds.`);
|
|
2293
|
+
}
|
|
2294
|
+
lines.push("");
|
|
2295
|
+
lines.push(`View logs: journalctl -u poll-${props.name} -f`);
|
|
2296
|
+
break;
|
|
2297
|
+
}
|
|
2298
|
+
case "disable-polling": {
|
|
2299
|
+
await execScript(client, SCRIPTS3.disablePolling(props.name), true);
|
|
2300
|
+
lines.push("Git polling disabled.");
|
|
2301
|
+
break;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
setOutput(lines);
|
|
2305
|
+
disconnect(client);
|
|
2306
|
+
setTimeout(() => exit(), 100);
|
|
2307
|
+
} catch (err) {
|
|
2308
|
+
setError(`Operation failed: ${err instanceof Error ? err.message : err}`);
|
|
2309
|
+
if (client) disconnect(client);
|
|
2310
|
+
setTimeout(() => exit(), 100);
|
|
2311
|
+
}
|
|
2312
|
+
};
|
|
2313
|
+
run();
|
|
2314
|
+
}, [client, action, inputValue, enteringValue]);
|
|
2315
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
2316
|
+
/* @__PURE__ */ jsx9(Header, { title: "App Configuration", subtitle: `App: ${props.name} | Host: ${props.host}` }),
|
|
2317
|
+
/* @__PURE__ */ jsx9(Task, { label: "Connect to server", status }),
|
|
2318
|
+
selectingAction && action === null && /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexDirection: "column", children: [
|
|
2319
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, children: "Select action:" }),
|
|
2320
|
+
/* @__PURE__ */ jsx9(SelectInput2, { items: configActions, onSelect: handleActionSelect })
|
|
2321
|
+
] }),
|
|
2322
|
+
enteringValue && /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexDirection: "column", children: [
|
|
2323
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, children: inputLabel }),
|
|
2324
|
+
/* @__PURE__ */ jsxs9(Box9, { children: [
|
|
2325
|
+
/* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "> " }),
|
|
2326
|
+
/* @__PURE__ */ jsx9(TextInput2, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit })
|
|
2327
|
+
] })
|
|
2328
|
+
] }),
|
|
2329
|
+
output.length > 0 && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, flexDirection: "column", children: output.map((line, i) => /* @__PURE__ */ jsx9(Text9, { color: line.startsWith("ssh-") ? "cyan" : "white", children: line }, i)) }),
|
|
2330
|
+
error && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
|
|
2331
|
+
"Error: ",
|
|
2332
|
+
error
|
|
2333
|
+
] }) })
|
|
2334
|
+
] });
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/cli.tsx
|
|
2338
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
2339
|
+
program.name("provisor").description("Server provisioning and deployment CLI").version("0.1.0");
|
|
2340
|
+
program.command("init").description("Initialize server with user, SSH, and firewall setup").requiredOption("-h, --host <host>", "Server hostname or IP").option("-u, --user <user>", "Username to create", "deploy").option("-k, --key <path>", "Path to SSH private key for root access").option("-p, --port <port>", "SSH port", "22").action((options) => {
|
|
2341
|
+
render(/* @__PURE__ */ jsx10(InitCommand, { ...options }));
|
|
2342
|
+
});
|
|
2343
|
+
program.command("app").description("Provision application (Caddy, Node.js, Git deploy)").requiredOption("-h, --host <host>", "Server hostname or IP").option("-u, --user <user>", "Username to connect as", "deploy").option("-k, --key <path>", "Path to SSH private key").option("-p, --port <port>", "SSH port", "22").option("-b, --branch <branch>", "Deploy branch", "main").option("-n, --name <name>", "Application name", "app").option("-r, --repo <url>", "Clone from repository URL (GitHub, GitLab, etc.)").action((options) => {
|
|
2344
|
+
render(/* @__PURE__ */ jsx10(AppCommand, { ...options }));
|
|
2345
|
+
});
|
|
2346
|
+
program.command("ssh-key").description("Manage SSH keys on the server").requiredOption("-h, --host <host>", "Server hostname or IP").option("-u, --user <user>", "Username to connect as", "deploy").option("-k, --key <path>", "Path to SSH private key").option("-p, --port <port>", "SSH port", "22").option("--add <pubkey>", "Add a new public key").option("--list", "List authorized keys").action((options) => {
|
|
2347
|
+
render(/* @__PURE__ */ jsx10(SshKeyCommand, { ...options }));
|
|
2348
|
+
});
|
|
2349
|
+
program.command("status").description("Check server status and services").requiredOption("-h, --host <host>", "Server hostname or IP").option("-u, --user <user>", "Username to connect as", "deploy").option("-k, --key <path>", "Path to SSH private key").option("-p, --port <port>", "SSH port", "22").action((options) => {
|
|
2350
|
+
render(/* @__PURE__ */ jsx10(StatusCommand, { ...options }));
|
|
2351
|
+
});
|
|
2352
|
+
program.command("deploy").description("Trigger deployment for an application").requiredOption("-h, --host <host>", "Server hostname or IP").requiredOption("-n, --name <name>", "Application name").option("-u, --user <user>", "Username to connect as", "deploy").option("-k, --key <path>", "Path to SSH private key").option("-p, --port <port>", "SSH port", "22").action((options) => {
|
|
2353
|
+
render(/* @__PURE__ */ jsx10(DeployCommand, { ...options }));
|
|
2354
|
+
});
|
|
2355
|
+
program.command("config").description("Manage application configuration").requiredOption("-h, --host <host>", "Server hostname or IP").requiredOption("-n, --name <name>", "Application name").option("-u, --user <user>", "Username to connect as", "deploy").option("-k, --key <path>", "Path to SSH private key").option("-p, --port <port>", "SSH port", "22").option("--show", "Show current configuration").option("--repo <url>", "Change repository URL").option("--branch <branch>", "Change deploy branch").option("--new-key", "Generate new deploy key").option("--delete-key", "Delete deploy key").option("--webhook-secret <secret>", "Update webhook secret").option("--disable-webhook", "Disable webhook").option("--polling-interval <seconds>", "Set git polling interval in seconds", parseInt).option("--enable-polling", "Enable git polling for auto-deploy").option("--disable-polling", "Disable git polling").action((options) => {
|
|
2356
|
+
render(/* @__PURE__ */ jsx10(ConfigCommand, { ...options }));
|
|
2357
|
+
});
|
|
2358
|
+
program.parse();
|
|
2359
|
+
//# sourceMappingURL=cli.js.map
|