@amiable-dev/docusaurus-plugin-stentorosaur 0.16.4 → 0.18.0
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 +72 -1
- package/lib/hooks/index.d.ts +2 -1
- package/lib/hooks/index.d.ts.map +1 -1
- package/lib/hooks/index.js +4 -2
- package/lib/hooks/useDailySummary.d.ts +38 -0
- package/lib/hooks/useDailySummary.d.ts.map +1 -0
- package/lib/hooks/useDailySummary.js +194 -0
- package/lib/theme/StatusItem/MiniHeatmap.d.ts +1 -1
- package/lib/theme/StatusItem/MiniHeatmap.d.ts.map +1 -1
- package/lib/theme/StatusItem/MiniHeatmap.js +1 -1
- package/lib/theme/StatusItem/MiniHeatmap.module.css +10 -12
- package/lib/theme/StatusItem/index.d.ts +5 -1
- package/lib/theme/StatusItem/index.d.ts.map +1 -1
- package/lib/theme/StatusItem/index.js +58 -2
- package/lib/types.d.ts +40 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +11 -2
- package/scripts/bootstrap-summary.js +252 -0
- package/scripts/config.js +359 -0
- package/scripts/init.js +58 -0
- package/scripts/monitor.js +173 -1
- package/src/plugin-status.d.ts +127 -0
- package/templates/Makefile.status +146 -0
- package/templates/README.md +96 -0
- package/.notifyrc.example.json +0 -43
- package/ANALYSIS-MANIFEST.txt +0 -358
- package/ARCHITECTURE-DIAGRAM.txt +0 -220
- package/QUICKSTART.md +0 -440
- package/copyUntypedFiles.js +0 -24
- package/docs/README.md +0 -122
- package/docs/reference/CONFIGURATION.md +0 -994
- package/jest.setup.js +0 -17
- package/settings.json +0 -5
- package/tsconfig.tsbuildinfo +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amiable-dev/docusaurus-plugin-stentorosaur",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "A Docusaurus plugin for displaying status monitoring dashboard powered by GitHub Issues and Actions, similar to Upptime",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "src/plugin-status.d.ts",
|
|
@@ -10,8 +10,17 @@
|
|
|
10
10
|
"stentorosaur-notify": "scripts/notify.js",
|
|
11
11
|
"stentorosaur-setup-status-branch": "scripts/setup-status-branch.js",
|
|
12
12
|
"stentorosaur-migrate-to-status-branch": "scripts/migrate-to-status-branch.js",
|
|
13
|
-
"stentorosaur-cleanup-status-branch": "scripts/cleanup-status-branch.js"
|
|
13
|
+
"stentorosaur-cleanup-status-branch": "scripts/cleanup-status-branch.js",
|
|
14
|
+
"stentorosaur-bootstrap-summary": "scripts/bootstrap-summary.js",
|
|
15
|
+
"stentorosaur-init": "scripts/init.js",
|
|
16
|
+
"stentorosaur-config": "scripts/config.js"
|
|
14
17
|
},
|
|
18
|
+
"files": [
|
|
19
|
+
"lib",
|
|
20
|
+
"scripts",
|
|
21
|
+
"templates",
|
|
22
|
+
"src/plugin-status.d.ts"
|
|
23
|
+
],
|
|
15
24
|
"keywords": [
|
|
16
25
|
"docusaurus",
|
|
17
26
|
"plugin",
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bootstrap script to generate initial daily-summary.json from existing archives
|
|
5
|
+
*
|
|
6
|
+
* This script is typically run once when upgrading to a version that supports
|
|
7
|
+
* historical data aggregation (ADR-002). It reads all existing archive files
|
|
8
|
+
* and generates the initial daily-summary.json.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node scripts/bootstrap-summary.js --output-dir status-data
|
|
12
|
+
* node scripts/bootstrap-summary.js --output-dir status-data --window 90
|
|
13
|
+
*
|
|
14
|
+
* Options:
|
|
15
|
+
* --output-dir <path> Output directory containing archives/ (default: status-data)
|
|
16
|
+
* --window <days> Number of days to aggregate (default: 90)
|
|
17
|
+
* --verbose Enable verbose logging
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const zlib = require('zlib');
|
|
23
|
+
|
|
24
|
+
// Parse command line arguments
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const options = {
|
|
27
|
+
outputDir: 'status-data',
|
|
28
|
+
windowDays: 90,
|
|
29
|
+
verbose: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
switch (args[i]) {
|
|
34
|
+
case '--output-dir':
|
|
35
|
+
options.outputDir = args[++i];
|
|
36
|
+
break;
|
|
37
|
+
case '--window':
|
|
38
|
+
options.windowDays = parseInt(args[++i]);
|
|
39
|
+
break;
|
|
40
|
+
case '--verbose':
|
|
41
|
+
options.verbose = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function log(...msg) {
|
|
47
|
+
console.log('[bootstrap-summary]', ...msg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function verbose(...msg) {
|
|
51
|
+
if (options.verbose) {
|
|
52
|
+
console.log('[bootstrap-summary:verbose]', ...msg);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Calculate p95 latency from an array of latency values
|
|
58
|
+
*/
|
|
59
|
+
function calculateP95(latencies) {
|
|
60
|
+
if (latencies.length === 0) return null;
|
|
61
|
+
const sorted = [...latencies].sort((a, b) => a - b);
|
|
62
|
+
const index = Math.ceil(sorted.length * 0.95) - 1;
|
|
63
|
+
return sorted[Math.max(0, index)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Aggregate readings for a specific day into a DailySummaryEntry
|
|
68
|
+
*/
|
|
69
|
+
function aggregateDayReadings(date, readings) {
|
|
70
|
+
const checksTotal = readings.length;
|
|
71
|
+
const checksPassed = readings.filter(r => r.state === 'up' || r.state === 'maintenance').length;
|
|
72
|
+
const uptimePct = checksTotal > 0 ? checksPassed / checksTotal : 0;
|
|
73
|
+
|
|
74
|
+
const latencies = readings
|
|
75
|
+
.filter(r => r.state === 'up')
|
|
76
|
+
.map(r => r.lat);
|
|
77
|
+
|
|
78
|
+
const avgLatencyMs = latencies.length > 0
|
|
79
|
+
? Math.round(latencies.reduce((sum, lat) => sum + lat, 0) / latencies.length)
|
|
80
|
+
: null;
|
|
81
|
+
|
|
82
|
+
const p95LatencyMs = calculateP95(latencies);
|
|
83
|
+
|
|
84
|
+
// Count incidents (transitions from up to down)
|
|
85
|
+
let incidentCount = 0;
|
|
86
|
+
for (let i = 1; i < readings.length; i++) {
|
|
87
|
+
if (readings[i - 1].state === 'up' && readings[i].state === 'down') {
|
|
88
|
+
incidentCount++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
date,
|
|
94
|
+
uptimePct,
|
|
95
|
+
avgLatencyMs,
|
|
96
|
+
p95LatencyMs,
|
|
97
|
+
checksTotal,
|
|
98
|
+
checksPassed,
|
|
99
|
+
incidentCount,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generate daily-summary.json from existing archives
|
|
105
|
+
*/
|
|
106
|
+
function generateDailySummary(archivesDir, outputDir, windowDays) {
|
|
107
|
+
const now = new Date();
|
|
108
|
+
|
|
109
|
+
// Group all readings by service and date
|
|
110
|
+
const serviceReadings = new Map();
|
|
111
|
+
|
|
112
|
+
// Collect from last N days
|
|
113
|
+
for (let d = 0; d < windowDays; d++) {
|
|
114
|
+
const date = new Date(now.getTime() - d * 24 * 60 * 60 * 1000);
|
|
115
|
+
const year = date.getFullYear();
|
|
116
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
117
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
118
|
+
const dateKey = `${year}-${month}-${day}`;
|
|
119
|
+
|
|
120
|
+
const dir = path.join(archivesDir, String(year), month);
|
|
121
|
+
const plainFile = path.join(dir, `history-${year}-${month}-${day}.jsonl`);
|
|
122
|
+
const gzFile = path.join(dir, `history-${year}-${month}-${day}.jsonl.gz`);
|
|
123
|
+
|
|
124
|
+
let content = null;
|
|
125
|
+
|
|
126
|
+
// Try plain file first
|
|
127
|
+
if (fs.existsSync(plainFile)) {
|
|
128
|
+
try {
|
|
129
|
+
content = fs.readFileSync(plainFile, 'utf8');
|
|
130
|
+
verbose(`Read ${plainFile}`);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
verbose(`Failed to read ${plainFile}:`, err.message);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Try gzipped file
|
|
136
|
+
else if (fs.existsSync(gzFile)) {
|
|
137
|
+
try {
|
|
138
|
+
const compressed = fs.readFileSync(gzFile);
|
|
139
|
+
content = zlib.gunzipSync(compressed).toString('utf8');
|
|
140
|
+
verbose(`Read ${gzFile}`);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
verbose(`Failed to decompress ${gzFile}:`, err.message);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Parse and group by service
|
|
147
|
+
if (content) {
|
|
148
|
+
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
149
|
+
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
try {
|
|
152
|
+
const obj = JSON.parse(line);
|
|
153
|
+
const svc = obj.svc;
|
|
154
|
+
|
|
155
|
+
if (!serviceReadings.has(svc)) {
|
|
156
|
+
serviceReadings.set(svc, new Map());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const svcMap = serviceReadings.get(svc);
|
|
160
|
+
if (!svcMap.has(dateKey)) {
|
|
161
|
+
svcMap.set(dateKey, []);
|
|
162
|
+
}
|
|
163
|
+
svcMap.get(dateKey).push(obj);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
verbose('Failed to parse line:', line);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Aggregate each service's daily data
|
|
172
|
+
const services = {};
|
|
173
|
+
let totalDays = 0;
|
|
174
|
+
|
|
175
|
+
for (const [svc, dateMap] of serviceReadings.entries()) {
|
|
176
|
+
const entries = [];
|
|
177
|
+
|
|
178
|
+
// Sort dates in reverse chronological order (most recent first)
|
|
179
|
+
const sortedDates = [...dateMap.keys()].sort().reverse();
|
|
180
|
+
totalDays = Math.max(totalDays, sortedDates.length);
|
|
181
|
+
|
|
182
|
+
for (const dateKey of sortedDates) {
|
|
183
|
+
const readings = dateMap.get(dateKey);
|
|
184
|
+
// Sort readings by timestamp for accurate incident counting
|
|
185
|
+
readings.sort((a, b) => a.t - b.t);
|
|
186
|
+
|
|
187
|
+
const entry = aggregateDayReadings(dateKey, readings);
|
|
188
|
+
entries.push(entry);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
services[svc] = entries;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Build the daily summary file (ADR-002 schema v1)
|
|
195
|
+
const summary = {
|
|
196
|
+
version: 1,
|
|
197
|
+
lastUpdated: now.toISOString(),
|
|
198
|
+
windowDays,
|
|
199
|
+
services,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Atomic write: temp file → rename
|
|
203
|
+
const summaryPath = path.join(outputDir, 'daily-summary.json');
|
|
204
|
+
const tempPath = path.join(outputDir, 'daily-summary.tmp');
|
|
205
|
+
|
|
206
|
+
fs.writeFileSync(tempPath, JSON.stringify(summary, null, 2));
|
|
207
|
+
fs.renameSync(tempPath, summaryPath);
|
|
208
|
+
|
|
209
|
+
return { summary, totalDays };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Main execution
|
|
213
|
+
function main() {
|
|
214
|
+
try {
|
|
215
|
+
const archivesDir = path.join(options.outputDir, 'archives');
|
|
216
|
+
|
|
217
|
+
if (!fs.existsSync(archivesDir)) {
|
|
218
|
+
console.error(`Error: Archives directory not found: ${archivesDir}`);
|
|
219
|
+
console.error('Make sure --output-dir points to the correct status data directory.');
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
log(`Generating daily-summary.json from ${archivesDir}...`);
|
|
224
|
+
log(`Window: ${options.windowDays} days`);
|
|
225
|
+
|
|
226
|
+
const { summary, totalDays } = generateDailySummary(
|
|
227
|
+
archivesDir,
|
|
228
|
+
options.outputDir,
|
|
229
|
+
options.windowDays
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const serviceCount = Object.keys(summary.services).length;
|
|
233
|
+
log(`\nGenerated daily-summary.json:`);
|
|
234
|
+
log(` Services: ${serviceCount}`);
|
|
235
|
+
log(` Days with data: ${totalDays}`);
|
|
236
|
+
log(` Window: ${options.windowDays} days`);
|
|
237
|
+
log(` Output: ${path.join(options.outputDir, 'daily-summary.json')}`);
|
|
238
|
+
|
|
239
|
+
if (serviceCount === 0) {
|
|
240
|
+
log('\nWarning: No data found. Make sure archives contain JSONL files.');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error('Error:', error.message);
|
|
245
|
+
if (options.verbose) {
|
|
246
|
+
console.error(error);
|
|
247
|
+
}
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
main();
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stentorosaur configuration management CLI
|
|
5
|
+
* Manages .monitorrc.json and docusaurus.config plugin options
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const CONFIG_FILE = '.monitorrc.json';
|
|
12
|
+
const ENTITIES_FILE = '.stentorosaur-entities.json';
|
|
13
|
+
|
|
14
|
+
// Parse command line arguments
|
|
15
|
+
function parseArgs(args) {
|
|
16
|
+
const result = { command: args[0], options: {} };
|
|
17
|
+
|
|
18
|
+
for (let i = 1; i < args.length; i++) {
|
|
19
|
+
const arg = args[i];
|
|
20
|
+
if (arg.startsWith('--')) {
|
|
21
|
+
const key = arg.slice(2);
|
|
22
|
+
const nextArg = args[i + 1];
|
|
23
|
+
if (nextArg && !nextArg.startsWith('--')) {
|
|
24
|
+
result.options[key] = nextArg;
|
|
25
|
+
i++;
|
|
26
|
+
} else {
|
|
27
|
+
result.options[key] = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Load monitor config
|
|
36
|
+
async function loadConfig() {
|
|
37
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE);
|
|
38
|
+
|
|
39
|
+
if (!await fs.pathExists(configPath)) {
|
|
40
|
+
return {
|
|
41
|
+
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
42
|
+
"description": "Configuration file for status monitoring script",
|
|
43
|
+
"systems": []
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return await fs.readJson(configPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Save monitor config
|
|
51
|
+
async function saveConfig(config) {
|
|
52
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE);
|
|
53
|
+
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Load entities config (for processes and additional metadata)
|
|
57
|
+
async function loadEntities() {
|
|
58
|
+
const entitiesPath = path.join(process.cwd(), ENTITIES_FILE);
|
|
59
|
+
|
|
60
|
+
if (!await fs.pathExists(entitiesPath)) {
|
|
61
|
+
return { systems: [], processes: [] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return await fs.readJson(entitiesPath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Save entities config
|
|
68
|
+
async function saveEntities(entities) {
|
|
69
|
+
const entitiesPath = path.join(process.cwd(), ENTITIES_FILE);
|
|
70
|
+
await fs.writeJson(entitiesPath, entities, { spaces: 2 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Add a system to monitor
|
|
74
|
+
async function addSystem(options) {
|
|
75
|
+
const name = options.name;
|
|
76
|
+
const url = options.url;
|
|
77
|
+
const method = options.method || 'GET';
|
|
78
|
+
const timeout = options.timeout || '10000';
|
|
79
|
+
const expectedCodes = options['expected-codes'] || '200';
|
|
80
|
+
|
|
81
|
+
if (!name || !url) {
|
|
82
|
+
console.error('\x1b[31mError:\x1b[0m Both --name and --url are required');
|
|
83
|
+
console.log('\nUsage: stentorosaur-config add-system --name api --url https://api.example.com/health');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const config = await loadConfig();
|
|
88
|
+
|
|
89
|
+
// Check if system already exists
|
|
90
|
+
const existingIndex = config.systems.findIndex(s => s.system === name);
|
|
91
|
+
|
|
92
|
+
const newSystem = {
|
|
93
|
+
system: name,
|
|
94
|
+
url: url,
|
|
95
|
+
method: method.toUpperCase(),
|
|
96
|
+
timeout: parseInt(timeout, 10),
|
|
97
|
+
expectedCodes: expectedCodes.split(',').map(c => parseInt(c.trim(), 10)),
|
|
98
|
+
maxResponseTime: 30000
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (existingIndex >= 0) {
|
|
102
|
+
config.systems[existingIndex] = newSystem;
|
|
103
|
+
console.log(`\x1b[33mUpdated:\x1b[0m System '${name}' configuration updated`);
|
|
104
|
+
} else {
|
|
105
|
+
config.systems.push(newSystem);
|
|
106
|
+
console.log(`\x1b[32mAdded:\x1b[0m System '${name}' added to ${CONFIG_FILE}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await saveConfig(config);
|
|
110
|
+
|
|
111
|
+
// Also add to entities for docusaurus config reference
|
|
112
|
+
const entities = await loadEntities();
|
|
113
|
+
if (!entities.systems.find(s => s.name === name)) {
|
|
114
|
+
entities.systems.push({ name, type: 'system', url });
|
|
115
|
+
await saveEntities(entities);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(`\n URL: ${url}`);
|
|
119
|
+
console.log(` Method: ${method.toUpperCase()}`);
|
|
120
|
+
console.log(` Timeout: ${timeout}ms`);
|
|
121
|
+
console.log(` Expected codes: ${expectedCodes}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add a business process
|
|
125
|
+
async function addProcess(options) {
|
|
126
|
+
const name = options.name;
|
|
127
|
+
const description = options.description || '';
|
|
128
|
+
|
|
129
|
+
if (!name) {
|
|
130
|
+
console.error('\x1b[31mError:\x1b[0m --name is required');
|
|
131
|
+
console.log('\nUsage: stentorosaur-config add-process --name deployments --description "Deployment pipeline"');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const entities = await loadEntities();
|
|
136
|
+
|
|
137
|
+
// Check if process already exists
|
|
138
|
+
const existingIndex = entities.processes.findIndex(p => p.name === name);
|
|
139
|
+
|
|
140
|
+
const newProcess = {
|
|
141
|
+
name,
|
|
142
|
+
type: 'process',
|
|
143
|
+
description: description || `${name} process tracking`
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (existingIndex >= 0) {
|
|
147
|
+
entities.processes[existingIndex] = newProcess;
|
|
148
|
+
console.log(`\x1b[33mUpdated:\x1b[0m Process '${name}' configuration updated`);
|
|
149
|
+
} else {
|
|
150
|
+
entities.processes.push(newProcess);
|
|
151
|
+
console.log(`\x1b[32mAdded:\x1b[0m Process '${name}' added to ${ENTITIES_FILE}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await saveEntities(entities);
|
|
155
|
+
|
|
156
|
+
console.log(`\n Description: ${newProcess.description}`);
|
|
157
|
+
console.log(`\n\x1b[33mNote:\x1b[0m Add 'process:${name}' label to GitHub Issues to track this process.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Remove a system
|
|
161
|
+
async function removeSystem(options) {
|
|
162
|
+
const name = options.name;
|
|
163
|
+
|
|
164
|
+
if (!name) {
|
|
165
|
+
console.error('\x1b[31mError:\x1b[0m --name is required');
|
|
166
|
+
console.log('\nUsage: stentorosaur-config remove-system --name api');
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const config = await loadConfig();
|
|
171
|
+
const originalLength = config.systems.length;
|
|
172
|
+
config.systems = config.systems.filter(s => s.system !== name);
|
|
173
|
+
|
|
174
|
+
if (config.systems.length === originalLength) {
|
|
175
|
+
console.log(`\x1b[33mWarning:\x1b[0m System '${name}' not found in ${CONFIG_FILE}`);
|
|
176
|
+
} else {
|
|
177
|
+
await saveConfig(config);
|
|
178
|
+
console.log(`\x1b[32mRemoved:\x1b[0m System '${name}' from ${CONFIG_FILE}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Also remove from entities
|
|
182
|
+
const entities = await loadEntities();
|
|
183
|
+
entities.systems = entities.systems.filter(s => s.name !== name);
|
|
184
|
+
await saveEntities(entities);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// List all configured systems and processes
|
|
188
|
+
async function list() {
|
|
189
|
+
const config = await loadConfig();
|
|
190
|
+
const entities = await loadEntities();
|
|
191
|
+
|
|
192
|
+
console.log('\n\x1b[32mMonitored Systems:\x1b[0m');
|
|
193
|
+
console.log('─'.repeat(60));
|
|
194
|
+
|
|
195
|
+
if (config.systems.length === 0) {
|
|
196
|
+
console.log(' No systems configured');
|
|
197
|
+
console.log(' Run: make status-add-system name=api url=https://...');
|
|
198
|
+
} else {
|
|
199
|
+
for (const system of config.systems) {
|
|
200
|
+
console.log(`\n \x1b[36m${system.system}\x1b[0m`);
|
|
201
|
+
console.log(` URL: ${system.url}`);
|
|
202
|
+
console.log(` Method: ${system.method}`);
|
|
203
|
+
console.log(` Timeout: ${system.timeout}ms`);
|
|
204
|
+
console.log(` Expected: ${system.expectedCodes.join(', ')}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log('\n\n\x1b[32mBusiness Processes:\x1b[0m');
|
|
209
|
+
console.log('─'.repeat(60));
|
|
210
|
+
|
|
211
|
+
if (!entities.processes || entities.processes.length === 0) {
|
|
212
|
+
console.log(' No processes configured');
|
|
213
|
+
console.log(' Run: make status-add-process name=deployments');
|
|
214
|
+
} else {
|
|
215
|
+
for (const process of entities.processes) {
|
|
216
|
+
console.log(`\n \x1b[36m${process.name}\x1b[0m`);
|
|
217
|
+
console.log(` Label: process:${process.name}`);
|
|
218
|
+
if (process.description) {
|
|
219
|
+
console.log(` Description: ${process.description}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log('\n');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Validate configuration
|
|
228
|
+
async function validate() {
|
|
229
|
+
const configPath = path.join(process.cwd(), CONFIG_FILE);
|
|
230
|
+
|
|
231
|
+
if (!await fs.pathExists(configPath)) {
|
|
232
|
+
console.error(`\x1b[31mError:\x1b[0m ${CONFIG_FILE} not found`);
|
|
233
|
+
console.log('Run: make status-init');
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const config = await fs.readJson(configPath);
|
|
239
|
+
|
|
240
|
+
const errors = [];
|
|
241
|
+
const warnings = [];
|
|
242
|
+
|
|
243
|
+
if (!config.systems || !Array.isArray(config.systems)) {
|
|
244
|
+
errors.push('Missing or invalid "systems" array');
|
|
245
|
+
} else {
|
|
246
|
+
for (let i = 0; i < config.systems.length; i++) {
|
|
247
|
+
const system = config.systems[i];
|
|
248
|
+
|
|
249
|
+
if (!system.system) {
|
|
250
|
+
errors.push(`System ${i + 1}: Missing "system" name`);
|
|
251
|
+
}
|
|
252
|
+
if (!system.url) {
|
|
253
|
+
errors.push(`System ${i + 1}: Missing "url"`);
|
|
254
|
+
} else {
|
|
255
|
+
try {
|
|
256
|
+
new URL(system.url);
|
|
257
|
+
} catch {
|
|
258
|
+
errors.push(`System ${i + 1}: Invalid URL "${system.url}"`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (system.timeout && (typeof system.timeout !== 'number' || system.timeout < 0)) {
|
|
262
|
+
warnings.push(`System ${i + 1}: Invalid timeout value`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (errors.length > 0) {
|
|
268
|
+
console.log('\x1b[31mValidation failed:\x1b[0m\n');
|
|
269
|
+
errors.forEach(e => console.log(` - ${e}`));
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (warnings.length > 0) {
|
|
274
|
+
console.log('\x1b[33mWarnings:\x1b[0m\n');
|
|
275
|
+
warnings.forEach(w => console.log(` - ${w}`));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(`\x1b[32mConfiguration valid!\x1b[0m`);
|
|
279
|
+
console.log(` ${config.systems.length} system(s) configured`);
|
|
280
|
+
|
|
281
|
+
} catch (err) {
|
|
282
|
+
console.error(`\x1b[31mError:\x1b[0m Failed to parse ${CONFIG_FILE}`);
|
|
283
|
+
console.error(err.message);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Generate docusaurus config snippet
|
|
289
|
+
async function generateConfig() {
|
|
290
|
+
const entities = await loadEntities();
|
|
291
|
+
|
|
292
|
+
const allEntities = [
|
|
293
|
+
...entities.systems.map(s => ({ name: s.name, type: 'system' })),
|
|
294
|
+
...entities.processes.map(p => ({ name: p.name, type: 'process' }))
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
console.log('\n\x1b[32mDocusaurus Config Snippet:\x1b[0m');
|
|
298
|
+
console.log('─'.repeat(60));
|
|
299
|
+
console.log(`
|
|
300
|
+
// Add to docusaurus.config.js plugins array:
|
|
301
|
+
[
|
|
302
|
+
'@amiable-dev/docusaurus-plugin-stentorosaur',
|
|
303
|
+
{
|
|
304
|
+
owner: 'your-org',
|
|
305
|
+
repo: 'your-repo',
|
|
306
|
+
entities: ${JSON.stringify(allEntities, null, 6).replace(/\n/g, '\n ')},
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Main CLI
|
|
313
|
+
async function main() {
|
|
314
|
+
const args = process.argv.slice(2);
|
|
315
|
+
|
|
316
|
+
if (args.length === 0) {
|
|
317
|
+
console.log('Stentorosaur Configuration Manager\n');
|
|
318
|
+
console.log('Commands:');
|
|
319
|
+
console.log(' add-system Add a system to monitor');
|
|
320
|
+
console.log(' add-process Add a business process');
|
|
321
|
+
console.log(' remove-system Remove a system');
|
|
322
|
+
console.log(' list List all configured systems and processes');
|
|
323
|
+
console.log(' validate Validate configuration files');
|
|
324
|
+
console.log(' generate Generate docusaurus.config.js snippet');
|
|
325
|
+
console.log('\nRun with --help for more info');
|
|
326
|
+
process.exit(0);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const { command, options } = parseArgs(args);
|
|
330
|
+
|
|
331
|
+
switch (command) {
|
|
332
|
+
case 'add-system':
|
|
333
|
+
await addSystem(options);
|
|
334
|
+
break;
|
|
335
|
+
case 'add-process':
|
|
336
|
+
await addProcess(options);
|
|
337
|
+
break;
|
|
338
|
+
case 'remove-system':
|
|
339
|
+
await removeSystem(options);
|
|
340
|
+
break;
|
|
341
|
+
case 'list':
|
|
342
|
+
await list();
|
|
343
|
+
break;
|
|
344
|
+
case 'validate':
|
|
345
|
+
await validate();
|
|
346
|
+
break;
|
|
347
|
+
case 'generate':
|
|
348
|
+
await generateConfig();
|
|
349
|
+
break;
|
|
350
|
+
default:
|
|
351
|
+
console.error(`\x1b[31mError:\x1b[0m Unknown command '${command}'`);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
main().catch(err => {
|
|
357
|
+
console.error('\x1b[31mError:\x1b[0m', err.message);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
});
|
package/scripts/init.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Initialize Stentorosaur status monitoring in a consuming project
|
|
5
|
+
* Creates .monitorrc.json and status-data directory
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const CONFIG_FILE = '.monitorrc.json';
|
|
12
|
+
const STATUS_DATA_DIR = 'status-data';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CONFIG = {
|
|
15
|
+
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
16
|
+
"description": "Configuration file for status monitoring script",
|
|
17
|
+
"systems": []
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function init() {
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
const configPath = path.join(cwd, CONFIG_FILE);
|
|
23
|
+
const statusDataPath = path.join(cwd, STATUS_DATA_DIR);
|
|
24
|
+
|
|
25
|
+
console.log('Initializing Stentorosaur status monitoring...\n');
|
|
26
|
+
|
|
27
|
+
// Create .monitorrc.json if it doesn't exist
|
|
28
|
+
if (await fs.pathExists(configPath)) {
|
|
29
|
+
console.log(`\x1b[33mWarning:\x1b[0m ${CONFIG_FILE} already exists. Skipping...`);
|
|
30
|
+
} else {
|
|
31
|
+
await fs.writeJson(configPath, DEFAULT_CONFIG, { spaces: 2 });
|
|
32
|
+
console.log(`\x1b[32mCreated:\x1b[0m ${CONFIG_FILE}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Create status-data directory
|
|
36
|
+
await fs.ensureDir(statusDataPath);
|
|
37
|
+
console.log(`\x1b[32mCreated:\x1b[0m ${STATUS_DATA_DIR}/`);
|
|
38
|
+
|
|
39
|
+
// Create .gitkeep in status-data
|
|
40
|
+
const gitkeepPath = path.join(statusDataPath, '.gitkeep');
|
|
41
|
+
if (!await fs.pathExists(gitkeepPath)) {
|
|
42
|
+
await fs.writeFile(gitkeepPath, '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('\n\x1b[32mStatus monitoring initialized!\x1b[0m\n');
|
|
46
|
+
console.log('Next steps:');
|
|
47
|
+
console.log(' 1. Add systems to monitor:');
|
|
48
|
+
console.log(' make status-add-system name=api url=https://api.example.com/health\n');
|
|
49
|
+
console.log(' 2. Copy GitHub Actions workflows:');
|
|
50
|
+
console.log(' make status-workflows\n');
|
|
51
|
+
console.log(' 3. Test your configuration:');
|
|
52
|
+
console.log(' make status-test\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
init().catch(err => {
|
|
56
|
+
console.error('\x1b[31mError:\x1b[0m', err.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|