@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,141 @@
1
+ /**
2
+ * core/service-controller.js
3
+ * Stop/Start services trên runners qua SSH
4
+ */
5
+
6
+ const ssh = require("../adapters/ssh");
7
+
8
+ /**
9
+ * Parse input
10
+ */
11
+ function parseInput(config, previousRunner, logger) {
12
+ return {
13
+ remoteHost: previousRunner?.dnsName || previousRunner?.ips?.[0],
14
+ services: config.servicesToStop,
15
+ sshPath: config.sshPath,
16
+ logger,
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Validate
22
+ */
23
+ function validate(input) {
24
+ const errors = [];
25
+
26
+ if (!input.remoteHost) {
27
+ errors.push("Remote host is required");
28
+ }
29
+
30
+ if (!input.services || input.services.length === 0) {
31
+ errors.push("No services specified to stop");
32
+ }
33
+
34
+ return errors;
35
+ }
36
+
37
+ /**
38
+ * Plan
39
+ */
40
+ function plan(input) {
41
+ return {
42
+ action: "stop_remote_services",
43
+ host: input.remoteHost,
44
+ services: input.services,
45
+ sshPath: input.sshPath,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Execute - stop services
51
+ */
52
+ async function execute(planResult, input) {
53
+ const { logger } = input;
54
+
55
+ logger.info(`Stopping services on ${planResult.host}...`);
56
+
57
+ // Check SSH connection first
58
+ const connected = ssh.checkConnection(planResult.host, {
59
+ logger,
60
+ sshPath: planResult.sshPath,
61
+ });
62
+
63
+ if (!connected) {
64
+ logger.warn(`Cannot connect to ${planResult.host} via SSH - services may still be running`);
65
+ return {
66
+ success: false,
67
+ stoppedServices: [],
68
+ };
69
+ }
70
+
71
+ // Stop services
72
+ await ssh.stopServices(planResult.host, planResult.services, {
73
+ logger,
74
+ sshPath: planResult.sshPath,
75
+ });
76
+
77
+ return {
78
+ success: true,
79
+ stoppedServices: planResult.services,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Report
85
+ */
86
+ function report(result, input) {
87
+ const { logger } = input;
88
+
89
+ if (result.success) {
90
+ logger.success(`Stopped ${result.stoppedServices.length} services`);
91
+ return {
92
+ success: true,
93
+ stoppedServices: result.stoppedServices,
94
+ };
95
+ } else {
96
+ logger.warn("Failed to stop some services");
97
+ return {
98
+ success: false,
99
+ stoppedServices: [],
100
+ };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Main stop services function
106
+ */
107
+ async function stopRemoteServices(config, previousRunner, logger) {
108
+ if (!previousRunner) {
109
+ logger.info("No previous runner - skipping service stop");
110
+ return { success: true, stoppedServices: [] };
111
+ }
112
+
113
+ // Step 1: Parse Input
114
+ const input = parseInput(config, previousRunner, logger);
115
+
116
+ // Step 2: Validate
117
+ const errors = validate(input);
118
+ if (errors.length > 0) {
119
+ // Not critical - just warn
120
+ logger.warn(`Service stop validation: ${errors.join(", ")}`);
121
+ return { success: true, stoppedServices: [] };
122
+ }
123
+
124
+ // Step 3: Plan
125
+ const planResult = plan(input);
126
+
127
+ // Step 4: Execute
128
+ const execResult = await execute(planResult, input);
129
+
130
+ // Step 5: Report
131
+ return report(execResult, input);
132
+ }
133
+
134
+ module.exports = {
135
+ stopRemoteServices,
136
+ parseInput,
137
+ validate,
138
+ plan,
139
+ execute,
140
+ report,
141
+ };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * core/status.js
3
+ * Show Tailscale status and runner info
4
+ */
5
+
6
+ const tailscale = require("../adapters/tailscale");
7
+ const fs_adapter = require("../adapters/fs");
8
+
9
+ /**
10
+ * Parse input
11
+ */
12
+ function parseInput(config, logger) {
13
+ return {
14
+ tailscaleEnable: config.tailscaleEnable,
15
+ tailscaleTags: config.tailscaleTags,
16
+ runnerDataDir: config.runnerDataDir,
17
+ logger,
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Validate
23
+ */
24
+ function validate() {
25
+ return [];
26
+ }
27
+
28
+ /**
29
+ * Plan
30
+ */
31
+ function plan() {
32
+ return {
33
+ action: "status",
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Execute
39
+ */
40
+ async function execute(planResult, input) {
41
+ const { logger } = input;
42
+
43
+ const result = {
44
+ tailscale: null,
45
+ peers: [],
46
+ peerCount: 0,
47
+ runnerData: null,
48
+ };
49
+
50
+ if (input.tailscaleEnable) {
51
+ const status = tailscale.getStatus(logger);
52
+ result.tailscale = status || null;
53
+
54
+ if (status) {
55
+ result.peerCount = status.Peer ? Object.keys(status.Peer).length : 0;
56
+ result.peers = tailscale.findPeersWithTag(input.tailscaleTags, logger);
57
+ }
58
+ }
59
+
60
+ if (fs_adapter.exists(input.runnerDataDir)) {
61
+ result.runnerData = {
62
+ path: input.runnerDataDir,
63
+ size: fs_adapter.getDirSize(input.runnerDataDir),
64
+ };
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * Report
72
+ */
73
+ function report(result, input) {
74
+ const { logger } = input;
75
+
76
+ if (input.tailscaleEnable) {
77
+ if (result.tailscale) {
78
+ logger.info("━━━ Tailscale Status ━━━");
79
+ logger.info(`Backend: ${result.tailscale.BackendState}`);
80
+
81
+ if (result.tailscale.Self) {
82
+ logger.info(`Hostname: ${result.tailscale.Self.HostName || "N/A"}`);
83
+ logger.info(`DNS: ${result.tailscale.Self.DNSName || "N/A"}`);
84
+ logger.info(`IPs: ${result.tailscale.Self.TailscaleIPs?.join(", ") || "N/A"}`);
85
+ }
86
+
87
+ if (result.peers.length > 0) {
88
+ logger.info(`Peers: ${result.peerCount} connected`);
89
+ logger.info(`Peers with tag '${input.tailscaleTags}':`);
90
+ result.peers.forEach((peer, i) => {
91
+ logger.info(` ${i + 1}. ${peer.hostname} (${peer.ips[0]})`);
92
+ });
93
+ }
94
+ } else {
95
+ logger.warn("Tailscale not connected");
96
+ }
97
+ } else {
98
+ logger.info("Tailscale disabled");
99
+ }
100
+
101
+ logger.info("━━━ Runner Data ━━━");
102
+ if (result.runnerData) {
103
+ logger.info(`Directory: ${result.runnerData.path}`);
104
+ logger.info(`Size: ${fs_adapter.formatBytes(result.runnerData.size)}`);
105
+ } else {
106
+ logger.warn(`Directory not found: ${input.runnerDataDir}`);
107
+ }
108
+
109
+ return { success: true };
110
+ }
111
+
112
+ /**
113
+ * Main status function
114
+ */
115
+ async function showStatus(config, logger) {
116
+ const input = parseInput(config, logger);
117
+ validate(input);
118
+ const planResult = plan(input);
119
+ const execResult = await execute(planResult, input);
120
+ return report(execResult, input);
121
+ }
122
+
123
+ module.exports = {
124
+ showStatus,
125
+ parseInput,
126
+ validate,
127
+ plan,
128
+ execute,
129
+ report,
130
+ };
@@ -0,0 +1,260 @@
1
+ /**
2
+ * core/sync-orchestrator.js
3
+ * Điều phối toàn bộ quy trình sync
4
+ */
5
+
6
+ const tailscale = require("../adapters/tailscale");
7
+ const git = require("../adapters/git");
8
+ const fs_adapter = require("../adapters/fs");
9
+ const runnerDetector = require("./runner-detector");
10
+ const dataSync = require("./data-sync");
11
+ const serviceController = require("./service-controller");
12
+ const { getTimestamp } = require("../utils/time");
13
+ const { ValidationError, ProcessError } = require("../utils/errors");
14
+
15
+ /**
16
+ * Parse input
17
+ */
18
+ function parseInput(config, logger) {
19
+ return {
20
+ config,
21
+ logger,
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Validate
27
+ */
28
+ function validate(input) {
29
+ const errors = input.config.validate();
30
+ if (errors.length > 0) {
31
+ throw new ValidationError(`Validation failed:\n - ${errors.join("\n - ")}`);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Plan - xác định các bước cần thực hiện
37
+ */
38
+ function plan(input) {
39
+ const { config } = input;
40
+
41
+ return {
42
+ steps: [
43
+ { name: "setup_directories", enabled: true },
44
+ { name: "connect_tailscale", enabled: config.tailscaleEnable },
45
+ { name: "detect_previous_runner", enabled: config.tailscaleEnable },
46
+ { name: "pull_data", enabled: config.tailscaleEnable },
47
+ { name: "stop_remote_services", enabled: config.tailscaleEnable },
48
+ { name: "push_to_git", enabled: config.gitEnabled },
49
+ ],
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Execute - chạy từng bước
55
+ */
56
+ async function execute(planResult, input) {
57
+ const { config, logger } = input;
58
+ const results = {};
59
+
60
+ for (const step of planResult.steps) {
61
+ if (!step.enabled) {
62
+ logger.debug(`Skipping step: ${step.name}`);
63
+ continue;
64
+ }
65
+
66
+ logger.info(`━━━ Step: ${step.name} ━━━`);
67
+
68
+ try {
69
+ switch (step.name) {
70
+ case "setup_directories":
71
+ results.setupDirs = await setupDirectories(config, logger);
72
+ break;
73
+
74
+ case "connect_tailscale":
75
+ results.tailscale = await connectTailscale(config, logger);
76
+ break;
77
+
78
+ case "detect_previous_runner":
79
+ results.detection = await runnerDetector.detectPreviousRunner(config, logger);
80
+ break;
81
+
82
+ case "pull_data":
83
+ results.pullData = await dataSync.pullData(
84
+ config,
85
+ results.detection?.previousRunner,
86
+ logger
87
+ );
88
+ break;
89
+
90
+ case "stop_remote_services":
91
+ results.stopServices = await serviceController.stopRemoteServices(
92
+ config,
93
+ results.detection?.previousRunner,
94
+ logger
95
+ );
96
+ break;
97
+
98
+ case "push_to_git":
99
+ results.pushGit = await pushToGit(config, logger);
100
+ break;
101
+
102
+ default:
103
+ logger.warn(`Unknown step: ${step.name}`);
104
+ }
105
+ } catch (err) {
106
+ logger.error(`Step failed: ${step.name} - ${err.message}`);
107
+ throw err;
108
+ }
109
+ }
110
+
111
+ return results;
112
+ }
113
+
114
+ /**
115
+ * Report
116
+ */
117
+ function report(results, input) {
118
+ const { logger } = input;
119
+
120
+ logger.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
121
+ logger.success("Sync orchestration completed!");
122
+
123
+ if (results.tailscale) {
124
+ logger.info(`Tailscale IP: ${results.tailscale.ip || "N/A"}`);
125
+ logger.info(`Tailscale Hostname: ${results.tailscale.hostname || "N/A"}`);
126
+ }
127
+
128
+ if (results.detection?.previousRunner) {
129
+ logger.info(`Previous runner: ${results.detection.previousRunner.hostname}`);
130
+ }
131
+
132
+ if (results.pullData?.syncedSize) {
133
+ logger.info(`Synced data: ${fs_adapter.formatBytes(results.pullData.syncedSize)}`);
134
+ }
135
+
136
+ if (results.stopServices?.stoppedServices?.length > 0) {
137
+ logger.info(`Stopped services: ${results.stopServices.stoppedServices.join(", ")}`);
138
+ }
139
+
140
+ logger.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
141
+
142
+ return {
143
+ success: true,
144
+ results,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Setup directories
150
+ */
151
+ async function setupDirectories(config, logger) {
152
+ logger.info("Setting up directories...");
153
+
154
+ const dirs = config.getDirectoriesToEnsure();
155
+ fs_adapter.ensureDirs(dirs);
156
+
157
+ logger.success(`Created ${dirs.length} directories`);
158
+
159
+ return {
160
+ success: true,
161
+ directories: dirs,
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Connect to Tailscale
167
+ */
168
+ async function connectTailscale(config, logger) {
169
+ logger.info("Connecting to Tailscale network...");
170
+
171
+ // Install if needed
172
+ const installed = tailscale.install(logger);
173
+ if (!installed) {
174
+ throw new ProcessError("Failed to install Tailscale");
175
+ }
176
+
177
+ // Login
178
+ await tailscale.login(
179
+ config.tailscaleClientId,
180
+ config.tailscaleClientSecret,
181
+ config.tailscaleTags,
182
+ logger,
183
+ config
184
+ );
185
+
186
+ // Get connection info
187
+ const ip = tailscale.getIP(logger);
188
+ const hostname = tailscale.getHostname(logger);
189
+
190
+ logger.success(`Connected to Tailscale: ${ip || hostname}`);
191
+
192
+ return {
193
+ success: true,
194
+ ip,
195
+ hostname,
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Push to git
201
+ */
202
+ async function pushToGit(config, logger) {
203
+ logger.info("Pushing data to git repository...");
204
+
205
+ if (!git.isAvailable()) {
206
+ logger.warn("Git not available - skipping push");
207
+ return { success: false };
208
+ }
209
+
210
+ if (!git.isGitRepo(config.cwd)) {
211
+ logger.warn("Not a git repository - skipping push");
212
+ return { success: false };
213
+ }
214
+
215
+ const timestamp = getTimestamp();
216
+ const message = `[runner-sync] Update .runner-data at ${timestamp}`;
217
+
218
+ const pushed = await git.commitAndPush(message, config.gitBranch, {
219
+ logger,
220
+ cwd: config.cwd,
221
+ });
222
+
223
+ if (pushed) {
224
+ logger.success("Pushed to git repository");
225
+ return { success: true };
226
+ } else {
227
+ logger.info("No changes to push");
228
+ return { success: true, noChanges: true };
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Main orchestrate function
234
+ */
235
+ async function orchestrate(config, logger) {
236
+ // Step 1: Parse Input
237
+ const input = parseInput(config, logger);
238
+
239
+ // Step 2: Validate
240
+ validate(input);
241
+
242
+ // Step 3: Plan
243
+ const planResult = plan(input);
244
+ logger.debug(`Planned ${planResult.steps.filter(s => s.enabled).length} steps`);
245
+
246
+ // Step 4: Execute
247
+ const execResult = await execute(planResult, input);
248
+
249
+ // Step 5: Report
250
+ return report(execResult, input);
251
+ }
252
+
253
+ module.exports = {
254
+ orchestrate,
255
+ parseInput,
256
+ validate,
257
+ plan,
258
+ execute,
259
+ report,
260
+ };
package/src/index.js ADDED
@@ -0,0 +1,140 @@
1
+ /**
2
+ * src/index.js
3
+ * Main library export - cho phép import như library
4
+ */
5
+
6
+ const Config = require("./utils/config");
7
+ const Logger = require("./utils/logger");
8
+ const syncOrchestrator = require("./core/sync-orchestrator");
9
+ const runnerDetector = require("./core/runner-detector");
10
+ const dataSync = require("./core/data-sync");
11
+ const serviceController = require("./core/service-controller");
12
+ const initRunner = require("./core/init");
13
+ const pushRunner = require("./core/push");
14
+ const statusRunner = require("./core/status");
15
+
16
+ // Adapters
17
+ const tailscale = require("./adapters/tailscale");
18
+ const git = require("./adapters/git");
19
+ const ssh = require("./adapters/ssh");
20
+ const fs_adapter = require("./adapters/fs");
21
+ const process_adapter = require("./adapters/process");
22
+ const http_adapter = require("./adapters/http");
23
+
24
+ // Utils
25
+ const time = require("./utils/time");
26
+ const errors = require("./utils/errors");
27
+ const constants = require("./utils/constants");
28
+
29
+ /**
30
+ * Main API - orchestrate full sync
31
+ */
32
+ async function sync(options = {}) {
33
+ const config = new Config(options);
34
+ const pkg = require("../package.json");
35
+
36
+ const logger = new Logger({
37
+ packageName: pkg.name,
38
+ version: pkg.version,
39
+ command: "sync",
40
+ verbose: options.verbose || false,
41
+ quiet: options.quiet || false,
42
+ });
43
+
44
+ logger.printBanner();
45
+
46
+ return await syncOrchestrator.orchestrate(config, logger);
47
+ }
48
+
49
+ /**
50
+ * Init only
51
+ */
52
+ async function init(options = {}) {
53
+ const config = new Config(options);
54
+ const pkg = require("../package.json");
55
+
56
+ const logger = new Logger({
57
+ packageName: pkg.name,
58
+ version: pkg.version,
59
+ command: "init",
60
+ verbose: options.verbose || false,
61
+ quiet: options.quiet || false,
62
+ });
63
+
64
+ logger.printBanner();
65
+
66
+ return await initRunner.initRunner(config, logger);
67
+ }
68
+
69
+ /**
70
+ * Push to git only
71
+ */
72
+ async function push(options = {}) {
73
+ const config = new Config(options);
74
+ const pkg = require("../package.json");
75
+
76
+ const logger = new Logger({
77
+ packageName: pkg.name,
78
+ version: pkg.version,
79
+ command: "push",
80
+ verbose: options.verbose || false,
81
+ quiet: options.quiet || false,
82
+ });
83
+
84
+ logger.printBanner();
85
+
86
+ return await pushRunner.pushRunnerData(config, logger);
87
+ }
88
+
89
+ /**
90
+ * Show status
91
+ */
92
+ async function status(options = {}) {
93
+ const config = new Config(options);
94
+ const pkg = require("../package.json");
95
+
96
+ const logger = new Logger({
97
+ packageName: pkg.name,
98
+ version: pkg.version,
99
+ command: "status",
100
+ verbose: options.verbose || false,
101
+ quiet: options.quiet || false,
102
+ });
103
+
104
+ logger.printBanner();
105
+
106
+ return await statusRunner.showStatus(config, logger);
107
+ }
108
+
109
+ // Export API và modules
110
+ module.exports = {
111
+ // Main API
112
+ sync,
113
+ init,
114
+ push,
115
+ status,
116
+
117
+ // Core modules
118
+ syncOrchestrator,
119
+ runnerDetector,
120
+ dataSync,
121
+ serviceController,
122
+ initRunner,
123
+ pushRunner,
124
+ statusRunner,
125
+
126
+ // Adapters
127
+ tailscale,
128
+ git,
129
+ ssh,
130
+ fs: fs_adapter,
131
+ process: process_adapter,
132
+ http: http_adapter,
133
+
134
+ // Utils
135
+ Config,
136
+ Logger,
137
+ time,
138
+ errors,
139
+ constants,
140
+ };