@massu/core 0.1.0 → 0.1.2
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 +71 -0
- package/README.md +2 -2
- package/dist/hooks/cost-tracker.js +149 -11527
- package/dist/hooks/post-edit-context.js +127 -11493
- package/dist/hooks/post-tool-use.js +169 -11550
- package/dist/hooks/pre-compact.js +149 -11530
- package/dist/hooks/pre-delete-check.js +144 -11523
- package/dist/hooks/quality-event.js +149 -11527
- package/dist/hooks/session-end.js +188 -11570
- package/dist/hooks/session-start.js +159 -11534
- package/dist/hooks/user-prompt.js +149 -11530
- package/package.json +14 -19
- package/src/adr-generator.ts +292 -0
- package/src/analytics.ts +373 -0
- package/src/audit-trail.ts +450 -0
- package/src/backfill-sessions.ts +180 -0
- package/src/cli.ts +105 -0
- package/src/cloud-sync.ts +190 -0
- package/src/commands/doctor.ts +300 -0
- package/src/commands/init.ts +395 -0
- package/src/commands/install-hooks.ts +26 -0
- package/src/config.ts +357 -0
- package/src/cost-tracker.ts +355 -0
- package/src/db.ts +233 -0
- package/src/dependency-scorer.ts +337 -0
- package/src/docs-map.json +100 -0
- package/src/docs-tools.ts +517 -0
- package/src/domains.ts +181 -0
- package/src/hooks/cost-tracker.ts +66 -0
- package/src/hooks/intent-suggester.ts +131 -0
- package/src/hooks/post-edit-context.ts +91 -0
- package/src/hooks/post-tool-use.ts +175 -0
- package/src/hooks/pre-compact.ts +146 -0
- package/src/hooks/pre-delete-check.ts +153 -0
- package/src/hooks/quality-event.ts +127 -0
- package/src/hooks/security-gate.ts +121 -0
- package/src/hooks/session-end.ts +467 -0
- package/src/hooks/session-start.ts +210 -0
- package/src/hooks/user-prompt.ts +91 -0
- package/src/import-resolver.ts +224 -0
- package/src/memory-db.ts +1376 -0
- package/src/memory-tools.ts +391 -0
- package/src/middleware-tree.ts +70 -0
- package/src/observability-tools.ts +343 -0
- package/src/observation-extractor.ts +411 -0
- package/src/page-deps.ts +283 -0
- package/src/prompt-analyzer.ts +332 -0
- package/src/regression-detector.ts +319 -0
- package/src/rules.ts +57 -0
- package/src/schema-mapper.ts +232 -0
- package/src/security-scorer.ts +405 -0
- package/src/security-utils.ts +133 -0
- package/src/sentinel-db.ts +578 -0
- package/src/sentinel-scanner.ts +405 -0
- package/src/sentinel-tools.ts +512 -0
- package/src/sentinel-types.ts +140 -0
- package/src/server.ts +189 -0
- package/src/session-archiver.ts +112 -0
- package/src/session-state-generator.ts +174 -0
- package/src/team-knowledge.ts +407 -0
- package/src/tools.ts +847 -0
- package/src/transcript-parser.ts +458 -0
- package/src/trpc-index.ts +214 -0
- package/src/validate-features-runner.ts +106 -0
- package/src/validation-engine.ts +358 -0
- package/dist/cli.js +0 -7890
- package/dist/server.js +0 -7008
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
3
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
4
|
+
|
|
5
|
+
// ============================================================
|
|
6
|
+
// PreToolUse Hook: Pre-Deletion Feature Impact Check
|
|
7
|
+
// Detects file deletion patterns (rm, git rm, Write with empty content)
|
|
8
|
+
// and runs sentinel impact analysis. Blocks if critical features orphaned.
|
|
9
|
+
// Must complete in <500ms.
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
import Database from 'better-sqlite3';
|
|
13
|
+
import { resolve } from 'path';
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
import { getFeatureImpact } from '../sentinel-db.ts';
|
|
16
|
+
import { getProjectRoot, getResolvedPaths } from '../config.ts';
|
|
17
|
+
|
|
18
|
+
interface HookInput {
|
|
19
|
+
session_id: string;
|
|
20
|
+
tool_name: string;
|
|
21
|
+
tool_input: {
|
|
22
|
+
command?: string;
|
|
23
|
+
file_path?: string;
|
|
24
|
+
content?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
29
|
+
|
|
30
|
+
function getDataDb(): Database.Database | null {
|
|
31
|
+
const dbPath = getResolvedPaths().dataDbPath;
|
|
32
|
+
if (!existsSync(dbPath)) return null;
|
|
33
|
+
try {
|
|
34
|
+
const db = new Database(dbPath, { readonly: true });
|
|
35
|
+
db.pragma('journal_mode = WAL');
|
|
36
|
+
return db;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extractDeletedFiles(input: HookInput): string[] {
|
|
43
|
+
const files: string[] = [];
|
|
44
|
+
|
|
45
|
+
if (input.tool_name === 'Bash' && input.tool_input.command) {
|
|
46
|
+
const cmd = input.tool_input.command;
|
|
47
|
+
|
|
48
|
+
// Detect rm commands
|
|
49
|
+
const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
|
|
50
|
+
if (rmMatch) {
|
|
51
|
+
const paths = rmMatch[1].split(/\s+/).filter(p => !p.startsWith('-'));
|
|
52
|
+
for (const p of paths) {
|
|
53
|
+
const relPath = p.startsWith('src/') ? p : p.replace(PROJECT_ROOT + '/', '');
|
|
54
|
+
if (relPath.startsWith('src/')) {
|
|
55
|
+
files.push(relPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Detect Write tool with empty content (file replacement that empties)
|
|
62
|
+
if (input.tool_name === 'Write' && input.tool_input.file_path) {
|
|
63
|
+
const content = input.tool_input.content || '';
|
|
64
|
+
if (content.trim().length === 0) {
|
|
65
|
+
const relPath = input.tool_input.file_path.replace(PROJECT_ROOT + '/', '');
|
|
66
|
+
if (relPath.startsWith('src/')) {
|
|
67
|
+
files.push(relPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main(): Promise<void> {
|
|
76
|
+
try {
|
|
77
|
+
const input = await readStdin();
|
|
78
|
+
const hookInput = JSON.parse(input) as HookInput;
|
|
79
|
+
|
|
80
|
+
const deletedFiles = extractDeletedFiles(hookInput);
|
|
81
|
+
if (deletedFiles.length === 0) {
|
|
82
|
+
process.exit(0);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const db = getDataDb();
|
|
87
|
+
if (!db) {
|
|
88
|
+
// No database available - can't check
|
|
89
|
+
process.exit(0);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Check if any sentinel tables exist
|
|
95
|
+
const tableExists = db.prepare(
|
|
96
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='massu_sentinel'"
|
|
97
|
+
).get();
|
|
98
|
+
|
|
99
|
+
if (!tableExists) {
|
|
100
|
+
process.exit(0);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const impact = getFeatureImpact(db, deletedFiles);
|
|
105
|
+
|
|
106
|
+
if (impact.blocked) {
|
|
107
|
+
const msg = [
|
|
108
|
+
`SENTINEL IMPACT WARNING: Deleting ${deletedFiles.length} file(s) would affect features:`,
|
|
109
|
+
'',
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
if (impact.orphaned.length > 0) {
|
|
113
|
+
msg.push(`ORPHANED (${impact.orphaned.length} features - no primary components left):`);
|
|
114
|
+
for (const item of impact.orphaned) {
|
|
115
|
+
msg.push(` - ${item.feature.feature_key} [${item.feature.priority}]: ${item.feature.title}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (impact.degraded.length > 0) {
|
|
120
|
+
msg.push(`DEGRADED (${impact.degraded.length} features - some components removed):`);
|
|
121
|
+
for (const item of impact.degraded) {
|
|
122
|
+
msg.push(` - ${item.feature.feature_key}: ${item.feature.title}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
msg.push('');
|
|
127
|
+
msg.push('Create a migration plan before deleting these files.');
|
|
128
|
+
|
|
129
|
+
// Output warning but don't block (user can proceed)
|
|
130
|
+
process.stdout.write(JSON.stringify({ message: msg.join('\n') }));
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
db.close();
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// Hooks must never crash
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readStdin(): Promise<string> {
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
let data = '';
|
|
145
|
+
process.stdin.setEncoding('utf-8');
|
|
146
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
147
|
+
process.stdin.on('end', () => resolve(data));
|
|
148
|
+
// Timeout to prevent hanging
|
|
149
|
+
setTimeout(() => resolve(data), 400);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main();
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
3
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
4
|
+
|
|
5
|
+
// ============================================================
|
|
6
|
+
// PostToolUse Hook: Quality Event Recorder
|
|
7
|
+
// Parses tool responses for quality signals (test failures,
|
|
8
|
+
// type errors, build failures) and records them for analytics.
|
|
9
|
+
// Must complete in <500ms.
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
import { getMemoryDb } from '../memory-db.ts';
|
|
13
|
+
|
|
14
|
+
interface HookInput {
|
|
15
|
+
session_id: string;
|
|
16
|
+
transcript_path: string;
|
|
17
|
+
cwd: string;
|
|
18
|
+
hook_event_name: string;
|
|
19
|
+
tool_name: string;
|
|
20
|
+
tool_input: Record<string, unknown>;
|
|
21
|
+
tool_response: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface QualitySignal {
|
|
25
|
+
event_type: string;
|
|
26
|
+
details: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TEST_FAILURE_PATTERNS: RegExp[] = [
|
|
30
|
+
/\bFAIL\b/,
|
|
31
|
+
/✗/,
|
|
32
|
+
/\bfailed\b/i,
|
|
33
|
+
/\bError:/,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const TYPE_ERROR_PATTERNS: RegExp[] = [
|
|
37
|
+
/error TS\d+/,
|
|
38
|
+
/\btsc\b.*error/i,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const BUILD_FAILURE_PATTERNS: RegExp[] = [
|
|
42
|
+
/Build failed/i,
|
|
43
|
+
/\besbuild\b.*error/i,
|
|
44
|
+
/\besbuild\b.*failed/i,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function detectQualitySignals(toolResponse: string): QualitySignal[] {
|
|
48
|
+
const signals: QualitySignal[] = [];
|
|
49
|
+
const response = toolResponse ?? '';
|
|
50
|
+
|
|
51
|
+
for (const pattern of TEST_FAILURE_PATTERNS) {
|
|
52
|
+
if (pattern.test(response)) {
|
|
53
|
+
const match = response.match(pattern);
|
|
54
|
+
signals.push({
|
|
55
|
+
event_type: 'test_failure',
|
|
56
|
+
details: match ? match[0].slice(0, 500) : 'Test failure detected',
|
|
57
|
+
});
|
|
58
|
+
break; // One signal per category
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const pattern of TYPE_ERROR_PATTERNS) {
|
|
63
|
+
if (pattern.test(response)) {
|
|
64
|
+
const match = response.match(pattern);
|
|
65
|
+
signals.push({
|
|
66
|
+
event_type: 'type_error',
|
|
67
|
+
details: match ? match[0].slice(0, 500) : 'TypeScript error detected',
|
|
68
|
+
});
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const pattern of BUILD_FAILURE_PATTERNS) {
|
|
74
|
+
if (pattern.test(response)) {
|
|
75
|
+
const match = response.match(pattern);
|
|
76
|
+
signals.push({
|
|
77
|
+
event_type: 'build_failure',
|
|
78
|
+
details: match ? match[0].slice(0, 500) : 'Build failure detected',
|
|
79
|
+
});
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return signals;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function main(): Promise<void> {
|
|
88
|
+
try {
|
|
89
|
+
const input = await readStdin();
|
|
90
|
+
const hookInput = JSON.parse(input) as HookInput;
|
|
91
|
+
const { session_id, tool_name, tool_response } = hookInput;
|
|
92
|
+
|
|
93
|
+
const signals = detectQualitySignals(tool_response);
|
|
94
|
+
if (signals.length === 0) {
|
|
95
|
+
process.exit(0);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const db = getMemoryDb();
|
|
100
|
+
try {
|
|
101
|
+
const stmt = db.prepare(`
|
|
102
|
+
INSERT INTO quality_events (session_id, event_type, tool_name, details)
|
|
103
|
+
VALUES (?, ?, ?, ?)
|
|
104
|
+
`);
|
|
105
|
+
for (const signal of signals) {
|
|
106
|
+
stmt.run(session_id, signal.event_type, tool_name, signal.details);
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
db.close();
|
|
110
|
+
}
|
|
111
|
+
} catch (_e) {
|
|
112
|
+
// Best-effort: never block Claude Code
|
|
113
|
+
}
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readStdin(): Promise<string> {
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
let data = '';
|
|
120
|
+
process.stdin.setEncoding('utf-8');
|
|
121
|
+
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
122
|
+
process.stdin.on('end', () => resolve(data));
|
|
123
|
+
setTimeout(() => resolve(data), 3000);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
main();
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
3
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
4
|
+
|
|
5
|
+
// ============================================================
|
|
6
|
+
// PreToolUse Hook: Security Gate
|
|
7
|
+
// Validates tool calls against security policies.
|
|
8
|
+
// Checks Bash commands for dangerous patterns and Write/Edit
|
|
9
|
+
// tool calls for protected file paths.
|
|
10
|
+
// Must complete in <500ms.
|
|
11
|
+
// ============================================================
|
|
12
|
+
|
|
13
|
+
// Force module mode for TypeScript (no external deps needed)
|
|
14
|
+
export {};
|
|
15
|
+
|
|
16
|
+
interface HookInput {
|
|
17
|
+
session_id: string;
|
|
18
|
+
tool_name: string;
|
|
19
|
+
tool_input: {
|
|
20
|
+
command?: string;
|
|
21
|
+
file_path?: string;
|
|
22
|
+
content?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DANGEROUS_BASH_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
|
|
27
|
+
{ pattern: /rm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(?:\s|$)/, label: 'rm -rf /' },
|
|
28
|
+
{ pattern: /rm\s+-[a-z]*f[a-z]*r[a-z]*\s+\/(?:\s|$)/, label: 'rm -rf /' },
|
|
29
|
+
{ pattern: /curl\s+.*\|\s*(?:bash|sh|zsh)/, label: 'curl | bash (remote code execution)' },
|
|
30
|
+
{ pattern: /wget\s+.*\|\s*(?:bash|sh|zsh)/, label: 'wget | bash (remote code execution)' },
|
|
31
|
+
{ pattern: /chmod\s+777/, label: 'chmod 777 (world-writable permissions)' },
|
|
32
|
+
{ pattern: /chmod\s+-R\s+777/, label: 'chmod -R 777 (world-writable permissions)' },
|
|
33
|
+
{ pattern: />\s*\/etc\/passwd/, label: 'write to /etc/passwd' },
|
|
34
|
+
{ pattern: />\s*\/etc\/shadow/, label: 'write to /etc/shadow' },
|
|
35
|
+
{ pattern: />\s*\/etc\/sudoers/, label: 'write to /etc/sudoers' },
|
|
36
|
+
{ pattern: /dd\s+if=.*of=\/dev\/(?:sda|sdb|hda|hdb|nvme)/, label: 'dd to raw device' },
|
|
37
|
+
{ pattern: /mkfs\s+\/dev\//, label: 'format disk device' },
|
|
38
|
+
{ pattern: /:\(\)\s*\{\s*:\|:\s*&\s*\}/, label: 'fork bomb' },
|
|
39
|
+
{ pattern: /eval\s+.*\$\(.*curl/, label: 'eval with remote curl' },
|
|
40
|
+
{ pattern: /base64\s+-d\s+.*\|\s*(?:bash|sh|zsh)/, label: 'base64 decoded shell exec' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const PROTECTED_FILE_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
|
|
44
|
+
{ pattern: /\.env$/, label: '.env file' },
|
|
45
|
+
{ pattern: /\.env\./, label: '.env.* file' },
|
|
46
|
+
{ pattern: /credentials(?:\.json)?$/, label: 'credentials file' },
|
|
47
|
+
{ pattern: /\.pem$/, label: '.pem certificate/key file' },
|
|
48
|
+
{ pattern: /\.key$/, label: '.key file' },
|
|
49
|
+
{ pattern: /\.p12$/, label: '.p12 keystore file' },
|
|
50
|
+
{ pattern: /\.pfx$/, label: '.pfx keystore file' },
|
|
51
|
+
{ pattern: /id_rsa$/, label: 'RSA private key' },
|
|
52
|
+
{ pattern: /id_ed25519$/, label: 'Ed25519 private key' },
|
|
53
|
+
{ pattern: /id_ecdsa$/, label: 'ECDSA private key' },
|
|
54
|
+
{ pattern: /\.ssh\/config$/, label: 'SSH config file' },
|
|
55
|
+
{ pattern: /secrets\.yaml$/, label: 'secrets.yaml file' },
|
|
56
|
+
{ pattern: /secrets\.yml$/, label: 'secrets.yml file' },
|
|
57
|
+
{ pattern: /\.netrc$/, label: '.netrc credentials file' },
|
|
58
|
+
{ pattern: /aws\/credentials$/, label: 'AWS credentials file' },
|
|
59
|
+
{ pattern: /kubeconfig$/, label: 'Kubernetes config file' },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
function checkBashCommand(command: string): string | null {
|
|
63
|
+
for (const { pattern, label } of DANGEROUS_BASH_PATTERNS) {
|
|
64
|
+
if (pattern.test(command)) {
|
|
65
|
+
return label;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function checkFilePath(filePath: string): string | null {
|
|
72
|
+
for (const { pattern, label } of PROTECTED_FILE_PATTERNS) {
|
|
73
|
+
if (pattern.test(filePath)) {
|
|
74
|
+
return label;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function main(): Promise<void> {
|
|
81
|
+
try {
|
|
82
|
+
const input = await readStdin();
|
|
83
|
+
const hookInput = JSON.parse(input) as HookInput;
|
|
84
|
+
const { tool_name, tool_input } = hookInput;
|
|
85
|
+
|
|
86
|
+
if (tool_name === 'Bash' && tool_input.command) {
|
|
87
|
+
const violation = checkBashCommand(tool_input.command);
|
|
88
|
+
if (violation) {
|
|
89
|
+
process.stdout.write(JSON.stringify({
|
|
90
|
+
message: `SECURITY GATE: Dangerous command pattern detected: ${violation}\nCommand: ${tool_input.command.slice(0, 200)}\nReview carefully before proceeding.`,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ((tool_name === 'Write' || tool_name === 'Edit') && tool_input.file_path) {
|
|
96
|
+
const violation = checkFilePath(tool_input.file_path);
|
|
97
|
+
if (violation) {
|
|
98
|
+
process.stdout.write(JSON.stringify({
|
|
99
|
+
message: `SECURITY GATE: Attempt to write to protected file: ${violation}\nPath: ${tool_input.file_path}\nEnsure this is intentional and no secrets will be exposed.`,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Hooks must never crash
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readStdin(): Promise<string> {
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
let data = '';
|
|
113
|
+
process.stdin.setEncoding('utf-8');
|
|
114
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
115
|
+
process.stdin.on('end', () => resolve(data));
|
|
116
|
+
// Timeout to prevent hanging
|
|
117
|
+
setTimeout(() => resolve(data), 400);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main();
|