@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/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