@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,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
|
+
};
|