marmot-logger 1.0.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 +172 -0
- package/bin/marmot.js +71 -0
- package/package.json +34 -0
- package/src/cli/disable.js +40 -0
- package/src/cli/enable.js +40 -0
- package/src/cli/init.js +85 -0
- package/src/cli/log.js +102 -0
- package/src/cli/login.js +120 -0
- package/src/cli/logs.js +93 -0
- package/src/cli/monitor.js +32 -0
- package/src/cli/status.js +72 -0
- package/src/cli/verify.js +81 -0
- package/src/core/config.js +190 -0
- package/src/core/gitignore.js +109 -0
- package/src/core/logger.js +120 -0
- package/src/core/signer.js +136 -0
- package/src/index.js +20 -0
- package/src/plugins/claude-hooks.js +134 -0
- package/src/plugins/file-monitor.js +233 -0
- package/src/plugins/git-hooks.js +177 -0
- package/src/plugins/index.js +7 -0
- package/src/plugins/makefile.js +68 -0
- package/src/plugins/terminal.js +136 -0
package/src/cli/logs.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const config = require('../core/config');
|
|
5
|
+
const logger = require('../core/logger');
|
|
6
|
+
|
|
7
|
+
module.exports = async function logs(options) {
|
|
8
|
+
const projectConfig = config.loadConfig();
|
|
9
|
+
|
|
10
|
+
if (!projectConfig) {
|
|
11
|
+
console.log(chalk.red('Marmot not initialized. Run: marmot init'));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let logFile;
|
|
16
|
+
|
|
17
|
+
if (options.today) {
|
|
18
|
+
logFile = config.getLogFile(projectConfig);
|
|
19
|
+
} else {
|
|
20
|
+
// Find the most recent log file
|
|
21
|
+
const logDir = config.getLogDir(projectConfig);
|
|
22
|
+
if (!fs.existsSync(logDir)) {
|
|
23
|
+
console.log(chalk.yellow('No logs found.'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const files = fs.readdirSync(logDir)
|
|
28
|
+
.filter(f => f.startsWith('file_events_') && f.endsWith('.log'))
|
|
29
|
+
.sort()
|
|
30
|
+
.reverse();
|
|
31
|
+
|
|
32
|
+
if (files.length === 0) {
|
|
33
|
+
console.log(chalk.yellow('No logs found.'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logFile = path.join(logDir, files[0]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(logFile)) {
|
|
41
|
+
console.log(chalk.yellow('No logs found.'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const last = parseInt(options.last) || 10;
|
|
46
|
+
const entries = logger.readLogs(logFile, { last });
|
|
47
|
+
|
|
48
|
+
console.log(`Showing last ${entries.length} entries from: ${chalk.cyan(logFile)}`);
|
|
49
|
+
console.log('');
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const time = entry.timestamp ? entry.timestamp.split('T')[1].replace('Z', '') : '??:??:??';
|
|
53
|
+
const event = entry.event || 'unknown';
|
|
54
|
+
const eventPath = entry.path || '';
|
|
55
|
+
|
|
56
|
+
let signedStatus = '';
|
|
57
|
+
if (entry.signed === true) {
|
|
58
|
+
signedStatus = chalk.green(' ✓');
|
|
59
|
+
} else if (entry.signed === false) {
|
|
60
|
+
signedStatus = chalk.red(' ✗');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Color code by event type
|
|
64
|
+
let eventColor = chalk.white;
|
|
65
|
+
if (event.startsWith('claude_hook_')) {
|
|
66
|
+
eventColor = chalk.magenta;
|
|
67
|
+
} else if (event.startsWith('git_')) {
|
|
68
|
+
eventColor = chalk.blue;
|
|
69
|
+
} else if (event === 'terminal') {
|
|
70
|
+
eventColor = chalk.yellow;
|
|
71
|
+
} else if (event === 'created') {
|
|
72
|
+
eventColor = chalk.green;
|
|
73
|
+
} else if (event === 'deleted') {
|
|
74
|
+
eventColor = chalk.red;
|
|
75
|
+
} else if (event === 'modified') {
|
|
76
|
+
eventColor = chalk.cyan;
|
|
77
|
+
} else if (event === 'make_command') {
|
|
78
|
+
eventColor = chalk.gray;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let details = eventPath;
|
|
82
|
+
if (entry.command) {
|
|
83
|
+
details = entry.command;
|
|
84
|
+
}
|
|
85
|
+
if (entry.additions !== undefined && entry.deletions !== undefined) {
|
|
86
|
+
details += ` (+${entry.additions}/-${entry.deletions})`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(`${chalk.gray(time)} ${eventColor(event.padEnd(25))} ${details}${signedStatus}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log('');
|
|
93
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const config = require('../core/config');
|
|
3
|
+
const fileMonitor = require('../plugins/file-monitor');
|
|
4
|
+
|
|
5
|
+
module.exports = async function monitor() {
|
|
6
|
+
const projectConfig = config.loadConfig();
|
|
7
|
+
|
|
8
|
+
if (!projectConfig) {
|
|
9
|
+
console.log(chalk.red('Marmot not initialized. Run: marmot init'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!config.isPluginEnabled(projectConfig, 'file-monitor')) {
|
|
14
|
+
console.log(chalk.yellow('File monitor plugin is not enabled.'));
|
|
15
|
+
console.log('Run:', chalk.cyan('marmot enable file-monitor'));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const changes = await fileMonitor.run(projectConfig);
|
|
21
|
+
|
|
22
|
+
if (changes === 0) {
|
|
23
|
+
// No changes, silent exit for cron usage
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(chalk.green(`✓ Detected ${changes} change(s)`));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error(chalk.red(`Monitor error: ${err.message}`));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const config = require('../core/config');
|
|
4
|
+
const signer = require('../core/signer');
|
|
5
|
+
|
|
6
|
+
module.exports = async function status() {
|
|
7
|
+
const projectDir = process.cwd();
|
|
8
|
+
const projectConfig = config.loadConfig(projectDir);
|
|
9
|
+
|
|
10
|
+
if (!projectConfig) {
|
|
11
|
+
console.log(chalk.red('Marmot not initialized.'));
|
|
12
|
+
console.log('Run:', chalk.cyan('marmot init'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log(chalk.bold('Marmot Status'));
|
|
17
|
+
console.log('─'.repeat(40));
|
|
18
|
+
|
|
19
|
+
// Configuration
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(chalk.bold('Configuration:'));
|
|
22
|
+
console.log(` Marmot dir: ${chalk.cyan(config.getMarmotDir(projectDir))}`);
|
|
23
|
+
console.log(` Config file: ${chalk.cyan(config.getConfigPath(projectDir))}`);
|
|
24
|
+
console.log(` Snapshot dir: ${chalk.cyan(config.getSnapshotDir(projectConfig, projectDir))}`);
|
|
25
|
+
console.log(` Log directory: ${chalk.cyan(config.getLogDir(projectConfig, projectDir))}`);
|
|
26
|
+
console.log(` Signing URL: ${chalk.cyan(config.getSigningUrl(projectDir))}`);
|
|
27
|
+
|
|
28
|
+
// API Key status
|
|
29
|
+
const apiKey = config.getApiKey(projectDir);
|
|
30
|
+
if (apiKey) {
|
|
31
|
+
console.log(` API Key: ${chalk.green('✓ Set')} (from .env or environment)`);
|
|
32
|
+
} else {
|
|
33
|
+
console.log(` API Key: ${chalk.red('✗ Not set')} (run: marmot login)`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Signing service health
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(chalk.bold('Signing Service:'));
|
|
39
|
+
try {
|
|
40
|
+
await signer.healthCheck();
|
|
41
|
+
console.log(` Status: ${chalk.green('✓ Healthy')}`);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.log(` Status: ${chalk.red('✗ Unreachable')} - ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Plugins
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log(chalk.bold('Plugins:'));
|
|
49
|
+
const plugins = ['file-monitor', 'terminal', 'git-hooks', 'makefile', 'claude-hooks'];
|
|
50
|
+
|
|
51
|
+
for (const plugin of plugins) {
|
|
52
|
+
const enabled = config.isPluginEnabled(projectConfig, plugin);
|
|
53
|
+
const statusIcon = enabled ? chalk.green('✓ enabled') : chalk.gray('○ disabled');
|
|
54
|
+
console.log(` ${plugin.padEnd(15)} ${statusIcon}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Recent logs
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(chalk.bold('Recent Activity:'));
|
|
60
|
+
const logFile = config.getLogFile(projectConfig, projectDir);
|
|
61
|
+
if (fs.existsSync(logFile)) {
|
|
62
|
+
const stats = fs.statSync(logFile);
|
|
63
|
+
const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n').length;
|
|
64
|
+
console.log(` Today's log: ${chalk.cyan(logFile)}`);
|
|
65
|
+
console.log(` Entries: ${lines}`);
|
|
66
|
+
console.log(` Size: ${(stats.size / 1024).toFixed(1)} KB`);
|
|
67
|
+
} else {
|
|
68
|
+
console.log(` ${chalk.gray('No logs yet today')}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log('');
|
|
72
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const config = require('../core/config');
|
|
4
|
+
const logger = require('../core/logger');
|
|
5
|
+
|
|
6
|
+
module.exports = async function verify(options) {
|
|
7
|
+
const projectConfig = config.loadConfig();
|
|
8
|
+
|
|
9
|
+
if (!projectConfig) {
|
|
10
|
+
console.log(chalk.red('Marmot not initialized. Run: marmot init'));
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const logFile = options.file || config.getLogFile(projectConfig);
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(logFile)) {
|
|
17
|
+
console.log(chalk.red(`Log file not found: ${logFile}`));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log(`Verifying: ${chalk.cyan(logFile)}`);
|
|
22
|
+
console.log('');
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const results = await logger.verifyLogs(logFile);
|
|
26
|
+
|
|
27
|
+
let valid = 0;
|
|
28
|
+
let invalid = 0;
|
|
29
|
+
let unsigned = 0;
|
|
30
|
+
|
|
31
|
+
for (const result of results) {
|
|
32
|
+
if (result.verified === true) {
|
|
33
|
+
valid++;
|
|
34
|
+
} else if (result.verified === false) {
|
|
35
|
+
invalid++;
|
|
36
|
+
} else {
|
|
37
|
+
unsigned++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(chalk.bold('Results:'));
|
|
42
|
+
console.log(` ${chalk.green('✓ Valid:')} ${valid}`);
|
|
43
|
+
console.log(` ${chalk.red('✗ Invalid:')} ${invalid}`);
|
|
44
|
+
console.log(` ${chalk.gray('○ Unsigned:')} ${unsigned}`);
|
|
45
|
+
console.log(` Total: ${results.length}`);
|
|
46
|
+
|
|
47
|
+
if (invalid > 0) {
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(chalk.yellow('Invalid entries:'));
|
|
50
|
+
for (const result of results) {
|
|
51
|
+
if (result.verified === false) {
|
|
52
|
+
console.log(` - ${result.entry.timestamp} ${result.entry.event}: ${result.reason}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Update log file with verification results
|
|
58
|
+
if (invalid > 0) {
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log('Updating log file with verification status...');
|
|
61
|
+
|
|
62
|
+
const entries = logger.readLogs(logFile);
|
|
63
|
+
const updatedEntries = entries.map((entry, i) => {
|
|
64
|
+
const result = results[i];
|
|
65
|
+
if (result.verified === true) {
|
|
66
|
+
return { ...entry, signed: true };
|
|
67
|
+
} else if (result.verified === false) {
|
|
68
|
+
return { ...entry, signed: false };
|
|
69
|
+
}
|
|
70
|
+
return entry;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
fs.writeFileSync(logFile, updatedEntries.map(e => JSON.stringify(e)).join('\n') + '\n');
|
|
74
|
+
console.log(chalk.green('✓ Log file updated.'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.log(chalk.red(`Verification failed: ${err.message}`));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const CONFIG_FILE = '.marmotrc.json';
|
|
6
|
+
const MARMOT_TMP_BASE = '/tmp/marmot';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
logDir: './marmot-logs',
|
|
10
|
+
bearerToken: 'broken-bearer',
|
|
11
|
+
plugins: {
|
|
12
|
+
'file-monitor': {
|
|
13
|
+
enabled: false
|
|
14
|
+
},
|
|
15
|
+
'terminal': {
|
|
16
|
+
enabled: false
|
|
17
|
+
},
|
|
18
|
+
'git-hooks': {
|
|
19
|
+
enabled: false,
|
|
20
|
+
events: ['commit', 'push', 'checkout', 'merge']
|
|
21
|
+
},
|
|
22
|
+
'makefile': {
|
|
23
|
+
enabled: false
|
|
24
|
+
},
|
|
25
|
+
'claude-hooks': {
|
|
26
|
+
enabled: false,
|
|
27
|
+
events: ['PreToolUse', 'PostToolUse', 'Stop', 'SubagentStop', 'UserPromptSubmit', 'SessionStart', 'SessionEnd']
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function getProjectHash(projectDir) {
|
|
33
|
+
return crypto.createHash('md5').update(path.resolve(projectDir)).digest('hex');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getMarmotDir(projectDir = process.cwd()) {
|
|
37
|
+
const hash = getProjectHash(projectDir);
|
|
38
|
+
return path.join(MARMOT_TMP_BASE, hash);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getConfigPath(projectDir = process.cwd()) {
|
|
42
|
+
return path.join(getMarmotDir(projectDir), CONFIG_FILE);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadConfig(projectDir = process.cwd()) {
|
|
46
|
+
const configPath = getConfigPath(projectDir);
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(configPath)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
54
|
+
return JSON.parse(content);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
throw new Error(`Failed to parse ${CONFIG_FILE}: ${err.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function saveConfig(config, projectDir = process.cwd()) {
|
|
61
|
+
const configPath = getConfigPath(projectDir);
|
|
62
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createDefaultConfig(projectDir = process.cwd()) {
|
|
66
|
+
const config = { ...DEFAULT_CONFIG };
|
|
67
|
+
saveConfig(config, projectDir);
|
|
68
|
+
return config;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function loadEnvFile(projectDir = process.cwd()) {
|
|
72
|
+
const envPath = path.join(projectDir, '.env');
|
|
73
|
+
const env = {};
|
|
74
|
+
|
|
75
|
+
if (fs.existsSync(envPath)) {
|
|
76
|
+
try {
|
|
77
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
78
|
+
for (const line of content.split('\n')) {
|
|
79
|
+
const trimmed = line.trim();
|
|
80
|
+
// Skip comments and empty lines
|
|
81
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
82
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
83
|
+
if (match) {
|
|
84
|
+
const key = match[1].trim();
|
|
85
|
+
let value = match[2].trim();
|
|
86
|
+
// Remove quotes if present
|
|
87
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
88
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
89
|
+
value = value.slice(1, -1);
|
|
90
|
+
}
|
|
91
|
+
env[key] = value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
// Ignore .env read errors
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return env;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getSigningUrl(projectDir = process.cwd()) {
|
|
103
|
+
// Priority: env var > .env file > default
|
|
104
|
+
if (process.env.MARMOT_URL) {
|
|
105
|
+
return process.env.MARMOT_URL;
|
|
106
|
+
}
|
|
107
|
+
const envFile = loadEnvFile(projectDir);
|
|
108
|
+
return envFile.MARMOT_URL || 'https://logging.drazou.net';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getApiKey(projectDir = process.cwd()) {
|
|
112
|
+
// Priority: env var > .env file
|
|
113
|
+
if (process.env.MARMOT_API_KEY) {
|
|
114
|
+
return process.env.MARMOT_API_KEY;
|
|
115
|
+
}
|
|
116
|
+
const envFile = loadEnvFile(projectDir);
|
|
117
|
+
return envFile.MARMOT_API_KEY;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getLogDir(config, projectDir = process.cwd()) {
|
|
121
|
+
return path.resolve(projectDir, config.logDir || './logs');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getSnapshotDir(config, projectDir = process.cwd()) {
|
|
125
|
+
return path.join(getMarmotDir(projectDir), 'snapshot');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getLogFile(config, projectDir = process.cwd()) {
|
|
129
|
+
const logDir = getLogDir(config, projectDir);
|
|
130
|
+
const date = new Date().toISOString().split('T')[0];
|
|
131
|
+
return path.join(logDir, `file_events_${date}.log`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isPluginEnabled(config, pluginName) {
|
|
135
|
+
return config.plugins?.[pluginName]?.enabled || false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function enablePlugin(config, pluginName) {
|
|
139
|
+
if (!config.plugins) {
|
|
140
|
+
config.plugins = {};
|
|
141
|
+
}
|
|
142
|
+
if (!config.plugins[pluginName]) {
|
|
143
|
+
config.plugins[pluginName] = { ...DEFAULT_CONFIG.plugins[pluginName] };
|
|
144
|
+
}
|
|
145
|
+
config.plugins[pluginName].enabled = true;
|
|
146
|
+
return config;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function disablePlugin(config, pluginName) {
|
|
150
|
+
if (config.plugins?.[pluginName]) {
|
|
151
|
+
config.plugins[pluginName].enabled = false;
|
|
152
|
+
}
|
|
153
|
+
return config;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getBearerToken(projectDir = process.cwd()) {
|
|
157
|
+
const projectConfig = loadConfig(projectDir);
|
|
158
|
+
return projectConfig?.bearerToken || 'broken-bearer';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function setBearerToken(token, projectDir = process.cwd()) {
|
|
162
|
+
const projectConfig = loadConfig(projectDir);
|
|
163
|
+
if (projectConfig) {
|
|
164
|
+
projectConfig.bearerToken = token;
|
|
165
|
+
saveConfig(projectConfig, projectDir);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
CONFIG_FILE,
|
|
171
|
+
DEFAULT_CONFIG,
|
|
172
|
+
MARMOT_TMP_BASE,
|
|
173
|
+
getProjectHash,
|
|
174
|
+
getMarmotDir,
|
|
175
|
+
loadConfig,
|
|
176
|
+
saveConfig,
|
|
177
|
+
createDefaultConfig,
|
|
178
|
+
getConfigPath,
|
|
179
|
+
getSigningUrl,
|
|
180
|
+
getApiKey,
|
|
181
|
+
loadEnvFile,
|
|
182
|
+
getLogDir,
|
|
183
|
+
getSnapshotDir,
|
|
184
|
+
getLogFile,
|
|
185
|
+
isPluginEnabled,
|
|
186
|
+
enablePlugin,
|
|
187
|
+
disablePlugin,
|
|
188
|
+
getBearerToken,
|
|
189
|
+
setBearerToken
|
|
190
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse .gitignore file and return patterns
|
|
6
|
+
* Skips comments, empty lines, and negation patterns (!)
|
|
7
|
+
*/
|
|
8
|
+
function parseGitignore(projectDir) {
|
|
9
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
17
|
+
return content
|
|
18
|
+
.split('\n')
|
|
19
|
+
.map(line => line.trim())
|
|
20
|
+
.filter(line => line && !line.startsWith('#') && !line.startsWith('!'));
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get default exclusions (always exclude regardless of .gitignore)
|
|
28
|
+
*/
|
|
29
|
+
function getDefaultExclusions() {
|
|
30
|
+
return [
|
|
31
|
+
'.git',
|
|
32
|
+
'logs/'
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build rsync exclude arguments from .gitignore + defaults
|
|
38
|
+
*/
|
|
39
|
+
function buildRsyncExcludes(projectDir) {
|
|
40
|
+
const defaults = getDefaultExclusions();
|
|
41
|
+
const gitignorePatterns = parseGitignore(projectDir);
|
|
42
|
+
|
|
43
|
+
// Combine: defaults first, then gitignore patterns
|
|
44
|
+
const allPatterns = [...defaults, ...gitignorePatterns];
|
|
45
|
+
|
|
46
|
+
return allPatterns.map(pattern => `--exclude=${pattern}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if a file path should be excluded based on gitignore patterns
|
|
51
|
+
* Used for filtering git diff output
|
|
52
|
+
*/
|
|
53
|
+
function shouldExclude(filePath, projectDir) {
|
|
54
|
+
const defaults = getDefaultExclusions();
|
|
55
|
+
const gitignorePatterns = parseGitignore(projectDir);
|
|
56
|
+
const allPatterns = [...defaults, ...gitignorePatterns];
|
|
57
|
+
|
|
58
|
+
// Normalize path separators
|
|
59
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
60
|
+
|
|
61
|
+
for (const pattern of allPatterns) {
|
|
62
|
+
if (matchPattern(normalizedPath, pattern)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Simple pattern matching for gitignore-style patterns
|
|
71
|
+
* Supports: wildcards (*), directory markers (/), character classes ([])
|
|
72
|
+
*/
|
|
73
|
+
function matchPattern(filePath, pattern) {
|
|
74
|
+
// Handle trailing slash (directory pattern)
|
|
75
|
+
const isDir = pattern.endsWith('/');
|
|
76
|
+
let cleanPattern = isDir ? pattern.slice(0, -1) : pattern;
|
|
77
|
+
|
|
78
|
+
// Check if pattern has path separators (anchored pattern)
|
|
79
|
+
const isAnchored = cleanPattern.includes('/');
|
|
80
|
+
|
|
81
|
+
// Convert gitignore pattern to regex
|
|
82
|
+
let regex = cleanPattern
|
|
83
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except * and ?
|
|
84
|
+
.replace(/\*\*/g, '<<<GLOBSTAR>>>') // Preserve **
|
|
85
|
+
.replace(/\*/g, '[^/]*') // * matches anything except /
|
|
86
|
+
.replace(/<<<GLOBSTAR>>>/g, '.*') // ** matches anything including /
|
|
87
|
+
.replace(/\?/g, '[^/]'); // ? matches single char except /
|
|
88
|
+
|
|
89
|
+
// Patterns without / match anywhere in path
|
|
90
|
+
if (!isAnchored) {
|
|
91
|
+
regex = `(^|/)${regex}($|/)`;
|
|
92
|
+
} else {
|
|
93
|
+
regex = `^${regex}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
return new RegExp(regex).test(filePath);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
parseGitignore,
|
|
105
|
+
getDefaultExclusions,
|
|
106
|
+
buildRsyncExcludes,
|
|
107
|
+
shouldExclude,
|
|
108
|
+
matchPattern
|
|
109
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const config = require('./config');
|
|
4
|
+
const signer = require('./signer');
|
|
5
|
+
|
|
6
|
+
async function log(entry, projectConfig, projectDir = process.cwd()) {
|
|
7
|
+
const logFile = config.getLogFile(projectConfig, projectDir);
|
|
8
|
+
const logDir = path.dirname(logFile);
|
|
9
|
+
|
|
10
|
+
// Ensure log directory exists
|
|
11
|
+
if (!fs.existsSync(logDir)) {
|
|
12
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let signedEntry;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Try to sign the entry with cached token
|
|
19
|
+
signedEntry = await signer.sign(entry, projectDir);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
// If unauthorized, refresh token and retry once
|
|
22
|
+
if (err.statusCode === 401) {
|
|
23
|
+
try {
|
|
24
|
+
await signer.refreshToken(projectDir);
|
|
25
|
+
signedEntry = await signer.sign(entry, projectDir);
|
|
26
|
+
} catch (retryErr) {
|
|
27
|
+
// Second attempt failed, fall back to unsigned
|
|
28
|
+
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
29
|
+
signedEntry = {
|
|
30
|
+
timestamp,
|
|
31
|
+
...entry,
|
|
32
|
+
signed: false
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
// Other error, fall back to unsigned
|
|
37
|
+
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
38
|
+
signedEntry = {
|
|
39
|
+
timestamp,
|
|
40
|
+
...entry,
|
|
41
|
+
signed: false
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Append to log file
|
|
47
|
+
fs.appendFileSync(logFile, JSON.stringify(signedEntry) + '\n');
|
|
48
|
+
|
|
49
|
+
return signedEntry;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function logEvent(eventType, eventPath, size = 0, extra = {}, projectDir = process.cwd()) {
|
|
53
|
+
const projectConfig = config.loadConfig(projectDir);
|
|
54
|
+
if (!projectConfig) {
|
|
55
|
+
throw new Error('Marmot not initialized. Run: marmot init');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const entry = {
|
|
59
|
+
event: eventType,
|
|
60
|
+
path: eventPath,
|
|
61
|
+
size,
|
|
62
|
+
...extra
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return log(entry, projectConfig, projectDir);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readLogs(logFile, options = {}) {
|
|
69
|
+
if (!fs.existsSync(logFile)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const content = fs.readFileSync(logFile, 'utf8');
|
|
74
|
+
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
75
|
+
|
|
76
|
+
let entries = lines.map(line => {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(line);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}).filter(e => e !== null);
|
|
83
|
+
|
|
84
|
+
if (options.last) {
|
|
85
|
+
entries = entries.slice(-options.last);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return entries;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function verifyLogs(logFile, projectDir = process.cwd()) {
|
|
92
|
+
const entries = readLogs(logFile);
|
|
93
|
+
const results = [];
|
|
94
|
+
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (!entry.uuid) {
|
|
97
|
+
// Entry without UUID cannot be verified
|
|
98
|
+
results.push({ entry, verified: null, reason: 'no uuid' });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Create entry with signed:true for verification
|
|
104
|
+
const verifyEntry = { ...entry, signed: true };
|
|
105
|
+
const result = await signer.verify(verifyEntry, projectDir);
|
|
106
|
+
results.push({ entry, verified: result.valid, reason: result.reason });
|
|
107
|
+
} catch (err) {
|
|
108
|
+
results.push({ entry, verified: false, reason: err.message });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
log,
|
|
117
|
+
logEvent,
|
|
118
|
+
readLogs,
|
|
119
|
+
verifyLogs
|
|
120
|
+
};
|