commandmate 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/.env.example +64 -0
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/bin/commandmate.js +7 -0
- package/dist/cli/commands/init.d.ts +11 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +126 -0
- package/dist/cli/commands/start.d.ts +11 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +117 -0
- package/dist/cli/commands/status.d.ts +10 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +55 -0
- package/dist/cli/commands/stop.d.ts +11 -0
- package/dist/cli/commands/stop.d.ts.map +1 -0
- package/dist/cli/commands/stop.js +82 -0
- package/dist/cli/config/cli-dependencies.d.ts +23 -0
- package/dist/cli/config/cli-dependencies.d.ts.map +1 -0
- package/dist/cli/config/cli-dependencies.js +65 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +64 -0
- package/dist/cli/types/index.d.ts +124 -0
- package/dist/cli/types/index.d.ts.map +1 -0
- package/dist/cli/types/index.js +20 -0
- package/dist/cli/utils/daemon.d.ts +39 -0
- package/dist/cli/utils/daemon.d.ts.map +1 -0
- package/dist/cli/utils/daemon.js +141 -0
- package/dist/cli/utils/env-setup.d.ts +62 -0
- package/dist/cli/utils/env-setup.d.ts.map +1 -0
- package/dist/cli/utils/env-setup.js +157 -0
- package/dist/cli/utils/logger.d.ts +48 -0
- package/dist/cli/utils/logger.d.ts.map +1 -0
- package/dist/cli/utils/logger.js +99 -0
- package/dist/cli/utils/pid-manager.d.ts +42 -0
- package/dist/cli/utils/pid-manager.d.ts.map +1 -0
- package/dist/cli/utils/pid-manager.js +111 -0
- package/dist/cli/utils/preflight.d.ts +34 -0
- package/dist/cli/utils/preflight.d.ts.map +1 -0
- package/dist/cli/utils/preflight.js +129 -0
- package/dist/cli/utils/security-logger.d.ts +29 -0
- package/dist/cli/utils/security-logger.d.ts.map +1 -0
- package/dist/cli/utils/security-logger.js +53 -0
- package/package.json +78 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Environment Setup Utility
|
|
4
|
+
* Issue #96: npm install CLI support
|
|
5
|
+
* Migrated from scripts/setup-env.sh
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.EnvSetup = exports.ENV_DEFAULTS = void 0;
|
|
9
|
+
exports.sanitizeInput = sanitizeInput;
|
|
10
|
+
exports.sanitizePath = sanitizePath;
|
|
11
|
+
exports.validatePort = validatePort;
|
|
12
|
+
exports.escapeEnvValue = escapeEnvValue;
|
|
13
|
+
const fs_1 = require("fs");
|
|
14
|
+
const path_1 = require("path");
|
|
15
|
+
const crypto_1 = require("crypto");
|
|
16
|
+
/**
|
|
17
|
+
* Default environment configuration values
|
|
18
|
+
* SF-4: DRY - Centralized defaults
|
|
19
|
+
*/
|
|
20
|
+
exports.ENV_DEFAULTS = {
|
|
21
|
+
CM_PORT: 3000,
|
|
22
|
+
CM_BIND: '127.0.0.1',
|
|
23
|
+
CM_DB_PATH: './data/cm.db',
|
|
24
|
+
CM_LOG_LEVEL: 'info',
|
|
25
|
+
CM_LOG_FORMAT: 'text',
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Sanitize input by removing control characters
|
|
29
|
+
* SF-SEC-3: Input sanitization
|
|
30
|
+
*/
|
|
31
|
+
function sanitizeInput(input) {
|
|
32
|
+
// Remove control characters (0x00-0x1F and 0x7F)
|
|
33
|
+
return input.replace(/[\x00-\x1F\x7F]/g, '');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Sanitize path input
|
|
37
|
+
* SF-SEC-3: Path sanitization
|
|
38
|
+
*/
|
|
39
|
+
function sanitizePath(input) {
|
|
40
|
+
const sanitized = sanitizeInput(input);
|
|
41
|
+
return (0, path_1.normalize)(sanitized);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate port number
|
|
45
|
+
* SF-SEC-3: Port validation
|
|
46
|
+
*/
|
|
47
|
+
function validatePort(input) {
|
|
48
|
+
const port = parseInt(input, 10);
|
|
49
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
50
|
+
throw new Error('Port must be an integer between 1 and 65535');
|
|
51
|
+
}
|
|
52
|
+
return port;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Escape value for .env file
|
|
56
|
+
* SF-SEC-3: Safe .env value escaping
|
|
57
|
+
*/
|
|
58
|
+
function escapeEnvValue(value) {
|
|
59
|
+
// Escape backslashes and double quotes
|
|
60
|
+
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
61
|
+
// Quote if contains special characters
|
|
62
|
+
if (/[\s"'$`!]/.test(value)) {
|
|
63
|
+
return `"${escaped}"`;
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Environment setup utility
|
|
69
|
+
*/
|
|
70
|
+
class EnvSetup {
|
|
71
|
+
envPath;
|
|
72
|
+
constructor(envPath) {
|
|
73
|
+
this.envPath = envPath || (0, path_1.join)(process.cwd(), '.env');
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Create .env file
|
|
77
|
+
* SF-SEC-1: Sets file permissions to 0600
|
|
78
|
+
*/
|
|
79
|
+
async createEnvFile(config, options = {}) {
|
|
80
|
+
if ((0, fs_1.existsSync)(this.envPath) && !options.force) {
|
|
81
|
+
throw new Error('.env file already exists. Use --force to overwrite.');
|
|
82
|
+
}
|
|
83
|
+
// Build .env content
|
|
84
|
+
const lines = [
|
|
85
|
+
'# CommandMate Environment Configuration',
|
|
86
|
+
'# Generated by commandmate init',
|
|
87
|
+
'',
|
|
88
|
+
`CM_ROOT_DIR=${escapeEnvValue(config.CM_ROOT_DIR)}`,
|
|
89
|
+
`CM_PORT=${config.CM_PORT}`,
|
|
90
|
+
`CM_BIND=${config.CM_BIND}`,
|
|
91
|
+
`CM_DB_PATH=${escapeEnvValue(config.CM_DB_PATH)}`,
|
|
92
|
+
`CM_LOG_LEVEL=${config.CM_LOG_LEVEL}`,
|
|
93
|
+
`CM_LOG_FORMAT=${config.CM_LOG_FORMAT}`,
|
|
94
|
+
];
|
|
95
|
+
if (config.CM_AUTH_TOKEN) {
|
|
96
|
+
lines.push(`CM_AUTH_TOKEN=${config.CM_AUTH_TOKEN}`);
|
|
97
|
+
}
|
|
98
|
+
lines.push('');
|
|
99
|
+
const content = lines.join('\n');
|
|
100
|
+
// Write with secure permissions
|
|
101
|
+
(0, fs_1.writeFileSync)(this.envPath, content, { mode: 0o600 });
|
|
102
|
+
// Ensure permissions are set (for existing file updates)
|
|
103
|
+
(0, fs_1.chmodSync)(this.envPath, 0o600);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Backup existing .env file
|
|
107
|
+
*/
|
|
108
|
+
async backupExisting() {
|
|
109
|
+
if (!(0, fs_1.existsSync)(this.envPath)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const timestamp = Date.now();
|
|
113
|
+
const backupPath = `${this.envPath}.backup.${timestamp}`;
|
|
114
|
+
(0, fs_1.copyFileSync)(this.envPath, backupPath);
|
|
115
|
+
return backupPath;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Generate secure authentication token
|
|
119
|
+
*/
|
|
120
|
+
generateAuthToken() {
|
|
121
|
+
return (0, crypto_1.randomBytes)(32).toString('hex');
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Validate configuration
|
|
125
|
+
*/
|
|
126
|
+
validateConfig(config) {
|
|
127
|
+
const errors = [];
|
|
128
|
+
// Validate port
|
|
129
|
+
if (config.CM_PORT < 1 || config.CM_PORT > 65535) {
|
|
130
|
+
errors.push('Invalid port: must be between 1 and 65535');
|
|
131
|
+
}
|
|
132
|
+
// Validate bind address
|
|
133
|
+
const validBinds = ['127.0.0.1', '0.0.0.0', 'localhost'];
|
|
134
|
+
if (!validBinds.includes(config.CM_BIND)) {
|
|
135
|
+
errors.push(`Invalid bind address: must be one of ${validBinds.join(', ')}`);
|
|
136
|
+
}
|
|
137
|
+
// Require auth token for external access
|
|
138
|
+
if (config.CM_BIND === '0.0.0.0' && !config.CM_AUTH_TOKEN) {
|
|
139
|
+
errors.push('auth token is required when binding to 0.0.0.0');
|
|
140
|
+
}
|
|
141
|
+
// Validate log level
|
|
142
|
+
const validLogLevels = ['debug', 'info', 'warn', 'error'];
|
|
143
|
+
if (!validLogLevels.includes(config.CM_LOG_LEVEL)) {
|
|
144
|
+
errors.push(`Invalid log level: must be one of ${validLogLevels.join(', ')}`);
|
|
145
|
+
}
|
|
146
|
+
// Validate log format
|
|
147
|
+
const validLogFormats = ['text', 'json'];
|
|
148
|
+
if (!validLogFormats.includes(config.CM_LOG_FORMAT)) {
|
|
149
|
+
errors.push(`Invalid log format: must be one of ${validLogFormats.join(', ')}`);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
valid: errors.length === 0,
|
|
153
|
+
errors,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.EnvSetup = EnvSetup;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Logger with colored output
|
|
3
|
+
* Issue #96: npm install CLI support
|
|
4
|
+
*/
|
|
5
|
+
export interface LoggerOptions {
|
|
6
|
+
/** Enable verbose/debug output */
|
|
7
|
+
verbose?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* CLI Logger with colored output
|
|
11
|
+
*/
|
|
12
|
+
export declare class CLILogger {
|
|
13
|
+
private verbose;
|
|
14
|
+
constructor(options?: LoggerOptions);
|
|
15
|
+
/**
|
|
16
|
+
* Log info message
|
|
17
|
+
*/
|
|
18
|
+
info(message: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Log success message with checkmark
|
|
21
|
+
*/
|
|
22
|
+
success(message: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Log warning message
|
|
25
|
+
*/
|
|
26
|
+
warn(message: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Log error message to stderr
|
|
29
|
+
*/
|
|
30
|
+
error(message: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Log debug message (only when verbose is enabled)
|
|
33
|
+
*/
|
|
34
|
+
debug(message: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Print blank line
|
|
37
|
+
*/
|
|
38
|
+
blank(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Print formatted header
|
|
41
|
+
*/
|
|
42
|
+
header(title: string): void;
|
|
43
|
+
/**
|
|
44
|
+
* Format duration in human-readable format
|
|
45
|
+
*/
|
|
46
|
+
static formatDuration(seconds: number): string;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/logger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,aAAa;IAC5B,kCAAkC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAoBD;;GAEG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAAU;gBAEb,OAAO,GAAE,aAAkB;IAIvC;;OAEG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B;;OAEG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI9B;;OAEG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5B;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAM5B;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAO3B;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;CAkB/C"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CLI Logger with colored output
|
|
4
|
+
* Issue #96: npm install CLI support
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.CLILogger = void 0;
|
|
8
|
+
/**
|
|
9
|
+
* ANSI color codes for terminal output
|
|
10
|
+
*/
|
|
11
|
+
const colors = {
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
bold: '\x1b[1m',
|
|
14
|
+
dim: '\x1b[2m',
|
|
15
|
+
// Text colors
|
|
16
|
+
red: '\x1b[31m',
|
|
17
|
+
green: '\x1b[32m',
|
|
18
|
+
yellow: '\x1b[33m',
|
|
19
|
+
blue: '\x1b[34m',
|
|
20
|
+
cyan: '\x1b[36m',
|
|
21
|
+
white: '\x1b[37m',
|
|
22
|
+
gray: '\x1b[90m',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* CLI Logger with colored output
|
|
26
|
+
*/
|
|
27
|
+
class CLILogger {
|
|
28
|
+
verbose;
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
this.verbose = options.verbose ?? false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Log info message
|
|
34
|
+
*/
|
|
35
|
+
info(message) {
|
|
36
|
+
console.log(`${colors.blue}[INFO]${colors.reset} ${message}`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Log success message with checkmark
|
|
40
|
+
*/
|
|
41
|
+
success(message) {
|
|
42
|
+
console.log(`${colors.green}[✓]${colors.reset} ${message}`);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Log warning message
|
|
46
|
+
*/
|
|
47
|
+
warn(message) {
|
|
48
|
+
console.log(`${colors.yellow}[WARN]${colors.reset} ${message}`);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Log error message to stderr
|
|
52
|
+
*/
|
|
53
|
+
error(message) {
|
|
54
|
+
console.error(`${colors.red}[ERROR]${colors.reset} ${message}`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Log debug message (only when verbose is enabled)
|
|
58
|
+
*/
|
|
59
|
+
debug(message) {
|
|
60
|
+
if (this.verbose) {
|
|
61
|
+
console.log(`${colors.gray}[DEBUG]${colors.reset} ${message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Print blank line
|
|
66
|
+
*/
|
|
67
|
+
blank() {
|
|
68
|
+
console.log('');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Print formatted header
|
|
72
|
+
*/
|
|
73
|
+
header(title) {
|
|
74
|
+
const line = '='.repeat(title.length + 4);
|
|
75
|
+
console.log(`${colors.bold}${colors.cyan}${line}${colors.reset}`);
|
|
76
|
+
console.log(`${colors.bold}${colors.cyan} ${title} ${colors.reset}`);
|
|
77
|
+
console.log(`${colors.bold}${colors.cyan}${line}${colors.reset}`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Format duration in human-readable format
|
|
81
|
+
*/
|
|
82
|
+
static formatDuration(seconds) {
|
|
83
|
+
if (seconds < 60) {
|
|
84
|
+
return `${seconds}s`;
|
|
85
|
+
}
|
|
86
|
+
const days = Math.floor(seconds / 86400);
|
|
87
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
88
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
89
|
+
const secs = seconds % 60;
|
|
90
|
+
if (days > 0) {
|
|
91
|
+
return `${days}d ${hours}h`;
|
|
92
|
+
}
|
|
93
|
+
if (hours > 0) {
|
|
94
|
+
return `${hours}h ${minutes}m`;
|
|
95
|
+
}
|
|
96
|
+
return `${minutes}m ${secs}s`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.CLILogger = CLILogger;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PID File Manager
|
|
3
|
+
* Issue #96: npm install CLI support
|
|
4
|
+
* SF-1: SRP - Separated from daemon.ts for single responsibility
|
|
5
|
+
* MF-SEC-2: TOCTOU protection with O_EXCL atomic writes
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* PID file manager for daemon process tracking
|
|
9
|
+
*/
|
|
10
|
+
export declare class PidManager {
|
|
11
|
+
private readonly pidFilePath;
|
|
12
|
+
constructor(pidFilePath: string);
|
|
13
|
+
/**
|
|
14
|
+
* Check if PID file exists
|
|
15
|
+
*/
|
|
16
|
+
exists(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Read PID from file
|
|
19
|
+
* @returns PID number or null if file doesn't exist or is invalid
|
|
20
|
+
*/
|
|
21
|
+
readPid(): number | null;
|
|
22
|
+
/**
|
|
23
|
+
* Write PID to file atomically
|
|
24
|
+
* MF-SEC-2: Uses O_EXCL to prevent TOCTOU race conditions
|
|
25
|
+
*
|
|
26
|
+
* @returns true if successful, false if file already exists
|
|
27
|
+
* @throws Error for other filesystem errors
|
|
28
|
+
*/
|
|
29
|
+
writePid(pid: number): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Remove PID file
|
|
32
|
+
*/
|
|
33
|
+
removePid(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Check if the process recorded in PID file is running
|
|
36
|
+
* NTH-1: ISP - Lightweight process check API
|
|
37
|
+
*
|
|
38
|
+
* @returns true if process is running, false otherwise
|
|
39
|
+
*/
|
|
40
|
+
isProcessRunning(): boolean;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=pid-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pid-manager.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/pid-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH;;GAEG;AACH,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAAX,WAAW,EAAE,MAAM;IAEhD;;OAEG;IACH,MAAM,IAAI,OAAO;IAIjB;;;OAGG;IACH,OAAO,IAAI,MAAM,GAAG,IAAI;IAmBxB;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAwB9B;;OAEG;IACH,SAAS,IAAI,IAAI;IAUjB;;;;;OAKG;IACH,gBAAgB,IAAI,OAAO;CAmB5B"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PID File Manager
|
|
4
|
+
* Issue #96: npm install CLI support
|
|
5
|
+
* SF-1: SRP - Separated from daemon.ts for single responsibility
|
|
6
|
+
* MF-SEC-2: TOCTOU protection with O_EXCL atomic writes
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.PidManager = void 0;
|
|
10
|
+
const fs_1 = require("fs");
|
|
11
|
+
/**
|
|
12
|
+
* PID file manager for daemon process tracking
|
|
13
|
+
*/
|
|
14
|
+
class PidManager {
|
|
15
|
+
pidFilePath;
|
|
16
|
+
constructor(pidFilePath) {
|
|
17
|
+
this.pidFilePath = pidFilePath;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if PID file exists
|
|
21
|
+
*/
|
|
22
|
+
exists() {
|
|
23
|
+
return (0, fs_1.existsSync)(this.pidFilePath);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read PID from file
|
|
27
|
+
* @returns PID number or null if file doesn't exist or is invalid
|
|
28
|
+
*/
|
|
29
|
+
readPid() {
|
|
30
|
+
if (!this.exists()) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const content = (0, fs_1.readFileSync)(this.pidFilePath, 'utf-8').trim();
|
|
35
|
+
const pid = parseInt(content, 10);
|
|
36
|
+
if (isNaN(pid) || pid <= 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return pid;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Write PID to file atomically
|
|
47
|
+
* MF-SEC-2: Uses O_EXCL to prevent TOCTOU race conditions
|
|
48
|
+
*
|
|
49
|
+
* @returns true if successful, false if file already exists
|
|
50
|
+
* @throws Error for other filesystem errors
|
|
51
|
+
*/
|
|
52
|
+
writePid(pid) {
|
|
53
|
+
try {
|
|
54
|
+
// O_EXCL: Fail if file already exists (atomic check-and-create)
|
|
55
|
+
const fd = (0, fs_1.openSync)(this.pidFilePath, fs_1.constants.O_WRONLY | fs_1.constants.O_CREAT | fs_1.constants.O_EXCL, 0o600);
|
|
56
|
+
try {
|
|
57
|
+
(0, fs_1.writeSync)(fd, String(pid));
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
(0, fs_1.closeSync)(fd);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (err.code === 'EEXIST') {
|
|
66
|
+
// File already exists - another process is likely running
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Remove PID file
|
|
74
|
+
*/
|
|
75
|
+
removePid() {
|
|
76
|
+
if (this.exists()) {
|
|
77
|
+
try {
|
|
78
|
+
(0, fs_1.unlinkSync)(this.pidFilePath);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Ignore errors during cleanup
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check if the process recorded in PID file is running
|
|
87
|
+
* NTH-1: ISP - Lightweight process check API
|
|
88
|
+
*
|
|
89
|
+
* @returns true if process is running, false otherwise
|
|
90
|
+
*/
|
|
91
|
+
isProcessRunning() {
|
|
92
|
+
const pid = this.readPid();
|
|
93
|
+
if (pid === null) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
// Sending signal 0 checks if process exists without killing it
|
|
98
|
+
process.kill(pid, 0);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (err.code === 'ESRCH') {
|
|
103
|
+
// Process not found
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// Other errors (e.g., EPERM) mean process exists but we can't signal it
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.PidManager = PidManager;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preflight System Dependency Checker
|
|
3
|
+
* Issue #96: npm install CLI support
|
|
4
|
+
* Migrated from scripts/preflight-check.sh
|
|
5
|
+
*/
|
|
6
|
+
import { DependencyCheck, DependencyStatus, PreflightResult } from '../types';
|
|
7
|
+
/**
|
|
8
|
+
* Preflight checker for system dependencies
|
|
9
|
+
*/
|
|
10
|
+
export declare class PreflightChecker {
|
|
11
|
+
/**
|
|
12
|
+
* Check a single dependency
|
|
13
|
+
* MF-SEC-1: Uses spawnSync with array args (no shell injection)
|
|
14
|
+
*/
|
|
15
|
+
checkDependency(dep: DependencyCheck): Promise<DependencyStatus>;
|
|
16
|
+
/**
|
|
17
|
+
* Check all dependencies
|
|
18
|
+
*/
|
|
19
|
+
checkAll(): Promise<PreflightResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Extract version number from command output
|
|
22
|
+
*/
|
|
23
|
+
private extractVersion;
|
|
24
|
+
/**
|
|
25
|
+
* Compare two version strings
|
|
26
|
+
* @returns -1 if a < b, 0 if a == b, 1 if a > b
|
|
27
|
+
*/
|
|
28
|
+
private compareVersions;
|
|
29
|
+
/**
|
|
30
|
+
* Get installation hint for a dependency
|
|
31
|
+
*/
|
|
32
|
+
static getInstallHint(name: string): string;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=preflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/preflight.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,eAAe,EAChB,MAAM,UAAU,CAAC;AAGlB;;GAEG;AACH,qBAAa,gBAAgB;IAC3B;;;OAGG;IACG,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA6CtE;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,eAAe,CAAC;IAoB1C;;OAEG;IACH,OAAO,CAAC,cAAc;IAmBtB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAevB;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAW5C"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Preflight System Dependency Checker
|
|
4
|
+
* Issue #96: npm install CLI support
|
|
5
|
+
* Migrated from scripts/preflight-check.sh
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.PreflightChecker = void 0;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const cli_dependencies_1 = require("../config/cli-dependencies");
|
|
11
|
+
/**
|
|
12
|
+
* Preflight checker for system dependencies
|
|
13
|
+
*/
|
|
14
|
+
class PreflightChecker {
|
|
15
|
+
/**
|
|
16
|
+
* Check a single dependency
|
|
17
|
+
* MF-SEC-1: Uses spawnSync with array args (no shell injection)
|
|
18
|
+
*/
|
|
19
|
+
async checkDependency(dep) {
|
|
20
|
+
try {
|
|
21
|
+
const result = (0, child_process_1.spawnSync)(dep.command, [dep.versionArg], {
|
|
22
|
+
encoding: 'utf-8',
|
|
23
|
+
timeout: 5000,
|
|
24
|
+
});
|
|
25
|
+
if (result.error) {
|
|
26
|
+
const errnoError = result.error;
|
|
27
|
+
if (errnoError.code === 'ENOENT') {
|
|
28
|
+
return { name: dep.name, status: 'missing' };
|
|
29
|
+
}
|
|
30
|
+
throw result.error;
|
|
31
|
+
}
|
|
32
|
+
if (result.status !== 0) {
|
|
33
|
+
return { name: dep.name, status: 'missing' };
|
|
34
|
+
}
|
|
35
|
+
// Extract version from output (encoding: 'utf-8' ensures strings)
|
|
36
|
+
const output = ((result.stdout || result.stderr) || '').trim();
|
|
37
|
+
const version = this.extractVersion(output) || output;
|
|
38
|
+
// Check minimum version if specified
|
|
39
|
+
if (dep.minVersion && version) {
|
|
40
|
+
const cleanVersion = version.replace(/^v/, '');
|
|
41
|
+
if (this.compareVersions(cleanVersion, dep.minVersion) < 0) {
|
|
42
|
+
return {
|
|
43
|
+
name: dep.name,
|
|
44
|
+
status: 'version_mismatch',
|
|
45
|
+
version,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
name: dep.name,
|
|
51
|
+
status: 'ok',
|
|
52
|
+
version,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { name: dep.name, status: 'missing' };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check all dependencies
|
|
61
|
+
*/
|
|
62
|
+
async checkAll() {
|
|
63
|
+
const dependencies = (0, cli_dependencies_1.getDependencies)();
|
|
64
|
+
const results = [];
|
|
65
|
+
let allRequiredMet = true;
|
|
66
|
+
for (const dep of dependencies) {
|
|
67
|
+
const status = await this.checkDependency(dep);
|
|
68
|
+
results.push(status);
|
|
69
|
+
if (dep.required && status.status !== 'ok') {
|
|
70
|
+
allRequiredMet = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
success: allRequiredMet,
|
|
75
|
+
results,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Extract version number from command output
|
|
80
|
+
*/
|
|
81
|
+
extractVersion(output) {
|
|
82
|
+
// Match various version formats:
|
|
83
|
+
// v20.0.0, 2.39.0, "git version 2.39.0", etc.
|
|
84
|
+
const patterns = [
|
|
85
|
+
/v?(\d+\.\d+\.\d+)/,
|
|
86
|
+
/version\s+(\d+\.\d+\.\d+)/i,
|
|
87
|
+
/(\d+\.\d+\.\d+)/,
|
|
88
|
+
];
|
|
89
|
+
for (const pattern of patterns) {
|
|
90
|
+
const match = output.match(pattern);
|
|
91
|
+
if (match) {
|
|
92
|
+
return match[1];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Compare two version strings
|
|
99
|
+
* @returns -1 if a < b, 0 if a == b, 1 if a > b
|
|
100
|
+
*/
|
|
101
|
+
compareVersions(a, b) {
|
|
102
|
+
const partsA = a.split('.').map(Number);
|
|
103
|
+
const partsB = b.split('.').map(Number);
|
|
104
|
+
const len = Math.max(partsA.length, partsB.length);
|
|
105
|
+
for (let i = 0; i < len; i++) {
|
|
106
|
+
const numA = partsA[i] || 0;
|
|
107
|
+
const numB = partsB[i] || 0;
|
|
108
|
+
if (numA > numB)
|
|
109
|
+
return 1;
|
|
110
|
+
if (numA < numB)
|
|
111
|
+
return -1;
|
|
112
|
+
}
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get installation hint for a dependency
|
|
117
|
+
*/
|
|
118
|
+
static getInstallHint(name) {
|
|
119
|
+
const hints = {
|
|
120
|
+
'Node.js': 'Install with: nvm install 20 or visit https://nodejs.org',
|
|
121
|
+
npm: 'npm is included with Node.js. Install Node.js first.',
|
|
122
|
+
tmux: 'Install with: brew install tmux (macOS) or apt install tmux (Linux)',
|
|
123
|
+
git: 'Install with: brew install git (macOS) or apt install git (Linux)',
|
|
124
|
+
'Claude CLI': 'Install with: npm install -g @anthropic-ai/claude-cli',
|
|
125
|
+
};
|
|
126
|
+
return hints[name] || `Please install ${name}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
exports.PreflightChecker = PreflightChecker;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Event Logger
|
|
3
|
+
* Issue #96: npm install CLI support
|
|
4
|
+
* SF-SEC-2: Security event logging for CLI commands
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Security event types
|
|
8
|
+
*/
|
|
9
|
+
export interface SecurityEvent {
|
|
10
|
+
/** ISO timestamp */
|
|
11
|
+
timestamp: string;
|
|
12
|
+
/** Command that triggered the event */
|
|
13
|
+
command: string;
|
|
14
|
+
/** Event outcome */
|
|
15
|
+
action: 'success' | 'failure' | 'warning';
|
|
16
|
+
/** Additional details */
|
|
17
|
+
details?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Log a security event
|
|
21
|
+
* Silently fails if file operations fail (non-critical)
|
|
22
|
+
*/
|
|
23
|
+
export declare function logSecurityEvent(event: SecurityEvent): void;
|
|
24
|
+
/**
|
|
25
|
+
* Mask sensitive data in log messages
|
|
26
|
+
* SF-SEC-2: Mask authentication tokens in logs
|
|
27
|
+
*/
|
|
28
|
+
export declare function maskSensitiveData(input: string | undefined): string | undefined;
|
|
29
|
+
//# sourceMappingURL=security-logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-logger.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/security-logger.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,oBAAoB;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB;IACpB,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IAC1C,yBAAyB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAaD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAU3D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAc/E"}
|