@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amiable-dev/docusaurus-plugin-stentorosaur",
3
- "version": "0.16.4",
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
+ });
@@ -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
+ });