@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.
Files changed (3) hide show
  1. package/lib/generate.js +141 -127
  2. package/lib/init.js +233 -250
  3. 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
- mkdir -p "$DIR/logs" "$DIR/workspace/fetched" "$DIR/workspace/patches"
63
- cd "$DIR"
64
- PYTHONPATH="${codeDir}" "${pythonBin}" -m sentinel.main --config ./config \\
65
- >> "$DIR/logs/sentinel.log" 2>&1 &
66
- echo $! > "$PID_FILE"
67
- echo "[sentinel] ${name} started (PID $!)"
68
- `, { mode: 0o755 });
69
-
70
- // stop.sh
71
- fs.writeFileSync(path.join(projectDir, 'stop.sh'), `#!/usr/bin/env bash
72
- # Stop this Sentinel instance
73
- set -euo pipefail
74
- DIR="$(cd "$(dirname "$0")" && pwd)"
75
- PID_FILE="$DIR/sentinel.pid"
76
-
77
- if [[ ! -f "$PID_FILE" ]]; then
78
- echo "[sentinel] ${name} no PID file, not running"
79
- exit 0
80
- fi
81
-
82
- PID=$(cat "$PID_FILE")
83
- if kill -0 "$PID" 2>/dev/null; then
84
- kill "$PID"
85
- echo "[sentinel] ${name} stopped (PID $PID)"
86
- else
87
- echo "[sentinel] ${name} — PID $PID not running"
88
- fi
89
- rm -f "$PID_FILE"
90
- `, { mode: 0o755 });
91
- }
92
-
93
- // ── Workspace-level startAll / stopAll ────────────────────────────────────────
94
-
95
- function generateWorkspaceScripts(workspace) {
96
- // startAll.sh
97
- fs.writeFileSync(path.join(workspace, 'startAll.sh'), `#!/usr/bin/env bash
98
- # Start all Sentinel project instances
99
- WORKSPACE="$(cd "$(dirname "$0")" && pwd)"
100
- started=0
101
- for project_dir in "$WORKSPACE"/*/; do
102
- name=$(basename "$project_dir")
103
- [[ "$name" == "code" ]] && continue
104
- [[ -f "$project_dir/start.sh" ]] || continue
105
- bash "$project_dir/start.sh"
106
- started=$((started + 1))
107
- done
108
- echo "[sentinel] $started project(s) started"
109
- `, { mode: 0o755 });
110
-
111
- // stopAll.sh
112
- fs.writeFileSync(path.join(workspace, 'stopAll.sh'), `#!/usr/bin/env bash
113
- # Stop all Sentinel project instances
114
- WORKSPACE="$(cd "$(dirname "$0")" && pwd)"
115
- stopped=0
116
- for project_dir in "$WORKSPACE"/*/; do
117
- name=$(basename "$project_dir")
118
- [[ "$name" == "code" ]] && continue
119
- [[ -f "$project_dir/stop.sh" ]] || continue
120
- bash "$project_dir/stop.sh"
121
- stopped=$((stopped + 1))
122
- done
123
- echo "[sentinel] $stopped project(s) stopped"
124
- `, { mode: 0o755 });
125
- }
126
-
127
- module.exports = { writeExampleProject, generateProjectScripts, generateWorkspaceScripts };
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
- console.log(chalk.yellow(
105
- '\n Claude Code OAuth requires an interactive step.\n' +
106
- ' After setup completes, run this command on the server:\n\n' +
107
- chalk.bold(' claude\n\n') +
108
- ' It will print a URL like:\n' +
109
- ' https://claude.ai/oauth/authorize?...\n\n' +
110
- ' Open that URL in any browser, log in with your Claude Pro account,\n' +
111
- ' and the server will be authenticated. The token is stored in ~/.claude/\n' +
112
- ' and persists across restarts.\n'
113
- ));
114
- warn('OAuth not completed yet — run "claude" after setup to authenticate');
115
- } else {
116
- warn('Skipping auth — set ANTHROPIC_API_KEY in sentinel.properties or run "claude" to authenticate');
117
- }
118
-
119
- // ── Workspace structure ─────────────────────────────────────────────────────
120
- step('Creating workspace…');
121
- fs.ensureDirSync(workspace);
122
- ok(`Workspace: ${workspace}`);
123
-
124
- // ── Example project ─────────────────────────────────────────────────────────
125
- if (example) {
126
- step('Creating example project…');
127
- const exampleDir = path.join(workspace, 'my-project');
128
- writeExampleProject(exampleDir, codeDir, pythonBin, anthropicKey || '');
129
- ok(`Example project: ${exampleDir}`);
130
- }
131
-
132
- // ── Workspace start/stop scripts ─────────────────────────────────────────────
133
- step('Generating scripts…');
134
- generateWorkspaceScripts(workspace);
135
- ok(`${workspace}/startAll.sh`);
136
- ok(`${workspace}/stopAll.sh`);
137
-
138
- // ── systemd ──────────────────────────────────────────────────────────────────
139
- if (systemd) {
140
- step('Setting up systemd…');
141
- setupSystemd(workspace);
142
- }
143
-
144
- // ── Done ─────────────────────────────────────────────────────────────────────
145
- console.log('\n' + chalk.bold.green('══ Done! ══') + '\n');
146
- console.log(`${chalk.bold('Next steps:')}`);
147
- if (example) {
148
- console.log(`
149
- 1. Configure your first project:
150
- ${chalk.cyan(`${workspace}/my-project/config/sentinel.properties`)}
151
- ${chalk.cyan(`${workspace}/my-project/config/log-configs/`)}
152
- ${chalk.cyan(`${workspace}/my-project/config/repo-configs/`)}
153
-
154
- 2. First-time init (clones repos, tests SSH, sends test email):
155
- ${chalk.cyan(`${workspace}/my-project/init.sh`)}
156
-
157
- 3. Start all projects:
158
- ${chalk.cyan(`${workspace}/startAll.sh`)}
159
-
160
- 4. Stop all projects:
161
- ${chalk.cyan(`${workspace}/stopAll.sh`)}
162
- `);
163
- }
164
- if (systemd) {
165
- console.log(` Auto-start is enabled. To manage:
166
- ${chalk.cyan('sudo systemctl start sentinel')}
167
- ${chalk.cyan('sudo systemctl status sentinel')}
168
- ${chalk.cyan('journalctl -u sentinel -f')}
169
- `);
170
- }
171
- if (authMode === 'oauth') {
172
- console.log(
173
- chalk.bold.yellow(' ⚠ Complete Claude Code login now:\n') +
174
- ` ${chalk.bold.cyan('claude')}\n` +
175
- ' Open the URL it prints in any browser → log in with Claude Pro.\n'
176
- );
177
- }
178
-
179
- console.log(` Add another project anytime:
180
- ${chalk.cyan('sentinel add <project-name>')}
181
- `);
182
- };
183
-
184
- // ── Helpers ──────────────────────────────────────────────────────────────────
185
-
186
- function findPython() {
187
- for (const bin of ['python3', 'python']) {
188
- try {
189
- execSync(`${bin} --version`, { stdio: 'pipe' });
190
- return bin;
191
- } catch (_) {}
192
- }
193
- return null;
194
- }
195
-
196
- function run(bin, args) {
197
- const r = spawnSync(bin, args, { encoding: 'utf8' });
198
- return (r.stdout || '') + (r.stderr || '');
199
- }
200
-
201
- function runLive(bin, args) {
202
- const r = spawnSync(bin, args, { stdio: 'inherit' });
203
- if (r.status !== 0) {
204
- console.error(chalk.red(` Command failed: ${bin} ${args.join(' ')}`));
205
- process.exit(1);
206
- }
207
- }
208
-
209
- function installNpmGlobal(pkg, checkBin) {
210
- try {
211
- execSync(`${checkBin} --version`, { stdio: 'pipe' });
212
- ok(`${pkg} already installed`);
213
- } catch (_) {
214
- info(`Installing ${pkg}…`);
215
- runLive('npm', ['install', '-g', pkg]);
216
- ok(`${pkg} installed`);
217
- }
218
- }
219
-
220
- function setupSystemd(workspace) {
221
- const user = os.userInfo().username;
222
- const svc = `/etc/systemd/system/sentinel.service`;
223
- const content = `[Unit]
224
- Description=Sentinel — Autonomous DevOps Agent
225
- After=network-online.target
226
- Wants=network-online.target
227
-
228
- [Service]
229
- Type=forking
230
- User=${user}
231
- WorkingDirectory=${workspace}
232
- ExecStart=${workspace}/startAll.sh
233
- ExecStop=${workspace}/stopAll.sh
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.2",
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
+ }