metwatch 0.1.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/LICENSE +21 -0
- package/README.md +336 -0
- package/bin/mw.ts +20 -0
- package/index.ts +174 -0
- package/metwatch.config.json +9 -0
- package/package.json +50 -0
- package/src/cli/args.ts +115 -0
- package/src/cli/commands/list.ts +77 -0
- package/src/cli/commands/logs.ts +69 -0
- package/src/cli/commands/monitor.ts +12 -0
- package/src/cli/commands/start.ts +124 -0
- package/src/cli/commands/stop.ts +33 -0
- package/src/cli/help.ts +96 -0
- package/src/core/event-bus.ts +113 -0
- package/src/core/launcher.ts +280 -0
- package/src/core/log-manager.ts +116 -0
- package/src/core/metrics-manager.ts +115 -0
- package/src/core/process-manager.ts +79 -0
- package/src/core/runtime-manager.ts +299 -0
- package/src/core/state-manager.ts +88 -0
- package/src/services/disk.service.ts +76 -0
- package/src/services/network.service.ts +131 -0
- package/src/services/process.service.ts +49 -0
- package/src/services/system.service.ts +63 -0
- package/src/types/blessed.d.ts +332 -0
- package/src/types/config.types.ts +77 -0
- package/src/types/managed-process.types.ts +55 -0
- package/src/types/metrics.types.ts +182 -0
- package/src/types/process.types.ts +49 -0
- package/src/ui/layout.ts +318 -0
- package/src/ui/screen.ts +45 -0
- package/src/ui/widgets/cpu.widget.ts +98 -0
- package/src/ui/widgets/disk.widget.ts +134 -0
- package/src/ui/widgets/logs.widget.ts +168 -0
- package/src/ui/widgets/memory.widget.ts +94 -0
- package/src/ui/widgets/network.widget.ts +185 -0
- package/src/ui/widgets/process-table.widget.ts +293 -0
- package/src/ui/widgets/runtime.widget.ts +119 -0
- package/src/utils/formatters.ts +74 -0
package/src/cli/args.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// CLI argument router
|
|
3
|
+
//
|
|
4
|
+
// Dispatches to the appropriate command handler based on the first positional
|
|
5
|
+
// argument in process.argv. Uses the built-in `parseArgs` from 'util' for
|
|
6
|
+
// top-level flags; each subcommand parses its own flags independently.
|
|
7
|
+
//
|
|
8
|
+
// Subcommand dispatch table:
|
|
9
|
+
// (none) / monitor → open TUI, no managed processes
|
|
10
|
+
// start <file> → launch managed process + open TUI
|
|
11
|
+
// list → print managed process table
|
|
12
|
+
// logs <name> → print / tail buffered logs
|
|
13
|
+
// stop <name|all> → stop managed process(es)
|
|
14
|
+
// help [cmd] → print help
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
import { parseArgs } from 'util';
|
|
18
|
+
import {
|
|
19
|
+
HELP_ROOT,
|
|
20
|
+
HELP_START,
|
|
21
|
+
HELP_LIST,
|
|
22
|
+
HELP_LOGS,
|
|
23
|
+
HELP_STOP,
|
|
24
|
+
} from './help.ts';
|
|
25
|
+
import { runMonitor } from './commands/monitor.ts';
|
|
26
|
+
import { runStart } from './commands/start.ts';
|
|
27
|
+
import { runList } from './commands/list.ts';
|
|
28
|
+
import { runLogs } from './commands/logs.ts';
|
|
29
|
+
import { runStop } from './commands/stop.ts';
|
|
30
|
+
|
|
31
|
+
// Top-level flags only (--help / --version).
|
|
32
|
+
// Subcommand flags are parsed inside each command module.
|
|
33
|
+
const { values: topFlags, positionals } = parseArgs({
|
|
34
|
+
args: process.argv.slice(2),
|
|
35
|
+
options: {
|
|
36
|
+
help: { type: 'boolean', short: 'h' },
|
|
37
|
+
version: { type: 'boolean', short: 'v' },
|
|
38
|
+
},
|
|
39
|
+
allowPositionals: true,
|
|
40
|
+
strict: false,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (topFlags.version) {
|
|
44
|
+
// Version is injected at publish time; fall back to package.json read.
|
|
45
|
+
try {
|
|
46
|
+
const { readFileSync } = await import('fs');
|
|
47
|
+
const { resolve } = await import('path');
|
|
48
|
+
const pkg = JSON.parse(
|
|
49
|
+
readFileSync(resolve(import.meta.dir, '../../package.json'), 'utf-8')
|
|
50
|
+
) as { version?: string };
|
|
51
|
+
console.log(`metwatch ${pkg.version ?? '0.0.0'}`);
|
|
52
|
+
} catch {
|
|
53
|
+
console.log('metwatch 0.0.0');
|
|
54
|
+
}
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const [subcommand, ...subArgs] = positionals;
|
|
59
|
+
|
|
60
|
+
if (topFlags.help && !subcommand) {
|
|
61
|
+
console.log(HELP_ROOT);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (topFlags.help && subcommand) {
|
|
66
|
+
const helpMap: Record<string, string> = {
|
|
67
|
+
start: HELP_START,
|
|
68
|
+
list: HELP_LIST,
|
|
69
|
+
logs: HELP_LOGS,
|
|
70
|
+
stop: HELP_STOP,
|
|
71
|
+
monitor: 'Usage: mw monitor\n\nOpen the TUI dashboard in read-only (observe) mode.\n',
|
|
72
|
+
};
|
|
73
|
+
console.log(helpMap[subcommand] ?? HELP_ROOT);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
switch (subcommand) {
|
|
78
|
+
case undefined:
|
|
79
|
+
case 'monitor':
|
|
80
|
+
await runMonitor();
|
|
81
|
+
break;
|
|
82
|
+
|
|
83
|
+
case 'start':
|
|
84
|
+
await runStart(subArgs);
|
|
85
|
+
break;
|
|
86
|
+
|
|
87
|
+
case 'list':
|
|
88
|
+
runList();
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case 'logs':
|
|
92
|
+
runLogs(subArgs);
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
case 'stop':
|
|
96
|
+
runStop(subArgs);
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case 'help': {
|
|
100
|
+
const helpMap: Record<string, string> = {
|
|
101
|
+
start: HELP_START,
|
|
102
|
+
list: HELP_LIST,
|
|
103
|
+
logs: HELP_LOGS,
|
|
104
|
+
stop: HELP_STOP,
|
|
105
|
+
};
|
|
106
|
+
const target = subArgs[0];
|
|
107
|
+
console.log(target && helpMap[target] ? helpMap[target] : HELP_ROOT);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
default:
|
|
112
|
+
console.error(`Unknown command: "${subcommand}"\n`);
|
|
113
|
+
console.log(HELP_ROOT);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// CLI command: list
|
|
3
|
+
//
|
|
4
|
+
// Prints the current managed process states to stdout as a table.
|
|
5
|
+
// Does NOT open the TUI — intended for scripting / quick checks.
|
|
6
|
+
//
|
|
7
|
+
// If no managed processes are configured or running, prints a notice.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
import { readFileSync, existsSync } from 'fs';
|
|
11
|
+
import { resolve } from 'path';
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_CONFIG,
|
|
14
|
+
type MetWatchConfig,
|
|
15
|
+
type ResolvedConfig,
|
|
16
|
+
} from '../../types/config.types.ts';
|
|
17
|
+
import { formatUptime } from '../../utils/formatters.ts';
|
|
18
|
+
|
|
19
|
+
function loadConfig(): ResolvedConfig {
|
|
20
|
+
const p = resolve(process.cwd(), 'metwatch.config.json');
|
|
21
|
+
if (!existsSync(p)) return { ...DEFAULT_CONFIG };
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(readFileSync(p, 'utf-8')) as Partial<MetWatchConfig>;
|
|
24
|
+
return {
|
|
25
|
+
watchedProcesses: parsed.watchedProcesses ?? DEFAULT_CONFIG.watchedProcesses,
|
|
26
|
+
managedProcesses: parsed.managedProcesses ?? DEFAULT_CONFIG.managedProcesses,
|
|
27
|
+
refreshInterval: Math.max(250, parsed.refreshInterval ?? DEFAULT_CONFIG.refreshInterval),
|
|
28
|
+
maxProcesses: parsed.maxProcesses ?? DEFAULT_CONFIG.maxProcesses,
|
|
29
|
+
logScrollback: parsed.logScrollback ?? DEFAULT_CONFIG.logScrollback,
|
|
30
|
+
panels: { ...DEFAULT_CONFIG.panels, ...(parsed.panels ?? {}) },
|
|
31
|
+
};
|
|
32
|
+
} catch {
|
|
33
|
+
return { ...DEFAULT_CONFIG };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function runList(): void {
|
|
38
|
+
const config = loadConfig();
|
|
39
|
+
const defs = config.managedProcesses ?? [];
|
|
40
|
+
|
|
41
|
+
if (defs.length === 0) {
|
|
42
|
+
console.log('No managed processes defined in metwatch.config.json.');
|
|
43
|
+
console.log('Use `mw start <file>` to launch a managed process.');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const COL = { name: 20, status: 12, pid: 8, restarts: 9, cmd: 30 };
|
|
48
|
+
const header = [
|
|
49
|
+
'NAME'.padEnd(COL.name),
|
|
50
|
+
'STATUS'.padEnd(COL.status),
|
|
51
|
+
'PID'.padEnd(COL.pid),
|
|
52
|
+
'RESTARTS'.padEnd(COL.restarts),
|
|
53
|
+
'COMMAND'.padEnd(COL.cmd),
|
|
54
|
+
].join(' ');
|
|
55
|
+
|
|
56
|
+
const sep = '-'.repeat(header.length);
|
|
57
|
+
console.log(header);
|
|
58
|
+
console.log(sep);
|
|
59
|
+
|
|
60
|
+
for (const def of defs) {
|
|
61
|
+
const row = [
|
|
62
|
+
def.name.padEnd(COL.name),
|
|
63
|
+
'stopped'.padEnd(COL.status), // static — no live state here
|
|
64
|
+
'-'.padEnd(COL.pid),
|
|
65
|
+
'0'.padEnd(COL.restarts),
|
|
66
|
+
`${def.command} ${def.args.join(' ')}`.slice(0, COL.cmd).padEnd(COL.cmd),
|
|
67
|
+
].join(' ');
|
|
68
|
+
console.log(row);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log();
|
|
72
|
+
console.log('(Static view from config. Open the TUI with `mw` to see live states.)');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Suppress unused import warning for formatUptime — it will be used once
|
|
76
|
+
// live IPC is wired in Phase 3. Keep it here so the import stays auditable.
|
|
77
|
+
void formatUptime;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// CLI command: logs
|
|
3
|
+
//
|
|
4
|
+
// Prints buffered logs for a managed process to stdout.
|
|
5
|
+
// With --follow / -f, tails live output until Ctrl+C.
|
|
6
|
+
//
|
|
7
|
+
// In Phase 2 this reads from the in-process LogManager (same Bun process as
|
|
8
|
+
// the TUI). Phase 3 will replace this with a socket-based IPC read.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// mw logs <name> [--follow] [--lines <n>]
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
import { HELP_LOGS } from '../help.ts';
|
|
15
|
+
|
|
16
|
+
interface LogsOptions {
|
|
17
|
+
name: string;
|
|
18
|
+
follow: boolean;
|
|
19
|
+
lines: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseLogsArgs(argv: string[]): LogsOptions | null {
|
|
23
|
+
const [name, ...rest] = argv;
|
|
24
|
+
if (!name) {
|
|
25
|
+
console.error('Error: missing process name.\n');
|
|
26
|
+
console.log(HELP_LOGS);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let follow = false;
|
|
31
|
+
let lines = 50;
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < rest.length; i++) {
|
|
34
|
+
const arg = rest[i];
|
|
35
|
+
if (arg === '--follow' || arg === '-f') {
|
|
36
|
+
follow = true;
|
|
37
|
+
} else if (arg === '--lines') {
|
|
38
|
+
const n = parseInt(rest[++i] ?? '', 10);
|
|
39
|
+
if (isNaN(n) || n < 1) {
|
|
40
|
+
console.error('Error: --lines must be a positive integer.');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
lines = n;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { name, follow, lines };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `mw logs` launched WITHOUT the TUI — reads from a static snapshot.
|
|
52
|
+
*
|
|
53
|
+
* In the current architecture the log buffer only exists inside the TUI
|
|
54
|
+
* process. Until Phase 3 IPC is implemented we print a helpful message
|
|
55
|
+
* directing the user to the TUI.
|
|
56
|
+
*/
|
|
57
|
+
export function runLogs(argv: string[]): void {
|
|
58
|
+
const opts = parseLogsArgs(argv);
|
|
59
|
+
if (!opts) return;
|
|
60
|
+
|
|
61
|
+
// Phase 3 will establish a socket connection to the running TUI process
|
|
62
|
+
// and stream log lines from its LogManager over IPC. For now we inform
|
|
63
|
+
// the user that live logs are visible inside the TUI itself.
|
|
64
|
+
console.log(`[MetWatch] Logs for "${opts.name}" are available in the TUI dashboard.`);
|
|
65
|
+
console.log(' Open the dashboard: mw');
|
|
66
|
+
console.log(' Then press [l] to focus the Logs panel and scroll with ↑ / ↓.');
|
|
67
|
+
console.log();
|
|
68
|
+
console.log('Phase 3 will add out-of-process `mw logs` support via socket IPC.');
|
|
69
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// CLI command: monitor
|
|
3
|
+
//
|
|
4
|
+
// Opens the MetWatch TUI with no managed processes — pure observation mode.
|
|
5
|
+
// This is also the default when `mw` is invoked with no subcommand.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
import { main } from '../../../index.ts';
|
|
9
|
+
|
|
10
|
+
export async function runMonitor(): Promise<void> {
|
|
11
|
+
await main([]);
|
|
12
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// CLI command: start
|
|
3
|
+
//
|
|
4
|
+
// Parses CLI flags and launches one managed process, then opens the TUI.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// mw start <file> [options]
|
|
8
|
+
//
|
|
9
|
+
// Runtime inference:
|
|
10
|
+
// .ts / .tsx → bun
|
|
11
|
+
// .js / .mjs / .cjs → node
|
|
12
|
+
// .py → python
|
|
13
|
+
// other → execute directly (no runtime wrapper)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
import { basename, extname, resolve } from 'path';
|
|
17
|
+
import { HELP_START } from '../help.ts';
|
|
18
|
+
import type { ManagedProcessDef } from '../../types/managed-process.types.ts';
|
|
19
|
+
import { main } from '../../../index.ts';
|
|
20
|
+
|
|
21
|
+
function inferRuntime(file: string): { command: string; args: string[] } {
|
|
22
|
+
const ext = extname(file).toLowerCase();
|
|
23
|
+
switch (ext) {
|
|
24
|
+
case '.ts':
|
|
25
|
+
case '.tsx':
|
|
26
|
+
return { command: 'bun', args: ['run', file] };
|
|
27
|
+
case '.js':
|
|
28
|
+
case '.mjs':
|
|
29
|
+
case '.cjs':
|
|
30
|
+
return { command: 'node', args: [file] };
|
|
31
|
+
case '.py':
|
|
32
|
+
return { command: 'python', args: [file] };
|
|
33
|
+
default:
|
|
34
|
+
return { command: file, args: [] };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface StartOptions {
|
|
39
|
+
file: string;
|
|
40
|
+
name: string;
|
|
41
|
+
runtime: string | null;
|
|
42
|
+
autoRestart: boolean;
|
|
43
|
+
cwd: string;
|
|
44
|
+
env: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseStartArgs(argv: string[]): StartOptions | null {
|
|
48
|
+
const [file, ...rest] = argv;
|
|
49
|
+
|
|
50
|
+
if (!file || file.startsWith('-')) {
|
|
51
|
+
console.error('Error: missing <file> argument.\n');
|
|
52
|
+
console.log(HELP_START);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const absFile = resolve(process.cwd(), file);
|
|
57
|
+
let name = basename(file, extname(file));
|
|
58
|
+
let runtime: string | null = null;
|
|
59
|
+
let autoRestart = true;
|
|
60
|
+
let cwd = process.cwd();
|
|
61
|
+
const env: Record<string, string> = {};
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < rest.length; i++) {
|
|
64
|
+
const arg = rest[i];
|
|
65
|
+
if (arg === '--name') {
|
|
66
|
+
name = rest[++i] ?? name;
|
|
67
|
+
} else if (arg === '--runtime') {
|
|
68
|
+
runtime = rest[++i] ?? null;
|
|
69
|
+
} else if (arg === '--no-restart') {
|
|
70
|
+
autoRestart = false;
|
|
71
|
+
} else if (arg === '--cwd') {
|
|
72
|
+
cwd = resolve(rest[++i] ?? process.cwd());
|
|
73
|
+
} else if (arg === '--env') {
|
|
74
|
+
const pair = rest[++i] ?? '';
|
|
75
|
+
const eqIdx = pair.indexOf('=');
|
|
76
|
+
if (eqIdx > 0) {
|
|
77
|
+
env[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
|
|
78
|
+
} else {
|
|
79
|
+
console.error(`Error: --env value must be KEY=VALUE, got: ${pair}`);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
83
|
+
console.log(HELP_START);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
} else {
|
|
86
|
+
console.error(`Error: unknown option "${arg}"\n`);
|
|
87
|
+
console.log(HELP_START);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { file: absFile, name, runtime, autoRestart, cwd, env };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function runStart(argv: string[]): Promise<void> {
|
|
96
|
+
const opts = parseStartArgs(argv);
|
|
97
|
+
if (!opts) {
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const inferred = inferRuntime(opts.file);
|
|
102
|
+
|
|
103
|
+
let command: string;
|
|
104
|
+
let args: string[];
|
|
105
|
+
|
|
106
|
+
if (opts.runtime) {
|
|
107
|
+
command = opts.runtime;
|
|
108
|
+
args = [opts.file];
|
|
109
|
+
} else {
|
|
110
|
+
command = inferred.command;
|
|
111
|
+
args = inferred.args;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const def: ManagedProcessDef = {
|
|
115
|
+
name: opts.name,
|
|
116
|
+
command,
|
|
117
|
+
args,
|
|
118
|
+
autoRestart: opts.autoRestart,
|
|
119
|
+
cwd: opts.cwd,
|
|
120
|
+
env: Object.keys(opts.env).length > 0 ? opts.env : undefined,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
await main([def]);
|
|
124
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// CLI command: stop
|
|
3
|
+
//
|
|
4
|
+
// Gracefully stops a named managed process or all of them.
|
|
5
|
+
//
|
|
6
|
+
// In Phase 2 stop runs inside the same process as the TUI — it is wired
|
|
7
|
+
// through the event bus. Phase 3 will add out-of-process IPC.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// mw stop <name|all>
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
import { HELP_STOP } from '../help.ts';
|
|
14
|
+
|
|
15
|
+
export function runStop(argv: string[]): void {
|
|
16
|
+
const [target] = argv;
|
|
17
|
+
|
|
18
|
+
if (!target) {
|
|
19
|
+
console.error('Error: missing target name.\n');
|
|
20
|
+
console.log(HELP_STOP);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Phase 3: emit over IPC socket to the running TUI process.
|
|
25
|
+
// For now we inform the user to use the in-TUI keybindings.
|
|
26
|
+
if (target === 'all') {
|
|
27
|
+
console.log('[MetWatch] To stop all managed processes, press q in the TUI (graceful shutdown).');
|
|
28
|
+
} else {
|
|
29
|
+
console.log(`[MetWatch] To stop "${target}", select it in the TUI and press [s].`);
|
|
30
|
+
}
|
|
31
|
+
console.log();
|
|
32
|
+
console.log('Phase 3 will add out-of-process `mw stop` support via socket IPC.');
|
|
33
|
+
}
|
package/src/cli/help.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// CLI Help Text
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export const HELP_ROOT = `\
|
|
6
|
+
MetWatch — terminal process monitoring & management tool
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
mw [command] [options]
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
monitor Open the TUI dashboard (no managed processes)
|
|
13
|
+
start <file> Run a script as a managed process and open the TUI
|
|
14
|
+
list Print managed process states to stdout
|
|
15
|
+
logs <name> Print buffered logs for a managed process
|
|
16
|
+
stop <name|all> Stop a managed process (or all)
|
|
17
|
+
help [command] Show help
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
-h, --help Show this help message
|
|
21
|
+
-v, --version Print version
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
mw Open dashboard (same as "mw monitor")
|
|
25
|
+
mw start server.ts Run server.ts with Bun and watch it in the TUI
|
|
26
|
+
mw start app.py --name api Run app.py with Python, label it "api"
|
|
27
|
+
mw start ./bin --no-restart Run without auto-restart
|
|
28
|
+
mw list Show all managed process states
|
|
29
|
+
mw logs api Show buffered logs for "api"
|
|
30
|
+
mw logs api --follow Tail live logs for "api"
|
|
31
|
+
mw stop api Stop "api"
|
|
32
|
+
mw stop all Stop all managed processes
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export const HELP_START = `\
|
|
36
|
+
Usage:
|
|
37
|
+
mw start <file> [options]
|
|
38
|
+
|
|
39
|
+
Launch a script as a MetWatch-managed process, then open the TUI dashboard.
|
|
40
|
+
The runtime is inferred from the file extension unless --runtime is provided.
|
|
41
|
+
|
|
42
|
+
.ts / .tsx → bun
|
|
43
|
+
.js / .mjs / .cjs → node
|
|
44
|
+
.py → python
|
|
45
|
+
other → executed directly
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--name <label> Display name in the TUI (default: basename of <file>)
|
|
49
|
+
--runtime <cmd> Override the inferred runtime executable
|
|
50
|
+
--no-restart Disable auto-restart on crash
|
|
51
|
+
--cwd <dir> Working directory for the child process
|
|
52
|
+
--env KEY=VALUE Set an environment variable (repeatable)
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
mw start server.ts
|
|
56
|
+
mw start app.py --name api --no-restart
|
|
57
|
+
mw start worker.js --cwd ./workers --env PORT=4000
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
export const HELP_LIST = `\
|
|
61
|
+
Usage:
|
|
62
|
+
mw list
|
|
63
|
+
|
|
64
|
+
Print the current state of all managed processes to stdout.
|
|
65
|
+
Opens the TUI first if not already running (reads live state from the session).
|
|
66
|
+
|
|
67
|
+
Columns: NAME PID STATUS RESTARTS UPTIME
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
export const HELP_LOGS = `\
|
|
71
|
+
Usage:
|
|
72
|
+
mw logs <name> [options]
|
|
73
|
+
|
|
74
|
+
Print buffered stdout/stderr for a managed process.
|
|
75
|
+
|
|
76
|
+
Options:
|
|
77
|
+
--follow, -f Tail live output (Ctrl+C to exit)
|
|
78
|
+
--lines <n> Number of lines to show (default: 50)
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
mw logs api
|
|
82
|
+
mw logs api --follow
|
|
83
|
+
mw logs api --lines 100
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
export const HELP_STOP = `\
|
|
87
|
+
Usage:
|
|
88
|
+
mw stop <name|all>
|
|
89
|
+
|
|
90
|
+
Gracefully stop a managed process (SIGTERM).
|
|
91
|
+
Use "all" to stop every managed process.
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
mw stop api
|
|
95
|
+
mw stop all
|
|
96
|
+
`;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Event Bus
|
|
3
|
+
//
|
|
4
|
+
// A strongly-typed, singleton event bus built on Node's EventEmitter.
|
|
5
|
+
// All inter-module communication in MetWatch MUST go through this bus —
|
|
6
|
+
// never import one module directly into another to trigger side-effects.
|
|
7
|
+
//
|
|
8
|
+
// Design rationale:
|
|
9
|
+
// - Generic type parameter on emit/on enforces payload shapes at compile time.
|
|
10
|
+
// - Singleton export means any module can import { bus } without wiring.
|
|
11
|
+
// - The EventMap type is the canonical catalog of all events in the system.
|
|
12
|
+
// Adding a new event = add it here first, then implement producer + consumer.
|
|
13
|
+
//
|
|
14
|
+
// Event naming convention: <domain>:<noun>:<verb>
|
|
15
|
+
// e.g. metrics:cpu:updated | process:kill:requested | ui:view:toggled
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
import { EventEmitter } from 'events';
|
|
19
|
+
import type {
|
|
20
|
+
CpuMetrics,
|
|
21
|
+
MemoryMetrics,
|
|
22
|
+
DiskMetrics,
|
|
23
|
+
NetworkMetrics,
|
|
24
|
+
RuntimeMetrics,
|
|
25
|
+
} from '../types/metrics.types.ts';
|
|
26
|
+
import type { ProcessList, ProcessViewMode } from '../types/process.types.ts';
|
|
27
|
+
|
|
28
|
+
// ── Event catalog ─────────────────────────────────────────────────────────────
|
|
29
|
+
// Every event name and its payload type lives here. The bus is a closed system:
|
|
30
|
+
// if the event isn't in this map, it doesn't exist in MetWatch.
|
|
31
|
+
|
|
32
|
+
export interface EventMap {
|
|
33
|
+
// Metrics
|
|
34
|
+
'metrics:cpu:updated': CpuMetrics;
|
|
35
|
+
'metrics:memory:updated': MemoryMetrics;
|
|
36
|
+
'metrics:disk:updated': DiskMetrics;
|
|
37
|
+
'metrics:network:updated': NetworkMetrics;
|
|
38
|
+
'metrics:runtime:updated': RuntimeMetrics;
|
|
39
|
+
|
|
40
|
+
// System processes (observed, not owned)
|
|
41
|
+
'processes:updated': ProcessList;
|
|
42
|
+
'process:kill:requested': { pid: number };
|
|
43
|
+
'process:kill:result': { pid: number; success: boolean; error?: string };
|
|
44
|
+
|
|
45
|
+
// Managed processes (launched and owned by MetWatch)
|
|
46
|
+
'managed:started': { id: string; pid: number };
|
|
47
|
+
'managed:stopped': { id: string };
|
|
48
|
+
'managed:crashed': { id: string; exitCode: number | null; restarts: number };
|
|
49
|
+
'managed:restarted': { id: string; pid: number; restarts: number };
|
|
50
|
+
'managed:restart:requested': { id: string };
|
|
51
|
+
'managed:stop:requested': { id: string };
|
|
52
|
+
|
|
53
|
+
// Log streaming (stdout/stderr from managed processes)
|
|
54
|
+
'log:line': { id: string; stream: 'stdout' | 'stderr'; line: string; timestamp: number };
|
|
55
|
+
'log:stream:started': { id: string; pid: number };
|
|
56
|
+
'log:stream:stopped': { id: string; exitCode: number | null };
|
|
57
|
+
|
|
58
|
+
// UI
|
|
59
|
+
'ui:view:toggled': ProcessViewMode;
|
|
60
|
+
'ui:panel:toggled': { panel: PanelName; visible: boolean };
|
|
61
|
+
'ui:quit': undefined;
|
|
62
|
+
|
|
63
|
+
// Lifecycle
|
|
64
|
+
'app:error': { source: string; error: Error };
|
|
65
|
+
'app:ready': undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** All panel names that can be toggled on/off */
|
|
69
|
+
export type PanelName = 'cpu' | 'memory' | 'disk' | 'network' | 'runtime' | 'processes' | 'logs';
|
|
70
|
+
|
|
71
|
+
export type EventName = keyof EventMap;
|
|
72
|
+
|
|
73
|
+
// ── Typed EventEmitter wrapper ────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
class TypedEventBus {
|
|
76
|
+
private readonly emitter = new EventEmitter();
|
|
77
|
+
|
|
78
|
+
constructor() {
|
|
79
|
+
// Increase limit to accommodate all widget subscriptions without warnings.
|
|
80
|
+
this.emitter.setMaxListeners(100);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
emit<K extends EventName>(event: K, payload: EventMap[K]): void {
|
|
84
|
+
this.emitter.emit(event, payload);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
on<K extends EventName>(event: K, handler: (payload: EventMap[K]) => void): () => void {
|
|
88
|
+
this.emitter.on(event, handler);
|
|
89
|
+
// Return unsubscribe function — always clean up in widget destroy()
|
|
90
|
+
return () => this.emitter.off(event, handler);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
once<K extends EventName>(event: K, handler: (payload: EventMap[K]) => void): void {
|
|
94
|
+
this.emitter.once(event, handler);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
off<K extends EventName>(event: K, handler: (payload: EventMap[K]) => void): void {
|
|
98
|
+
this.emitter.off(event, handler);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Remove all listeners for a given event. Use sparingly. */
|
|
102
|
+
removeAllListeners<K extends EventName>(event?: K): void {
|
|
103
|
+
this.emitter.removeAllListeners(event);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
listenerCount<K extends EventName>(event: K): number {
|
|
107
|
+
return this.emitter.listenerCount(event);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Singleton ─────────────────────────────────────────────────────────────────
|
|
112
|
+
// One bus for the entire application lifetime.
|
|
113
|
+
export const bus = new TypedEventBus();
|