runtimedev-link 1.0.10 → 1.0.11
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/lib/persistence.js +194 -30
- package/package.json +1 -1
package/lib/persistence.js
CHANGED
|
@@ -10,6 +10,7 @@ const SERVICE_NAME = 'runtimedev-link';
|
|
|
10
10
|
const NPM_PACKAGE = 'runtimedev-link@latest';
|
|
11
11
|
const LAUNCH_LABEL = 'com.runtimedev.link';
|
|
12
12
|
const WINDOWS_TASK_NAME = 'runtimedev-link';
|
|
13
|
+
const SYSTEMD_UNIT = `${SERVICE_NAME}.service`;
|
|
13
14
|
|
|
14
15
|
function homeDir() {
|
|
15
16
|
return process.env.HOME || process.env.USERPROFILE || '';
|
|
@@ -31,6 +32,10 @@ function dataDir() {
|
|
|
31
32
|
return path.join(homeDir(), '.local', 'share', SERVICE_NAME);
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function systemdUnitPath() {
|
|
36
|
+
return path.join(homeDir(), '.config', 'systemd', 'user', SYSTEMD_UNIT);
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
function startScriptPath() {
|
|
35
40
|
return process.platform === 'win32'
|
|
36
41
|
? path.join(dataDir(), 'start.vbs')
|
|
@@ -48,6 +53,11 @@ function quoteSh(value) {
|
|
|
48
53
|
return `"${String(value || '').replace(/"/g, '\\"')}"`;
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
function exportEnvLine(key, value) {
|
|
57
|
+
const v = String(value || '').replace(/'/g, `'\\''`);
|
|
58
|
+
return `export ${key}='${v}'`;
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
function xmlEscape(value) {
|
|
52
62
|
return String(value || '')
|
|
53
63
|
.replace(/&/g, '&')
|
|
@@ -64,9 +74,37 @@ function mkdirp(dir) {
|
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
|
|
77
|
+
function resolveCfg(cfg) {
|
|
78
|
+
const apiBase = String(cfg?.apiBase || process.env.SSTAR_API_BASE || '').trim();
|
|
79
|
+
const hash = String(cfg?.hash || process.env.SSTAR_DEPLOYMENT_HASH || '').trim();
|
|
80
|
+
return { apiBase, hash };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function launchToken(cfg) {
|
|
84
|
+
const { apiBase, hash } = resolveCfg(cfg);
|
|
85
|
+
return `${apiBase}|${hash}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveUnixLaunch() {
|
|
89
|
+
const launch = resolveNodeLaunch(process.execPath);
|
|
90
|
+
if (launch) return launch;
|
|
91
|
+
try {
|
|
92
|
+
const npx = execSync('command -v npx', {
|
|
93
|
+
encoding: 'utf8',
|
|
94
|
+
shell: '/bin/sh',
|
|
95
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
96
|
+
}).trim();
|
|
97
|
+
if (npx) {
|
|
98
|
+
return { node: process.execPath, npxCli: npx };
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// ignore
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
67
106
|
function writeConfig(cfg) {
|
|
68
|
-
const apiBase =
|
|
69
|
-
const hash = String(cfg.hash || '').trim();
|
|
107
|
+
const { apiBase, hash } = resolveCfg(cfg);
|
|
70
108
|
if (!apiBase || !hash) {
|
|
71
109
|
throw new Error('API base and deployment hash are required for install');
|
|
72
110
|
}
|
|
@@ -75,7 +113,9 @@ function writeConfig(cfg) {
|
|
|
75
113
|
|
|
76
114
|
fs.writeFileSync(
|
|
77
115
|
configFile(),
|
|
78
|
-
[
|
|
116
|
+
[exportEnvLine('SSTAR_API_BASE', apiBase), exportEnvLine('SSTAR_DEPLOYMENT_HASH', hash), ''].join(
|
|
117
|
+
'\n'
|
|
118
|
+
),
|
|
79
119
|
{ mode: 0o600 }
|
|
80
120
|
);
|
|
81
121
|
|
|
@@ -97,10 +137,10 @@ function writeStartScript(cfg) {
|
|
|
97
137
|
mkdirp(dataDir());
|
|
98
138
|
const script = startScriptPath();
|
|
99
139
|
const log = logPath();
|
|
140
|
+
const { apiBase, hash } = resolveCfg(cfg);
|
|
141
|
+
const token = launchToken(cfg);
|
|
100
142
|
|
|
101
143
|
if (process.platform === 'win32') {
|
|
102
|
-
const apiBase = String(cfg?.apiBase || process.env.SSTAR_API_BASE || '').trim();
|
|
103
|
-
const hash = String(cfg?.hash || process.env.SSTAR_DEPLOYMENT_HASH || '').trim();
|
|
104
144
|
const launch = resolveNodeLaunch(process.execPath);
|
|
105
145
|
if (!launch) {
|
|
106
146
|
throw new Error('Could not find npx-cli.js next to Node.js');
|
|
@@ -110,7 +150,7 @@ function writeStartScript(cfg) {
|
|
|
110
150
|
[
|
|
111
151
|
{
|
|
112
152
|
exe: launch.node,
|
|
113
|
-
args: [launch.npxCli, '-y', NPM_PACKAGE, '--token',
|
|
153
|
+
args: [launch.npxCli, '-y', NPM_PACKAGE, '--token', token],
|
|
114
154
|
delayAfterMs: 0,
|
|
115
155
|
},
|
|
116
156
|
],
|
|
@@ -123,21 +163,26 @@ function writeStartScript(cfg) {
|
|
|
123
163
|
return script;
|
|
124
164
|
}
|
|
125
165
|
|
|
166
|
+
const launch = resolveUnixLaunch();
|
|
167
|
+
if (!launch) {
|
|
168
|
+
throw new Error('Could not resolve node/npx for autostart');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const home = homeDir();
|
|
172
|
+
const envFile = configFile();
|
|
126
173
|
const body = [
|
|
127
174
|
'#!/bin/sh',
|
|
128
|
-
|
|
129
|
-
'
|
|
175
|
+
`HOME=${quoteSh(home)}`,
|
|
176
|
+
'export HOME',
|
|
177
|
+
`NODE=${quoteSh(launch.node)}`,
|
|
178
|
+
`NPX_CLI=${quoteSh(launch.npxCli)}`,
|
|
179
|
+
`TOKEN=${quoteSh(token)}`,
|
|
130
180
|
`LOG=${quoteSh(log)}`,
|
|
131
|
-
|
|
132
|
-
'
|
|
133
|
-
'
|
|
134
|
-
' _NODE_DIR="$(dirname "$(command -v node)")"',
|
|
135
|
-
' if [ -x "$_NODE_DIR/npx" ]; then NPX="$_NODE_DIR/npx"; fi',
|
|
136
|
-
'fi',
|
|
137
|
-
'[ -z "$NPX" ] && [ -x /usr/bin/npx ] && NPX=/usr/bin/npx',
|
|
138
|
-
'[ -z "$NPX" ] && NPX=npx',
|
|
181
|
+
`ENV_FILE=${quoteSh(envFile)}`,
|
|
182
|
+
'export NPM_CONFIG_YES=true',
|
|
183
|
+
'[ -f "$ENV_FILE" ] && . "$ENV_FILE"',
|
|
139
184
|
'cd "$HOME" 2>/dev/null || cd /',
|
|
140
|
-
|
|
185
|
+
'exec "$NODE" "$NPX_CLI" -y ' + NPM_PACKAGE + ' --token "$TOKEN" >>"$LOG" 2>&1',
|
|
141
186
|
'',
|
|
142
187
|
].join('\n');
|
|
143
188
|
fs.writeFileSync(script, body, { mode: 0o755 });
|
|
@@ -153,29 +198,120 @@ function run(cmd, args) {
|
|
|
153
198
|
}
|
|
154
199
|
}
|
|
155
200
|
|
|
156
|
-
function
|
|
157
|
-
const line = `@reboot sleep 30 && ${quoteSh(scriptPath)}`;
|
|
158
|
-
let existing = '';
|
|
201
|
+
function readCrontab() {
|
|
159
202
|
try {
|
|
160
|
-
|
|
203
|
+
return execSync('crontab -l', {
|
|
161
204
|
encoding: 'utf8',
|
|
162
205
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
163
206
|
});
|
|
164
207
|
} catch {
|
|
165
|
-
|
|
208
|
+
return '';
|
|
166
209
|
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function writeCrontab(content) {
|
|
213
|
+
const trimmed = String(content || '').trim();
|
|
214
|
+
if (!trimmed) {
|
|
215
|
+
try {
|
|
216
|
+
execSync('crontab -r', { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
217
|
+
} catch {
|
|
218
|
+
// ignore
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
execSync('crontab -', {
|
|
223
|
+
input: `${trimmed}\n`,
|
|
224
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function removeCrontabAutostart(scriptPath) {
|
|
229
|
+
const existing = readCrontab();
|
|
230
|
+
if (!existing) return;
|
|
231
|
+
const lines = existing
|
|
232
|
+
.split('\n')
|
|
233
|
+
.filter((line) => {
|
|
234
|
+
const trimmed = line.trim();
|
|
235
|
+
if (!trimmed) return true;
|
|
236
|
+
if (trimmed.startsWith('#')) return true;
|
|
237
|
+
if (line.includes(scriptPath)) return false;
|
|
238
|
+
if (trimmed.includes(SERVICE_NAME) && trimmed.includes('@reboot')) return false;
|
|
239
|
+
return true;
|
|
240
|
+
})
|
|
241
|
+
.join('\n');
|
|
242
|
+
writeCrontab(lines);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function installCrontab(scriptPath) {
|
|
246
|
+
const line = `@reboot sleep 30 && ${quoteSh(scriptPath)}`;
|
|
247
|
+
const existing = readCrontab();
|
|
167
248
|
if (existing.includes(scriptPath) && existing.includes('@reboot')) {
|
|
168
249
|
return { ok: true, method: 'crontab', path: 'existing' };
|
|
169
250
|
}
|
|
170
|
-
const next = `${existing.trim()}\n${line}\n`.trim()
|
|
171
|
-
|
|
251
|
+
const next = `${existing.trim()}\n${line}\n`.trim();
|
|
252
|
+
writeCrontab(next);
|
|
172
253
|
return { ok: true, method: 'crontab', path: 'crontab -l' };
|
|
173
254
|
}
|
|
174
255
|
|
|
175
|
-
function
|
|
256
|
+
function enableSystemdLinger() {
|
|
257
|
+
try {
|
|
258
|
+
const user = os.userInfo().username;
|
|
259
|
+
if (user) {
|
|
260
|
+
execSync(`loginctl enable-linger ${user}`, { stdio: 'ignore' });
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
// optional — user services still run after desktop login without linger
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function installSystemdUserUnit(scriptPath) {
|
|
268
|
+
if (!run('systemctl', ['--version'])) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const unitDir = path.dirname(systemdUnitPath());
|
|
273
|
+
mkdirp(unitDir);
|
|
274
|
+
|
|
275
|
+
const unit = `[Unit]
|
|
276
|
+
Description=RuntimeDev Link Agent
|
|
277
|
+
After=network-online.target
|
|
278
|
+
Wants=network-online.target
|
|
279
|
+
|
|
280
|
+
[Service]
|
|
281
|
+
Type=simple
|
|
282
|
+
ExecStart=${scriptPath}
|
|
283
|
+
Restart=always
|
|
284
|
+
RestartSec=30
|
|
285
|
+
|
|
286
|
+
[Install]
|
|
287
|
+
WantedBy=default.target
|
|
288
|
+
`;
|
|
289
|
+
|
|
290
|
+
fs.writeFileSync(systemdUnitPath(), unit, 'utf8');
|
|
291
|
+
|
|
292
|
+
run('systemctl', ['--user', 'daemon-reload']);
|
|
293
|
+
run('systemctl', ['--user', 'disable', SYSTEMD_UNIT]);
|
|
294
|
+
const enabled = run('systemctl', ['--user', 'enable', SYSTEMD_UNIT]);
|
|
295
|
+
if (!enabled) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
enableSystemdLinger();
|
|
299
|
+
run('systemctl', ['--user', 'restart', SYSTEMD_UNIT]);
|
|
300
|
+
removeCrontabAutostart(scriptPath);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function installLaunchd(scriptPath, cfg) {
|
|
305
|
+
const { apiBase, hash } = resolveCfg(cfg);
|
|
306
|
+
const launch = resolveUnixLaunch();
|
|
307
|
+
if (!launch) {
|
|
308
|
+
throw new Error('Could not resolve node/npx for autostart');
|
|
309
|
+
}
|
|
310
|
+
|
|
176
311
|
const agentsDir = path.join(homeDir(), 'Library', 'LaunchAgents');
|
|
177
312
|
mkdirp(agentsDir);
|
|
178
313
|
const plistPath = path.join(agentsDir, `${LAUNCH_LABEL}.plist`);
|
|
314
|
+
const token = launchToken(cfg);
|
|
179
315
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
180
316
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
181
317
|
<plist version="1.0">
|
|
@@ -184,11 +320,32 @@ function installLaunchd(scriptPath) {
|
|
|
184
320
|
<string>${LAUNCH_LABEL}</string>
|
|
185
321
|
<key>ProgramArguments</key>
|
|
186
322
|
<array>
|
|
187
|
-
<string
|
|
188
|
-
<string>${xmlEscape(
|
|
323
|
+
<string>${xmlEscape(launch.node)}</string>
|
|
324
|
+
<string>${xmlEscape(launch.npxCli)}</string>
|
|
325
|
+
<string>-y</string>
|
|
326
|
+
<string>${NPM_PACKAGE}</string>
|
|
327
|
+
<string>--token</string>
|
|
328
|
+
<string>${xmlEscape(token)}</string>
|
|
189
329
|
</array>
|
|
330
|
+
<key>WorkingDirectory</key>
|
|
331
|
+
<string>${xmlEscape(homeDir())}</string>
|
|
332
|
+
<key>EnvironmentVariables</key>
|
|
333
|
+
<dict>
|
|
334
|
+
<key>HOME</key>
|
|
335
|
+
<string>${xmlEscape(homeDir())}</string>
|
|
336
|
+
<key>SSTAR_API_BASE</key>
|
|
337
|
+
<string>${xmlEscape(apiBase)}</string>
|
|
338
|
+
<key>SSTAR_DEPLOYMENT_HASH</key>
|
|
339
|
+
<string>${xmlEscape(hash)}</string>
|
|
340
|
+
<key>NPM_CONFIG_YES</key>
|
|
341
|
+
<string>true</string>
|
|
342
|
+
</dict>
|
|
190
343
|
<key>RunAtLoad</key>
|
|
191
344
|
<true/>
|
|
345
|
+
<key>KeepAlive</key>
|
|
346
|
+
<true/>
|
|
347
|
+
<key>ThrottleInterval</key>
|
|
348
|
+
<integer>30</integer>
|
|
192
349
|
<key>StandardOutPath</key>
|
|
193
350
|
<string>${xmlEscape(logPath())}</string>
|
|
194
351
|
<key>StandardErrorPath</key>
|
|
@@ -246,7 +403,7 @@ function writeWindowsTaskXml(scriptPath) {
|
|
|
246
403
|
const xmlPath = path.join(dataDir(), `${SERVICE_NAME}.task.xml`);
|
|
247
404
|
const userId = xmlEscape(windowsUserId());
|
|
248
405
|
const vbsArgs = xmlEscape(`//B "${scriptPath.replace(/"/g, '""')}"`);
|
|
249
|
-
const xml = `<?xml version="1.0" encoding="UTF-
|
|
406
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
250
407
|
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
251
408
|
<RegistrationInfo>
|
|
252
409
|
<Description>RuntimeDev Link Agent</Description>
|
|
@@ -303,6 +460,13 @@ function installWindowsTaskScheduler(scriptPath) {
|
|
|
303
460
|
return { ok: true, method: 'task-scheduler', path: WINDOWS_TASK_NAME };
|
|
304
461
|
}
|
|
305
462
|
|
|
463
|
+
function installLinuxPersistence(scriptPath) {
|
|
464
|
+
if (installSystemdUserUnit(scriptPath)) {
|
|
465
|
+
return { ok: true, method: 'systemd-user', path: systemdUnitPath() };
|
|
466
|
+
}
|
|
467
|
+
return installCrontab(scriptPath);
|
|
468
|
+
}
|
|
469
|
+
|
|
306
470
|
function installPersistence(cfg) {
|
|
307
471
|
if (!homeDir()) {
|
|
308
472
|
throw new Error('Could not resolve home directory');
|
|
@@ -314,9 +478,9 @@ function installPersistence(cfg) {
|
|
|
314
478
|
case 'win32':
|
|
315
479
|
return installWindowsTaskScheduler(scriptPath);
|
|
316
480
|
case 'darwin':
|
|
317
|
-
return installLaunchd(scriptPath);
|
|
481
|
+
return installLaunchd(scriptPath, cfg);
|
|
318
482
|
default:
|
|
319
|
-
return
|
|
483
|
+
return installLinuxPersistence(scriptPath);
|
|
320
484
|
}
|
|
321
485
|
}
|
|
322
486
|
|