@geekbeer/minion 3.5.6 → 3.5.30
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/package.json +4 -2
- package/postinstall.js +18 -0
- package/win/lib/process-manager.js +92 -25
- package/win/minion-cli.ps1 +29 -10
- package/win/postinstall.ps1 +82 -0
- package/win/routes/commands.js +42 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekbeer/minion",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.30",
|
|
4
4
|
"description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
|
|
5
5
|
"main": "linux/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"hq-win": "./win/bin/hq-win.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
+
"postinstall.js",
|
|
13
14
|
"core/",
|
|
14
15
|
"linux/",
|
|
15
16
|
"win/",
|
|
@@ -22,7 +23,8 @@
|
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
24
25
|
"start": "node linux/server.js",
|
|
25
|
-
"start:win": "node win/server.js"
|
|
26
|
+
"start:win": "node win/server.js",
|
|
27
|
+
"postinstall": "node postinstall.js"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
30
|
"better-sqlite3": "^11.0.0",
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// postinstall.js — Re-apply file ACLs on Windows after npm install -g.
|
|
3
|
+
// On Linux this is a no-op (permissions are not affected by reinstall).
|
|
4
|
+
|
|
5
|
+
if (process.platform !== 'win32') process.exit(0);
|
|
6
|
+
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const ps1 = path.join(__dirname, 'win', 'postinstall.ps1');
|
|
11
|
+
const result = spawnSync('powershell.exe', [
|
|
12
|
+
'-ExecutionPolicy', 'Bypass',
|
|
13
|
+
'-NoProfile',
|
|
14
|
+
'-NoLogo',
|
|
15
|
+
'-File', ps1,
|
|
16
|
+
], { stdio: 'inherit', timeout: 30_000 });
|
|
17
|
+
|
|
18
|
+
process.exit(result.status || 0);
|
|
@@ -35,6 +35,20 @@ function getNssmPath() {
|
|
|
35
35
|
return vendorNssm
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Derive the npm global prefix from the nssm.exe path.
|
|
40
|
+
* nssm is bundled at: <npm-prefix>/node_modules/@geekbeer/minion/win/vendor/nssm.exe
|
|
41
|
+
* So the prefix is the ancestor directory containing node_modules.
|
|
42
|
+
* @param {string} nssmPath - Absolute path to nssm.exe
|
|
43
|
+
* @returns {string|null} npm global prefix, or null if not derivable
|
|
44
|
+
*/
|
|
45
|
+
function getNpmPrefix(nssmPath) {
|
|
46
|
+
const parts = nssmPath.split(path.sep)
|
|
47
|
+
const nmIndex = parts.lastIndexOf('node_modules')
|
|
48
|
+
if (nmIndex > 0) return parts.slice(0, nmIndex).join(path.sep)
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
/**
|
|
39
53
|
* Detect Windows process manager. Always returns 'nssm'.
|
|
40
54
|
* @returns {'nssm'}
|
|
@@ -56,7 +70,7 @@ function buildGracefulStopBlock(agentPort, apiToken) {
|
|
|
56
70
|
return [
|
|
57
71
|
` try {`,
|
|
58
72
|
` Invoke-RestMethod -Uri 'http://localhost:${agentPort}/api/shutdown' -Method POST ` +
|
|
59
|
-
`-ContentType 'application/json' -Headers @{ Authorization = 'Bearer ${apiToken}' } -TimeoutSec 5 | Out-Null`,
|
|
73
|
+
`-Body '{}' -ContentType 'application/json' -Headers @{ Authorization = 'Bearer ${apiToken}' } -TimeoutSec 5 | Out-Null`,
|
|
60
74
|
` Log 'Graceful shutdown requested, waiting for offline heartbeat...'`,
|
|
61
75
|
` Start-Sleep -Seconds 4`,
|
|
62
76
|
` } catch {`,
|
|
@@ -71,6 +85,11 @@ function buildGracefulStopBlock(agentPort, apiToken) {
|
|
|
71
85
|
* 2. Stop service via NSSM
|
|
72
86
|
* 3. Run npm install -g
|
|
73
87
|
* 4. Start service via NSSM
|
|
88
|
+
* 5. Remove the temporary updater service (self-cleanup)
|
|
89
|
+
*
|
|
90
|
+
* This script is registered as a temporary NSSM service ("minion-update")
|
|
91
|
+
* so it runs independently of the minion-agent service and survives
|
|
92
|
+
* the agent's stop/restart cycle.
|
|
74
93
|
*
|
|
75
94
|
* @param {string} npmInstallCmd - The npm install command to run
|
|
76
95
|
* @param {string} nssmPath - Absolute path to nssm.exe
|
|
@@ -81,34 +100,63 @@ function buildGracefulStopBlock(agentPort, apiToken) {
|
|
|
81
100
|
*/
|
|
82
101
|
function buildUpdateScript(npmInstallCmd, nssmPath, scriptName = 'update-agent', agentPort = 8080, apiToken = '') {
|
|
83
102
|
const dataDir = path.join(os.homedir(), '.minion')
|
|
103
|
+
const homeDir = os.homedir()
|
|
104
|
+
const npmPrefix = getNpmPrefix(nssmPath)
|
|
84
105
|
const scriptPath = path.join(dataDir, `${scriptName}.ps1`)
|
|
85
|
-
const
|
|
86
|
-
const
|
|
106
|
+
const logDir = path.join(dataDir, 'logs')
|
|
107
|
+
const logPath = path.join(logDir, `${scriptName}.log`)
|
|
108
|
+
|
|
109
|
+
// Append --prefix to npm command so it installs to the correct global directory
|
|
110
|
+
const fullNpmCmd = npmPrefix
|
|
111
|
+
? `${npmInstallCmd} --prefix "${npmPrefix}"`
|
|
112
|
+
: npmInstallCmd
|
|
87
113
|
|
|
88
114
|
const gracefulStop = buildGracefulStopBlock(agentPort, apiToken)
|
|
89
115
|
|
|
90
116
|
const ps1 = [
|
|
117
|
+
`# Override LocalSystem's default profile so npm uses the correct paths`,
|
|
118
|
+
`$env:USERPROFILE = '${homeDir}'`,
|
|
119
|
+
`$env:HOME = '${homeDir}'`,
|
|
91
120
|
`$ErrorActionPreference = 'Stop'`,
|
|
92
|
-
|
|
121
|
+
`# Use nssm.exe from data dir (copied there to avoid EBUSY on package files)`,
|
|
122
|
+
`$nssm = '${path.join(dataDir, 'nssm.exe')}'`,
|
|
123
|
+
`$logFile = '${logPath}'`,
|
|
93
124
|
`function Log($msg) { "$(Get-Date -Format o) $msg" | Out-File -Append $logFile }`,
|
|
94
125
|
`Log 'Update started'`,
|
|
95
126
|
`try {`,
|
|
96
127
|
` Log 'Requesting graceful shutdown...'`,
|
|
97
128
|
gracefulStop,
|
|
98
129
|
` Log 'Stopping service via NSSM...'`,
|
|
99
|
-
` &
|
|
130
|
+
` & $nssm stop minion-agent`,
|
|
100
131
|
` Start-Sleep -Seconds 3`,
|
|
132
|
+
` # Retry npm install up to 5 times (Windows may hold file locks briefly after service stop)`,
|
|
101
133
|
` Log 'Installing package...'`,
|
|
102
|
-
` $
|
|
103
|
-
`
|
|
104
|
-
`
|
|
134
|
+
` $installed = $false`,
|
|
135
|
+
` for ($attempt = 1; $attempt -le 5; $attempt++) {`,
|
|
136
|
+
` $out = & cmd /c "${fullNpmCmd} 2>&1"`,
|
|
137
|
+
` if ($LASTEXITCODE -eq 0) {`,
|
|
138
|
+
` Log "npm output: $out"`,
|
|
139
|
+
` $installed = $true`,
|
|
140
|
+
` break`,
|
|
141
|
+
` }`,
|
|
142
|
+
` Log "npm install attempt $attempt failed (exit $LASTEXITCODE): $out"`,
|
|
143
|
+
` if ($attempt -lt 5) {`,
|
|
144
|
+
` Log "Retrying in 10 seconds..."`,
|
|
145
|
+
` Start-Sleep -Seconds 10`,
|
|
146
|
+
` }`,
|
|
147
|
+
` }`,
|
|
148
|
+
` if (-not $installed) { throw "npm install failed after 5 attempts" }`,
|
|
105
149
|
` Log 'Starting service...'`,
|
|
106
|
-
` &
|
|
150
|
+
` & $nssm start minion-agent`,
|
|
107
151
|
` Log 'Update completed successfully'`,
|
|
108
152
|
`} catch {`,
|
|
109
153
|
` Log "Update failed: $_"`,
|
|
110
154
|
` Log 'Attempting to start service anyway...'`,
|
|
111
|
-
` &
|
|
155
|
+
` & $nssm start minion-agent`,
|
|
156
|
+
`} finally {`,
|
|
157
|
+
` Log 'Cleaning up updater service...'`,
|
|
158
|
+
` & $nssm stop minion-update confirm 2>$null`,
|
|
159
|
+
` & $nssm remove minion-update confirm 2>$null`,
|
|
112
160
|
`}`,
|
|
113
161
|
].join('\n')
|
|
114
162
|
|
|
@@ -122,35 +170,47 @@ function buildUpdateScript(npmInstallCmd, nssmPath, scriptName = 'update-agent',
|
|
|
122
170
|
* Generate a temporary PowerShell restart script:
|
|
123
171
|
* 1. Graceful shutdown via HTTP API (offline heartbeat)
|
|
124
172
|
* 2. Restart service via NSSM
|
|
173
|
+
* 3. Remove the temporary updater service (self-cleanup)
|
|
174
|
+
*
|
|
175
|
+
* Registered as a temporary NSSM service ("minion-update") so the restart
|
|
176
|
+
* process is independent of the minion-agent service.
|
|
125
177
|
*
|
|
126
178
|
* @param {string} nssmPath - Absolute path to nssm.exe
|
|
127
179
|
* @param {number} agentPort - The agent's HTTP port
|
|
128
180
|
* @param {string} apiToken - The agent's API token
|
|
129
181
|
* @returns {string} - Path to the generated restart script (.ps1)
|
|
130
182
|
*/
|
|
131
|
-
function buildRestartScript(
|
|
183
|
+
function buildRestartScript(_nssmPath, agentPort, apiToken) {
|
|
132
184
|
const dataDir = path.join(os.homedir(), '.minion')
|
|
185
|
+
const homeDir = os.homedir()
|
|
133
186
|
const scriptPath = path.join(dataDir, 'restart-agent.ps1')
|
|
134
|
-
const
|
|
135
|
-
const
|
|
187
|
+
const logDir = path.join(dataDir, 'logs')
|
|
188
|
+
const logPath = path.join(logDir, 'restart-agent.log')
|
|
136
189
|
|
|
137
190
|
const gracefulStop = buildGracefulStopBlock(agentPort, apiToken)
|
|
138
191
|
|
|
139
192
|
const ps1 = [
|
|
193
|
+
`$env:USERPROFILE = '${homeDir}'`,
|
|
194
|
+
`$env:HOME = '${homeDir}'`,
|
|
140
195
|
`$ErrorActionPreference = 'Stop'`,
|
|
141
|
-
`$
|
|
196
|
+
`$nssm = '${path.join(dataDir, 'nssm.exe')}'`,
|
|
197
|
+
`$logFile = '${logPath}'`,
|
|
142
198
|
`function Log($msg) { "$(Get-Date -Format o) $msg" | Out-File -Append $logFile }`,
|
|
143
199
|
`Log 'Restart started'`,
|
|
144
200
|
`try {`,
|
|
145
201
|
` Log 'Requesting graceful shutdown...'`,
|
|
146
202
|
gracefulStop,
|
|
147
203
|
` Log 'Restarting service via NSSM...'`,
|
|
148
|
-
` &
|
|
204
|
+
` & $nssm restart minion-agent`,
|
|
149
205
|
` Log 'Restart completed successfully'`,
|
|
150
206
|
`} catch {`,
|
|
151
207
|
` Log "Restart failed: $_"`,
|
|
152
208
|
` Log 'Attempting to start service...'`,
|
|
153
|
-
` &
|
|
209
|
+
` & $nssm start minion-agent`,
|
|
210
|
+
`} finally {`,
|
|
211
|
+
` Log 'Cleaning up updater service...'`,
|
|
212
|
+
` & $nssm stop minion-update confirm 2>$null`,
|
|
213
|
+
` & $nssm remove minion-update confirm 2>$null`,
|
|
154
214
|
`}`,
|
|
155
215
|
].join('\n')
|
|
156
216
|
|
|
@@ -162,9 +222,15 @@ function buildRestartScript(nssmPath, agentPort, apiToken) {
|
|
|
162
222
|
|
|
163
223
|
/**
|
|
164
224
|
* Build allowed commands for NSSM-based service management.
|
|
225
|
+
*
|
|
226
|
+
* Deferred commands (update/restart) use a temporary NSSM service
|
|
227
|
+
* ("minion-update") to run PowerShell scripts independently of the
|
|
228
|
+
* minion-agent service. This allows the script to stop, update, and
|
|
229
|
+
* restart minion-agent without being killed in the process.
|
|
230
|
+
*
|
|
165
231
|
* @param {string} _procMgr - Ignored (always 'nssm')
|
|
166
232
|
* @param {{ AGENT_PORT?: number, API_TOKEN?: string }} [agentConfig] - Agent config
|
|
167
|
-
* @returns {Record<string, { description: string; command?: string;
|
|
233
|
+
* @returns {Record<string, { description: string; command?: string; nssmService?: { scriptPath: string }; deferred?: boolean }>}
|
|
168
234
|
*/
|
|
169
235
|
function buildAllowedCommands(_procMgr, agentConfig = {}) {
|
|
170
236
|
const commands = {}
|
|
@@ -174,28 +240,29 @@ function buildAllowedCommands(_procMgr, agentConfig = {}) {
|
|
|
174
240
|
|
|
175
241
|
commands['restart-agent'] = {
|
|
176
242
|
description: 'Restart the minion agent service',
|
|
177
|
-
|
|
178
|
-
`& '${buildRestartScript(nssmPath, agentPort, apiToken)}'`]],
|
|
243
|
+
nssmService: { scriptPath: buildRestartScript(nssmPath, agentPort, apiToken) },
|
|
179
244
|
deferred: true,
|
|
180
245
|
}
|
|
181
246
|
|
|
182
247
|
commands['update-agent'] = {
|
|
183
248
|
description: 'Update @geekbeer/minion to latest version and restart',
|
|
184
|
-
|
|
185
|
-
|
|
249
|
+
nssmService: {
|
|
250
|
+
scriptPath: buildUpdateScript(
|
|
186
251
|
'npm install -g @geekbeer/minion@latest',
|
|
187
252
|
nssmPath, 'update-agent', agentPort, apiToken,
|
|
188
|
-
)
|
|
253
|
+
),
|
|
254
|
+
},
|
|
189
255
|
deferred: true,
|
|
190
256
|
}
|
|
191
257
|
|
|
192
258
|
commands['update-agent-dev'] = {
|
|
193
259
|
description: 'Update @geekbeer/minion from Verdaccio (dev) and restart',
|
|
194
|
-
|
|
195
|
-
|
|
260
|
+
nssmService: {
|
|
261
|
+
scriptPath: buildUpdateScript(
|
|
196
262
|
'npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873',
|
|
197
263
|
nssmPath, 'update-agent-dev', agentPort, apiToken,
|
|
198
|
-
)
|
|
264
|
+
),
|
|
265
|
+
},
|
|
199
266
|
deferred: true,
|
|
200
267
|
}
|
|
201
268
|
|
package/win/minion-cli.ps1
CHANGED
|
@@ -360,7 +360,13 @@ function Stop-MinionService {
|
|
|
360
360
|
Write-Host "minion-agent: not installed" -ForegroundColor Red
|
|
361
361
|
return
|
|
362
362
|
}
|
|
363
|
-
|
|
363
|
+
if ($state -eq 'STOPPED') {
|
|
364
|
+
Write-Host "minion-agent service is already stopped"
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
# Trigger graceful shutdown (sends offline heartbeat to HQ) but do NOT wait
|
|
368
|
+
# for the process to exit — otherwise NSSM sees the exit as a crash and
|
|
369
|
+
# restarts the process before sc.exe stop can run.
|
|
364
370
|
$token = ''
|
|
365
371
|
if (Test-Path $EnvFile) {
|
|
366
372
|
$envVars = Read-EnvFile $EnvFile
|
|
@@ -369,13 +375,18 @@ function Stop-MinionService {
|
|
|
369
375
|
try {
|
|
370
376
|
$headers = @{}
|
|
371
377
|
if ($token) { $headers['Authorization'] = "Bearer $token" }
|
|
372
|
-
Invoke-RestMethod -Uri "$AgentUrl/api/shutdown" -Method POST -ContentType 'application/json' -Headers $headers -TimeoutSec
|
|
373
|
-
Write-Host "Graceful shutdown requested, waiting..."
|
|
374
|
-
Start-Sleep -Seconds 4
|
|
378
|
+
Invoke-RestMethod -Uri "$AgentUrl/api/shutdown" -Method POST -ContentType 'application/json' -Headers $headers -TimeoutSec 3 | Out-Null
|
|
375
379
|
} catch {
|
|
376
|
-
|
|
380
|
+
# Agent may not be responding — sc.exe stop will handle it
|
|
377
381
|
}
|
|
382
|
+
# Immediately stop via NSSM (prevents auto-restart, sends kill if still alive)
|
|
378
383
|
sc.exe stop minion-agent 2>&1 | Out-Null
|
|
384
|
+
# Wait for service to fully stop (up to 10 seconds)
|
|
385
|
+
for ($i = 0; $i -lt 10; $i++) {
|
|
386
|
+
$s = Get-ServiceState 'minion-agent'
|
|
387
|
+
if ($s -eq 'STOPPED') { break }
|
|
388
|
+
Start-Sleep -Seconds 1
|
|
389
|
+
}
|
|
379
390
|
Write-Host "minion-agent service stopped"
|
|
380
391
|
}
|
|
381
392
|
|
|
@@ -1326,11 +1337,19 @@ function Invoke-Configure {
|
|
|
1326
1337
|
# ============================================================
|
|
1327
1338
|
|
|
1328
1339
|
function Show-Status {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1340
|
+
try {
|
|
1341
|
+
$response = Invoke-RestMethod -Uri "$AgentUrl/api/status" -TimeoutSec 5 -ErrorAction Stop
|
|
1342
|
+
# Pretty-print the JSON response
|
|
1343
|
+
$response | ConvertTo-Json -Depth 5 | Write-Host
|
|
1344
|
+
}
|
|
1345
|
+
catch {
|
|
1346
|
+
# API unreachable — fall back to service state
|
|
1347
|
+
$state = Get-ServiceState 'minion-agent'
|
|
1348
|
+
if ($state) {
|
|
1349
|
+
Write-Host "minion-agent: $state (API unreachable)"
|
|
1350
|
+
} else {
|
|
1351
|
+
Write-Host "minion-agent: not installed"
|
|
1352
|
+
}
|
|
1334
1353
|
}
|
|
1335
1354
|
}
|
|
1336
1355
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# postinstall.ps1 — Re-apply file ACLs after npm install -g replaces the package directory.
|
|
2
|
+
# Called automatically from postinstall.js on Windows.
|
|
3
|
+
# Requires Administrator privileges (npm install -g typically runs as admin).
|
|
4
|
+
# Exit silently if not admin or if setup has never been run (no .target-user-profile).
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
7
|
+
|
|
8
|
+
# --- Locate setup metadata ---------------------------------------------------
|
|
9
|
+
# During setup, .minion/.target-user-profile is written to the target user's
|
|
10
|
+
# home directory. We need to find it by scanning user profiles.
|
|
11
|
+
|
|
12
|
+
function Find-TargetUserProfile {
|
|
13
|
+
$profiles = Get-CimInstance Win32_UserProfile -ErrorAction SilentlyContinue |
|
|
14
|
+
Where-Object { -not $_.Special -and $_.LocalPath -and $_.LocalPath -notmatch '\\(systemprofile|LocalService|NetworkService)$' }
|
|
15
|
+
|
|
16
|
+
foreach ($p in $profiles) {
|
|
17
|
+
$candidate = Join-Path $p.LocalPath '.minion\.target-user-profile'
|
|
18
|
+
if (Test-Path $candidate) {
|
|
19
|
+
$saved = ([System.IO.File]::ReadAllText($candidate)).Trim()
|
|
20
|
+
if ($saved -and (Test-Path $saved)) { return $saved }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return $null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# --- Main ---------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
# Must be admin (npm install -g runs as admin)
|
|
29
|
+
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
|
|
30
|
+
[Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
31
|
+
if (-not $isAdmin) { exit 0 }
|
|
32
|
+
|
|
33
|
+
$TargetUserProfile = Find-TargetUserProfile
|
|
34
|
+
if (-not $TargetUserProfile) { exit 0 } # setup never ran — nothing to fix
|
|
35
|
+
|
|
36
|
+
$targetUserName = Split-Path $TargetUserProfile -Leaf
|
|
37
|
+
|
|
38
|
+
# Determine npm global bin directory (where minion-cli-win.cmd lives)
|
|
39
|
+
$adminNpmBin = Split-Path (Get-Command minion-cli-win -ErrorAction SilentlyContinue).Source -ErrorAction SilentlyContinue
|
|
40
|
+
if (-not $adminNpmBin) {
|
|
41
|
+
$adminNpmBin = & npm config get prefix 2>$null
|
|
42
|
+
}
|
|
43
|
+
if (-not $adminNpmBin) { exit 0 }
|
|
44
|
+
|
|
45
|
+
# Skip if the target user's own npm prefix (no ACL fix needed)
|
|
46
|
+
if ($adminNpmBin -eq (Join-Path $TargetUserProfile 'AppData\Roaming\npm')) { exit 0 }
|
|
47
|
+
|
|
48
|
+
# 1. Grant ReadAndExecute on bin links (minion-cli-win.cmd, hq-win.cmd, etc.)
|
|
49
|
+
$binFiles = @()
|
|
50
|
+
$binFiles += Get-ChildItem -Path $adminNpmBin -Filter '*minion*' -ErrorAction SilentlyContinue
|
|
51
|
+
$binFiles += Get-ChildItem -Path $adminNpmBin -Filter '*hq-win*' -ErrorAction SilentlyContinue
|
|
52
|
+
foreach ($f in $binFiles) {
|
|
53
|
+
icacls $f.FullName /grant "${targetUserName}:(RX)" /Q 2>$null | Out-Null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# 2. Grant ReadAndExecute on the @geekbeer/minion package directory (recursive)
|
|
57
|
+
$minionPkgDir = Join-Path (Join-Path $adminNpmBin 'node_modules') '@geekbeer\minion'
|
|
58
|
+
if (Test-Path $minionPkgDir) {
|
|
59
|
+
icacls $minionPkgDir /grant "${targetUserName}:(OI)(CI)RX" /T /Q 2>$null | Out-Null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# 3. Grant traverse access on ancestor directories (idempotent)
|
|
63
|
+
$traverseDirs = @(
|
|
64
|
+
$adminNpmBin,
|
|
65
|
+
(Join-Path $adminNpmBin 'node_modules'),
|
|
66
|
+
(Join-Path $adminNpmBin 'node_modules\@geekbeer')
|
|
67
|
+
)
|
|
68
|
+
$walkDir = $adminNpmBin
|
|
69
|
+
while ($walkDir) {
|
|
70
|
+
$parent = Split-Path $walkDir -Parent
|
|
71
|
+
if (-not $parent -or $parent -eq $walkDir) { break }
|
|
72
|
+
$traverseDirs += $parent
|
|
73
|
+
$walkDir = $parent
|
|
74
|
+
if ($parent.Length -le 3) { break }
|
|
75
|
+
}
|
|
76
|
+
foreach ($dir in ($traverseDirs | Select-Object -Unique)) {
|
|
77
|
+
if (Test-Path $dir) {
|
|
78
|
+
icacls $dir /grant "${targetUserName}:(RX)" /Q 2>$null | Out-Null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Write-Host "[@geekbeer/minion] postinstall: file permissions restored for user '$targetUserName'"
|
package/win/routes/commands.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Same API as routes/commands.js but uses win/process-manager.js
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const { exec,
|
|
7
|
+
const { exec, execSync } = require('child_process')
|
|
8
8
|
const { promisify } = require('util')
|
|
9
9
|
const fs = require('fs')
|
|
10
10
|
const path = require('path')
|
|
@@ -13,11 +13,11 @@ const execAsync = promisify(exec)
|
|
|
13
13
|
|
|
14
14
|
const { verifyToken } = require('../../core/lib/auth')
|
|
15
15
|
const { config } = require('../../core/config')
|
|
16
|
-
const { detectProcessManager, buildAllowedCommands } = require('../lib/process-manager')
|
|
16
|
+
const { detectProcessManager, buildAllowedCommands, getNssmPath } = require('../lib/process-manager')
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
function
|
|
20
|
-
try { fs.appendFileSync(
|
|
18
|
+
const DEFERRED_LOG = path.join(os.homedir(), '.minion', 'logs', 'deferred-debug.log')
|
|
19
|
+
function deferredLog(msg) {
|
|
20
|
+
try { fs.appendFileSync(DEFERRED_LOG, `${new Date().toISOString()} ${msg}\n`) } catch {}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const PROC_MGR = detectProcessManager()
|
|
@@ -64,29 +64,45 @@ async function commandRoutes(fastify) {
|
|
|
64
64
|
if (allowedCommand.deferred) {
|
|
65
65
|
console.log(`[Command] Scheduling deferred command: ${command}`)
|
|
66
66
|
setTimeout(() => {
|
|
67
|
-
if (allowedCommand.
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
if (allowedCommand.nssmService) {
|
|
68
|
+
// Register and start a temporary NSSM service to run the script
|
|
69
|
+
// independently of the minion-agent service process tree.
|
|
70
|
+
const { scriptPath } = allowedCommand.nssmService
|
|
71
|
+
const nssmPath = getNssmPath()
|
|
72
|
+
const svcName = 'minion-update'
|
|
73
|
+
deferredLog(`[${command}] registering NSSM service: ${svcName}`)
|
|
70
74
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
75
|
+
// Copy nssm.exe to data dir so the service wrapper binary is
|
|
76
|
+
// outside the npm package directory. This prevents EBUSY when
|
|
77
|
+
// npm tries to replace the package during update.
|
|
78
|
+
const dataDirNssm = path.join(os.homedir(), '.minion', 'nssm.exe')
|
|
79
|
+
fs.copyFileSync(nssmPath, dataDirNssm)
|
|
80
|
+
const svcNssm = dataDirNssm
|
|
81
|
+
|
|
82
|
+
// Clean up any leftover service from a previous run
|
|
83
|
+
try { execSync(`"${svcNssm}" stop ${svcName} confirm`, { stdio: 'ignore', timeout: 10000 }) } catch {}
|
|
84
|
+
try { execSync(`"${svcNssm}" remove ${svcName} confirm`, { stdio: 'ignore', timeout: 10000 }) } catch {}
|
|
85
|
+
|
|
86
|
+
const powershellPath = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
|
87
|
+
const args = `-NoProfile -ExecutionPolicy Bypass -File "${scriptPath}"`
|
|
88
|
+
|
|
89
|
+
execSync(`"${svcNssm}" install ${svcName} "${powershellPath}" ${args}`, { timeout: 10000 })
|
|
90
|
+
execSync(`"${svcNssm}" set ${svcName} AppDirectory "${path.dirname(scriptPath)}"`, { stdio: 'ignore', timeout: 5000 })
|
|
91
|
+
execSync(`"${svcNssm}" set ${svcName} Start SERVICE_DEMAND_START`, { stdio: 'ignore', timeout: 5000 })
|
|
92
|
+
execSync(`"${svcNssm}" set ${svcName} AppExit Default Exit`, { stdio: 'ignore', timeout: 5000 })
|
|
93
|
+
|
|
94
|
+
// Redirect service stdout/stderr to log files
|
|
95
|
+
const svcLogDir = path.join(os.homedir(), '.minion', 'logs')
|
|
96
|
+
execSync(`"${svcNssm}" set ${svcName} AppStdout "${path.join(svcLogDir, `${command}-svc-stdout.log`)}"`, { stdio: 'ignore', timeout: 5000 })
|
|
97
|
+
execSync(`"${svcNssm}" set ${svcName} AppStderr "${path.join(svcLogDir, `${command}-svc-stderr.log`)}"`, { stdio: 'ignore', timeout: 5000 })
|
|
98
|
+
|
|
99
|
+
// Start the service (runs asynchronously, independent of minion-agent)
|
|
100
|
+
execSync(`"${svcNssm}" start ${svcName}`, { timeout: 10000 })
|
|
101
|
+
deferredLog(`[${command}] NSSM service started: ${svcName}`)
|
|
102
|
+
console.log(`[Command] Deferred command started via NSSM service: ${command}`)
|
|
87
103
|
} catch (err) {
|
|
88
|
-
|
|
89
|
-
console.error(`[Command] Deferred
|
|
104
|
+
deferredLog(`[${command}] NSSM service failed: ${err.message}`)
|
|
105
|
+
console.error(`[Command] Deferred NSSM service failed: ${command} - ${err.message}`)
|
|
90
106
|
}
|
|
91
107
|
} else {
|
|
92
108
|
exec(allowedCommand.command, { timeout: 60000, shell: true }, (err) => {
|