@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,406 @@
1
+ /**
2
+ * adapters/tailscale.js
3
+ * Tailscale operations: install, connect, status
4
+ *
5
+ * FIXED VERSION - Corrected OAuth login flow
6
+ */
7
+
8
+ const os = require("os");
9
+ const process_adapter = require("./process");
10
+ const { ProcessError } = require("../utils/errors");
11
+ const CONST = require("../utils/constants");
12
+
13
+ const isWindows = os.platform() === "win32";
14
+ const isLinux = os.platform() === "linux";
15
+ const isMacOS = os.platform() === "darwin";
16
+
17
+ /**
18
+ * Install Tailscale
19
+ */
20
+ function install(logger) {
21
+ // Check if already installed
22
+ if (process_adapter.commandExists("tailscale")) {
23
+ logger.success("Tailscale already installed");
24
+
25
+ try {
26
+ const version = process_adapter.runCapture("tailscale version");
27
+ if (version) {
28
+ logger.info(`Version: ${version.split("\n")[0]}`);
29
+ }
30
+ } catch (err) {
31
+ logger.debug(`Could not get version: ${err.message}`);
32
+ }
33
+
34
+ return true;
35
+ }
36
+
37
+ logger.info("Installing Tailscale...");
38
+
39
+ if (isLinux) {
40
+ try {
41
+ // Download and run install script
42
+ logger.info("Downloading Tailscale install script...");
43
+ process_adapter.run("curl -fsSL https://tailscale.com/install.sh | sh", {
44
+ logger,
45
+ ignoreError: false,
46
+ });
47
+
48
+ // Enable and start tailscaled service
49
+ logger.info("Enabling tailscaled service...");
50
+ process_adapter.run("sudo systemctl enable --now tailscaled", {
51
+ logger,
52
+ ignoreError: true, // May already be enabled
53
+ });
54
+
55
+ // Verify installation
56
+ if (!process_adapter.commandExists("tailscale")) {
57
+ throw new ProcessError("Tailscale installed but command not found in PATH");
58
+ }
59
+
60
+ logger.success("Tailscale installed successfully on Linux");
61
+ return true;
62
+ } catch (err) {
63
+ throw new ProcessError(`Failed to install Tailscale on Linux: ${err.message}`);
64
+ }
65
+ }
66
+
67
+ if (isMacOS) {
68
+ logger.error("macOS detected. Please install Tailscale manually:");
69
+ logger.info(" Option 1: brew install tailscale");
70
+ logger.info(" Option 2: Download from https://tailscale.com/download/mac");
71
+ return false;
72
+ }
73
+
74
+ if (isWindows) {
75
+ logger.error("Windows detected. Please install Tailscale manually:");
76
+ logger.info(" 1. Download from: https://tailscale.com/download/windows");
77
+ logger.info(" 2. Run the installer");
78
+ logger.info(" 3. Ensure 'tailscale' is in PATH");
79
+ logger.info(" 4. Restart terminal and try again");
80
+ return false;
81
+ }
82
+
83
+ logger.error(`Unsupported OS for auto-install: ${os.platform()}`);
84
+ return false;
85
+ }
86
+
87
+ /**
88
+ * Get Tailscale status as JSON
89
+ */
90
+ function getStatus(logger) {
91
+ try {
92
+ const statusStr = process_adapter.runCapture("tailscale status --json");
93
+ if (!statusStr) return null;
94
+
95
+ return JSON.parse(statusStr);
96
+ } catch (err) {
97
+ if (logger) {
98
+ logger.debug(`Failed to get Tailscale status: ${err.message}`);
99
+ }
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Check if logged in and connected
106
+ */
107
+ function isLoggedIn(logger) {
108
+ const status = getStatus(logger);
109
+ return status && status.BackendState === "Running";
110
+ }
111
+
112
+ /**
113
+ * Get Tailscale IPv4 address
114
+ */
115
+ function getIP(logger) {
116
+ const status = getStatus(logger);
117
+ if (!status || !status.Self) return null;
118
+
119
+ // Find first IPv4 address (no colons)
120
+ const ipv4 = status.Self.TailscaleIPs?.find((ip) => !ip.includes(":"));
121
+ return ipv4 || null;
122
+ }
123
+
124
+ /**
125
+ * Get Tailscale hostname
126
+ */
127
+ function getHostname(logger) {
128
+ const status = getStatus(logger);
129
+ if (!status || !status.Self) return null;
130
+
131
+ // Remove trailing dot from DNS name
132
+ return status.Self.DNSName?.replace(/\.$/, "") || null;
133
+ }
134
+
135
+ /**
136
+ * Login với OAuth credentials
137
+ *
138
+ * OAuth login format: --auth-key=CLIENT_ID:CLIENT_SECRET
139
+ * Platform differences:
140
+ * - Linux: needs sudo, supports --ssh
141
+ * - Windows: no sudo, no --ssh
142
+ * - macOS: no sudo, no --ssh (usually)
143
+ */
144
+ async function login(clientId, clientSecret, tags, logger, config) {
145
+ logger.info("Logging in to Tailscale with OAuth client...");
146
+
147
+ // Validate inputs
148
+ if (!clientId || !clientSecret) {
149
+ throw new ProcessError("TAILSCALE_CLIENT_ID and TAILSCALE_CLIENT_SECRET are required");
150
+ }
151
+
152
+ // Build tag parameter
153
+ const tagStr = tags ? `--advertise-tags=${tags}` : "";
154
+
155
+ // Build command array (will be filtered and joined)
156
+ const cmdParts = [
157
+ // Linux needs sudo
158
+ config.isLinux ? "sudo" : "",
159
+
160
+ "tailscale",
161
+ "up",
162
+
163
+ // OAuth auth key
164
+ `--client-id=${clientId}`,
165
+ `--client-secret=${clientSecret}`,
166
+
167
+ // Network settings
168
+ "--accept-routes",
169
+ "--accept-dns=true",
170
+
171
+ // SSH support (Linux only)
172
+ config.isLinux ? "--ssh" : "",
173
+
174
+ // Tags
175
+ tagStr,
176
+ ].filter(Boolean); // Remove empty strings
177
+
178
+ const cmd = cmdParts.join(" ");
179
+
180
+ // Log command (with masked auth key)
181
+ const maskedCmd = cmd.replace(clientId, "***MASKED***").replace(clientSecret, "***MASKED***");
182
+ logger.debug(`Executing: ${maskedCmd}`);
183
+
184
+ // Execute tailscale up
185
+ try {
186
+ process_adapter.run(cmd, {
187
+ logger,
188
+ ignoreError: false,
189
+ });
190
+ } catch (err) {
191
+ // Provide helpful error messages
192
+ let errorMsg = `Tailscale up failed: ${err.message}`;
193
+
194
+ if (err.message.includes("authentication")) {
195
+ errorMsg += "\n → Check: OAuth credentials are valid and not expired";
196
+ }
197
+ if (err.message.includes("tag")) {
198
+ errorMsg += "\n → Check: Tags are authorized in Tailscale ACL";
199
+ }
200
+ if (err.message.includes("permission")) {
201
+ errorMsg += "\n → Check: Running with sufficient permissions (sudo on Linux)";
202
+ }
203
+
204
+ throw new ProcessError(errorMsg);
205
+ }
206
+
207
+ // Wait for Tailscale to be fully connected
208
+ logger.info("Waiting for Tailscale connection...");
209
+
210
+ const connected = await process_adapter.waitForCondition(
211
+ () => {
212
+ try {
213
+ return isLoggedIn(logger);
214
+ } catch (err) {
215
+ logger.debug(`Status check error: ${err.message}`);
216
+ return false;
217
+ }
218
+ },
219
+ CONST.CONNECTION_TIMEOUT,
220
+ CONST.STATUS_CHECK_INTERVAL,
221
+ );
222
+
223
+ if (!connected) {
224
+ // Get current status for debugging
225
+ const status = getStatus(logger);
226
+ const backendState = status?.BackendState || "unknown";
227
+
228
+ throw new ProcessError(
229
+ `Tailscale failed to connect after ${CONST.CONNECTION_TIMEOUT / 1000}s. ` +
230
+ `Backend state: ${backendState}.\n` +
231
+ ` → Check: (1) OAuth credentials valid, (2) Network accessible, (3) Tags authorized`,
232
+ );
233
+ }
234
+
235
+ // Verify we got IP and hostname
236
+ const ip = getIP(logger);
237
+ const hostname = getHostname(logger);
238
+
239
+ if (!ip && !hostname) {
240
+ throw new ProcessError("Tailscale connected but no IP/hostname assigned");
241
+ }
242
+
243
+ logger.success("Tailscale connected successfully");
244
+ logger.info(` IP: ${ip || "N/A"}`);
245
+ logger.info(` Hostname: ${hostname || "N/A"}`);
246
+
247
+ return true;
248
+ }
249
+
250
+ /**
251
+ * Logout and cleanup
252
+ */
253
+ function logout(logger, config) {
254
+ logger.info("Logging out of Tailscale...");
255
+
256
+ const sudoPrefix = config.isLinux ? "sudo " : "";
257
+
258
+ // Try to logout gracefully
259
+ process_adapter.run(`${sudoPrefix}tailscale logout`, {
260
+ logger,
261
+ ignoreError: true,
262
+ });
263
+
264
+ logger.success("Logged out of Tailscale");
265
+ }
266
+
267
+ /**
268
+ * Disconnect (down) but don't logout
269
+ */
270
+ function down(logger, config) {
271
+ logger.info("Bringing down Tailscale connection...");
272
+
273
+ const sudoPrefix = config.isLinux ? "sudo " : "";
274
+
275
+ process_adapter.run(`${sudoPrefix}tailscale down`, {
276
+ logger,
277
+ ignoreError: true,
278
+ });
279
+
280
+ logger.success("Tailscale connection down");
281
+ }
282
+
283
+ /**
284
+ * Full cleanup (down + logout)
285
+ */
286
+ function cleanup(logger, config) {
287
+ logger.info("Cleaning up Tailscale...");
288
+
289
+ down(logger, config);
290
+ logout(logger, config);
291
+
292
+ logger.success("Tailscale cleanup complete");
293
+ }
294
+
295
+ /**
296
+ * Find peers with same tag (excluding self)
297
+ *
298
+ * Returns array of peer objects with:
299
+ * - id: Peer ID
300
+ * - hostname: Machine hostname
301
+ * - dnsName: Tailscale DNS name
302
+ * - ips: Array of Tailscale IPs
303
+ * - online: Boolean
304
+ * - lastSeen: Timestamp or null
305
+ */
306
+ function findPeersWithTag(tags, logger) {
307
+ // Parse tags (support both string and array)
308
+ const tagList = Array.isArray(tags)
309
+ ? tags
310
+ : String(tags || "")
311
+ .split(",")
312
+ .map((s) => s.trim())
313
+ .filter(Boolean);
314
+
315
+ // Get status
316
+ const status = getStatus(logger);
317
+ if (!status || !status.Peer) {
318
+ logger.debug("No peers found or status unavailable");
319
+ return [];
320
+ }
321
+
322
+ const selfId = status.Self?.ID;
323
+ const peers = [];
324
+
325
+ // Iterate through all peers
326
+ for (const [peerId, peerInfo] of Object.entries(status.Peer)) {
327
+ // Skip self
328
+ if (peerId === selfId) {
329
+ logger.debug(`Skipping self: ${peerId}`);
330
+ continue;
331
+ }
332
+
333
+ // Check if peer has any of the required tags
334
+ let hasSameTag = false;
335
+
336
+ if (tagList.length === 0) {
337
+ // No tags specified - include all peers
338
+ hasSameTag = true;
339
+ } else {
340
+ // Check if peer has any of the specified tags
341
+ hasSameTag = tagList.some((tag) => peerInfo.Tags?.includes(tag));
342
+ }
343
+
344
+ // Check if peer is online
345
+ const isOnline = peerInfo.Online === true;
346
+
347
+ if (hasSameTag && isOnline) {
348
+ peers.push({
349
+ id: peerId,
350
+ hostname: peerInfo.HostName,
351
+ dnsName: peerInfo.DNSName?.replace(/\.$/, ""),
352
+ ips: peerInfo.TailscaleIPs || [],
353
+ online: isOnline,
354
+ lastSeen: peerInfo.LastSeen || null,
355
+ tags: peerInfo.Tags || [],
356
+ });
357
+
358
+ logger.debug(`Found peer: ${peerInfo.HostName} (${peerInfo.TailscaleIPs?.[0]})`);
359
+ }
360
+ }
361
+
362
+ logger.info(`Found ${peers.length} peer(s) with matching tags`);
363
+ return peers;
364
+ }
365
+
366
+ /**
367
+ * Get detailed peer info by hostname or IP
368
+ */
369
+ function getPeerInfo(hostnameOrIp, logger) {
370
+ const status = getStatus(logger);
371
+ if (!status || !status.Peer) return null;
372
+
373
+ for (const [peerId, peerInfo] of Object.entries(status.Peer)) {
374
+ if (
375
+ peerInfo.HostName === hostnameOrIp ||
376
+ peerInfo.DNSName?.replace(/\.$/, "") === hostnameOrIp ||
377
+ peerInfo.TailscaleIPs?.includes(hostnameOrIp)
378
+ ) {
379
+ return {
380
+ id: peerId,
381
+ hostname: peerInfo.HostName,
382
+ dnsName: peerInfo.DNSName?.replace(/\.$/, ""),
383
+ ips: peerInfo.TailscaleIPs || [],
384
+ online: peerInfo.Online === true,
385
+ lastSeen: peerInfo.LastSeen || null,
386
+ tags: peerInfo.Tags || [],
387
+ };
388
+ }
389
+ }
390
+
391
+ return null;
392
+ }
393
+
394
+ module.exports = {
395
+ install,
396
+ getStatus,
397
+ isLoggedIn,
398
+ getIP,
399
+ getHostname,
400
+ login,
401
+ logout,
402
+ down,
403
+ cleanup,
404
+ findPeersWithTag,
405
+ getPeerInfo,
406
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * cli/commands/init.js
3
+ * Initialize Tailscale and detect previous runner
4
+ */
5
+
6
+ const initRunner = require("../../core/init");
7
+
8
+ async function run(config, logger) {
9
+ logger.info("Initializing runner sync...");
10
+ return await initRunner.initRunner(config, logger);
11
+ }
12
+
13
+ module.exports = { run };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * cli/commands/push.js
3
+ * Push .runner-data to git repository
4
+ */
5
+
6
+ const pushRunner = require("../../core/push");
7
+
8
+ async function run(config, logger) {
9
+ logger.info("Pushing .runner-data to git...");
10
+ return await pushRunner.pushRunnerData(config, logger);
11
+ }
12
+
13
+ module.exports = { run };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * cli/commands/status.js
3
+ * Show Tailscale status and runner info
4
+ */
5
+
6
+ const statusCore = require("../../core/status");
7
+
8
+ async function run(config, logger) {
9
+ logger.info("Checking runner status...");
10
+ return await statusCore.showStatus(config, logger);
11
+ }
12
+
13
+ module.exports = { run };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * cli/commands/sync.js
3
+ * Full synchronization workflow
4
+ */
5
+
6
+ const syncOrchestrator = require("../../core/sync-orchestrator");
7
+ const { SyncError } = require("../../utils/errors");
8
+
9
+ async function run(config, logger) {
10
+ logger.info("Starting full synchronization...");
11
+
12
+ const result = await syncOrchestrator.orchestrate(config, logger);
13
+
14
+ if (result.success) {
15
+ logger.success("Synchronization completed successfully!");
16
+ return result;
17
+ } else {
18
+ throw new SyncError("Synchronization failed");
19
+ }
20
+ }
21
+
22
+ module.exports = { run };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * cli/parser.js
3
+ * Parse command-line arguments
4
+ */
5
+
6
+ /**
7
+ * Parse command line arguments
8
+ * Returns: { command, options }
9
+ */
10
+ function parseArgs(argv) {
11
+ const args = argv.slice(2); // Remove node and script path
12
+
13
+ const options = {
14
+ cwd: null,
15
+ verbose: false,
16
+ quiet: false,
17
+ help: false,
18
+ };
19
+
20
+ let command = "sync"; // Default command
21
+
22
+ for (let i = 0; i < args.length; i++) {
23
+ const arg = args[i];
24
+
25
+ // Commands
26
+ if (arg === "init" || arg === "sync" || arg === "push" || arg === "status") {
27
+ command = arg;
28
+ continue;
29
+ }
30
+
31
+ // Options
32
+ if (arg === "--cwd" && i + 1 < args.length) {
33
+ options.cwd = args[++i];
34
+ continue;
35
+ }
36
+
37
+ if (arg === "--verbose" || arg === "-v") {
38
+ options.verbose = true;
39
+ continue;
40
+ }
41
+
42
+ if (arg === "--quiet" || arg === "-q") {
43
+ options.quiet = true;
44
+ continue;
45
+ }
46
+
47
+ if (arg === "--help" || arg === "-h") {
48
+ options.help = true;
49
+ continue;
50
+ }
51
+
52
+ if (arg === "--version") {
53
+ command = "version";
54
+ continue;
55
+ }
56
+ }
57
+
58
+ return { command, options };
59
+ }
60
+
61
+ /**
62
+ * Print help
63
+ */
64
+ function printHelp() {
65
+ console.log(`
66
+ runner-tailscale-sync - Đồng bộ runner data qua Tailscale network
67
+
68
+ USAGE:
69
+ runner-sync [command] [options]
70
+
71
+ COMMANDS:
72
+ init Khởi tạo Tailscale và detect previous runner
73
+ sync Full sync workflow (init + pull + stop services + push git)
74
+ push Push .runner-data to git only
75
+ status Show Tailscale status
76
+ --version Show version
77
+
78
+ OPTIONS:
79
+ --cwd <path> Set working directory (default: current dir)
80
+ --verbose, -v Enable verbose logging
81
+ --quiet, -q Suppress non-error output
82
+ --help, -h Show this help
83
+
84
+ ENVIRONMENT VARIABLES:
85
+ TAILSCALE_CLIENT_ID OAuth client ID (required if TAILSCALE_ENABLE=1)
86
+ TAILSCALE_CLIENT_SECRET OAuth client secret (required if TAILSCALE_ENABLE=1)
87
+ TAILSCALE_TAGS Tailscale tags (default: tag:ci)
88
+ TAILSCALE_ENABLE Enable Tailscale (0 or 1, default: 0)
89
+ SERVICES_TO_STOP Comma-separated services to stop (default: cloudflared,pocketbase,http-server)
90
+ GIT_PUSH_ENABLED Enable git push (0 or 1, default: 1)
91
+ GIT_BRANCH Git branch (default: main)
92
+ TOOL_CWD Working directory (can be overridden by --cwd)
93
+
94
+ EXAMPLES:
95
+ # Full sync with Tailscale
96
+ TAILSCALE_ENABLE=1 runner-sync
97
+
98
+ # Just push to git
99
+ runner-sync push
100
+
101
+ # Custom working directory
102
+ runner-sync --cwd /path/to/project
103
+
104
+ # Verbose mode
105
+ runner-sync -v
106
+
107
+ For more info: https://github.com/yourname/runner-tailscale-sync
108
+ `);
109
+ }
110
+
111
+ module.exports = {
112
+ parseArgs,
113
+ printHelp,
114
+ };