@ongtrieuhau861457/runner-tailscale-sync 1.260202.11920

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.
@@ -0,0 +1,185 @@
1
+ /**
2
+ * adapters/git.js
3
+ * Git operations: push, pull, conflict resolution
4
+ */
5
+
6
+ const process_adapter = require("./process");
7
+ const { ProcessError } = require("../utils/errors");
8
+ const CONST = require("../utils/constants");
9
+
10
+ /**
11
+ * Check if git is available
12
+ */
13
+ function isAvailable() {
14
+ return process_adapter.commandExists("git");
15
+ }
16
+
17
+ /**
18
+ * Check if current directory is a git repo
19
+ */
20
+ function isGitRepo(cwd) {
21
+ try {
22
+ const output = process_adapter.runCapture("git rev-parse --git-dir", { cwd });
23
+ return output !== null;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Add files to git
31
+ */
32
+ function add(files, options = {}) {
33
+ const { logger, cwd } = options;
34
+
35
+ const filesStr = Array.isArray(files) ? files.join(" ") : files;
36
+ const cmd = `git add ${filesStr}`;
37
+
38
+ try {
39
+ process_adapter.run(cmd, { logger, cwd, ignoreError: false });
40
+ return true;
41
+ } catch (err) {
42
+ throw new ProcessError(`Git add failed: ${err.message}`);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Commit changes
48
+ */
49
+ function commit(message, options = {}) {
50
+ const { logger, cwd } = options;
51
+
52
+ const safeMessage = message.replace(/"/g, '\\"');
53
+ const cmd = `git commit -m "${safeMessage}"`;
54
+
55
+ try {
56
+ process_adapter.run(cmd, { logger, cwd, ignoreError: false });
57
+ return true;
58
+ } catch (err) {
59
+ // No changes to commit is OK
60
+ if (err.message && err.message.includes("nothing to commit")) {
61
+ if (logger) {
62
+ logger.debug("No changes to commit");
63
+ }
64
+ return false;
65
+ }
66
+ throw new ProcessError(`Git commit failed: ${err.message}`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Push to remote
72
+ */
73
+ async function push(branch, options = {}) {
74
+ const { logger, cwd, retries = CONST.GIT_RETRY_COUNT } = options;
75
+
76
+ const cmd = `git push origin ${branch}`;
77
+
78
+ for (let attempt = 1; attempt <= retries; attempt++) {
79
+ try {
80
+ process_adapter.run(cmd, { logger, cwd, ignoreError: false });
81
+ if (logger) {
82
+ logger.success(`Pushed to ${branch}`);
83
+ }
84
+ return true;
85
+ } catch (err) {
86
+ if (attempt < retries) {
87
+ if (logger) {
88
+ logger.warn(`Push failed (attempt ${attempt}/${retries}), retrying...`);
89
+ }
90
+ await process_adapter.sleep(CONST.GIT_RETRY_DELAY);
91
+ } else {
92
+ throw new ProcessError(`Git push failed after ${retries} attempts: ${err.message}`);
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Pull from remote
100
+ */
101
+ async function pull(branch, options = {}) {
102
+ const { logger, cwd, retries = CONST.GIT_RETRY_COUNT } = options;
103
+
104
+ const cmd = `git pull origin ${branch}`;
105
+
106
+ for (let attempt = 1; attempt <= retries; attempt++) {
107
+ try {
108
+ process_adapter.run(cmd, { logger, cwd, ignoreError: false });
109
+ if (logger) {
110
+ logger.success(`Pulled from ${branch}`);
111
+ }
112
+ return true;
113
+ } catch (err) {
114
+ if (attempt < retries) {
115
+ if (logger) {
116
+ logger.warn(`Pull failed (attempt ${attempt}/${retries}), retrying...`);
117
+ }
118
+ await process_adapter.sleep(CONST.GIT_RETRY_DELAY);
119
+ } else {
120
+ throw new ProcessError(`Git pull failed after ${retries} attempts: ${err.message}`);
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Get current branch
128
+ */
129
+ function getCurrentBranch(cwd) {
130
+ try {
131
+ return process_adapter.runCapture("git rev-parse --abbrev-ref HEAD", { cwd });
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Check if there are uncommitted changes
139
+ */
140
+ function hasUncommittedChanges(cwd) {
141
+ try {
142
+ const output = process_adapter.runCapture("git status --porcelain", { cwd });
143
+ return output && output.length > 0;
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Full workflow: add, commit, push
151
+ */
152
+ async function commitAndPush(message, branch, options = {}) {
153
+ const { logger, cwd } = options;
154
+
155
+ if (!hasUncommittedChanges(cwd)) {
156
+ if (logger) {
157
+ logger.info("No changes to commit");
158
+ }
159
+ return false;
160
+ }
161
+
162
+ // Add all changes in .runner-data
163
+ add(".runner-data/*", { logger, cwd });
164
+
165
+ // Commit
166
+ const committed = commit(message, { logger, cwd });
167
+ if (!committed) return false;
168
+
169
+ // Push
170
+ await push(branch, { logger, cwd });
171
+
172
+ return true;
173
+ }
174
+
175
+ module.exports = {
176
+ isAvailable,
177
+ isGitRepo,
178
+ add,
179
+ commit,
180
+ push,
181
+ pull,
182
+ getCurrentBranch,
183
+ hasUncommittedChanges,
184
+ commitAndPush,
185
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * adapters/http.js
3
+ * HTTP fetch with timeout + retry
4
+ */
5
+
6
+ const { NetworkError } = require("../utils/errors");
7
+
8
+ async function fetchWithTimeout(url, options = {}) {
9
+ const { timeoutMs = 10000, ...rest } = options;
10
+ const controller = new AbortController();
11
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
12
+
13
+ try {
14
+ const response = await fetch(url, {
15
+ ...rest,
16
+ signal: controller.signal,
17
+ });
18
+ return response;
19
+ } catch (err) {
20
+ if (err.name === "AbortError") {
21
+ throw new NetworkError(`Request timeout after ${timeoutMs}ms: ${url}`);
22
+ }
23
+ throw new NetworkError(`Request failed: ${err.message}`);
24
+ } finally {
25
+ clearTimeout(timer);
26
+ }
27
+ }
28
+
29
+ async function fetchWithRetry(url, options = {}) {
30
+ const {
31
+ retries = 3,
32
+ retryDelayMs = 1000,
33
+ timeoutMs = 10000,
34
+ ...rest
35
+ } = options;
36
+
37
+ let lastError;
38
+
39
+ for (let attempt = 1; attempt <= retries; attempt += 1) {
40
+ try {
41
+ return await fetchWithTimeout(url, { ...rest, timeoutMs });
42
+ } catch (err) {
43
+ lastError = err;
44
+ if (attempt < retries) {
45
+ await new Promise(resolve => setTimeout(resolve, retryDelayMs));
46
+ }
47
+ }
48
+ }
49
+
50
+ throw lastError || new NetworkError(`Request failed after ${retries} attempts: ${url}`);
51
+ }
52
+
53
+ module.exports = {
54
+ fetchWithTimeout,
55
+ fetchWithRetry,
56
+ };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * adapters/process.js
3
+ * Cross-platform process spawning
4
+ */
5
+
6
+ const { execSync, spawn } = require("child_process");
7
+ const os = require("os");
8
+
9
+ const isWindows = os.platform() === "win32";
10
+
11
+ /**
12
+ * Run command và wait for completion
13
+ */
14
+ function run(cmd, options = {}) {
15
+ const { ignoreError = false, cwd, logger } = options;
16
+
17
+ if (logger) {
18
+ logger.command(cmd);
19
+ }
20
+
21
+ try {
22
+ return execSync(cmd, {
23
+ stdio: "inherit",
24
+ cwd: cwd || process.cwd(),
25
+ ...options,
26
+ });
27
+ } catch (err) {
28
+ if (ignoreError) {
29
+ if (logger) {
30
+ logger.warn(`Command failed (ignored): ${cmd}`);
31
+ }
32
+ return null;
33
+ }
34
+ throw err;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Run command và capture output
40
+ */
41
+ function runCapture(cmd, options = {}) {
42
+ const { cwd } = options;
43
+
44
+ try {
45
+ return execSync(cmd, {
46
+ stdio: ["ignore", "pipe", "pipe"],
47
+ encoding: "utf8",
48
+ cwd: cwd || process.cwd(),
49
+ ...options,
50
+ }).trim();
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Check if command exists
58
+ */
59
+ function commandExists(cmd) {
60
+ const check = isWindows ? `where ${cmd}` : `command -v ${cmd}`;
61
+ return !!runCapture(check);
62
+ }
63
+
64
+ /**
65
+ * Run command with timeout
66
+ */
67
+ function runWithTimeout(cmd, timeoutMs, options = {}) {
68
+ const { logger } = options;
69
+
70
+ // Support both string commands and argv arrays.
71
+ // - If cmd is an array: [exe, ...args] -> spawn(exe, args)
72
+ // - If cmd is a string: spawn(cmd, { shell: true }) so quoting works cross-platform
73
+ const useArray = Array.isArray(cmd);
74
+
75
+ return new Promise((resolve, reject) => {
76
+ const spawnOptions = {
77
+ stdio: "inherit",
78
+ cwd: options.cwd || process.cwd(),
79
+ detached: !isWindows,
80
+ };
81
+ const child = useArray
82
+ ? spawn(cmd[0], cmd.slice(1), spawnOptions)
83
+ : spawn(cmd, { ...spawnOptions, shell: true });
84
+
85
+ const timer = setTimeout(() => {
86
+ if (isWindows) {
87
+ try {
88
+ execSync(`taskkill /pid ${child.pid} /T /F`);
89
+ } catch (err) {
90
+ if (logger) {
91
+ logger.warn(`Failed to terminate process tree: ${err.message}`);
92
+ }
93
+ }
94
+ } else {
95
+ try {
96
+ process.kill(-child.pid, "SIGKILL");
97
+ } catch (err) {
98
+ if (logger) {
99
+ logger.warn(`Failed to terminate process group: ${err.message}`);
100
+ }
101
+ }
102
+ }
103
+ reject(new Error(`Command timeout after ${timeoutMs}ms: ${useArray ? cmd.join(" ") : cmd}`));
104
+ }, timeoutMs);
105
+
106
+ child.on("close", (code) => {
107
+ clearTimeout(timer);
108
+
109
+ if (code === 0) {
110
+ resolve(true);
111
+ } else {
112
+ reject(new Error(`Command failed with code ${code}: ${useArray ? cmd.join(" ") : cmd}`));
113
+ }
114
+ });
115
+
116
+ child.on("error", (err) => {
117
+ clearTimeout(timer);
118
+ reject(err);
119
+ });
120
+ });
121
+ }
122
+
123
+
124
+ /**
125
+ * Sleep
126
+ */
127
+ function sleep(ms) {
128
+ return new Promise((resolve) => setTimeout(resolve, ms));
129
+ }
130
+
131
+ /**
132
+ * Wait for condition
133
+ */
134
+ async function waitForCondition(checkFn, timeoutMs = 30000, intervalMs = 1000) {
135
+ const start = Date.now();
136
+ while (Date.now() - start < timeoutMs) {
137
+ if (checkFn()) return true;
138
+ await sleep(intervalMs);
139
+ }
140
+ return false;
141
+ }
142
+
143
+ module.exports = {
144
+ run,
145
+ runCapture,
146
+ commandExists,
147
+ runWithTimeout,
148
+ sleep,
149
+ waitForCondition,
150
+ isWindows,
151
+ };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * adapters/ssh.js
3
+ * SSH operations: execute commands remotely
4
+ */
5
+
6
+ const process_adapter = require("./process");
7
+ const { ProcessError } = require("../utils/errors");
8
+ const CONST = require("../utils/constants");
9
+
10
+ /**
11
+ * Execute command via SSH
12
+ */
13
+ function executeCommand(host, command, options = {}) {
14
+ const { logger, sshPath = "ssh", timeout = CONST.SSH_TIMEOUT } = options;
15
+
16
+ // If sshPath contains spaces, spawn can still execute it if provided as argv[0].
17
+ // Avoid building a single shell string to keep quoting predictable.
18
+ const sshArgs = ["-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10", host, command];
19
+
20
+ if (logger) {
21
+ logger.command([sshPath, ...sshArgs].join(" "));
22
+ }
23
+
24
+ return process_adapter.runWithTimeout([sshPath, ...sshArgs], timeout, { logger });
25
+ }
26
+
27
+ /**
28
+ * Execute command và capture output
29
+ */
30
+ function executeCommandCapture(host, command, options = {}) {
31
+ const { sshPath = "ssh" } = options;
32
+
33
+ const sshCmd = `${sshPath} -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${host} "${command}"`;
34
+
35
+ try {
36
+ return process_adapter.runCapture(sshCmd);
37
+ } catch (err) {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Check if SSH connection works
44
+ */
45
+ function checkConnection(host, options = {}) {
46
+ const { logger, sshPath = "ssh" } = options;
47
+
48
+ if (logger) {
49
+ logger.debug(`Testing SSH connection to ${host}...`);
50
+ }
51
+
52
+ try {
53
+ const result = executeCommandCapture(host, "echo OK", { sshPath });
54
+ return result === "OK";
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Stop services on remote host
62
+ */
63
+ async function stopServices(host, services, options = {}) {
64
+ const { logger, sshPath = "ssh" } = options;
65
+
66
+ if (!services || services.length === 0) {
67
+ if (logger) {
68
+ logger.info("No services to stop");
69
+ }
70
+ return;
71
+ }
72
+
73
+ logger.info(`Stopping services on ${host}: ${services.join(", ")}`);
74
+
75
+ await Promise.all(
76
+ services.map(async (service) => {
77
+ try {
78
+ await executeCommand(host, `sudo systemctl stop ${service}`, {
79
+ logger,
80
+ sshPath,
81
+ });
82
+ logger.success(`Stopped service: ${service}`);
83
+ } catch (err) {
84
+ try {
85
+ await executeCommand(host, `pkill -f ${service}`, {
86
+ logger,
87
+ sshPath,
88
+ });
89
+ logger.success(`Killed process: ${service}`);
90
+ } catch (err2) {
91
+ logger.warn(`Failed to stop ${service}: ${err2.message}`);
92
+ }
93
+ }
94
+ }),
95
+ );
96
+ }
97
+
98
+ module.exports = {
99
+ executeCommand,
100
+ executeCommandCapture,
101
+ checkConnection,
102
+ stopServices,
103
+ };