@misterhuydo/sentinel 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/bin/sentinel.js +39 -0
- package/lib/add.js +57 -0
- package/lib/generate.js +111 -0
- package/lib/init.js +206 -0
- package/package.json +21 -0
- package/python/requirements.txt +5 -0
- package/python/sentinel/__init__.py +0 -0
- package/python/sentinel/cairn_client.py +45 -0
- package/python/sentinel/cicd_trigger.py +66 -0
- package/python/sentinel/config_loader.py +174 -0
- package/python/sentinel/fix_engine.py +123 -0
- package/python/sentinel/git_manager.py +227 -0
- package/python/sentinel/log_fetcher.py +200 -0
- package/python/sentinel/log_parser.py +149 -0
- package/python/sentinel/main.py +223 -0
- package/python/sentinel/repo_router.py +24 -0
- package/python/sentinel/reporter.py +173 -0
- package/python/sentinel/state_store.py +164 -0
- package/templates/log-configs/_example.properties +47 -0
- package/templates/repo-configs/_example.properties +37 -0
- package/templates/sentinel.properties +31 -0
package/bin/sentinel.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const [,, command = 'help', ...args] = process.argv;
|
|
6
|
+
|
|
7
|
+
const BANNER = `
|
|
8
|
+
${chalk.cyan('███████╗███████╗███╗ ██╗████████╗██╗███╗ ██╗███████╗██╗')}
|
|
9
|
+
${chalk.cyan('██╔════╝██╔════╝████╗ ██║╚══██╔══╝██║████╗ ██║██╔════╝██║')}
|
|
10
|
+
${chalk.cyan('███████╗█████╗ ██╔██╗ ██║ ██║ ██║██╔██╗ ██║█████╗ ██║')}
|
|
11
|
+
${chalk.cyan('╚════██║██╔══╝ ██║╚██╗██║ ██║ ██║██║╚██╗██║██╔══╝ ██║')}
|
|
12
|
+
${chalk.cyan('███████║███████╗██║ ╚████║ ██║ ██║██║ ╚████║███████╗███████╗')}
|
|
13
|
+
${chalk.cyan('╚══════╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝')}
|
|
14
|
+
${chalk.gray(' Autonomous DevOps Agent')}
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
console.log(BANNER);
|
|
19
|
+
|
|
20
|
+
switch (command) {
|
|
21
|
+
case 'init':
|
|
22
|
+
await require('../lib/init')();
|
|
23
|
+
break;
|
|
24
|
+
case 'add':
|
|
25
|
+
await require('../lib/add')(args[0]);
|
|
26
|
+
break;
|
|
27
|
+
case 'help':
|
|
28
|
+
default:
|
|
29
|
+
console.log(`${chalk.bold('Usage:')}
|
|
30
|
+
sentinel init Interactive setup — install everything and create workspace
|
|
31
|
+
sentinel add <name> Add a new project instance to an existing workspace
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
main().catch(err => {
|
|
37
|
+
console.error(chalk.red('Error:'), err.message);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
package/lib/add.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const prompts = require('prompts');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const { writeExampleProject, generateWorkspaceScripts } = require('./generate');
|
|
9
|
+
|
|
10
|
+
module.exports = async function add(nameArg) {
|
|
11
|
+
const answers = await prompts([
|
|
12
|
+
{
|
|
13
|
+
type: 'text',
|
|
14
|
+
name: 'workspace',
|
|
15
|
+
message: 'Workspace directory',
|
|
16
|
+
initial: path.join(os.homedir(), 'sentinel'),
|
|
17
|
+
format: v => v.replace(/^~/, os.homedir()),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: 'text',
|
|
21
|
+
name: 'name',
|
|
22
|
+
message: 'Project name',
|
|
23
|
+
initial: nameArg || 'my-project',
|
|
24
|
+
validate: v => /^[a-z0-9_-]+$/i.test(v) || 'Use letters, numbers, hyphens only',
|
|
25
|
+
},
|
|
26
|
+
], { onCancel: () => process.exit(0) });
|
|
27
|
+
|
|
28
|
+
const { workspace, name } = answers;
|
|
29
|
+
const codeDir = path.join(workspace, 'code');
|
|
30
|
+
const pythonBin = path.join(codeDir, '.venv', 'bin', 'python3');
|
|
31
|
+
const projectDir = path.join(workspace, name);
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(codeDir)) {
|
|
34
|
+
console.error(chalk.red(`Sentinel code not found at ${codeDir}`));
|
|
35
|
+
console.error(chalk.red('Run "sentinel init" first.'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (fs.existsSync(projectDir)) {
|
|
40
|
+
console.error(chalk.yellow(`Project "${name}" already exists at ${projectDir}`));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
writeExampleProject(projectDir, codeDir, pythonBin);
|
|
45
|
+
generateWorkspaceScripts(workspace);
|
|
46
|
+
|
|
47
|
+
console.log('\n' + chalk.green('✔'), `Project "${name}" created at ${projectDir}`);
|
|
48
|
+
console.log(`
|
|
49
|
+
Next steps:
|
|
50
|
+
1. Edit config files in:
|
|
51
|
+
${chalk.cyan(`${projectDir}/config/`)}
|
|
52
|
+
2. Run first-time init:
|
|
53
|
+
${chalk.cyan(`${projectDir}/init.sh`)}
|
|
54
|
+
3. Start this project:
|
|
55
|
+
${chalk.cyan(`${projectDir}/start.sh`)}
|
|
56
|
+
`);
|
|
57
|
+
};
|
package/lib/generate.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// ── Per-project files ─────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function writeExampleProject(projectDir, codeDir, pythonBin) {
|
|
9
|
+
const configDir = path.join(projectDir, 'config', 'log-configs');
|
|
10
|
+
const repoDir = path.join(projectDir, 'config', 'repo-configs');
|
|
11
|
+
fs.ensureDirSync(configDir);
|
|
12
|
+
fs.ensureDirSync(repoDir);
|
|
13
|
+
|
|
14
|
+
// Copy example config templates from bundled python/
|
|
15
|
+
const tplDir = path.join(__dirname, '..', 'templates');
|
|
16
|
+
fs.copySync(path.join(tplDir, 'sentinel.properties'), path.join(projectDir, 'config', 'sentinel.properties'));
|
|
17
|
+
fs.copySync(path.join(tplDir, 'log-configs', '_example.properties'), path.join(configDir, '_example.properties'));
|
|
18
|
+
fs.copySync(path.join(tplDir, 'repo-configs', '_example.properties'), path.join(repoDir, '_example.properties'));
|
|
19
|
+
|
|
20
|
+
generateProjectScripts(projectDir, codeDir, pythonBin);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function generateProjectScripts(projectDir, codeDir, pythonBin) {
|
|
24
|
+
const name = path.basename(projectDir);
|
|
25
|
+
|
|
26
|
+
// init.sh
|
|
27
|
+
fs.writeFileSync(path.join(projectDir, 'init.sh'), `#!/usr/bin/env bash
|
|
28
|
+
# First-time setup: clone repos, test SSH, send test email
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
cd "$(dirname "$0")"
|
|
31
|
+
PYTHONPATH="${codeDir}" "${pythonBin}" -m sentinel.main --config ./config --init
|
|
32
|
+
`, { mode: 0o755 });
|
|
33
|
+
|
|
34
|
+
// start.sh
|
|
35
|
+
fs.writeFileSync(path.join(projectDir, 'start.sh'), `#!/usr/bin/env bash
|
|
36
|
+
# Start this Sentinel instance
|
|
37
|
+
set -euo pipefail
|
|
38
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
39
|
+
PID_FILE="$DIR/sentinel.pid"
|
|
40
|
+
|
|
41
|
+
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
|
42
|
+
echo "[sentinel] ${name} already running (PID $(cat "$PID_FILE"))"
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
mkdir -p "$DIR/logs" "$DIR/workspace/fetched" "$DIR/workspace/patches"
|
|
47
|
+
cd "$DIR"
|
|
48
|
+
PYTHONPATH="${codeDir}" "${pythonBin}" -m sentinel.main --config ./config \\
|
|
49
|
+
>> "$DIR/logs/sentinel.log" 2>&1 &
|
|
50
|
+
echo $! > "$PID_FILE"
|
|
51
|
+
echo "[sentinel] ${name} started (PID $!)"
|
|
52
|
+
`, { mode: 0o755 });
|
|
53
|
+
|
|
54
|
+
// stop.sh
|
|
55
|
+
fs.writeFileSync(path.join(projectDir, 'stop.sh'), `#!/usr/bin/env bash
|
|
56
|
+
# Stop this Sentinel instance
|
|
57
|
+
set -euo pipefail
|
|
58
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
59
|
+
PID_FILE="$DIR/sentinel.pid"
|
|
60
|
+
|
|
61
|
+
if [[ ! -f "$PID_FILE" ]]; then
|
|
62
|
+
echo "[sentinel] ${name} — no PID file, not running"
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
PID=$(cat "$PID_FILE")
|
|
67
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
68
|
+
kill "$PID"
|
|
69
|
+
echo "[sentinel] ${name} stopped (PID $PID)"
|
|
70
|
+
else
|
|
71
|
+
echo "[sentinel] ${name} — PID $PID not running"
|
|
72
|
+
fi
|
|
73
|
+
rm -f "$PID_FILE"
|
|
74
|
+
`, { mode: 0o755 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Workspace-level startAll / stopAll ────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
function generateWorkspaceScripts(workspace) {
|
|
80
|
+
// startAll.sh
|
|
81
|
+
fs.writeFileSync(path.join(workspace, 'startAll.sh'), `#!/usr/bin/env bash
|
|
82
|
+
# Start all Sentinel project instances
|
|
83
|
+
WORKSPACE="$(cd "$(dirname "$0")" && pwd)"
|
|
84
|
+
started=0
|
|
85
|
+
for project_dir in "$WORKSPACE"/*/; do
|
|
86
|
+
name=$(basename "$project_dir")
|
|
87
|
+
[[ "$name" == "code" ]] && continue
|
|
88
|
+
[[ -f "$project_dir/start.sh" ]] || continue
|
|
89
|
+
bash "$project_dir/start.sh"
|
|
90
|
+
started=$((started + 1))
|
|
91
|
+
done
|
|
92
|
+
echo "[sentinel] $started project(s) started"
|
|
93
|
+
`, { mode: 0o755 });
|
|
94
|
+
|
|
95
|
+
// stopAll.sh
|
|
96
|
+
fs.writeFileSync(path.join(workspace, 'stopAll.sh'), `#!/usr/bin/env bash
|
|
97
|
+
# Stop all Sentinel project instances
|
|
98
|
+
WORKSPACE="$(cd "$(dirname "$0")" && pwd)"
|
|
99
|
+
stopped=0
|
|
100
|
+
for project_dir in "$WORKSPACE"/*/; do
|
|
101
|
+
name=$(basename "$project_dir")
|
|
102
|
+
[[ "$name" == "code" ]] && continue
|
|
103
|
+
[[ -f "$project_dir/stop.sh" ]] || continue
|
|
104
|
+
bash "$project_dir/stop.sh"
|
|
105
|
+
stopped=$((stopped + 1))
|
|
106
|
+
done
|
|
107
|
+
echo "[sentinel] $stopped project(s) stopped"
|
|
108
|
+
`, { mode: 0o755 });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { writeExampleProject, generateProjectScripts, generateWorkspaceScripts };
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { execSync, spawnSync } = require('child_process');
|
|
7
|
+
const prompts = require('prompts');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const { generateProjectScripts, generateWorkspaceScripts, writeExampleProject } = require('./generate');
|
|
10
|
+
|
|
11
|
+
const ok = msg => console.log(chalk.green(' ✔'), msg);
|
|
12
|
+
const info = msg => console.log(chalk.cyan(' →'), msg);
|
|
13
|
+
const warn = msg => console.log(chalk.yellow(' ⚠'), msg);
|
|
14
|
+
const step = msg => console.log('\n' + chalk.bold.white(msg));
|
|
15
|
+
|
|
16
|
+
module.exports = async function init() {
|
|
17
|
+
// ── Prompts ─────────────────────────────────────────────────────────────────
|
|
18
|
+
const answers = await prompts([
|
|
19
|
+
{
|
|
20
|
+
type: 'text',
|
|
21
|
+
name: 'workspace',
|
|
22
|
+
message: 'Workspace directory (each project lives here as a subdirectory)',
|
|
23
|
+
initial: path.join(os.homedir(), 'sentinel'),
|
|
24
|
+
format: v => v.replace(/^~/, os.homedir()),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'confirm',
|
|
28
|
+
name: 'example',
|
|
29
|
+
message: 'Create an example project to show how to configure?',
|
|
30
|
+
initial: true,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: 'confirm',
|
|
34
|
+
name: 'systemd',
|
|
35
|
+
message: 'Set up systemd service for auto-start on reboot?',
|
|
36
|
+
initial: process.platform === 'linux',
|
|
37
|
+
},
|
|
38
|
+
], { onCancel: () => process.exit(0) });
|
|
39
|
+
|
|
40
|
+
const { workspace, example, systemd } = answers;
|
|
41
|
+
const codeDir = path.join(workspace, 'code');
|
|
42
|
+
|
|
43
|
+
// ── Python ──────────────────────────────────────────────────────────────────
|
|
44
|
+
step('Checking Python…');
|
|
45
|
+
const python = findPython();
|
|
46
|
+
if (!python) {
|
|
47
|
+
console.error(chalk.red(' ✖ python3 not found. Install it first:'));
|
|
48
|
+
console.error(' sudo apt-get install -y python3 python3-pip python3-venv');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
ok(`Python: ${run(python, ['--version']).trim()}`);
|
|
52
|
+
|
|
53
|
+
// ── Copy Sentinel Python source ─────────────────────────────────────────────
|
|
54
|
+
step('Installing Sentinel code…');
|
|
55
|
+
const bundledPython = path.join(__dirname, '..', 'python');
|
|
56
|
+
if (!fs.existsSync(bundledPython)) {
|
|
57
|
+
console.error(chalk.red(' ✖ Bundled Python source not found (run npm publish from the Sentinel repo)'));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
fs.ensureDirSync(codeDir);
|
|
61
|
+
fs.copySync(bundledPython, codeDir, { overwrite: true });
|
|
62
|
+
ok(`Sentinel code → ${codeDir}`);
|
|
63
|
+
|
|
64
|
+
// ── Python venv ─────────────────────────────────────────────────────────────
|
|
65
|
+
step('Setting up Python environment…');
|
|
66
|
+
const venv = path.join(codeDir, '.venv');
|
|
67
|
+
if (!fs.existsSync(venv)) {
|
|
68
|
+
info('Creating virtual environment…');
|
|
69
|
+
runLive(python, ['-m', 'venv', venv]);
|
|
70
|
+
}
|
|
71
|
+
const pip = path.join(venv, 'bin', 'pip3');
|
|
72
|
+
const pythonBin = path.join(venv, 'bin', 'python3');
|
|
73
|
+
info('Installing Python packages…');
|
|
74
|
+
runLive(pip, ['install', '--quiet', '--upgrade', 'pip']);
|
|
75
|
+
runLive(pip, ['install', '--quiet', '-r', path.join(codeDir, 'requirements.txt')]);
|
|
76
|
+
ok('Python packages installed');
|
|
77
|
+
|
|
78
|
+
// ── Node tools ──────────────────────────────────────────────────────────────
|
|
79
|
+
step('Installing Node tools…');
|
|
80
|
+
installNpmGlobal('@misterhuydo/cairn-mcp', 'cairn');
|
|
81
|
+
installNpmGlobal('@anthropic-ai/claude-code', 'claude');
|
|
82
|
+
|
|
83
|
+
// ── Workspace structure ─────────────────────────────────────────────────────
|
|
84
|
+
step('Creating workspace…');
|
|
85
|
+
fs.ensureDirSync(workspace);
|
|
86
|
+
ok(`Workspace: ${workspace}`);
|
|
87
|
+
|
|
88
|
+
// ── Example project ─────────────────────────────────────────────────────────
|
|
89
|
+
if (example) {
|
|
90
|
+
step('Creating example project…');
|
|
91
|
+
const exampleDir = path.join(workspace, 'my-project');
|
|
92
|
+
writeExampleProject(exampleDir, codeDir, pythonBin);
|
|
93
|
+
ok(`Example project: ${exampleDir}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Workspace start/stop scripts ─────────────────────────────────────────────
|
|
97
|
+
step('Generating scripts…');
|
|
98
|
+
generateWorkspaceScripts(workspace);
|
|
99
|
+
ok(`${workspace}/startAll.sh`);
|
|
100
|
+
ok(`${workspace}/stopAll.sh`);
|
|
101
|
+
|
|
102
|
+
// ── systemd ──────────────────────────────────────────────────────────────────
|
|
103
|
+
if (systemd) {
|
|
104
|
+
step('Setting up systemd…');
|
|
105
|
+
setupSystemd(workspace);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Done ─────────────────────────────────────────────────────────────────────
|
|
109
|
+
console.log('\n' + chalk.bold.green('══ Done! ══') + '\n');
|
|
110
|
+
console.log(`${chalk.bold('Next steps:')}`);
|
|
111
|
+
if (example) {
|
|
112
|
+
console.log(`
|
|
113
|
+
1. Configure your first project:
|
|
114
|
+
${chalk.cyan(`${workspace}/my-project/config/sentinel.properties`)}
|
|
115
|
+
${chalk.cyan(`${workspace}/my-project/config/log-configs/`)}
|
|
116
|
+
${chalk.cyan(`${workspace}/my-project/config/repo-configs/`)}
|
|
117
|
+
|
|
118
|
+
2. First-time init (clones repos, tests SSH, sends test email):
|
|
119
|
+
${chalk.cyan(`${workspace}/my-project/init.sh`)}
|
|
120
|
+
|
|
121
|
+
3. Start all projects:
|
|
122
|
+
${chalk.cyan(`${workspace}/startAll.sh`)}
|
|
123
|
+
|
|
124
|
+
4. Stop all projects:
|
|
125
|
+
${chalk.cyan(`${workspace}/stopAll.sh`)}
|
|
126
|
+
`);
|
|
127
|
+
}
|
|
128
|
+
if (systemd) {
|
|
129
|
+
console.log(` Auto-start is enabled. To manage:
|
|
130
|
+
${chalk.cyan('sudo systemctl start sentinel')}
|
|
131
|
+
${chalk.cyan('sudo systemctl status sentinel')}
|
|
132
|
+
${chalk.cyan('journalctl -u sentinel -f')}
|
|
133
|
+
`);
|
|
134
|
+
}
|
|
135
|
+
console.log(` Add another project anytime:
|
|
136
|
+
${chalk.cyan('sentinel add <project-name>')}
|
|
137
|
+
`);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function findPython() {
|
|
143
|
+
for (const bin of ['python3', 'python']) {
|
|
144
|
+
try {
|
|
145
|
+
execSync(`${bin} --version`, { stdio: 'pipe' });
|
|
146
|
+
return bin;
|
|
147
|
+
} catch (_) {}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function run(bin, args) {
|
|
153
|
+
const r = spawnSync(bin, args, { encoding: 'utf8' });
|
|
154
|
+
return (r.stdout || '') + (r.stderr || '');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function runLive(bin, args) {
|
|
158
|
+
const r = spawnSync(bin, args, { stdio: 'inherit' });
|
|
159
|
+
if (r.status !== 0) {
|
|
160
|
+
console.error(chalk.red(` ✖ Command failed: ${bin} ${args.join(' ')}`));
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function installNpmGlobal(pkg, checkBin) {
|
|
166
|
+
try {
|
|
167
|
+
execSync(`${checkBin} --version`, { stdio: 'pipe' });
|
|
168
|
+
ok(`${pkg} already installed`);
|
|
169
|
+
} catch (_) {
|
|
170
|
+
info(`Installing ${pkg}…`);
|
|
171
|
+
runLive('npm', ['install', '-g', pkg]);
|
|
172
|
+
ok(`${pkg} installed`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function setupSystemd(workspace) {
|
|
177
|
+
const user = os.userInfo().username;
|
|
178
|
+
const svc = `/etc/systemd/system/sentinel.service`;
|
|
179
|
+
const content = `[Unit]
|
|
180
|
+
Description=Sentinel — Autonomous DevOps Agent
|
|
181
|
+
After=network-online.target
|
|
182
|
+
Wants=network-online.target
|
|
183
|
+
|
|
184
|
+
[Service]
|
|
185
|
+
Type=forking
|
|
186
|
+
User=${user}
|
|
187
|
+
WorkingDirectory=${workspace}
|
|
188
|
+
ExecStart=${workspace}/startAll.sh
|
|
189
|
+
ExecStop=${workspace}/stopAll.sh
|
|
190
|
+
Restart=on-failure
|
|
191
|
+
RestartSec=10
|
|
192
|
+
|
|
193
|
+
[Install]
|
|
194
|
+
WantedBy=multi-user.target
|
|
195
|
+
`;
|
|
196
|
+
try {
|
|
197
|
+
fs.writeFileSync('/tmp/sentinel.service', content);
|
|
198
|
+
execSync(`sudo mv /tmp/sentinel.service ${svc}`);
|
|
199
|
+
execSync('sudo systemctl daemon-reload');
|
|
200
|
+
execSync('sudo systemctl enable sentinel');
|
|
201
|
+
ok('sentinel.service enabled');
|
|
202
|
+
} catch (e) {
|
|
203
|
+
warn(`Could not write systemd service (need sudo): ${e.message}`);
|
|
204
|
+
warn(`Manually create ${svc} to auto-start on reboot`);
|
|
205
|
+
}
|
|
206
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@misterhuydo/sentinel",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Sentinel — Autonomous DevOps Agent installer and manager",
|
|
5
|
+
"bin": {
|
|
6
|
+
"sentinel": "./bin/sentinel.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"prepublishOnly": "node scripts/bundle.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"chalk": "^4.1.2",
|
|
13
|
+
"fs-extra": "^11.2.0",
|
|
14
|
+
"prompts": "^2.4.2"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=16"
|
|
18
|
+
},
|
|
19
|
+
"author": "misterhuydo",
|
|
20
|
+
"license": "MIT"
|
|
21
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cairn_client.py — Cairn MCP integration.
|
|
3
|
+
|
|
4
|
+
Cairn is a passive MCP tool: when properly installed and configured in Claude
|
|
5
|
+
Code's MCP settings, it is used automatically by Claude during fix generation.
|
|
6
|
+
Sentinel's only responsibility is to pre-index repos at startup so the index
|
|
7
|
+
exists before the first fix attempt.
|
|
8
|
+
|
|
9
|
+
Install: npm install -g @misterhuydo/cairn-mcp
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import subprocess
|
|
14
|
+
|
|
15
|
+
from .config_loader import RepoConfig
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
CAIRN_BIN = "cairn"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def ensure_installed() -> bool:
|
|
23
|
+
try:
|
|
24
|
+
r = subprocess.run([CAIRN_BIN, "--version"], capture_output=True, text=True, timeout=10)
|
|
25
|
+
if r.returncode == 0:
|
|
26
|
+
logger.debug("Cairn version: %s", r.stdout.strip())
|
|
27
|
+
return True
|
|
28
|
+
except FileNotFoundError:
|
|
29
|
+
pass
|
|
30
|
+
logger.error("Cairn not found. Run: npm install -g @misterhuydo/cairn-mcp")
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def index_repo(repo: RepoConfig) -> bool:
|
|
35
|
+
"""Index a repo so Cairn context is available for Claude Code. Run at --init."""
|
|
36
|
+
logger.info("Indexing %s with Cairn...", repo.repo_name)
|
|
37
|
+
r = subprocess.run(
|
|
38
|
+
[CAIRN_BIN, "index", "--path", repo.local_path],
|
|
39
|
+
capture_output=True, text=True, timeout=300,
|
|
40
|
+
)
|
|
41
|
+
if r.returncode != 0:
|
|
42
|
+
logger.error("Cairn index failed for %s:\n%s", repo.repo_name, r.stderr)
|
|
43
|
+
return False
|
|
44
|
+
logger.info("Cairn index complete for %s", repo.repo_name)
|
|
45
|
+
return True
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cicd_trigger.py — Trigger CI/CD pipelines after a successful push.
|
|
3
|
+
|
|
4
|
+
Only used when AUTO_PUBLISH=true. For AUTO_PUBLISH=false, CI/CD is
|
|
5
|
+
triggered by the normal GitHub merge webhook.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from .config_loader import RepoConfig
|
|
13
|
+
from .state_store import StateStore
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def trigger(repo: RepoConfig, store: StateStore, fingerprint: str) -> bool:
|
|
19
|
+
if not repo.cicd_type or not repo.cicd_job_url:
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
cicd_type = repo.cicd_type.lower()
|
|
23
|
+
if cicd_type == "jenkins":
|
|
24
|
+
return _trigger_jenkins(repo)
|
|
25
|
+
elif cicd_type in ("github_actions", "github-actions"):
|
|
26
|
+
return _trigger_github_actions(repo, fingerprint)
|
|
27
|
+
else:
|
|
28
|
+
logger.warning("Unknown CICD_TYPE '%s' for %s", repo.cicd_type, repo.repo_name)
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _trigger_jenkins(repo: RepoConfig) -> bool:
|
|
33
|
+
url = f"{repo.cicd_job_url.rstrip('/')}/build"
|
|
34
|
+
resp = requests.post(url, auth=("sentinel", repo.cicd_token), timeout=15)
|
|
35
|
+
success = resp.status_code in (200, 201, 204)
|
|
36
|
+
if success:
|
|
37
|
+
logger.info("Jenkins triggered: %s", repo.cicd_job_url)
|
|
38
|
+
else:
|
|
39
|
+
logger.error("Jenkins trigger failed (%s): %s", resp.status_code, resp.text[:200])
|
|
40
|
+
return success
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _trigger_github_actions(repo: RepoConfig, fingerprint: str) -> bool:
|
|
44
|
+
owner_repo = _owner_repo(repo.repo_url)
|
|
45
|
+
url = f"https://api.github.com/repos/{owner_repo}/dispatches"
|
|
46
|
+
resp = requests.post(
|
|
47
|
+
url,
|
|
48
|
+
json={"event_type": "sentinel-deploy", "client_payload": {"fingerprint": fingerprint}},
|
|
49
|
+
headers={
|
|
50
|
+
"Authorization": f"Bearer {repo.cicd_token}",
|
|
51
|
+
"Accept": "application/vnd.github+json",
|
|
52
|
+
},
|
|
53
|
+
timeout=15,
|
|
54
|
+
)
|
|
55
|
+
success = resp.status_code == 204
|
|
56
|
+
if success:
|
|
57
|
+
logger.info("GitHub Actions dispatch sent for %s", owner_repo)
|
|
58
|
+
else:
|
|
59
|
+
logger.error("GH Actions dispatch failed (%s): %s", resp.status_code, resp.text[:200])
|
|
60
|
+
return success
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _owner_repo(repo_url: str) -> str:
|
|
64
|
+
if repo_url.startswith("git@"):
|
|
65
|
+
return repo_url.split(":")[-1].removesuffix(".git")
|
|
66
|
+
return "/".join(repo_url.rstrip("/").split("/")[-2:]).removesuffix(".git")
|