@geekbeer/minion 1.0.5 → 1.3.1
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 +0 -53
- package/api.js +6 -12
- package/execution-store.js +127 -0
- package/lib/auth.js +21 -0
- package/lib/log-manager.js +183 -0
- package/lib/process-manager.js +103 -0
- package/minion-cli.sh +100 -41
- package/package.json +8 -1
- package/routes/commands.js +180 -0
- package/routes/health.js +75 -0
- package/routes/index.js +65 -0
- package/routes/skills.js +188 -0
- package/routes/terminal.js +646 -0
- package/routes/workflows.js +340 -0
- package/server.js +32 -435
- package/skills/execution-report/SKILL.md +106 -0
- package/workflow-runner.js +423 -0
- package/workflow-store.js +71 -0
package/README.md
CHANGED
|
@@ -37,59 +37,6 @@ minion-cli health # Run health check
|
|
|
37
37
|
minion-cli log -m "Task completed" -l info -s skill-name
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
## npm公開手順
|
|
41
|
-
|
|
42
|
-
### 初回セットアップ
|
|
43
|
-
|
|
44
|
-
1. npmにログイン:
|
|
45
|
-
```bash
|
|
46
|
-
npm login
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
2. `geekbeer` organizationがnpmに存在することを確認(なければ作成):
|
|
50
|
-
```bash
|
|
51
|
-
npm org create geekbeer
|
|
52
|
-
```
|
|
53
|
-
または https://www.npmjs.com/org/create から作成。
|
|
54
|
-
|
|
55
|
-
### パッケージ公開
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
cd packages/minion
|
|
59
|
-
|
|
60
|
-
# 含まれるファイルを事前確認
|
|
61
|
-
npm pack --dry-run
|
|
62
|
-
|
|
63
|
-
# 公開
|
|
64
|
-
npm publish --access public
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
`publishConfig.access: "public"` が `package.json` に設定済みのため、`--access public` は省略可能。
|
|
68
|
-
|
|
69
|
-
### バージョンアップ & 再公開
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
cd packages/minion
|
|
73
|
-
|
|
74
|
-
# バージョンを上げる(patch: 1.0.0 → 1.0.1)
|
|
75
|
-
npm version patch
|
|
76
|
-
|
|
77
|
-
# または minor: 1.0.0 → 1.1.0
|
|
78
|
-
npm version minor
|
|
79
|
-
|
|
80
|
-
# または major: 1.0.0 → 2.0.0
|
|
81
|
-
npm version major
|
|
82
|
-
|
|
83
|
-
# 公開
|
|
84
|
-
npm publish
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### 公開確認
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
npm view @geekbeer/minion
|
|
91
|
-
```
|
|
92
|
-
|
|
93
40
|
## Environment Variables
|
|
94
41
|
|
|
95
42
|
| Variable | Description | Default |
|
package/api.js
CHANGED
|
@@ -38,23 +38,17 @@ async function request(endpoint, options = {}) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
* @param {
|
|
43
|
-
* @param {'info'|'warning'|'error'} level - Log level
|
|
44
|
-
* @param {object|null} metadata - Additional metadata
|
|
41
|
+
* Report skill execution status to HQ
|
|
42
|
+
* @param {object} data - Execution record data
|
|
45
43
|
*/
|
|
46
|
-
async function
|
|
47
|
-
return request('/
|
|
44
|
+
async function reportExecution(data) {
|
|
45
|
+
return request('/execution', {
|
|
48
46
|
method: 'POST',
|
|
49
|
-
body: JSON.stringify(
|
|
50
|
-
message,
|
|
51
|
-
level,
|
|
52
|
-
metadata,
|
|
53
|
-
}),
|
|
47
|
+
body: JSON.stringify(data),
|
|
54
48
|
})
|
|
55
49
|
}
|
|
56
50
|
|
|
57
51
|
module.exports = {
|
|
58
52
|
request,
|
|
59
|
-
|
|
53
|
+
reportExecution,
|
|
60
54
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution Store
|
|
3
|
+
* Persists skill execution history to local JSON file.
|
|
4
|
+
* Single source of truth for execution records.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs').promises
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const os = require('os')
|
|
10
|
+
|
|
11
|
+
// Max executions to keep (older ones are pruned)
|
|
12
|
+
const MAX_EXECUTIONS = 200
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get execution file path
|
|
16
|
+
* Uses /opt/minion-agent/ if available, otherwise home dir
|
|
17
|
+
*/
|
|
18
|
+
function getExecutionFilePath() {
|
|
19
|
+
const optPath = '/opt/minion-agent/executions.json'
|
|
20
|
+
try {
|
|
21
|
+
require('fs').accessSync(path.dirname(optPath))
|
|
22
|
+
return optPath
|
|
23
|
+
} catch {
|
|
24
|
+
return path.join(os.homedir(), 'executions.json')
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const EXECUTION_FILE = getExecutionFilePath()
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load executions from local file
|
|
32
|
+
* @returns {Promise<Array>} Array of execution objects
|
|
33
|
+
*/
|
|
34
|
+
async function load() {
|
|
35
|
+
try {
|
|
36
|
+
const data = await fs.readFile(EXECUTION_FILE, 'utf-8')
|
|
37
|
+
return JSON.parse(data)
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (err.code === 'ENOENT') {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
console.error(`[ExecutionStore] Failed to load executions: ${err.message}`)
|
|
43
|
+
return []
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Save executions to local file
|
|
49
|
+
* @param {Array} executions - Array of execution objects
|
|
50
|
+
*/
|
|
51
|
+
async function saveAll(executions) {
|
|
52
|
+
try {
|
|
53
|
+
await fs.writeFile(EXECUTION_FILE, JSON.stringify(executions, null, 2), 'utf-8')
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`[ExecutionStore] Failed to save executions: ${err.message}`)
|
|
56
|
+
throw err
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Save a single execution record
|
|
62
|
+
* Upserts by ID, prunes old records if over limit
|
|
63
|
+
* @param {object} execution - Execution record
|
|
64
|
+
*/
|
|
65
|
+
async function save(execution) {
|
|
66
|
+
console.log(`[ExecutionStore] save() called: id=${execution.id}, skill=${execution.skill_name}, status=${execution.status}`)
|
|
67
|
+
|
|
68
|
+
const executions = await load()
|
|
69
|
+
console.log(`[ExecutionStore] Loaded ${executions.length} existing executions`)
|
|
70
|
+
|
|
71
|
+
// Find existing by ID and update, or add new
|
|
72
|
+
const existingIndex = executions.findIndex(e => e.id === execution.id)
|
|
73
|
+
if (existingIndex >= 0) {
|
|
74
|
+
const oldStatus = executions[existingIndex].status
|
|
75
|
+
executions[existingIndex] = { ...executions[existingIndex], ...execution }
|
|
76
|
+
console.log(`[ExecutionStore] Updated existing record at index ${existingIndex}: ${oldStatus} → ${execution.status}`)
|
|
77
|
+
} else {
|
|
78
|
+
executions.unshift(execution) // Add to beginning (newest first)
|
|
79
|
+
console.log(`[ExecutionStore] Added new record (now ${executions.length} total)`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Prune old executions
|
|
83
|
+
const pruned = executions.slice(0, MAX_EXECUTIONS)
|
|
84
|
+
|
|
85
|
+
await saveAll(pruned)
|
|
86
|
+
console.log(`[ExecutionStore] ✓ Saved to file: ${execution.id} (${execution.skill_name} → ${execution.status})`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* List executions with optional limit
|
|
91
|
+
* @param {number} limit - Max number of executions to return
|
|
92
|
+
* @returns {Promise<Array>} Array of execution objects (newest first)
|
|
93
|
+
*/
|
|
94
|
+
async function list(limit = 50) {
|
|
95
|
+
const executions = await load()
|
|
96
|
+
return executions.slice(0, limit)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get executions for a specific workflow
|
|
101
|
+
* @param {string} workflowId - Workflow UUID
|
|
102
|
+
* @param {number} limit - Max number to return
|
|
103
|
+
* @returns {Promise<Array>} Array of execution objects
|
|
104
|
+
*/
|
|
105
|
+
async function getByWorkflow(workflowId, limit = 20) {
|
|
106
|
+
const executions = await load()
|
|
107
|
+
return executions
|
|
108
|
+
.filter(e => e.workflow_id === workflowId)
|
|
109
|
+
.slice(0, limit)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get a single execution by ID
|
|
114
|
+
* @param {string} id - Execution UUID
|
|
115
|
+
* @returns {Promise<object|null>} Execution object or null
|
|
116
|
+
*/
|
|
117
|
+
async function getById(id) {
|
|
118
|
+
const executions = await load()
|
|
119
|
+
return executions.find(e => e.id === id) || null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
save,
|
|
124
|
+
list,
|
|
125
|
+
getByWorkflow,
|
|
126
|
+
getById,
|
|
127
|
+
}
|
package/lib/auth.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication utilities for minion agent
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { config } = require('../config')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Verify API token from Authorization header
|
|
9
|
+
* @param {import('fastify').FastifyRequest} request
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
function verifyToken(request) {
|
|
13
|
+
const authHeader = request.headers.authorization
|
|
14
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
const token = authHeader.substring(7)
|
|
18
|
+
return token === config.API_TOKEN
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { verifyToken }
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Manager
|
|
3
|
+
* Manages workflow execution log files.
|
|
4
|
+
* Logs are stored as text files and automatically pruned when exceeding MAX_LOG_FILES.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs').promises
|
|
8
|
+
const path = require('path')
|
|
9
|
+
|
|
10
|
+
// Log storage configuration
|
|
11
|
+
const LOG_DIR = '/opt/minion-agent/logs'
|
|
12
|
+
const MAX_LOG_FILES = 100
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ensure log directory exists
|
|
16
|
+
*/
|
|
17
|
+
async function ensureLogDir() {
|
|
18
|
+
try {
|
|
19
|
+
await fs.mkdir(LOG_DIR, { recursive: true })
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(`[LogManager] Failed to create log directory: ${err.message}`)
|
|
22
|
+
throw err
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get log file path for an execution
|
|
28
|
+
* @param {string} executionId - Execution UUID
|
|
29
|
+
* @returns {string} Full path to log file
|
|
30
|
+
*/
|
|
31
|
+
function getLogPath(executionId) {
|
|
32
|
+
return path.join(LOG_DIR, `${executionId}.log`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if log file exists
|
|
37
|
+
* @param {string} executionId - Execution UUID
|
|
38
|
+
* @returns {Promise<boolean>}
|
|
39
|
+
*/
|
|
40
|
+
async function logExists(executionId) {
|
|
41
|
+
try {
|
|
42
|
+
await fs.access(getLogPath(executionId))
|
|
43
|
+
return true
|
|
44
|
+
} catch {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read log file content
|
|
51
|
+
* @param {string} executionId - Execution UUID
|
|
52
|
+
* @param {object} options - Read options
|
|
53
|
+
* @param {number} options.tail - Return only last N lines
|
|
54
|
+
* @returns {Promise<{content: string, lines: number, size: number}>}
|
|
55
|
+
*/
|
|
56
|
+
async function readLog(executionId, options = {}) {
|
|
57
|
+
const logPath = getLogPath(executionId)
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const stats = await fs.stat(logPath)
|
|
61
|
+
const content = await fs.readFile(logPath, 'utf-8')
|
|
62
|
+
const allLines = content.split('\n')
|
|
63
|
+
|
|
64
|
+
let resultContent = content
|
|
65
|
+
if (options.tail && options.tail > 0) {
|
|
66
|
+
resultContent = allLines.slice(-options.tail).join('\n')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
content: resultContent,
|
|
71
|
+
lines: allLines.length,
|
|
72
|
+
size: stats.size,
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err.code === 'ENOENT') {
|
|
76
|
+
return null // File not found
|
|
77
|
+
}
|
|
78
|
+
throw err
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Prune old log files when exceeding MAX_LOG_FILES
|
|
84
|
+
* Deletes oldest files first (by mtime)
|
|
85
|
+
*/
|
|
86
|
+
async function pruneOldLogs() {
|
|
87
|
+
try {
|
|
88
|
+
// Ensure directory exists before reading
|
|
89
|
+
try {
|
|
90
|
+
await fs.access(LOG_DIR)
|
|
91
|
+
} catch {
|
|
92
|
+
// Directory doesn't exist yet, nothing to prune
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const files = await fs.readdir(LOG_DIR)
|
|
97
|
+
const logFiles = files.filter(f => f.endsWith('.log'))
|
|
98
|
+
|
|
99
|
+
if (logFiles.length <= MAX_LOG_FILES) {
|
|
100
|
+
return // No pruning needed
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(`[LogManager] Pruning logs: ${logFiles.length} files (max: ${MAX_LOG_FILES})`)
|
|
104
|
+
|
|
105
|
+
// Get file stats for sorting
|
|
106
|
+
const fileStats = await Promise.all(
|
|
107
|
+
logFiles.map(async f => {
|
|
108
|
+
const filePath = path.join(LOG_DIR, f)
|
|
109
|
+
try {
|
|
110
|
+
const stats = await fs.stat(filePath)
|
|
111
|
+
return { name: f, mtime: stats.mtime }
|
|
112
|
+
} catch {
|
|
113
|
+
// File may have been deleted, skip it
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
// Filter out null entries and sort by mtime (oldest first)
|
|
120
|
+
const validFiles = fileStats.filter(f => f !== null)
|
|
121
|
+
validFiles.sort((a, b) => a.mtime - b.mtime)
|
|
122
|
+
|
|
123
|
+
// Delete oldest files
|
|
124
|
+
const toDelete = validFiles.slice(0, validFiles.length - MAX_LOG_FILES)
|
|
125
|
+
for (const file of toDelete) {
|
|
126
|
+
try {
|
|
127
|
+
await fs.unlink(path.join(LOG_DIR, file.name))
|
|
128
|
+
console.log(`[LogManager] Deleted old log: ${file.name}`)
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(`[LogManager] Failed to delete ${file.name}: ${err.message}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(`[LogManager] Pruned ${toDelete.length} old log files`)
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error(`[LogManager] Failed to prune logs: ${err.message}`)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get log file statistics
|
|
142
|
+
* @returns {Promise<{count: number, totalSize: number}>}
|
|
143
|
+
*/
|
|
144
|
+
async function getLogStats() {
|
|
145
|
+
try {
|
|
146
|
+
try {
|
|
147
|
+
await fs.access(LOG_DIR)
|
|
148
|
+
} catch {
|
|
149
|
+
return { count: 0, totalSize: 0 }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const files = await fs.readdir(LOG_DIR)
|
|
153
|
+
const logFiles = files.filter(f => f.endsWith('.log'))
|
|
154
|
+
|
|
155
|
+
let totalSize = 0
|
|
156
|
+
for (const f of logFiles) {
|
|
157
|
+
try {
|
|
158
|
+
const stats = await fs.stat(path.join(LOG_DIR, f))
|
|
159
|
+
totalSize += stats.size
|
|
160
|
+
} catch {
|
|
161
|
+
// Skip files that can't be stat'd
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
count: logFiles.length,
|
|
167
|
+
totalSize,
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
return { count: 0, totalSize: 0 }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
LOG_DIR,
|
|
176
|
+
MAX_LOG_FILES,
|
|
177
|
+
ensureLogDir,
|
|
178
|
+
getLogPath,
|
|
179
|
+
logExists,
|
|
180
|
+
readLog,
|
|
181
|
+
pruneOldLogs,
|
|
182
|
+
getLogStats,
|
|
183
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process manager detection and command building
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { execSync } = require('child_process')
|
|
6
|
+
|
|
7
|
+
// Use sudo only when not running as root
|
|
8
|
+
const SUDO = process.getuid && process.getuid() !== 0 ? 'sudo ' : ''
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect process manager (matching minion-cli.sh logic)
|
|
12
|
+
* Checks if systemd is actually running as PID 1, not just if command exists
|
|
13
|
+
* @returns {'systemd' | 'supervisord' | 'standalone'}
|
|
14
|
+
*/
|
|
15
|
+
function detectProcessManager() {
|
|
16
|
+
const fs = require('fs')
|
|
17
|
+
|
|
18
|
+
// Check if systemd is actually running (not just installed)
|
|
19
|
+
// /run/systemd/system exists only when systemd is the init system
|
|
20
|
+
if (fs.existsSync('/run/systemd/system')) {
|
|
21
|
+
try {
|
|
22
|
+
execSync('systemctl --version', { stdio: 'ignore' })
|
|
23
|
+
return 'systemd'
|
|
24
|
+
} catch {
|
|
25
|
+
// systemctl not working
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
execSync('which supervisorctl', { stdio: 'ignore' })
|
|
31
|
+
return 'supervisord'
|
|
32
|
+
} catch {
|
|
33
|
+
// supervisord not available
|
|
34
|
+
}
|
|
35
|
+
return 'standalone'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build allowed commands based on detected process manager
|
|
40
|
+
* @param {string} procMgr - Process manager type
|
|
41
|
+
* @returns {Record<string, { description: string; command: string; deferred?: boolean }>}
|
|
42
|
+
*/
|
|
43
|
+
function buildAllowedCommands(procMgr) {
|
|
44
|
+
const commands = {}
|
|
45
|
+
|
|
46
|
+
if (procMgr === 'systemd') {
|
|
47
|
+
commands['restart-agent'] = {
|
|
48
|
+
description: 'Restart the minion agent service',
|
|
49
|
+
command: `${SUDO}systemctl restart minion-agent`,
|
|
50
|
+
deferred: true,
|
|
51
|
+
}
|
|
52
|
+
commands['update-agent'] = {
|
|
53
|
+
description: 'Update @geekbeer/minion to latest version and restart',
|
|
54
|
+
command: `npm update -g @geekbeer/minion && ${SUDO}systemctl restart minion-agent`,
|
|
55
|
+
deferred: true,
|
|
56
|
+
}
|
|
57
|
+
commands['restart-display'] = {
|
|
58
|
+
description: 'Restart Xvfb, Fluxbox, x11vnc and noVNC services',
|
|
59
|
+
command: `${SUDO}systemctl restart xvfb fluxbox x11vnc novnc`,
|
|
60
|
+
}
|
|
61
|
+
commands['status-services'] = {
|
|
62
|
+
description: 'Check status of all services',
|
|
63
|
+
command: 'systemctl status minion-agent xvfb fluxbox x11vnc novnc --no-pager',
|
|
64
|
+
}
|
|
65
|
+
} else if (procMgr === 'supervisord') {
|
|
66
|
+
commands['restart-agent'] = {
|
|
67
|
+
description: 'Restart the minion agent service',
|
|
68
|
+
command: `${SUDO}supervisorctl restart minion-agent`,
|
|
69
|
+
deferred: true,
|
|
70
|
+
}
|
|
71
|
+
commands['update-agent'] = {
|
|
72
|
+
description: 'Update @geekbeer/minion to latest version and restart',
|
|
73
|
+
command: `npm update -g @geekbeer/minion && ${SUDO}supervisorctl restart minion-agent`,
|
|
74
|
+
deferred: true,
|
|
75
|
+
}
|
|
76
|
+
commands['restart-display'] = {
|
|
77
|
+
description: 'Restart Xvfb, x11vnc and noVNC services',
|
|
78
|
+
command: `${SUDO}supervisorctl restart xvfb fluxbox x11vnc novnc`,
|
|
79
|
+
}
|
|
80
|
+
commands['status-services'] = {
|
|
81
|
+
description: 'Check status of all services',
|
|
82
|
+
command: `${SUDO}supervisorctl status`,
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
// Standalone mode: limited commands
|
|
86
|
+
commands['update-agent'] = {
|
|
87
|
+
description: 'Update @geekbeer/minion to latest version',
|
|
88
|
+
command: 'npm update -g @geekbeer/minion',
|
|
89
|
+
}
|
|
90
|
+
commands['status-services'] = {
|
|
91
|
+
description: 'Show agent process info',
|
|
92
|
+
command: 'echo "Process Manager: standalone (no systemd/supervisord)" && echo "Agent PID: $$" && echo "Node version: $(node -v)" && echo "Uptime: $(ps -o etime= -p $$)"',
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return commands
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
SUDO,
|
|
101
|
+
detectProcessManager,
|
|
102
|
+
buildAllowedCommands,
|
|
103
|
+
}
|