@ddtcorex/dsh-maestro-supervisor 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/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # dsh-maestro-supervisor
2
+
3
+ Supervisor daemon for DSH Web resilience — Phase 1 Guard & Report.
4
+
5
+ Runs **outside** the `pnpm → sh → node` tree. Polls `:3080` every 3s, manages last-known-good (LKG) snapshots in `~/.dsh/.supervisor/lkg/`, auto-rollbacks on crash, and writes `report-<ts>.md` for the next session. Telegram via `dsh-maestro-notifier`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm --dir packages/dsh-maestro-supervisor install
11
+ pnpm --dir packages/dsh-maestro-supervisor build
12
+
13
+ # systemd (recommended)
14
+ bash packages/dsh-maestro-supervisor/scripts/install-systemd.sh
15
+ systemctl --user daemon-reload && systemctl --user enable --now dsh-web-supervisor
16
+
17
+ # or manual sidecar
18
+ setsid node packages/dsh-maestro-supervisor/lib/index.js daemon &
19
+ ```
20
+
21
+ ## CLI
22
+
23
+ ```bash
24
+ node packages/dsh-maestro-supervisor/lib/index.js --help
25
+ node packages/dsh-maestro-supervisor/lib/index.js status
26
+ node packages/dsh-maestro-supervisor/lib/index.js daemon
27
+ ```
28
+
29
+ Reports: `~/.dsh/.supervisor/reports/report-<ts>.md`, LKG: `~/.dsh/.supervisor/lkg/<ts>/`, failed: `~/.dsh/.supervisor/failed/<ts>/`
30
+
31
+ ## Telegram
32
+
33
+ `src/host/notifier.ts` is loose by default: tries `import('@ddtcorex/dsh-maestro-notifier')`, then `TELEGRAM_BOT_TOKEN`/`TELEGRAM_CHAT_ID` env, then `console.log`. No hard dependency, daemon never blocks on Telegram.
34
+
35
+ - **Enable:** `systemctl --user edit dsh-web-supervisor` → uncomment `Environment=TELEGRAM_BOT_TOKEN`/`TELEGRAM_CHAT_ID` in `systemd/dsh-web-supervisor.service.template` → `systemctl --user daemon-reload && systemctl --user restart dsh-web-supervisor`.
36
+ - **Hard mode (optional):** `package.json` add `"@ddtcorex/dsh-maestro-notifier": "workspace:^0.1.0"` + `pnpm-workspace.yaml` `packages: ["../dsh-maestro-notifier"]` — then `pnpm install` links it and every `notify()` hits the hard import.
37
+
38
+ See `AGENTS.md` §Dependency patterns for details and for interdependent Cordis plugins (A ↔ B) — never mutual `inject`, use shared lib C / one-way + events / isolate+RPC.
39
+
40
+ ## Integration test
41
+
42
+ ```bash
43
+ DSH_INTEGRATION=1 pnpm --dir packages/dsh-maestro-supervisor test -- tests/integration.test.ts
44
+ ```
package/lib/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function runCli(args: string[]): Promise<void>;
package/lib/cli.js ADDED
@@ -0,0 +1,74 @@
1
+ import { Supervisor } from './supervisor.js';
2
+ import { pollHealth } from './health-poller.js';
3
+ import { writeLKG, verifyLKG } from './snapshot.js';
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+ import * as os from 'node:os';
7
+ export async function runCli(args) {
8
+ const cmd = args[2] ?? '--help';
9
+ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
10
+ console.log(`Usage: dsh-web-supervisor <command>
11
+
12
+ Commands:
13
+ daemon Run supervisor daemon (poll every 3s)
14
+ status Show health + LKG status
15
+ logs Tail supervisor reports
16
+ rollback --to <ts> Rollback to LKG <ts>
17
+ `);
18
+ return;
19
+ }
20
+ if (cmd === 'status') {
21
+ const health = await pollHealth();
22
+ console.log(`up: ${health.up}, httpCode: ${health.httpCode}, error: ${health.error ?? 'none'}`);
23
+ const lkgRoot = path.join(os.homedir(), '.dsh/.supervisor/lkg');
24
+ if (fs.existsSync(lkgRoot)) {
25
+ const entries = fs.readdirSync(lkgRoot).sort();
26
+ console.log(`LKG: ${entries.length} snapshots, latest: ${entries[entries.length - 1] ?? 'none'}`);
27
+ if (entries.length) {
28
+ const ok = await verifyLKG(path.join(lkgRoot, entries[entries.length - 1])).catch(() => false);
29
+ console.log(`latest LKG valid: ${ok}`);
30
+ }
31
+ }
32
+ else {
33
+ console.log('LKG: none');
34
+ }
35
+ return;
36
+ }
37
+ if (cmd === 'daemon') {
38
+ console.log('[supervisor] starting daemon — poll every 3s, Ctrl+C to stop');
39
+ const dshHome = path.join(os.homedir(), '.dsh');
40
+ const lkgRoot = path.join(os.homedir(), '.dsh/.supervisor/lkg');
41
+ const failedRoot = path.join(os.homedir(), '.dsh/.supervisor/failed');
42
+ const reportsRoot = path.join(os.homedir(), '.dsh/.supervisor/reports');
43
+ const supervisor = new Supervisor({
44
+ pollHealth: () => pollHealth(),
45
+ writeLKG: () => writeLKG(dshHome, lkgRoot),
46
+ writeFailed: () => writeLKG(dshHome, failedRoot),
47
+ writeReport: async ({ ts, health, action }) => {
48
+ const { writeReport } = await import('./report.js');
49
+ return writeReport({ reportsRoot, ts, health, gitDiff: '', logTail: '', action });
50
+ },
51
+ rollback: async () => {
52
+ // Find latest LKG and restore
53
+ const entries = fs.existsSync(lkgRoot) ? fs.readdirSync(lkgRoot).sort() : [];
54
+ if (!entries.length)
55
+ throw new Error('no LKG to rollback to');
56
+ const latest = entries[entries.length - 1];
57
+ const src = path.join(lkgRoot, latest);
58
+ // naive restore: copy files back
59
+ for (const entry of fs.readdirSync(src)) {
60
+ if (entry === 'manifest.json')
61
+ continue;
62
+ fs.cpSync(path.join(src, entry), path.join(dshHome, entry), { recursive: true, force: true });
63
+ }
64
+ console.log(`[supervisor] rolled back to ${latest}`);
65
+ },
66
+ notify: async (msg) => console.log(`[notify] ${msg}`),
67
+ intervalMs: 3000,
68
+ });
69
+ supervisor.start();
70
+ // keep process alive
71
+ await new Promise(() => { });
72
+ }
73
+ console.log(`unknown command: ${cmd} — try --help`);
74
+ }
@@ -0,0 +1,17 @@
1
+ export interface HealthState {
2
+ up: boolean;
3
+ httpCode?: number;
4
+ error?: string;
5
+ degraded?: boolean;
6
+ }
7
+ export interface PollHealthOpts {
8
+ fetch?: () => Promise<{
9
+ status: number;
10
+ text: () => Promise<string>;
11
+ }>;
12
+ psAlive?: () => Promise<boolean>;
13
+ logTail?: () => Promise<string>;
14
+ url?: string;
15
+ timeoutMs?: number;
16
+ }
17
+ export declare function pollHealth(opts?: PollHealthOpts): Promise<HealthState>;
@@ -0,0 +1,105 @@
1
+ const ERROR_PATTERNS = [
2
+ 'ERR_MODULE_NOT_FOUND',
3
+ 'ERR_PNPM',
4
+ 'assertChannel',
5
+ 'unhandledRejection',
6
+ 'SyntaxError',
7
+ 'YAMLParseError',
8
+ 'ParseError',
9
+ 'YAML',
10
+ 'JSON',
11
+ 'corrupted',
12
+ 'allowBuilds',
13
+ 'Cannot find module',
14
+ 'Failed to load',
15
+ ];
16
+ export async function pollHealth(opts = {}) {
17
+ const fetchFn = opts.fetch ?? defaultFetch(opts.url ?? 'http://127.0.0.1:3080/', opts.timeoutMs ?? 2000);
18
+ const psAliveFn = opts.psAlive ?? defaultPsAlive;
19
+ const logTailFn = opts.logTail ?? defaultLogTail;
20
+ let httpCode;
21
+ let fetchError;
22
+ try {
23
+ const res = await fetchFn();
24
+ httpCode = res.status;
25
+ if (res.status !== 200) {
26
+ fetchError = `http ${res.status}`;
27
+ }
28
+ }
29
+ catch (e) {
30
+ fetchError = e?.message ?? String(e);
31
+ }
32
+ let logContent = '';
33
+ try {
34
+ logContent = await logTailFn();
35
+ }
36
+ catch {
37
+ // ignore log read errors
38
+ }
39
+ let logError;
40
+ const lowerLog = logContent.toLowerCase();
41
+ for (const pat of ERROR_PATTERNS) {
42
+ if (lowerLog.includes(pat.toLowerCase())) {
43
+ // extract line containing pattern (case-insensitive)
44
+ const line = logContent.split('\n').find(l => l.toLowerCase().includes(pat.toLowerCase())) ?? pat;
45
+ logError = line.trim().slice(0, 500);
46
+ break;
47
+ }
48
+ }
49
+ // If either fetch failed or log has error, consider down
50
+ if (fetchError || logError) {
51
+ return {
52
+ up: false,
53
+ httpCode,
54
+ error: logError ?? fetchError,
55
+ degraded: !!logError && httpCode === 200,
56
+ };
57
+ }
58
+ // Also check psAlive as secondary signal — if fetch ok but ps dead, still down
59
+ try {
60
+ const alive = await psAliveFn();
61
+ if (!alive && httpCode === 200) {
62
+ // fetch succeeded but ps says dead — likely stale, still consider up if http 200
63
+ }
64
+ }
65
+ catch {
66
+ // ignore
67
+ }
68
+ return { up: httpCode === 200, httpCode };
69
+ }
70
+ function defaultFetch(url, timeoutMs) {
71
+ return async () => {
72
+ const controller = new AbortController();
73
+ const t = setTimeout(() => controller.abort(), timeoutMs);
74
+ try {
75
+ const res = await fetch(url, { signal: controller.signal });
76
+ return { status: res.status, text: async () => res.text() };
77
+ }
78
+ finally {
79
+ clearTimeout(t);
80
+ }
81
+ };
82
+ }
83
+ async function defaultPsAlive() {
84
+ // Check if any listener on 3080 exists via ss — fallback to true if ss unavailable
85
+ try {
86
+ const { execSync } = await import('node:child_process');
87
+ const out = execSync('ss -tln 2>/dev/null || true', { encoding: 'utf-8' });
88
+ return out.includes(':3080');
89
+ }
90
+ catch {
91
+ return true;
92
+ }
93
+ }
94
+ async function defaultLogTail() {
95
+ try {
96
+ const { readFileSync } = await import('node:fs');
97
+ const { homedir } = await import('node:os');
98
+ const logPath = `${homedir()}/.dsh.log`;
99
+ const content = readFileSync(logPath, 'utf-8');
100
+ return content.slice(-5000);
101
+ }
102
+ catch {
103
+ return '';
104
+ }
105
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/lib/index.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from './cli.js';
3
+ runCli(process.argv).catch(e => { console.error(e); process.exit(1); });
@@ -0,0 +1,7 @@
1
+ export interface NotifierOpts {
2
+ send?: (msg: string) => Promise<void>;
3
+ }
4
+ export declare function notify(msg: string, opts?: NotifierOpts): Promise<void>;
5
+ export declare function notifyCrash(reportPath: string, error: string, opts?: NotifierOpts): Promise<void>;
6
+ export declare function notifyDegraded(id: string, error: string, opts?: NotifierOpts): Promise<void>;
7
+ export declare function notifyFixed(branch: string, sessions: string[], opts?: NotifierOpts): Promise<void>;
@@ -0,0 +1,52 @@
1
+ export async function notify(msg, opts = {}) {
2
+ const send = opts.send ?? defaultSend;
3
+ try {
4
+ await send(msg);
5
+ }
6
+ catch {
7
+ // never block caller — rollback/report must not fail due to notifier
8
+ }
9
+ }
10
+ async function defaultSend(msg) {
11
+ // 1) Try hard dependency: @ddtcorex/dsh-maestro-notifier if installed (workspace:^)
12
+ try {
13
+ // dynamic import so daemon still runs when notifier is not installed (loose mode)
14
+ // @ts-ignore — optional hard dep, may not be installed
15
+ const mod = await import('@ddtcorex/dsh-maestro-notifier').catch(() => null);
16
+ if (mod?.sendTelegram || mod?.notify) {
17
+ const fn = mod.sendTelegram ?? mod.notify;
18
+ await fn(msg);
19
+ return;
20
+ }
21
+ }
22
+ catch {
23
+ // ignore and fall through
24
+ }
25
+ // 2) Try env Telegram token (TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID)
26
+ const token = process.env.TELEGRAM_BOT_TOKEN;
27
+ const chatId = process.env.TELEGRAM_CHAT_ID;
28
+ if (token && chatId) {
29
+ try {
30
+ await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify({ chat_id: chatId, text: msg, parse_mode: 'Markdown' }),
34
+ });
35
+ return;
36
+ }
37
+ catch {
38
+ // fall through to log
39
+ }
40
+ }
41
+ // 3) Fallback — log only, never throw
42
+ console.log(`[supervisor notify] ${msg}`);
43
+ }
44
+ export async function notifyCrash(reportPath, error, opts = {}) {
45
+ await notify(`CRASH detected → rollback (report: ${reportPath}, error: ${error})`, opts);
46
+ }
47
+ export async function notifyDegraded(id, error, opts = {}) {
48
+ await notify(`DEGRADED: ${id} failed — ${error}`, opts);
49
+ }
50
+ export async function notifyFixed(branch, sessions, opts = {}) {
51
+ await notify(`FIXED: ${branch}, sessions resumed: [${sessions.join(', ')}]`, opts);
52
+ }
@@ -0,0 +1,11 @@
1
+ import type { HealthState } from './health-poller.js';
2
+ export interface ReportOpts {
3
+ reportsRoot: string;
4
+ ts: string;
5
+ health: HealthState;
6
+ gitDiff: string;
7
+ logTail: string;
8
+ action: string;
9
+ }
10
+ export declare function writeReport(opts: ReportOpts): Promise<string>;
11
+ export declare function collectGitDiff(workspaceRoot: string): Promise<string>;
package/lib/report.js ADDED
@@ -0,0 +1,56 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ export async function writeReport(opts) {
4
+ fs.mkdirSync(opts.reportsRoot, { recursive: true });
5
+ const fileName = `report-${opts.ts}.md`;
6
+ const filePath = path.join(opts.reportsRoot, fileName);
7
+ const content = `# DSH Web Crash Report — ${opts.ts}
8
+
9
+ **Action:** ${opts.action}
10
+
11
+ ## Health Snapshot
12
+
13
+ - up: ${opts.health.up}
14
+ - httpCode: ${opts.health.httpCode ?? 'n/a'}
15
+ - error: ${opts.health.error ?? 'none'}
16
+ - degraded: ${opts.health.degraded ?? false}
17
+
18
+ ## Git Diff (link plugins)
19
+
20
+ \`\`\`diff
21
+ ${opts.gitDiff || '(no changes)'}
22
+ \`\`\`
23
+
24
+ ## Log Tail (last 200 lines)
25
+
26
+ \`\`\`
27
+ ${opts.logTail.slice(-5000) || '(empty)'}
28
+ \`\`\`
29
+
30
+ ## Next Steps
31
+
32
+ - Check \`~/.dsh/.supervisor/lkg/\` for last-known-good
33
+ - Review \`failed/${opts.ts}/\` snapshot
34
+ - If degraded, fix the listed plugin row and run \`pnpm verify\` + dry-boot
35
+
36
+ ---
37
+ *Generated by dsh-web-supervisor*
38
+ `;
39
+ fs.writeFileSync(filePath, content);
40
+ return filePath;
41
+ }
42
+ export async function collectGitDiff(workspaceRoot) {
43
+ try {
44
+ const { execSync } = await import('node:child_process');
45
+ // Find all link: plugins from profiles
46
+ const out = execSync('git -C ' + JSON.stringify(workspaceRoot) + ' status --porcelain 2>/dev/null || true', { encoding: 'utf-8' });
47
+ if (out.trim()) {
48
+ const diff = execSync('git -C ' + JSON.stringify(workspaceRoot) + ' diff 2>/dev/null | head -n 200', { encoding: 'utf-8' });
49
+ return diff || out;
50
+ }
51
+ return '';
52
+ }
53
+ catch {
54
+ return '';
55
+ }
56
+ }
@@ -0,0 +1,19 @@
1
+ interface ManifestEntry {
2
+ path: string;
3
+ sha256: string;
4
+ }
5
+ interface Manifest {
6
+ ts: string;
7
+ files: ManifestEntry[];
8
+ }
9
+ export declare function writeLKG(dshHome: string, lkgRoot: string): Promise<{
10
+ ts: string;
11
+ manifest: Manifest;
12
+ }>;
13
+ export declare function verifyLKG(lkgPath: string): Promise<boolean>;
14
+ export declare function rotateLKG(lkgRoot: string, keep?: number): Promise<void>;
15
+ export declare function writeFailed(dshHome: string, failedRoot: string): Promise<{
16
+ ts: string;
17
+ manifest: Manifest;
18
+ }>;
19
+ export {};
@@ -0,0 +1,82 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as crypto from 'node:crypto';
4
+ function sha256File(filePath) {
5
+ const data = fs.readFileSync(filePath);
6
+ return crypto.createHash('sha256').update(data).digest('hex');
7
+ }
8
+ function walkFiles(dir, base = dir) {
9
+ const out = [];
10
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
11
+ const full = path.join(dir, entry.name);
12
+ if (entry.isDirectory())
13
+ out.push(...walkFiles(full, base));
14
+ else if (entry.isFile())
15
+ out.push(path.relative(base, full));
16
+ }
17
+ return out;
18
+ }
19
+ export async function writeLKG(dshHome, lkgRoot) {
20
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
21
+ const dest = path.join(lkgRoot, ts);
22
+ fs.mkdirSync(dest, { recursive: true });
23
+ // Copy DSH home contents (if exists, copy recursively)
24
+ if (fs.existsSync(dshHome)) {
25
+ // Use cpSync if available
26
+ for (const entry of fs.readdirSync(dshHome)) {
27
+ if (entry === '.supervisor')
28
+ continue;
29
+ const src = path.join(dshHome, entry);
30
+ const dst = path.join(dest, entry);
31
+ fs.cpSync(src, dst, { recursive: true });
32
+ }
33
+ }
34
+ const files = fs.existsSync(dest) ? walkFiles(dest) : [];
35
+ const manifest = {
36
+ ts,
37
+ files: files
38
+ .filter(f => f !== 'manifest.json')
39
+ .map(f => ({ path: f, sha256: sha256File(path.join(dest, f)) })),
40
+ };
41
+ fs.writeFileSync(path.join(dest, 'manifest.json'), JSON.stringify(manifest, null, 2));
42
+ return { ts, manifest };
43
+ }
44
+ export async function verifyLKG(lkgPath) {
45
+ const manifestPath = path.join(lkgPath, 'manifest.json');
46
+ if (!fs.existsSync(manifestPath))
47
+ return false;
48
+ try {
49
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
50
+ for (const entry of manifest.files) {
51
+ const filePath = path.join(lkgPath, entry.path);
52
+ if (!fs.existsSync(filePath))
53
+ return false;
54
+ const hash = sha256File(filePath);
55
+ if (hash !== entry.sha256)
56
+ return false;
57
+ }
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ export async function rotateLKG(lkgRoot, keep = 3) {
65
+ if (!fs.existsSync(lkgRoot))
66
+ return;
67
+ const entries = fs.readdirSync(lkgRoot).filter((n) => {
68
+ try {
69
+ return fs.statSync(path.join(lkgRoot, n)).isDirectory();
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ }).sort();
75
+ const toDelete = entries.slice(0, Math.max(0, entries.length - keep));
76
+ for (const name of toDelete) {
77
+ fs.rmSync(path.join(lkgRoot, name), { recursive: true, force: true });
78
+ }
79
+ }
80
+ export async function writeFailed(dshHome, failedRoot) {
81
+ return writeLKG(dshHome, failedRoot);
82
+ }
@@ -0,0 +1,33 @@
1
+ import type { HealthState } from './health-poller.js';
2
+ export interface SupervisorDeps {
3
+ pollHealth: () => Promise<HealthState>;
4
+ writeLKG: () => Promise<{
5
+ ts: string;
6
+ manifest: any;
7
+ }>;
8
+ writeFailed: () => Promise<{
9
+ ts: string;
10
+ manifest: any;
11
+ }>;
12
+ writeReport: (opts: {
13
+ ts: string;
14
+ health: HealthState;
15
+ action: string;
16
+ }) => Promise<string>;
17
+ rollback: (ts?: string) => Promise<void>;
18
+ notify: (msg: string) => Promise<void>;
19
+ intervalMs?: number;
20
+ debounceMs?: number;
21
+ getTime?: () => number;
22
+ }
23
+ export declare class Supervisor {
24
+ private deps;
25
+ private lastRollback;
26
+ private rollingBack;
27
+ private lastLKGWrite;
28
+ private timer;
29
+ constructor(deps: SupervisorDeps);
30
+ tick(): Promise<void>;
31
+ start(): void;
32
+ stop(): void;
33
+ }
@@ -0,0 +1,58 @@
1
+ export class Supervisor {
2
+ deps;
3
+ lastRollback = 0;
4
+ rollingBack = false;
5
+ lastLKGWrite = 0;
6
+ timer = null;
7
+ constructor(deps) {
8
+ this.deps = deps;
9
+ }
10
+ async tick() {
11
+ const health = await this.deps.pollHealth();
12
+ if (health.up) {
13
+ // Throttle LKG writes to at most once per 5 minutes
14
+ const now = this.deps.getTime ? this.deps.getTime() : Date.now();
15
+ if (now - this.lastLKGWrite > 5 * 60 * 1000) {
16
+ try {
17
+ await this.deps.writeLKG();
18
+ this.lastLKGWrite = now;
19
+ }
20
+ catch {
21
+ // ignore snapshot errors
22
+ }
23
+ }
24
+ return;
25
+ }
26
+ // Down — check debounce and rolling state
27
+ if (this.rollingBack)
28
+ return;
29
+ const now = this.deps.getTime ? this.deps.getTime() : Date.now();
30
+ const debounceMs = this.deps.debounceMs ?? 60000;
31
+ if (now - this.lastRollback < debounceMs)
32
+ return;
33
+ this.rollingBack = true;
34
+ this.lastRollback = now;
35
+ try {
36
+ const failed = await this.deps.writeFailed().catch(() => ({ ts: new Date().toISOString().replace(/[:.]/g, '-'), manifest: null }));
37
+ const ts = failed?.ts ?? new Date().toISOString().replace(/[:.]/g, '-');
38
+ const reportPath = await this.deps.writeReport({ ts, health, action: `rollback — ${health.error ?? 'down'}` }).catch(() => '');
39
+ await this.deps.rollback();
40
+ await this.deps.notify(`CRASH detected → rollback (report: ${reportPath}, error: ${health.error ?? 'down'})`).catch(() => { });
41
+ }
42
+ finally {
43
+ this.rollingBack = false;
44
+ }
45
+ }
46
+ start() {
47
+ if (this.timer)
48
+ return;
49
+ const intervalMs = this.deps.intervalMs ?? 3000;
50
+ this.timer = setInterval(() => { this.tick().catch(() => { }); }, intervalMs);
51
+ }
52
+ stop() {
53
+ if (this.timer) {
54
+ clearInterval(this.timer);
55
+ this.timer = null;
56
+ }
57
+ }
58
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@ddtcorex/dsh-maestro-supervisor",
3
+ "version": "0.1.0",
4
+ "description": "Supervisor daemon for DSH Web resilience — auto-detect crashes, rollback to LKG, report",
5
+ "type": "module",
6
+ "bin": {
7
+ "dsh-web-supervisor": "./lib/index.js"
8
+ },
9
+ "files": [
10
+ "lib",
11
+ "README.md"
12
+ ],
13
+ "devDependencies": {
14
+ "@types/node": "^26.3.0",
15
+ "typescript": "^5.9.3",
16
+ "vitest": "^3.2.4"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "verify": "tsc --noEmit",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest"
23
+ }
24
+ }