@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.
- package/README.md +310 -0
- package/bin/runner-sync.js +85 -0
- package/package.json +41 -0
- package/src/adapters/fs.js +160 -0
- package/src/adapters/git.js +185 -0
- package/src/adapters/http.js +56 -0
- package/src/adapters/process.js +151 -0
- package/src/adapters/ssh.js +103 -0
- package/src/adapters/tailscale.js +406 -0
- package/src/cli/commands/init.js +13 -0
- package/src/cli/commands/push.js +13 -0
- package/src/cli/commands/status.js +13 -0
- package/src/cli/commands/sync.js +22 -0
- package/src/cli/parser.js +114 -0
- package/src/core/data-sync.js +177 -0
- package/src/core/init.js +141 -0
- package/src/core/push.js +113 -0
- package/src/core/runner-detector.js +167 -0
- package/src/core/service-controller.js +141 -0
- package/src/core/status.js +130 -0
- package/src/core/sync-orchestrator.js +260 -0
- package/src/index.js +140 -0
- package/src/utils/config.js +129 -0
- package/src/utils/constants.js +33 -0
- package/src/utils/errors.js +45 -0
- package/src/utils/logger.js +154 -0
- package/src/utils/time.js +65 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.js
|
|
3
|
+
* Load configuration từ .env, CLI flags, và defaults
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const CONST = require("./constants");
|
|
10
|
+
|
|
11
|
+
class Config {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
// Load .env file nếu có
|
|
14
|
+
this.loadEnvFile();
|
|
15
|
+
|
|
16
|
+
// Determine CWD (priority: CLI flag > env > process.cwd())
|
|
17
|
+
this.cwd = options.cwd || process.env.TOOL_CWD || process.cwd();
|
|
18
|
+
|
|
19
|
+
// Runner data directory
|
|
20
|
+
this.runnerDataDir = path.join(this.cwd, CONST.RUNNER_DATA_DIR);
|
|
21
|
+
this.logsDir = path.join(this.runnerDataDir, CONST.LOGS_DIR);
|
|
22
|
+
this.pidDir = path.join(this.runnerDataDir, CONST.PID_DIR);
|
|
23
|
+
this.dataServicesDir = path.join(this.runnerDataDir, CONST.DATA_SERVICES_DIR);
|
|
24
|
+
this.tmpDir = path.join(this.runnerDataDir, CONST.TMP_DIR);
|
|
25
|
+
|
|
26
|
+
// Tailscale config
|
|
27
|
+
this.tailscaleClientId = process.env.TAILSCALE_CLIENT_ID || "";
|
|
28
|
+
this.tailscaleClientSecret = process.env.TAILSCALE_CLIENT_SECRET || "";
|
|
29
|
+
this.tailscaleTags = process.env.TAILSCALE_TAGS || CONST.DEFAULT_TAG;
|
|
30
|
+
this.tailscaleEnable = String(process.env.TAILSCALE_ENABLE || "").trim() === "1";
|
|
31
|
+
|
|
32
|
+
// Services to stop on previous runner
|
|
33
|
+
this.servicesToStop = this.parseServicesList(
|
|
34
|
+
process.env.SERVICES_TO_STOP || "cloudflared,pocketbase,http-server"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Platform detection
|
|
38
|
+
this.isWindows = os.platform() === "win32";
|
|
39
|
+
this.isLinux = os.platform() === "linux";
|
|
40
|
+
this.isMacOS = os.platform() === "darwin";
|
|
41
|
+
|
|
42
|
+
// Logging
|
|
43
|
+
this.verbose = options.verbose || false;
|
|
44
|
+
this.quiet = options.quiet || false;
|
|
45
|
+
|
|
46
|
+
// Git
|
|
47
|
+
this.gitEnabled = String(process.env.GIT_PUSH_ENABLED || "1").trim() === "1";
|
|
48
|
+
this.gitBranch = process.env.GIT_BRANCH || "main";
|
|
49
|
+
|
|
50
|
+
// SSH/Rsync paths (for Windows)
|
|
51
|
+
this.sshPath = process.env.SSH_PATH || "ssh";
|
|
52
|
+
this.rsyncPath = process.env.RSYNC_PATH || "rsync";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load .env file
|
|
57
|
+
*/
|
|
58
|
+
loadEnvFile() {
|
|
59
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
60
|
+
if (!fs.existsSync(envPath)) return;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
64
|
+
content.split("\n").forEach(line => {
|
|
65
|
+
const trimmed = line.trim();
|
|
66
|
+
if (!trimmed || trimmed.startsWith("#")) return;
|
|
67
|
+
|
|
68
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
69
|
+
if (match) {
|
|
70
|
+
const key = match[1].trim();
|
|
71
|
+
let value = match[2].trim();
|
|
72
|
+
|
|
73
|
+
// Remove quotes
|
|
74
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
75
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
76
|
+
value = value.slice(1, -1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Only set if not already in env
|
|
80
|
+
if (!process.env[key]) {
|
|
81
|
+
process.env[key] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// Ignore errors
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse comma-separated services list
|
|
92
|
+
*/
|
|
93
|
+
parseServicesList(str) {
|
|
94
|
+
return str.split(",").map(s => s.trim()).filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate required config
|
|
99
|
+
*/
|
|
100
|
+
validate() {
|
|
101
|
+
const errors = [];
|
|
102
|
+
|
|
103
|
+
if (this.tailscaleEnable) {
|
|
104
|
+
if (!this.tailscaleClientId) {
|
|
105
|
+
errors.push("TAILSCALE_CLIENT_ID is required when TAILSCALE_ENABLE=1");
|
|
106
|
+
}
|
|
107
|
+
if (!this.tailscaleClientSecret) {
|
|
108
|
+
errors.push("TAILSCALE_CLIENT_SECRET is required when TAILSCALE_ENABLE=1");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return errors;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get all directories that need to be created
|
|
117
|
+
*/
|
|
118
|
+
getDirectoriesToEnsure() {
|
|
119
|
+
return [
|
|
120
|
+
this.runnerDataDir,
|
|
121
|
+
this.logsDir,
|
|
122
|
+
this.pidDir,
|
|
123
|
+
this.dataServicesDir,
|
|
124
|
+
this.tmpDir,
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = Config;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* constants.js
|
|
3
|
+
* Định nghĩa các hằng số dùng chung
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
// Exit codes
|
|
8
|
+
EXIT_SUCCESS: 0,
|
|
9
|
+
EXIT_UNKNOWN: 1,
|
|
10
|
+
EXIT_VALIDATION: 2,
|
|
11
|
+
EXIT_NETWORK: 10,
|
|
12
|
+
EXIT_PROCESS: 20,
|
|
13
|
+
|
|
14
|
+
// Directories
|
|
15
|
+
RUNNER_DATA_DIR: ".runner-data",
|
|
16
|
+
LOGS_DIR: "logs",
|
|
17
|
+
PID_DIR: "pid",
|
|
18
|
+
DATA_SERVICES_DIR: "data-services",
|
|
19
|
+
TMP_DIR: "tmp",
|
|
20
|
+
|
|
21
|
+
// Tailscale
|
|
22
|
+
DEFAULT_TAG: "tag:ci",
|
|
23
|
+
CONNECTION_TIMEOUT: 30000,
|
|
24
|
+
STATUS_CHECK_INTERVAL: 2000,
|
|
25
|
+
|
|
26
|
+
// Sync
|
|
27
|
+
RSYNC_TIMEOUT: 300000, // 5 minutes
|
|
28
|
+
SSH_TIMEOUT: 60000, // 1 minute
|
|
29
|
+
|
|
30
|
+
// Git
|
|
31
|
+
GIT_RETRY_COUNT: 3,
|
|
32
|
+
GIT_RETRY_DELAY: 2000,
|
|
33
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* errors.js
|
|
3
|
+
* Custom error classes với exit codes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class BaseError extends Error {
|
|
7
|
+
constructor(message, exitCode = 1) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = this.constructor.name;
|
|
10
|
+
this.exitCode = exitCode;
|
|
11
|
+
Error.captureStackTrace(this, this.constructor);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class ValidationError extends BaseError {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message, 2);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class NetworkError extends BaseError {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message, 10);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class ProcessError extends BaseError {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message, 20);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class SyncError extends BaseError {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message, 20);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
BaseError,
|
|
41
|
+
ValidationError,
|
|
42
|
+
NetworkError,
|
|
43
|
+
ProcessError,
|
|
44
|
+
SyncError,
|
|
45
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logger.js
|
|
3
|
+
* Logger với masking sensitive data và version info
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { getTimestamp } = require("./time");
|
|
8
|
+
|
|
9
|
+
class Logger {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.packageName = options.packageName || "runner-tailscale-sync";
|
|
12
|
+
this.version = options.version || "unknown";
|
|
13
|
+
this.verbose = options.verbose || false;
|
|
14
|
+
this.quiet = options.quiet || false;
|
|
15
|
+
this.command = options.command || "";
|
|
16
|
+
|
|
17
|
+
// Danh sách giá trị phổ biến KHÔNG mask
|
|
18
|
+
this.skipValues = new Set([
|
|
19
|
+
"true", "false", "TRUE", "FALSE",
|
|
20
|
+
"null", "undefined", "NULL",
|
|
21
|
+
"production", "development", "test", "staging"
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
// Danh sách key patterns cần mask
|
|
25
|
+
this.sensitivePatterns = [
|
|
26
|
+
"PASSWORD", "SECRET", "KEY", "TOKEN", "API",
|
|
27
|
+
"CLIENT_ID", "CLIENT_SECRET", "AUTH", "OAUTH",
|
|
28
|
+
"PRIVATE", "CREDENTIAL", "ACCESS", "PASSPHRASE",
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Mask sensitive values trong message
|
|
34
|
+
*/
|
|
35
|
+
maskSensitiveData(msg) {
|
|
36
|
+
let maskedMsg = msg;
|
|
37
|
+
|
|
38
|
+
const envValues = Object.entries(process.env)
|
|
39
|
+
.filter(([key, value]) => {
|
|
40
|
+
if (!value || typeof value !== "string") return false;
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
|
|
43
|
+
if (trimmed.length < 6) return false;
|
|
44
|
+
if (this.skipValues.has(trimmed)) return false;
|
|
45
|
+
if (/^\d+$/.test(trimmed)) return false;
|
|
46
|
+
|
|
47
|
+
const upperKey = key.toUpperCase();
|
|
48
|
+
return this.sensitivePatterns.some(pattern => upperKey.includes(pattern));
|
|
49
|
+
})
|
|
50
|
+
.map(([key, value]) => value.trim().replace(/\s+/g, " "))
|
|
51
|
+
.sort((a, b) => b.length - a.length);
|
|
52
|
+
|
|
53
|
+
const uniqueValues = [...new Set(envValues)];
|
|
54
|
+
|
|
55
|
+
for (const value of uniqueValues) {
|
|
56
|
+
const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
57
|
+
const whitespacePattern = escapedValue.replace(/\s+/g, "\\s+");
|
|
58
|
+
const regex = new RegExp(whitespacePattern, "g");
|
|
59
|
+
maskedMsg = maskedMsg.replace(regex, "*".repeat(value.length));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
maskedMsg = maskedMsg.replace(/tskey-[a-zA-Z0-9]{30,}/g, "***TAILSCALE_KEY***");
|
|
63
|
+
maskedMsg = maskedMsg.replace(/ghp_[a-zA-Z0-9]{36}/g, "***GITHUB_TOKEN***");
|
|
64
|
+
maskedMsg = maskedMsg.replace(/[A-Za-z0-9+/]{32,}={0,2}/g, "***BASE64_SECRET***");
|
|
65
|
+
|
|
66
|
+
return maskedMsg;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Format message với prefix
|
|
71
|
+
*/
|
|
72
|
+
format(level, msg) {
|
|
73
|
+
const timestamp = getTimestamp();
|
|
74
|
+
const prefix = `[${this.packageName}@${this.version}]`;
|
|
75
|
+
const commandPrefix = `[${this.command || "unknown"}]`;
|
|
76
|
+
const timePrefix = `[${timestamp}]`;
|
|
77
|
+
return `${timePrefix} ${prefix} ${commandPrefix} ${level} ${msg}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Log info
|
|
82
|
+
*/
|
|
83
|
+
info(msg) {
|
|
84
|
+
if (this.quiet) return;
|
|
85
|
+
const formatted = this.format("ℹ️", msg);
|
|
86
|
+
const masked = this.maskSensitiveData(formatted);
|
|
87
|
+
process.stdout.write(masked + "\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Log success
|
|
92
|
+
*/
|
|
93
|
+
success(msg) {
|
|
94
|
+
if (this.quiet) return;
|
|
95
|
+
const formatted = this.format("✅", msg);
|
|
96
|
+
const masked = this.maskSensitiveData(formatted);
|
|
97
|
+
process.stdout.write(masked + "\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Log warning
|
|
102
|
+
*/
|
|
103
|
+
warn(msg) {
|
|
104
|
+
const formatted = this.format("⚠️", msg);
|
|
105
|
+
const masked = this.maskSensitiveData(formatted);
|
|
106
|
+
process.stderr.write(masked + "\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Log error
|
|
111
|
+
*/
|
|
112
|
+
error(msg) {
|
|
113
|
+
const formatted = this.format("❌", msg);
|
|
114
|
+
const masked = this.maskSensitiveData(formatted);
|
|
115
|
+
process.stderr.write(masked + "\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Log debug (chỉ khi verbose)
|
|
120
|
+
*/
|
|
121
|
+
debug(msg) {
|
|
122
|
+
if (!this.verbose) return;
|
|
123
|
+
const formatted = this.format("🔍", msg);
|
|
124
|
+
const masked = this.maskSensitiveData(formatted);
|
|
125
|
+
process.stdout.write(masked + "\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Log command execution
|
|
130
|
+
*/
|
|
131
|
+
command(cmd) {
|
|
132
|
+
if (this.quiet) return;
|
|
133
|
+
const formatted = this.format("🔧", cmd);
|
|
134
|
+
const masked = this.maskSensitiveData(formatted);
|
|
135
|
+
process.stdout.write(masked + "\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Print banner khi khởi động
|
|
140
|
+
*/
|
|
141
|
+
printBanner() {
|
|
142
|
+
if (this.quiet) return;
|
|
143
|
+
this.info(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
144
|
+
this.info(`📦 ${this.packageName} - version ${this.version}`);
|
|
145
|
+
this.info(`🧾 Đang thực thi version: ${this.version}`);
|
|
146
|
+
if (this.command) {
|
|
147
|
+
this.info(`🎯 Command: ${this.command}`);
|
|
148
|
+
}
|
|
149
|
+
this.info(`🕐 Started at: ${getTimestamp()} (VN Time)`);
|
|
150
|
+
this.info(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = Logger;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* time.js
|
|
3
|
+
* Vietnam timezone utilities (Asia/Ho_Chi_Minh)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const VN_OFFSET = 7 * 60; // UTC+7 in minutes
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Lấy thời gian Việt Nam hiện tại
|
|
10
|
+
* @returns {Date}
|
|
11
|
+
*/
|
|
12
|
+
function getVietnamTime() {
|
|
13
|
+
const now = new Date();
|
|
14
|
+
const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
|
|
15
|
+
return new Date(utc + (VN_OFFSET * 60000));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format thời gian theo format cụ thể
|
|
20
|
+
* @param {Date} date
|
|
21
|
+
* @param {string} format - 'yyMMdd', 'HHmm', 'yyMMdd-HHmmss', etc
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
function formatVietnamTime(date, format = 'yyMMdd-HHmmss') {
|
|
25
|
+
const yy = String(date.getFullYear()).slice(-2);
|
|
26
|
+
const MM = String(date.getMonth() + 1).padStart(2, '0');
|
|
27
|
+
const dd = String(date.getDate()).padStart(2, '0');
|
|
28
|
+
const HH = String(date.getHours()).padStart(2, '0');
|
|
29
|
+
const mm = String(date.getMinutes()).padStart(2, '0');
|
|
30
|
+
const ss = String(date.getSeconds()).padStart(2, '0');
|
|
31
|
+
|
|
32
|
+
return format
|
|
33
|
+
.replace('yy', yy)
|
|
34
|
+
.replace('MM', MM)
|
|
35
|
+
.replace('dd', dd)
|
|
36
|
+
.replace('HH', HH)
|
|
37
|
+
.replace('mm', mm)
|
|
38
|
+
.replace('ss', ss);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Tạo version string theo format: 1.yyMMdd.1HHmm
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
function generateVersion() {
|
|
46
|
+
const vnTime = getVietnamTime();
|
|
47
|
+
const yyMMdd = formatVietnamTime(vnTime, 'yyMMdd');
|
|
48
|
+
const HHmm = formatVietnamTime(vnTime, 'HHmm');
|
|
49
|
+
return `1.${yyMMdd}.1${HHmm}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Tạo timestamp string cho logs
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function getTimestamp() {
|
|
57
|
+
return formatVietnamTime(getVietnamTime(), 'yyMMdd-HHmmss');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
getVietnamTime,
|
|
62
|
+
formatVietnamTime,
|
|
63
|
+
generateVersion,
|
|
64
|
+
getTimestamp,
|
|
65
|
+
};
|