switchman-dev 0.1.8 → 0.1.9

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.
@@ -0,0 +1,191 @@
1
+ export function registerMonitorCommands(program, {
2
+ chalk,
3
+ clearMonitorState,
4
+ getDb,
5
+ getRepo,
6
+ isProcessRunning,
7
+ monitorWorktreesOnce,
8
+ processExecPath,
9
+ readMonitorState,
10
+ renderMonitorEvent,
11
+ resolveMonitoredWorktrees,
12
+ spawn,
13
+ startBackgroundMonitor,
14
+ }) {
15
+ const monitorCmd = program.command('monitor').description('Observe workspaces for runtime file changes');
16
+ monitorCmd._switchmanAdvanced = true;
17
+
18
+ monitorCmd
19
+ .command('once')
20
+ .description('Capture one monitoring pass and log observed file changes')
21
+ .option('--json', 'Output raw JSON')
22
+ .option('--quarantine', 'Move or restore denied runtime changes immediately after detection')
23
+ .action((opts) => {
24
+ const repoRoot = getRepo();
25
+ const db = getDb(repoRoot);
26
+ const worktrees = resolveMonitoredWorktrees(db, repoRoot);
27
+ const result = monitorWorktreesOnce(db, repoRoot, worktrees, { quarantine: opts.quarantine });
28
+ db.close();
29
+
30
+ if (opts.json) {
31
+ console.log(JSON.stringify(result, null, 2));
32
+ return;
33
+ }
34
+
35
+ if (result.events.length === 0) {
36
+ console.log(chalk.dim('No file changes observed since the last monitor snapshot.'));
37
+ return;
38
+ }
39
+
40
+ console.log(`${chalk.green('✓')} Observed ${result.summary.total} file change(s)`);
41
+ for (const event of result.events) {
42
+ renderMonitorEvent(event);
43
+ }
44
+ });
45
+
46
+ monitorCmd
47
+ .command('watch')
48
+ .description('Poll workspaces continuously and log observed file changes')
49
+ .option('--interval-ms <ms>', 'Polling interval in milliseconds', '2000')
50
+ .option('--quarantine', 'Move or restore denied runtime changes immediately after detection')
51
+ .option('--daemonized', 'Internal flag used by monitor start', false)
52
+ .action(async (opts) => {
53
+ const repoRoot = getRepo();
54
+ const intervalMs = Number.parseInt(opts.intervalMs, 10);
55
+
56
+ if (!Number.isFinite(intervalMs) || intervalMs < 100) {
57
+ console.error(chalk.red('--interval-ms must be at least 100'));
58
+ process.exit(1);
59
+ }
60
+
61
+ console.log(chalk.cyan(`Watching workspaces every ${intervalMs}ms. Press Ctrl+C to stop.`));
62
+
63
+ let stopped = false;
64
+ const stop = () => {
65
+ stopped = true;
66
+ process.stdout.write('\n');
67
+ if (opts.daemonized) {
68
+ clearMonitorState(repoRoot);
69
+ }
70
+ };
71
+ process.on('SIGINT', stop);
72
+ process.on('SIGTERM', stop);
73
+
74
+ while (!stopped) {
75
+ const db = getDb(repoRoot);
76
+ const worktrees = resolveMonitoredWorktrees(db, repoRoot);
77
+ const result = monitorWorktreesOnce(db, repoRoot, worktrees, { quarantine: opts.quarantine });
78
+ db.close();
79
+
80
+ for (const event of result.events) {
81
+ renderMonitorEvent(event);
82
+ }
83
+
84
+ if (stopped) break;
85
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, intervalMs));
86
+ }
87
+
88
+ console.log(chalk.dim('Stopped worktree monitor.'));
89
+ });
90
+
91
+ monitorCmd
92
+ .command('start')
93
+ .description('Start the worktree monitor as a background process')
94
+ .option('--interval-ms <ms>', 'Polling interval in milliseconds', '2000')
95
+ .option('--quarantine', 'Move or restore denied runtime changes immediately after detection')
96
+ .action((opts) => {
97
+ const repoRoot = getRepo();
98
+ const intervalMs = Number.parseInt(opts.intervalMs, 10);
99
+ const state = startBackgroundMonitor(repoRoot, {
100
+ intervalMs,
101
+ quarantine: Boolean(opts.quarantine),
102
+ });
103
+
104
+ if (state.already_running) {
105
+ console.log(chalk.yellow(`Monitor already running with pid ${state.state.pid}`));
106
+ return;
107
+ }
108
+
109
+ console.log(`${chalk.green('✓')} Started monitor pid ${chalk.cyan(String(state.state.pid))}`);
110
+ console.log(`${chalk.dim('State:')} ${state.state_path}`);
111
+ });
112
+
113
+ monitorCmd
114
+ .command('stop')
115
+ .description('Stop the background worktree monitor')
116
+ .action(() => {
117
+ const repoRoot = getRepo();
118
+ const state = readMonitorState(repoRoot);
119
+
120
+ if (!state) {
121
+ console.log(chalk.dim('Monitor is not running.'));
122
+ return;
123
+ }
124
+
125
+ if (!isProcessRunning(state.pid)) {
126
+ clearMonitorState(repoRoot);
127
+ console.log(chalk.dim('Monitor state was stale and has been cleared.'));
128
+ return;
129
+ }
130
+
131
+ process.kill(state.pid, 'SIGTERM');
132
+ clearMonitorState(repoRoot);
133
+ console.log(`${chalk.green('✓')} Stopped monitor pid ${chalk.cyan(String(state.pid))}`);
134
+ });
135
+
136
+ monitorCmd
137
+ .command('status')
138
+ .description('Show background monitor process status')
139
+ .action(() => {
140
+ const repoRoot = getRepo();
141
+ const state = readMonitorState(repoRoot);
142
+
143
+ if (!state) {
144
+ console.log(chalk.dim('Monitor is not running.'));
145
+ return;
146
+ }
147
+
148
+ const running = isProcessRunning(state.pid);
149
+ if (!running) {
150
+ clearMonitorState(repoRoot);
151
+ console.log(chalk.yellow('Monitor state existed but the process is no longer running.'));
152
+ return;
153
+ }
154
+
155
+ console.log(`${chalk.green('✓')} Monitor running`);
156
+ console.log(` ${chalk.dim('pid')} ${state.pid}`);
157
+ console.log(` ${chalk.dim('interval_ms')} ${state.interval_ms}`);
158
+ console.log(` ${chalk.dim('quarantine')} ${state.quarantine ? 'true' : 'false'}`);
159
+ console.log(` ${chalk.dim('started_at')} ${state.started_at}`);
160
+ });
161
+
162
+ program
163
+ .command('watch')
164
+ .description('Watch worktrees for direct writes and rogue edits in real time')
165
+ .option('--interval-ms <ms>', 'Polling interval in milliseconds', '2000')
166
+ .option('--quarantine', 'Move or restore denied runtime changes immediately after detection')
167
+ .action(async (opts) => {
168
+ const repoRoot = getRepo();
169
+ const child = spawn(processExecPath, [
170
+ process.argv[1],
171
+ 'monitor',
172
+ 'watch',
173
+ '--interval-ms',
174
+ String(opts.intervalMs || '2000'),
175
+ ...(opts.quarantine ? ['--quarantine'] : []),
176
+ ], {
177
+ cwd: repoRoot,
178
+ stdio: 'inherit',
179
+ });
180
+
181
+ await new Promise((resolve, reject) => {
182
+ child.on('exit', (code) => {
183
+ process.exitCode = code ?? 0;
184
+ resolve();
185
+ });
186
+ child.on('error', reject);
187
+ });
188
+ });
189
+
190
+ return monitorCmd;
191
+ }