@misterhuydo/sentinel 1.0.2 → 1.0.4
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/lib/generate.js +141 -127
- package/lib/init.js +233 -250
- package/package.json +21 -21
package/lib/generate.js
CHANGED
|
@@ -1,127 +1,141 @@
|
|
|
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, anthropicKey = '') {
|
|
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
|
-
const tplDir = path.join(__dirname, '..', 'templates');
|
|
15
|
-
// Inject API key into sentinel.properties if provided
|
|
16
|
-
let sentinelProps = fs.readFileSync(path.join(tplDir, 'sentinel.properties'), 'utf8');
|
|
17
|
-
if (anthropicKey) {
|
|
18
|
-
sentinelProps += `\n# Anthropic API key for Claude Code (headless server auth)\nANTHROPIC_API_KEY=${anthropicKey}\n`;
|
|
19
|
-
} else {
|
|
20
|
-
sentinelProps += `\n# Anthropic API key — set this if using API key auth, or leave blank for OAuth\n# ANTHROPIC_API_KEY=sk-ant-...\n`;
|
|
21
|
-
}
|
|
22
|
-
fs.writeFileSync(path.join(projectDir, 'config', 'sentinel.properties'), sentinelProps);
|
|
23
|
-
fs.copySync(path.join(tplDir, 'log-configs', '_example.properties'), path.join(configDir, '_example.properties'));
|
|
24
|
-
fs.copySync(path.join(tplDir, 'repo-configs', '_example.properties'), path.join(repoDir, '_example.properties'));
|
|
25
|
-
|
|
26
|
-
generateProjectScripts(projectDir, codeDir, pythonBin);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function generateProjectScripts(projectDir, codeDir, pythonBin) {
|
|
30
|
-
const name = path.basename(projectDir);
|
|
31
|
-
|
|
32
|
-
// init.sh
|
|
33
|
-
fs.writeFileSync(path.join(projectDir, 'init.sh'), `#!/usr/bin/env bash
|
|
34
|
-
# First-time setup for this Sentinel project instance.
|
|
35
|
-
#
|
|
36
|
-
# What this does:
|
|
37
|
-
# - Clones any repos defined in config/repo-configs/ that don't exist locally yet
|
|
38
|
-
# (skips repos that are already cloned — safe to run multiple times)
|
|
39
|
-
# - Indexes each repo with Cairn MCP for codebase context
|
|
40
|
-
# - Tests SSH connectivity to each configured log source
|
|
41
|
-
# - Sends a test email to verify SMTP settings
|
|
42
|
-
#
|
|
43
|
-
# Note: ongoing repo management (git pull, conflict resolution) is handled
|
|
44
|
-
# automatically by Sentinel on each fix cycle — you don't need to do it manually.
|
|
45
|
-
set -euo pipefail
|
|
46
|
-
cd "$(dirname "$0")"
|
|
47
|
-
PYTHONPATH="${codeDir}" "${pythonBin}" -m sentinel.main --config ./config --init
|
|
48
|
-
`, { mode: 0o755 });
|
|
49
|
-
|
|
50
|
-
// start.sh
|
|
51
|
-
fs.writeFileSync(path.join(projectDir, 'start.sh'), `#!/usr/bin/env bash
|
|
52
|
-
# Start this Sentinel instance
|
|
53
|
-
set -euo pipefail
|
|
54
|
-
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
55
|
-
PID_FILE="$DIR/sentinel.pid"
|
|
56
|
-
|
|
57
|
-
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
|
58
|
-
echo "[sentinel] ${name} already running (PID $(cat "$PID_FILE"))"
|
|
59
|
-
exit 0
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
echo
|
|
67
|
-
echo "
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
[[ "$
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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, anthropicKey = '') {
|
|
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
|
+
const tplDir = path.join(__dirname, '..', 'templates');
|
|
15
|
+
// Inject API key into sentinel.properties if provided
|
|
16
|
+
let sentinelProps = fs.readFileSync(path.join(tplDir, 'sentinel.properties'), 'utf8');
|
|
17
|
+
if (anthropicKey) {
|
|
18
|
+
sentinelProps += `\n# Anthropic API key for Claude Code (headless server auth)\nANTHROPIC_API_KEY=${anthropicKey}\n`;
|
|
19
|
+
} else {
|
|
20
|
+
sentinelProps += `\n# Anthropic API key — set this if using API key auth, or leave blank for OAuth\n# ANTHROPIC_API_KEY=sk-ant-...\n`;
|
|
21
|
+
}
|
|
22
|
+
fs.writeFileSync(path.join(projectDir, 'config', 'sentinel.properties'), sentinelProps);
|
|
23
|
+
fs.copySync(path.join(tplDir, 'log-configs', '_example.properties'), path.join(configDir, '_example.properties'));
|
|
24
|
+
fs.copySync(path.join(tplDir, 'repo-configs', '_example.properties'), path.join(repoDir, '_example.properties'));
|
|
25
|
+
|
|
26
|
+
generateProjectScripts(projectDir, codeDir, pythonBin);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function generateProjectScripts(projectDir, codeDir, pythonBin) {
|
|
30
|
+
const name = path.basename(projectDir);
|
|
31
|
+
|
|
32
|
+
// init.sh
|
|
33
|
+
fs.writeFileSync(path.join(projectDir, 'init.sh'), `#!/usr/bin/env bash
|
|
34
|
+
# First-time setup for this Sentinel project instance.
|
|
35
|
+
#
|
|
36
|
+
# What this does:
|
|
37
|
+
# - Clones any repos defined in config/repo-configs/ that don't exist locally yet
|
|
38
|
+
# (skips repos that are already cloned — safe to run multiple times)
|
|
39
|
+
# - Indexes each repo with Cairn MCP for codebase context
|
|
40
|
+
# - Tests SSH connectivity to each configured log source
|
|
41
|
+
# - Sends a test email to verify SMTP settings
|
|
42
|
+
#
|
|
43
|
+
# Note: ongoing repo management (git pull, conflict resolution) is handled
|
|
44
|
+
# automatically by Sentinel on each fix cycle — you don't need to do it manually.
|
|
45
|
+
set -euo pipefail
|
|
46
|
+
cd "$(dirname "$0")"
|
|
47
|
+
PYTHONPATH="${codeDir}" "${pythonBin}" -m sentinel.main --config ./config --init
|
|
48
|
+
`, { mode: 0o755 });
|
|
49
|
+
|
|
50
|
+
// start.sh
|
|
51
|
+
fs.writeFileSync(path.join(projectDir, 'start.sh'), `#!/usr/bin/env bash
|
|
52
|
+
# Start this Sentinel instance
|
|
53
|
+
set -euo pipefail
|
|
54
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
55
|
+
PID_FILE="$DIR/sentinel.pid"
|
|
56
|
+
|
|
57
|
+
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
|
58
|
+
echo "[sentinel] ${name} already running (PID $(cat "$PID_FILE"))"
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Check Claude Code authentication
|
|
63
|
+
AUTH_OUT=$(claude --print \"hi\" 2>&1 || true)
|
|
64
|
+
if echo "$AUTH_OUT" | grep -Eqi "not logged in|/login"; then
|
|
65
|
+
echo ""
|
|
66
|
+
echo "[sentinel] Claude Code is not authenticated."
|
|
67
|
+
echo " 1. Open a new terminal and run: claude"
|
|
68
|
+
echo " 2. Type /login at the prompt"
|
|
69
|
+
echo " 3. Open the URL in any browser and log in"
|
|
70
|
+
echo " 4. Type /exit when done"
|
|
71
|
+
echo " 5. Re-run this script"
|
|
72
|
+
echo ""
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
mkdir -p "$DIR/logs" "$DIR/workspace/fetched" "$DIR/workspace/patches"
|
|
77
|
+
cd "$DIR"
|
|
78
|
+
PYTHONPATH="${codeDir}" "${pythonBin}" -m sentinel.main --config ./config \\
|
|
79
|
+
>> "$DIR/logs/sentinel.log" 2>&1 &
|
|
80
|
+
echo $! > "$PID_FILE"
|
|
81
|
+
echo "[sentinel] ${name} started (PID $!)"
|
|
82
|
+
`, { mode: 0o755 });
|
|
83
|
+
|
|
84
|
+
// stop.sh
|
|
85
|
+
fs.writeFileSync(path.join(projectDir, 'stop.sh'), `#!/usr/bin/env bash
|
|
86
|
+
# Stop this Sentinel instance
|
|
87
|
+
set -euo pipefail
|
|
88
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
89
|
+
PID_FILE="$DIR/sentinel.pid"
|
|
90
|
+
|
|
91
|
+
if [[ ! -f "$PID_FILE" ]]; then
|
|
92
|
+
echo "[sentinel] ${name} — no PID file, not running"
|
|
93
|
+
exit 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
PID=$(cat "$PID_FILE")
|
|
97
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
98
|
+
kill "$PID"
|
|
99
|
+
echo "[sentinel] ${name} stopped (PID $PID)"
|
|
100
|
+
else
|
|
101
|
+
echo "[sentinel] ${name} — PID $PID not running"
|
|
102
|
+
fi
|
|
103
|
+
rm -f "$PID_FILE"
|
|
104
|
+
`, { mode: 0o755 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Workspace-level startAll / stopAll ────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function generateWorkspaceScripts(workspace) {
|
|
110
|
+
// startAll.sh
|
|
111
|
+
fs.writeFileSync(path.join(workspace, 'startAll.sh'), `#!/usr/bin/env bash
|
|
112
|
+
# Start all Sentinel project instances
|
|
113
|
+
WORKSPACE="$(cd "$(dirname "$0")" && pwd)"
|
|
114
|
+
started=0
|
|
115
|
+
for project_dir in "$WORKSPACE"/*/; do
|
|
116
|
+
name=$(basename "$project_dir")
|
|
117
|
+
[[ "$name" == "code" ]] && continue
|
|
118
|
+
[[ -f "$project_dir/start.sh" ]] || continue
|
|
119
|
+
bash "$project_dir/start.sh"
|
|
120
|
+
started=$((started + 1))
|
|
121
|
+
done
|
|
122
|
+
echo "[sentinel] $started project(s) started"
|
|
123
|
+
`, { mode: 0o755 });
|
|
124
|
+
|
|
125
|
+
// stopAll.sh
|
|
126
|
+
fs.writeFileSync(path.join(workspace, 'stopAll.sh'), `#!/usr/bin/env bash
|
|
127
|
+
# Stop all Sentinel project instances
|
|
128
|
+
WORKSPACE="$(cd "$(dirname "$0")" && pwd)"
|
|
129
|
+
stopped=0
|
|
130
|
+
for project_dir in "$WORKSPACE"/*/; do
|
|
131
|
+
name=$(basename "$project_dir")
|
|
132
|
+
[[ "$name" == "code" ]] && continue
|
|
133
|
+
[[ -f "$project_dir/stop.sh" ]] || continue
|
|
134
|
+
bash "$project_dir/stop.sh"
|
|
135
|
+
stopped=$((stopped + 1))
|
|
136
|
+
done
|
|
137
|
+
echo "[sentinel] $stopped project(s) stopped"
|
|
138
|
+
`, { mode: 0o755 });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { writeExampleProject, generateProjectScripts, generateWorkspaceScripts };
|
package/lib/init.js
CHANGED
|
@@ -1,250 +1,233 @@
|
|
|
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: 'select',
|
|
28
|
-
name: 'authMode',
|
|
29
|
-
message: 'How will Claude Code authenticate?',
|
|
30
|
-
choices: [
|
|
31
|
-
{ title: 'API key (Anthropic API account — recommended for servers)', value: 'apikey' },
|
|
32
|
-
{ title: 'Claude Pro / OAuth (will give you a URL to open in any browser)', value: 'oauth' },
|
|
33
|
-
{ title: 'Skip (I will configure this later)', value: 'skip' },
|
|
34
|
-
],
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
type: prev => prev === 'apikey' ? 'password' : null,
|
|
38
|
-
name: 'anthropicKey',
|
|
39
|
-
message: 'Anthropic API key (sk-ant-...)',
|
|
40
|
-
validate: v => v.startsWith('sk-ant-') ? true : 'Key should start with sk-ant-',
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
type: 'confirm',
|
|
44
|
-
name: 'example',
|
|
45
|
-
message: 'Create an example project to show how to configure?',
|
|
46
|
-
initial: true,
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
type: 'confirm',
|
|
50
|
-
name: 'systemd',
|
|
51
|
-
message: 'Set up systemd service for auto-start on reboot?',
|
|
52
|
-
initial: process.platform === 'linux',
|
|
53
|
-
},
|
|
54
|
-
], { onCancel: () => process.exit(0) });
|
|
55
|
-
|
|
56
|
-
const { workspace, authMode, anthropicKey, example, systemd } = answers;
|
|
57
|
-
const codeDir = path.join(workspace, 'code');
|
|
58
|
-
|
|
59
|
-
// ── Python ──────────────────────────────────────────────────────────────────
|
|
60
|
-
step('Checking Python…');
|
|
61
|
-
const python = findPython();
|
|
62
|
-
if (!python) {
|
|
63
|
-
console.error(chalk.red(' ✖ python3 not found. Install it first:'));
|
|
64
|
-
console.error(' sudo apt-get install -y python3 python3-pip python3-venv');
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
ok(`Python: ${run(python, ['--version']).trim()}`);
|
|
68
|
-
|
|
69
|
-
// ── Copy Sentinel Python source ─────────────────────────────────────────────
|
|
70
|
-
step('Installing Sentinel code…');
|
|
71
|
-
const bundledPython = path.join(__dirname, '..', 'python');
|
|
72
|
-
if (!fs.existsSync(bundledPython)) {
|
|
73
|
-
console.error(chalk.red(' ✖ Bundled Python source not found (run npm publish from the Sentinel repo)'));
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
fs.ensureDirSync(codeDir);
|
|
77
|
-
fs.copySync(bundledPython, codeDir, { overwrite: true });
|
|
78
|
-
ok(`Sentinel code → ${codeDir}`);
|
|
79
|
-
|
|
80
|
-
// ── Python venv ─────────────────────────────────────────────────────────────
|
|
81
|
-
step('Setting up Python environment…');
|
|
82
|
-
const venv = path.join(codeDir, '.venv');
|
|
83
|
-
if (!fs.existsSync(venv)) {
|
|
84
|
-
info('Creating virtual environment…');
|
|
85
|
-
runLive(python, ['-m', 'venv', venv]);
|
|
86
|
-
}
|
|
87
|
-
const pip = path.join(venv, 'bin', 'pip3');
|
|
88
|
-
const pythonBin = path.join(venv, 'bin', 'python3');
|
|
89
|
-
info('Installing Python packages…');
|
|
90
|
-
runLive(pip, ['install', '--quiet', '--upgrade', 'pip']);
|
|
91
|
-
runLive(pip, ['install', '--quiet', '-r', path.join(codeDir, 'requirements.txt')]);
|
|
92
|
-
ok('Python packages installed');
|
|
93
|
-
|
|
94
|
-
// ── Node tools ──────────────────────────────────────────────────────────────
|
|
95
|
-
step('Installing Node tools…');
|
|
96
|
-
installNpmGlobal('@misterhuydo/cairn-mcp', 'cairn');
|
|
97
|
-
installNpmGlobal('@anthropic-ai/claude-code', 'claude');
|
|
98
|
-
|
|
99
|
-
// ── Claude Code auth ─────────────────────────────────────────────────────────
|
|
100
|
-
step('Claude Code authentication…');
|
|
101
|
-
if (authMode === 'apikey' && anthropicKey) {
|
|
102
|
-
ok('API key will be written to each project\'s sentinel.properties');
|
|
103
|
-
} else if (authMode === 'oauth') {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
${chalk.cyan(`${workspace}/
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
${chalk.cyan(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
`
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
Restart=on-failure
|
|
235
|
-
RestartSec=10
|
|
236
|
-
|
|
237
|
-
[Install]
|
|
238
|
-
WantedBy=multi-user.target
|
|
239
|
-
`;
|
|
240
|
-
try {
|
|
241
|
-
fs.writeFileSync('/tmp/sentinel.service', content);
|
|
242
|
-
execSync(`sudo mv /tmp/sentinel.service ${svc}`);
|
|
243
|
-
execSync('sudo systemctl daemon-reload');
|
|
244
|
-
execSync('sudo systemctl enable sentinel');
|
|
245
|
-
ok('sentinel.service enabled');
|
|
246
|
-
} catch (e) {
|
|
247
|
-
warn(`Could not write systemd service (need sudo): ${e.message}`);
|
|
248
|
-
warn(`Manually create ${svc} to auto-start on reboot`);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
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: 'select',
|
|
28
|
+
name: 'authMode',
|
|
29
|
+
message: 'How will Claude Code authenticate?',
|
|
30
|
+
choices: [
|
|
31
|
+
{ title: 'API key (Anthropic API account — recommended for servers)', value: 'apikey' },
|
|
32
|
+
{ title: 'Claude Pro / OAuth (will give you a URL to open in any browser)', value: 'oauth' },
|
|
33
|
+
{ title: 'Skip (I will configure this later)', value: 'skip' },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: prev => prev === 'apikey' ? 'password' : null,
|
|
38
|
+
name: 'anthropicKey',
|
|
39
|
+
message: 'Anthropic API key (sk-ant-...)',
|
|
40
|
+
validate: v => v.startsWith('sk-ant-') ? true : 'Key should start with sk-ant-',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'confirm',
|
|
44
|
+
name: 'example',
|
|
45
|
+
message: 'Create an example project to show how to configure?',
|
|
46
|
+
initial: true,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'confirm',
|
|
50
|
+
name: 'systemd',
|
|
51
|
+
message: 'Set up systemd service for auto-start on reboot?',
|
|
52
|
+
initial: process.platform === 'linux',
|
|
53
|
+
},
|
|
54
|
+
], { onCancel: () => process.exit(0) });
|
|
55
|
+
|
|
56
|
+
const { workspace, authMode, anthropicKey, example, systemd } = answers;
|
|
57
|
+
const codeDir = path.join(workspace, 'code');
|
|
58
|
+
|
|
59
|
+
// ── Python ──────────────────────────────────────────────────────────────────
|
|
60
|
+
step('Checking Python…');
|
|
61
|
+
const python = findPython();
|
|
62
|
+
if (!python) {
|
|
63
|
+
console.error(chalk.red(' ✖ python3 not found. Install it first:'));
|
|
64
|
+
console.error(' sudo apt-get install -y python3 python3-pip python3-venv');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
ok(`Python: ${run(python, ['--version']).trim()}`);
|
|
68
|
+
|
|
69
|
+
// ── Copy Sentinel Python source ─────────────────────────────────────────────
|
|
70
|
+
step('Installing Sentinel code…');
|
|
71
|
+
const bundledPython = path.join(__dirname, '..', 'python');
|
|
72
|
+
if (!fs.existsSync(bundledPython)) {
|
|
73
|
+
console.error(chalk.red(' ✖ Bundled Python source not found (run npm publish from the Sentinel repo)'));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
fs.ensureDirSync(codeDir);
|
|
77
|
+
fs.copySync(bundledPython, codeDir, { overwrite: true });
|
|
78
|
+
ok(`Sentinel code → ${codeDir}`);
|
|
79
|
+
|
|
80
|
+
// ── Python venv ─────────────────────────────────────────────────────────────
|
|
81
|
+
step('Setting up Python environment…');
|
|
82
|
+
const venv = path.join(codeDir, '.venv');
|
|
83
|
+
if (!fs.existsSync(venv)) {
|
|
84
|
+
info('Creating virtual environment…');
|
|
85
|
+
runLive(python, ['-m', 'venv', venv]);
|
|
86
|
+
}
|
|
87
|
+
const pip = path.join(venv, 'bin', 'pip3');
|
|
88
|
+
const pythonBin = path.join(venv, 'bin', 'python3');
|
|
89
|
+
info('Installing Python packages…');
|
|
90
|
+
runLive(pip, ['install', '--quiet', '--upgrade', 'pip']);
|
|
91
|
+
runLive(pip, ['install', '--quiet', '-r', path.join(codeDir, 'requirements.txt')]);
|
|
92
|
+
ok('Python packages installed');
|
|
93
|
+
|
|
94
|
+
// ── Node tools ──────────────────────────────────────────────────────────────
|
|
95
|
+
step('Installing Node tools…');
|
|
96
|
+
installNpmGlobal('@misterhuydo/cairn-mcp', 'cairn');
|
|
97
|
+
installNpmGlobal('@anthropic-ai/claude-code', 'claude');
|
|
98
|
+
|
|
99
|
+
// ── Claude Code auth ─────────────────────────────────────────────────────────
|
|
100
|
+
step('Claude Code authentication…');
|
|
101
|
+
if (authMode === 'apikey' && anthropicKey) {
|
|
102
|
+
ok('API key will be written to each project\'s sentinel.properties');
|
|
103
|
+
} else if (authMode === 'oauth') {
|
|
104
|
+
info('OAuth selected — start.sh will prompt for login if not yet authenticated');
|
|
105
|
+
} else {
|
|
106
|
+
info('Skipping auth — start.sh will prompt for login if needed');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Workspace structure ─────────────────────────────────────────────────────
|
|
110
|
+
step('Creating workspace…');
|
|
111
|
+
fs.ensureDirSync(workspace);
|
|
112
|
+
ok(`Workspace: ${workspace}`);
|
|
113
|
+
|
|
114
|
+
// ── Example project ─────────────────────────────────────────────────────────
|
|
115
|
+
if (example) {
|
|
116
|
+
step('Creating example project…');
|
|
117
|
+
const exampleDir = path.join(workspace, 'my-project');
|
|
118
|
+
writeExampleProject(exampleDir, codeDir, pythonBin, anthropicKey || '');
|
|
119
|
+
ok(`Example project: ${exampleDir}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Workspace start/stop scripts ─────────────────────────────────────────────
|
|
123
|
+
step('Generating scripts…');
|
|
124
|
+
generateWorkspaceScripts(workspace);
|
|
125
|
+
ok(`${workspace}/startAll.sh`);
|
|
126
|
+
ok(`${workspace}/stopAll.sh`);
|
|
127
|
+
|
|
128
|
+
// ── systemd ──────────────────────────────────────────────────────────────────
|
|
129
|
+
if (systemd) {
|
|
130
|
+
step('Setting up systemd…');
|
|
131
|
+
setupSystemd(workspace);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Done ─────────────────────────────────────────────────────────────────────
|
|
135
|
+
console.log('\n' + chalk.bold.green('══ Done! ══') + '\n');
|
|
136
|
+
console.log(`${chalk.bold('Next steps:')}`);
|
|
137
|
+
if (example) {
|
|
138
|
+
console.log(`
|
|
139
|
+
1. Configure your first project:
|
|
140
|
+
${chalk.cyan(`${workspace}/my-project/config/sentinel.properties`)}
|
|
141
|
+
${chalk.cyan(`${workspace}/my-project/config/log-configs/`)}
|
|
142
|
+
${chalk.cyan(`${workspace}/my-project/config/repo-configs/`)}
|
|
143
|
+
|
|
144
|
+
2. First-time init (clones repos, tests SSH, sends test email):
|
|
145
|
+
${chalk.cyan(`${workspace}/my-project/init.sh`)}
|
|
146
|
+
|
|
147
|
+
3. Start all projects:
|
|
148
|
+
${chalk.cyan(`${workspace}/startAll.sh`)}
|
|
149
|
+
|
|
150
|
+
4. Stop all projects:
|
|
151
|
+
${chalk.cyan(`${workspace}/stopAll.sh`)}
|
|
152
|
+
`);
|
|
153
|
+
}
|
|
154
|
+
if (systemd) {
|
|
155
|
+
console.log(` Auto-start is enabled. To manage:
|
|
156
|
+
${chalk.cyan('sudo systemctl start sentinel')}
|
|
157
|
+
${chalk.cyan('sudo systemctl status sentinel')}
|
|
158
|
+
${chalk.cyan('journalctl -u sentinel -f')}
|
|
159
|
+
`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(` Add another project anytime:
|
|
163
|
+
${chalk.cyan('sentinel add <project-name>')}
|
|
164
|
+
`);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
function findPython() {
|
|
170
|
+
for (const bin of ['python3', 'python']) {
|
|
171
|
+
try {
|
|
172
|
+
execSync(`${bin} --version`, { stdio: 'pipe' });
|
|
173
|
+
return bin;
|
|
174
|
+
} catch (_) {}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function run(bin, args) {
|
|
180
|
+
const r = spawnSync(bin, args, { encoding: 'utf8' });
|
|
181
|
+
return (r.stdout || '') + (r.stderr || '');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function runLive(bin, args) {
|
|
185
|
+
const r = spawnSync(bin, args, { stdio: 'inherit' });
|
|
186
|
+
if (r.status !== 0) {
|
|
187
|
+
console.error(chalk.red(` ✖ Command failed: ${bin} ${args.join(' ')}`));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function installNpmGlobal(pkg, checkBin) {
|
|
193
|
+
try {
|
|
194
|
+
execSync(`${checkBin} --version`, { stdio: 'pipe' });
|
|
195
|
+
ok(`${pkg} already installed`);
|
|
196
|
+
} catch (_) {
|
|
197
|
+
info(`Installing ${pkg}…`);
|
|
198
|
+
runLive('npm', ['install', '-g', pkg]);
|
|
199
|
+
ok(`${pkg} installed`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function setupSystemd(workspace) {
|
|
204
|
+
const user = os.userInfo().username;
|
|
205
|
+
const svc = `/etc/systemd/system/sentinel.service`;
|
|
206
|
+
const content = `[Unit]
|
|
207
|
+
Description=Sentinel — Autonomous DevOps Agent
|
|
208
|
+
After=network-online.target
|
|
209
|
+
Wants=network-online.target
|
|
210
|
+
|
|
211
|
+
[Service]
|
|
212
|
+
Type=forking
|
|
213
|
+
User=${user}
|
|
214
|
+
WorkingDirectory=${workspace}
|
|
215
|
+
ExecStart=${workspace}/startAll.sh
|
|
216
|
+
ExecStop=${workspace}/stopAll.sh
|
|
217
|
+
Restart=on-failure
|
|
218
|
+
RestartSec=10
|
|
219
|
+
|
|
220
|
+
[Install]
|
|
221
|
+
WantedBy=multi-user.target
|
|
222
|
+
`;
|
|
223
|
+
try {
|
|
224
|
+
fs.writeFileSync('/tmp/sentinel.service', content);
|
|
225
|
+
execSync(`sudo mv /tmp/sentinel.service ${svc}`);
|
|
226
|
+
execSync('sudo systemctl daemon-reload');
|
|
227
|
+
execSync('sudo systemctl enable sentinel');
|
|
228
|
+
ok('sentinel.service enabled');
|
|
229
|
+
} catch (e) {
|
|
230
|
+
warn(`Could not write systemd service (need sudo): ${e.message}`);
|
|
231
|
+
warn(`Manually create ${svc} to auto-start on reboot`);
|
|
232
|
+
}
|
|
233
|
+
}
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@misterhuydo/sentinel",
|
|
3
|
-
"version": "1.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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@misterhuydo/sentinel",
|
|
3
|
+
"version": "1.0.4",
|
|
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
|
+
}
|