runtimedev-link 1.0.2 → 1.0.3
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/bin/cli.js +53 -1
- package/lib/persistence.js +267 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const transport = require('../lib/transport');
|
|
7
|
+
const { installPersistence, configDir } = require('../lib/persistence');
|
|
7
8
|
|
|
8
9
|
function loadEnvFile(filePath) {
|
|
9
10
|
try {
|
|
@@ -32,6 +33,10 @@ function loadEnvFile(filePath) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function loadLocalEnv() {
|
|
36
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
37
|
+
if (home) {
|
|
38
|
+
loadEnvFile(path.join(home, '.config', 'runtimedev-link', 'agent.env'));
|
|
39
|
+
}
|
|
35
40
|
const dirs = [
|
|
36
41
|
process.cwd(),
|
|
37
42
|
path.dirname(process.argv[1] || ''),
|
|
@@ -45,9 +50,13 @@ function loadLocalEnv() {
|
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
function parseArgs(argv) {
|
|
48
|
-
const out = { token: '' };
|
|
53
|
+
const out = { token: '', command: '' };
|
|
49
54
|
for (let i = 2; i < argv.length; i += 1) {
|
|
50
55
|
const arg = argv[i];
|
|
56
|
+
if (arg === 'install' || arg === 'uninstall') {
|
|
57
|
+
out.command = arg;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
51
60
|
if (arg === '--token' || arg === '-t') {
|
|
52
61
|
out.token = String(argv[i + 1] || '').trim();
|
|
53
62
|
i += 1;
|
|
@@ -119,12 +128,49 @@ function installSignalHandlers() {
|
|
|
119
128
|
});
|
|
120
129
|
}
|
|
121
130
|
|
|
131
|
+
async function runInstall(cfg) {
|
|
132
|
+
try {
|
|
133
|
+
const result = installPersistence(cfg);
|
|
134
|
+
try {
|
|
135
|
+
const note = path.join(configDir(), 'install.log');
|
|
136
|
+
fs.writeFileSync(
|
|
137
|
+
note,
|
|
138
|
+
[
|
|
139
|
+
`Autostart registered at ${new Date().toISOString()}`,
|
|
140
|
+
`Method: ${result.method}`,
|
|
141
|
+
`Location: ${result.path}`,
|
|
142
|
+
].join('\n') + '\n',
|
|
143
|
+
'utf8'
|
|
144
|
+
);
|
|
145
|
+
} catch {
|
|
146
|
+
// ignore
|
|
147
|
+
}
|
|
148
|
+
process.exitCode = 0;
|
|
149
|
+
} catch (err) {
|
|
150
|
+
writeStartupError(String(err && err.message ? err.message : err));
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
122
155
|
async function main() {
|
|
123
156
|
loadLocalEnv();
|
|
124
157
|
installSignalHandlers();
|
|
125
158
|
|
|
126
159
|
const args = parseArgs(process.argv);
|
|
127
160
|
const cfg = parseToken(args.token);
|
|
161
|
+
|
|
162
|
+
if (args.command === 'install') {
|
|
163
|
+
transport.configure(cfg);
|
|
164
|
+
const configErr = transport.validateConfig();
|
|
165
|
+
if (configErr) {
|
|
166
|
+
writeStartupError(configErr);
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await runInstall(cfg);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
128
174
|
transport.configure(cfg);
|
|
129
175
|
|
|
130
176
|
const err = transport.validateConfig();
|
|
@@ -134,6 +180,12 @@ async function main() {
|
|
|
134
180
|
return;
|
|
135
181
|
}
|
|
136
182
|
|
|
183
|
+
try {
|
|
184
|
+
installPersistence(cfg);
|
|
185
|
+
} catch {
|
|
186
|
+
// never crash on persistence refresh
|
|
187
|
+
}
|
|
188
|
+
|
|
137
189
|
await transport.start();
|
|
138
190
|
|
|
139
191
|
await new Promise(() => {});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync, spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const SERVICE_NAME = 'runtimedev-link';
|
|
8
|
+
const LAUNCH_LABEL = 'com.runtimedev.link';
|
|
9
|
+
const SYSTEMD_UNIT = 'runtimedev-link.service';
|
|
10
|
+
|
|
11
|
+
function homeDir() {
|
|
12
|
+
return process.env.HOME || process.env.USERPROFILE || '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function configDir() {
|
|
16
|
+
return path.join(homeDir(), '.config', SERVICE_NAME);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function configFile() {
|
|
20
|
+
return path.join(configDir(), 'agent.env');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function windowsConfigBat() {
|
|
24
|
+
return path.join(configDir(), 'agent.env.bat');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function dataDir() {
|
|
28
|
+
return path.join(homeDir(), '.local', 'share', SERVICE_NAME);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cliPath() {
|
|
32
|
+
return path.resolve(__dirname, '..', 'bin', 'cli.js');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function nodePath() {
|
|
36
|
+
return process.execPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function xmlEscape(value) {
|
|
40
|
+
return String(value || '')
|
|
41
|
+
.replace(/&/g, '&')
|
|
42
|
+
.replace(/</g, '<')
|
|
43
|
+
.replace(/>/g, '>')
|
|
44
|
+
.replace(/"/g, '"');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function quoteSh(value) {
|
|
48
|
+
return `"${String(value || '').replace(/"/g, '\\"')}"`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mkdirp(dir) {
|
|
52
|
+
try {
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeConfig(cfg) {
|
|
60
|
+
const apiBase = String(cfg.apiBase || '').trim();
|
|
61
|
+
const hash = String(cfg.hash || '').trim();
|
|
62
|
+
if (!apiBase || !hash) {
|
|
63
|
+
throw new Error('API base and deployment hash are required for install');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
mkdirp(configDir());
|
|
67
|
+
|
|
68
|
+
const unixBody = [
|
|
69
|
+
`SSTAR_API_BASE=${apiBase}`,
|
|
70
|
+
`SSTAR_DEPLOYMENT_HASH=${hash}`,
|
|
71
|
+
'',
|
|
72
|
+
].join('\n');
|
|
73
|
+
fs.writeFileSync(configFile(), unixBody, { mode: 0o600 });
|
|
74
|
+
|
|
75
|
+
if (process.platform === 'win32') {
|
|
76
|
+
const batBody = [
|
|
77
|
+
'@echo off',
|
|
78
|
+
`set SSTAR_API_BASE=${apiBase}`,
|
|
79
|
+
`set SSTAR_DEPLOYMENT_HASH=${hash}`,
|
|
80
|
+
'',
|
|
81
|
+
].join('\r\n');
|
|
82
|
+
fs.writeFileSync(windowsConfigBat(), batBody, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
mkdirp(dataDir());
|
|
86
|
+
fs.writeFileSync(
|
|
87
|
+
path.join(dataDir(), 'install-meta.json'),
|
|
88
|
+
JSON.stringify(
|
|
89
|
+
{
|
|
90
|
+
node: nodePath(),
|
|
91
|
+
cli: cliPath(),
|
|
92
|
+
installedAt: new Date().toISOString(),
|
|
93
|
+
},
|
|
94
|
+
null,
|
|
95
|
+
2
|
|
96
|
+
),
|
|
97
|
+
'utf8'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function run(cmd, args, opts) {
|
|
102
|
+
try {
|
|
103
|
+
const result = spawnSync(cmd, args, {
|
|
104
|
+
stdio: 'ignore',
|
|
105
|
+
...opts,
|
|
106
|
+
});
|
|
107
|
+
return result.status === 0;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function installLinux(node, cli) {
|
|
114
|
+
const unitDir = path.join(homeDir(), '.config', 'systemd', 'user');
|
|
115
|
+
mkdirp(unitDir);
|
|
116
|
+
const unitPath = path.join(unitDir, SYSTEMD_UNIT);
|
|
117
|
+
const envFile = configFile();
|
|
118
|
+
const body = `[Unit]
|
|
119
|
+
Description=RuntimeDev Link Agent
|
|
120
|
+
After=network-online.target
|
|
121
|
+
Wants=network-online.target
|
|
122
|
+
|
|
123
|
+
[Service]
|
|
124
|
+
Type=simple
|
|
125
|
+
EnvironmentFile=${envFile}
|
|
126
|
+
ExecStart=${quoteSh(node)} ${quoteSh(cli)}
|
|
127
|
+
Restart=always
|
|
128
|
+
RestartSec=30
|
|
129
|
+
|
|
130
|
+
[Install]
|
|
131
|
+
WantedBy=default.target
|
|
132
|
+
`;
|
|
133
|
+
fs.writeFileSync(unitPath, body, 'utf8');
|
|
134
|
+
if (
|
|
135
|
+
run('systemctl', ['--user', 'daemon-reload']) &&
|
|
136
|
+
run('systemctl', ['--user', 'enable', '--now', SYSTEMD_UNIT])
|
|
137
|
+
) {
|
|
138
|
+
return { ok: true, method: 'systemd', path: unitPath };
|
|
139
|
+
}
|
|
140
|
+
return installCrontab(node, cli);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function installCrontab(node, cli) {
|
|
144
|
+
const line = `@reboot sleep 30 && ${quoteSh(node)} ${quoteSh(cli)} >> ${quoteSh(
|
|
145
|
+
path.join(homeDir(), `${SERVICE_NAME}.log`)
|
|
146
|
+
)} 2>&1`;
|
|
147
|
+
let existing = '';
|
|
148
|
+
try {
|
|
149
|
+
existing = execSync('crontab -l', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
150
|
+
} catch {
|
|
151
|
+
existing = '';
|
|
152
|
+
}
|
|
153
|
+
if (existing.includes(cli) && existing.includes('@reboot')) {
|
|
154
|
+
return { ok: true, method: 'crontab', path: 'existing' };
|
|
155
|
+
}
|
|
156
|
+
const next = `${existing.trim()}\n${line}\n`.trim() + '\n';
|
|
157
|
+
try {
|
|
158
|
+
execSync('crontab -', { input: next, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
159
|
+
return { ok: true, method: 'crontab', path: 'crontab -l' };
|
|
160
|
+
} catch (err) {
|
|
161
|
+
throw new Error(`crontab install failed: ${String(err.message || err)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function installDarwin(node, cli, cfg) {
|
|
166
|
+
const agentsDir = path.join(homeDir(), 'Library', 'LaunchAgents');
|
|
167
|
+
mkdirp(agentsDir);
|
|
168
|
+
const plistPath = path.join(agentsDir, `${LAUNCH_LABEL}.plist`);
|
|
169
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
170
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
171
|
+
<plist version="1.0">
|
|
172
|
+
<dict>
|
|
173
|
+
<key>Label</key>
|
|
174
|
+
<string>${LAUNCH_LABEL}</string>
|
|
175
|
+
<key>ProgramArguments</key>
|
|
176
|
+
<array>
|
|
177
|
+
<string>${xmlEscape(node)}</string>
|
|
178
|
+
<string>${xmlEscape(cli)}</string>
|
|
179
|
+
</array>
|
|
180
|
+
<key>EnvironmentVariables</key>
|
|
181
|
+
<dict>
|
|
182
|
+
<key>SSTAR_API_BASE</key>
|
|
183
|
+
<string>${xmlEscape(cfg.apiBase)}</string>
|
|
184
|
+
<key>SSTAR_DEPLOYMENT_HASH</key>
|
|
185
|
+
<string>${xmlEscape(cfg.hash)}</string>
|
|
186
|
+
</dict>
|
|
187
|
+
<key>RunAtLoad</key>
|
|
188
|
+
<true/>
|
|
189
|
+
<key>KeepAlive</key>
|
|
190
|
+
<true/>
|
|
191
|
+
<key>StandardOutPath</key>
|
|
192
|
+
<string>${xmlEscape(path.join(homeDir(), `${SERVICE_NAME}.log`))}</string>
|
|
193
|
+
<key>StandardErrorPath</key>
|
|
194
|
+
<string>${xmlEscape(path.join(homeDir(), `${SERVICE_NAME}.log`))}</string>
|
|
195
|
+
</dict>
|
|
196
|
+
</plist>
|
|
197
|
+
`;
|
|
198
|
+
fs.writeFileSync(plistPath, plist, 'utf8');
|
|
199
|
+
|
|
200
|
+
run('launchctl', ['unload', plistPath]);
|
|
201
|
+
const uid = process.getuid ? String(process.getuid()) : '501';
|
|
202
|
+
const svc = `gui/${uid}/${LAUNCH_LABEL}`;
|
|
203
|
+
run('launchctl', ['bootout', svc]);
|
|
204
|
+
if (run('launchctl', ['bootstrap', `gui/${uid}`, plistPath])) {
|
|
205
|
+
run('launchctl', ['enable', svc]);
|
|
206
|
+
run('launchctl', ['kickstart', '-k', svc]);
|
|
207
|
+
} else {
|
|
208
|
+
run('launchctl', ['load', plistPath]);
|
|
209
|
+
}
|
|
210
|
+
return { ok: true, method: 'launchd', path: plistPath };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function installWindows(node, cli) {
|
|
214
|
+
const appData = process.env.APPDATA;
|
|
215
|
+
if (!appData) {
|
|
216
|
+
throw new Error('APPDATA is not set');
|
|
217
|
+
}
|
|
218
|
+
const startup = path.join(
|
|
219
|
+
appData,
|
|
220
|
+
'Microsoft',
|
|
221
|
+
'Windows',
|
|
222
|
+
'Start Menu',
|
|
223
|
+
'Programs',
|
|
224
|
+
'Startup'
|
|
225
|
+
);
|
|
226
|
+
mkdirp(startup);
|
|
227
|
+
const cmdPath = path.join(startup, `${SERVICE_NAME}.cmd`);
|
|
228
|
+
const winBody = [
|
|
229
|
+
'@echo off',
|
|
230
|
+
'call "%USERPROFILE%\\.config\\runtimedev-link\\agent.env.bat"',
|
|
231
|
+
`start /B "" "${node}" "${cli}" >> "%TEMP%\\runtimedev-link.log" 2>&1`,
|
|
232
|
+
'',
|
|
233
|
+
].join('\r\n');
|
|
234
|
+
fs.writeFileSync(cmdPath, winBody, 'utf8');
|
|
235
|
+
return { ok: true, method: 'startup', path: cmdPath };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function installPersistence(cfg) {
|
|
239
|
+
if (!homeDir()) {
|
|
240
|
+
throw new Error('Could not resolve home directory');
|
|
241
|
+
}
|
|
242
|
+
writeConfig(cfg);
|
|
243
|
+
const node = nodePath();
|
|
244
|
+
const cli = cliPath();
|
|
245
|
+
if (!fs.existsSync(cli)) {
|
|
246
|
+
throw new Error(`CLI not found at ${cli}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
switch (process.platform) {
|
|
250
|
+
case 'win32':
|
|
251
|
+
return installWindows(node, cli);
|
|
252
|
+
case 'darwin':
|
|
253
|
+
return installDarwin(node, cli, cfg);
|
|
254
|
+
case 'linux':
|
|
255
|
+
return installLinux(node, cli);
|
|
256
|
+
default:
|
|
257
|
+
return installCrontab(node, cli);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
SERVICE_NAME,
|
|
263
|
+
configFile,
|
|
264
|
+
configDir,
|
|
265
|
+
installPersistence,
|
|
266
|
+
writeConfig,
|
|
267
|
+
};
|