@misterhuydo/sentinel 1.4.78 → 1.4.80
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/.cairn/.hint-lock +1 -1
- package/.cairn/session.json +2 -2
- package/lib/.cairn/minify-map.json +1 -8
- package/lib/add.js +57 -6
- package/lib/init.js +2 -91
- package/lib/slack-setup.js +113 -0
- package/package.json +1 -1
- package/lib/.cairn/views/2a85cc_init.js +0 -462
package/.cairn/.hint-lock
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-27T02:03:40.951Z
|
package/.cairn/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-03-
|
|
3
|
-
"checkpoint_at": "2026-03-
|
|
2
|
+
"message": "Auto-checkpoint at 2026-03-27T02:01:01.379Z",
|
|
3
|
+
"checkpoint_at": "2026-03-27T02:01:01.380Z",
|
|
4
4
|
"active_files": [],
|
|
5
5
|
"notes": [],
|
|
6
6
|
"mtime_snapshot": {}
|
package/lib/add.js
CHANGED
|
@@ -6,6 +6,7 @@ const { execSync, spawnSync } = require('child_process');
|
|
|
6
6
|
const prompts = require('prompts');
|
|
7
7
|
const chalk = require('chalk');
|
|
8
8
|
const { writeExampleProject, generateWorkspaceScripts, generateProjectScripts } = require('./generate');
|
|
9
|
+
const { printSlackSetupGuide } = require('./slack-setup');
|
|
9
10
|
const ok = msg => console.log(chalk.green(' ✔'), msg);
|
|
10
11
|
const info = msg => console.log(chalk.cyan(' →'), msg);
|
|
11
12
|
const warn = msg => console.log(chalk.yellow(' ⚠'), msg);
|
|
@@ -539,6 +540,38 @@ async function addFromGit(gitUrl, workspace) {
|
|
|
539
540
|
info('GITHUB_TOKEN will be written when project files are created');
|
|
540
541
|
}
|
|
541
542
|
|
|
543
|
+
// ── Project-specific Slack (optional override) ─────────────────────────────
|
|
544
|
+
const workspaceSlackToken = fs.existsSync(workspaceProps)
|
|
545
|
+
? (fs.readFileSync(workspaceProps, 'utf8').match(/^SLACK_BOT_TOKEN\s*=\s*(.+)$/m) || [])[1]?.trim()
|
|
546
|
+
: '';
|
|
547
|
+
const { ownSlack } = await prompts({
|
|
548
|
+
type: 'confirm', name: 'ownSlack',
|
|
549
|
+
message: workspaceSlackToken
|
|
550
|
+
? 'Does this project use a different Slack workspace than the default?'
|
|
551
|
+
: 'Does this project use Slack (Sentinel Boss)?',
|
|
552
|
+
initial: false,
|
|
553
|
+
}, { onCancel: () => process.exit(0) });
|
|
554
|
+
|
|
555
|
+
let projectSlackBotToken = '';
|
|
556
|
+
let projectSlackAppToken = '';
|
|
557
|
+
if (ownSlack) {
|
|
558
|
+
printSlackSetupGuide(false, 'Setting up project Slack Bot…');
|
|
559
|
+
const slackAnswers = await prompts([
|
|
560
|
+
{
|
|
561
|
+
type: 'password', name: 'botToken',
|
|
562
|
+
message: 'Slack Bot Token (xoxb-...)',
|
|
563
|
+
validate: v => !v || v.startsWith('xoxb-') ? true : 'Should start with xoxb-',
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
type: 'password', name: 'appToken',
|
|
567
|
+
message: 'Slack App-Level Token (xapp-...)',
|
|
568
|
+
validate: v => !v || v.startsWith('xapp-') ? true : 'Should start with xapp-',
|
|
569
|
+
},
|
|
570
|
+
], { onCancel: () => process.exit(0) });
|
|
571
|
+
projectSlackBotToken = slackAnswers.botToken || '';
|
|
572
|
+
projectSlackAppToken = slackAnswers.appToken || '';
|
|
573
|
+
}
|
|
574
|
+
|
|
542
575
|
// ── Preview + confirm ──────────────────────────────────────────────────────
|
|
543
576
|
step('Dry-run preview');
|
|
544
577
|
info(`Will create: ${projectDir}/`);
|
|
@@ -568,9 +601,6 @@ async function addFromGit(gitUrl, workspace) {
|
|
|
568
601
|
if (discovered.length > 0) {
|
|
569
602
|
// Config already exists in the cloned repo — just generate scripts
|
|
570
603
|
generateProjectScripts(projectDir, codeDir, pythonBin);
|
|
571
|
-
ok(`Project "${name}" ready at ${projectDir}`);
|
|
572
|
-
printNextSteps(projectDir, autoPublish);
|
|
573
|
-
await offerToStart(projectDir);
|
|
574
604
|
} else {
|
|
575
605
|
// No existing repo-configs — scaffold fresh project
|
|
576
606
|
writeExampleProject(projectDir, codeDir, pythonBin);
|
|
@@ -584,10 +614,31 @@ async function addFromGit(gitUrl, workspace) {
|
|
|
584
614
|
const example = path.join(repoDir, '_example.properties');
|
|
585
615
|
if (fs.existsSync(example)) fs.removeSync(example);
|
|
586
616
|
generateWorkspaceScripts(workspace, {}, {}, {}, effectiveToken);
|
|
587
|
-
ok(`Project "${name}" created at ${projectDir}`);
|
|
588
|
-
printNextSteps(projectDir, autoPublish);
|
|
589
|
-
await offerToStart(projectDir);
|
|
590
617
|
}
|
|
618
|
+
|
|
619
|
+
// Write project-level Slack tokens if collected
|
|
620
|
+
if (projectSlackBotToken || projectSlackAppToken) {
|
|
621
|
+
const projProps = path.join(projectDir, 'config', 'sentinel.properties');
|
|
622
|
+
if (fs.existsSync(projProps)) {
|
|
623
|
+
let props = fs.readFileSync(projProps, 'utf8');
|
|
624
|
+
if (projectSlackBotToken) {
|
|
625
|
+
props = /^#?\s*SLACK_BOT_TOKEN\s*=/m.test(props)
|
|
626
|
+
? props.replace(/^#?\s*SLACK_BOT_TOKEN\s*=.*/m, `SLACK_BOT_TOKEN=${projectSlackBotToken}`)
|
|
627
|
+
: props.trimEnd() + `\nSLACK_BOT_TOKEN=${projectSlackBotToken}\n`;
|
|
628
|
+
}
|
|
629
|
+
if (projectSlackAppToken) {
|
|
630
|
+
props = /^#?\s*SLACK_APP_TOKEN\s*=/m.test(props)
|
|
631
|
+
? props.replace(/^#?\s*SLACK_APP_TOKEN\s*=.*/m, `SLACK_APP_TOKEN=${projectSlackAppToken}`)
|
|
632
|
+
: props.trimEnd() + `\nSLACK_APP_TOKEN=${projectSlackAppToken}\n`;
|
|
633
|
+
}
|
|
634
|
+
fs.writeFileSync(projProps, props);
|
|
635
|
+
ok('Project-level Slack tokens saved to config/sentinel.properties');
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
ok(`Project "${name}" ${discovered.length > 0 ? 'ready' : 'created'} at ${projectDir}`);
|
|
640
|
+
printNextSteps(projectDir, autoPublish);
|
|
641
|
+
await offerToStart(projectDir);
|
|
591
642
|
}
|
|
592
643
|
|
|
593
644
|
// ── addFromName ───────────────────────────────────────────────────────────────
|
package/lib/init.js
CHANGED
|
@@ -7,6 +7,7 @@ const { execSync, spawnSync } = require('child_process');
|
|
|
7
7
|
const prompts = require('prompts');
|
|
8
8
|
const chalk = require('chalk');
|
|
9
9
|
const { generateProjectScripts, generateWorkspaceScripts, writeExampleProject } = require('./generate');
|
|
10
|
+
const { buildSlackManifest, printSlackSetupGuide } = require('./slack-setup');
|
|
10
11
|
|
|
11
12
|
const ok = msg => console.log(chalk.green(' ✔'), msg);
|
|
12
13
|
const info = msg => console.log(chalk.cyan(' →'), msg);
|
|
@@ -98,7 +99,7 @@ module.exports = async function init() {
|
|
|
98
99
|
|
|
99
100
|
// ── Slack app creation helper (shown before token prompts) ───────────────────
|
|
100
101
|
if (answers.setupSlack) {
|
|
101
|
-
printSlackSetupGuide();
|
|
102
|
+
printSlackSetupGuide(!!existing.SLACK_BOT_TOKEN);
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
const tokenAnswers = await prompts([
|
|
@@ -401,96 +402,6 @@ function ensureNpmUserPrefix() {
|
|
|
401
402
|
}
|
|
402
403
|
}
|
|
403
404
|
|
|
404
|
-
function buildSlackManifest() {
|
|
405
|
-
return {
|
|
406
|
-
display_information: {
|
|
407
|
-
name: 'Sentinel',
|
|
408
|
-
description: 'Autonomous DevOps Agent — monitors logs, fixes bugs, manages deployments',
|
|
409
|
-
background_color: '#1a1a2e',
|
|
410
|
-
},
|
|
411
|
-
features: {
|
|
412
|
-
bot_user: {
|
|
413
|
-
display_name: 'Sentinel',
|
|
414
|
-
always_online: true,
|
|
415
|
-
},
|
|
416
|
-
},
|
|
417
|
-
oauth_config: {
|
|
418
|
-
scopes: {
|
|
419
|
-
bot: [
|
|
420
|
-
'app_mentions:read', // receive @Sentinel mentions
|
|
421
|
-
'channels:history', // read public channel messages
|
|
422
|
-
'channels:read', // list channels
|
|
423
|
-
'chat:write', // post messages
|
|
424
|
-
'chat:write.customize', // post with custom name/icon
|
|
425
|
-
'chat:write.public', // post in channels without joining
|
|
426
|
-
'files:read', // read files shared with the bot
|
|
427
|
-
'files:write', // upload files (DB exports, reports)
|
|
428
|
-
'groups:history', // read private channel messages
|
|
429
|
-
'groups:read', // list private channels
|
|
430
|
-
'im:history', // read DM messages
|
|
431
|
-
'im:read', // list DMs
|
|
432
|
-
'im:write', // open DM channels (for user notifications)
|
|
433
|
-
'mpim:history', // read multi-person DMs
|
|
434
|
-
'mpim:read', // list multi-person DMs
|
|
435
|
-
'reactions:write', // add emoji reactions
|
|
436
|
-
'users:read', // look up user info / timezone
|
|
437
|
-
'users:read.email', // look up user by email
|
|
438
|
-
'usergroups:read', // resolve @here / @channel for admin checks
|
|
439
|
-
],
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
|
-
settings: {
|
|
443
|
-
event_subscriptions: {
|
|
444
|
-
bot_events: [
|
|
445
|
-
'app_mention', // @Sentinel in any channel
|
|
446
|
-
'message.channels', // messages in public channels (thread replies)
|
|
447
|
-
'message.groups', // messages in private channels (thread replies)
|
|
448
|
-
'message.im', // direct messages to Sentinel
|
|
449
|
-
'message.mpim', // multi-person DMs
|
|
450
|
-
],
|
|
451
|
-
},
|
|
452
|
-
interactivity: { is_enabled: false },
|
|
453
|
-
org_deploy_enabled: false,
|
|
454
|
-
socket_mode_enabled: true, // no public URL needed
|
|
455
|
-
token_rotation_enabled: false,
|
|
456
|
-
},
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function printSlackSetupGuide() {
|
|
461
|
-
const manifest = buildSlackManifest();
|
|
462
|
-
const manifestJson = JSON.stringify(manifest);
|
|
463
|
-
const encoded = encodeURIComponent(manifestJson);
|
|
464
|
-
const createUrl = `https://api.slack.com/apps?new_app=1&manifest_json=${encoded}`;
|
|
465
|
-
|
|
466
|
-
step('Setting up Slack Bot (Sentinel Boss)…');
|
|
467
|
-
console.log(`
|
|
468
|
-
${chalk.bold('One-click Slack app setup:')}
|
|
469
|
-
|
|
470
|
-
${chalk.cyan(createUrl)}
|
|
471
|
-
|
|
472
|
-
${chalk.bold('Steps after clicking the link:')}
|
|
473
|
-
${chalk.white('1.')} Slack opens with all permissions pre-filled → click ${chalk.green('"Create App"')}
|
|
474
|
-
${chalk.white('2.')} Click ${chalk.green('"Install to Workspace"')} → Allow
|
|
475
|
-
${chalk.white('3.')} Copy ${chalk.yellow('Bot Token')} (OAuth & Permissions → Bot User OAuth Token → xoxb-...)
|
|
476
|
-
${chalk.white('4.')} Go to ${chalk.cyan('Settings → Basic Information → App-Level Tokens')}
|
|
477
|
-
→ ${chalk.green('"Generate Token and Scopes"')} name it anything, add scope: ${chalk.yellow('connections:write')}
|
|
478
|
-
→ Copy ${chalk.yellow('App-Level Token')} (xapp-...)
|
|
479
|
-
${chalk.white('5.')} Paste both tokens below ↓
|
|
480
|
-
`);
|
|
481
|
-
|
|
482
|
-
// Try to open in browser (Linux/Mac/WSL)
|
|
483
|
-
try {
|
|
484
|
-
const { execSync: _exec } = require('child_process');
|
|
485
|
-
const opener = process.platform === 'darwin' ? 'open'
|
|
486
|
-
: process.platform === 'win32' ? 'start'
|
|
487
|
-
: 'xdg-open';
|
|
488
|
-
_exec(`${opener} "${createUrl}"`, { stdio: 'ignore' });
|
|
489
|
-
console.log(chalk.green(' ✔') + ' Opened in your browser\n');
|
|
490
|
-
} catch (_) {
|
|
491
|
-
// Silently ignore if browser open fails
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
405
|
|
|
495
406
|
function setupSystemd(workspace) {
|
|
496
407
|
const user = os.userInfo().username;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const step = msg => console.log('\n' + chalk.bold.white(msg));
|
|
5
|
+
|
|
6
|
+
function buildSlackManifest() {
|
|
7
|
+
return {
|
|
8
|
+
display_information: {
|
|
9
|
+
name: 'Sentinel',
|
|
10
|
+
description: 'Autonomous DevOps Agent — monitors logs, fixes bugs, manages deployments',
|
|
11
|
+
background_color: '#1a1a2e',
|
|
12
|
+
},
|
|
13
|
+
features: {
|
|
14
|
+
bot_user: {
|
|
15
|
+
display_name: 'Sentinel',
|
|
16
|
+
always_online: true,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
oauth_config: {
|
|
20
|
+
scopes: {
|
|
21
|
+
bot: [
|
|
22
|
+
'app_mentions:read', // receive @Sentinel mentions
|
|
23
|
+
'channels:history', // read public channel messages
|
|
24
|
+
'channels:read', // list channels
|
|
25
|
+
'chat:write', // post messages
|
|
26
|
+
'chat:write.customize', // post with custom name/icon
|
|
27
|
+
'chat:write.public', // post in channels without joining
|
|
28
|
+
'files:read', // read files shared with the bot
|
|
29
|
+
'files:write', // upload files (DB exports, reports)
|
|
30
|
+
'groups:history', // read private channel messages
|
|
31
|
+
'groups:read', // list private channels
|
|
32
|
+
'im:history', // read DM messages
|
|
33
|
+
'im:read', // list DMs
|
|
34
|
+
'im:write', // open DM channels (for user notifications)
|
|
35
|
+
'mpim:history', // read multi-person DMs
|
|
36
|
+
'mpim:read', // list multi-person DMs
|
|
37
|
+
'reactions:write', // add emoji reactions
|
|
38
|
+
'users:read', // look up user info / timezone
|
|
39
|
+
'users:read.email', // look up user by email
|
|
40
|
+
'usergroups:read', // resolve @here / @channel for admin checks
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
settings: {
|
|
45
|
+
event_subscriptions: {
|
|
46
|
+
bot_events: [
|
|
47
|
+
'app_mention', // @Sentinel in any channel
|
|
48
|
+
'message.channels', // messages in public channels (thread replies)
|
|
49
|
+
'message.groups', // messages in private channels (thread replies)
|
|
50
|
+
'message.im', // direct messages to Sentinel
|
|
51
|
+
'message.mpim', // multi-person DMs
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
interactivity: { is_enabled: false },
|
|
55
|
+
org_deploy_enabled: false,
|
|
56
|
+
socket_mode_enabled: true,
|
|
57
|
+
token_rotation_enabled: false,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Print Slack app setup guide.
|
|
64
|
+
* @param {boolean} hasExisting true = update existing app, false = create new
|
|
65
|
+
* @param {string} [stepTitle] optional override for the step heading
|
|
66
|
+
*/
|
|
67
|
+
function printSlackSetupGuide(hasExisting, stepTitle) {
|
|
68
|
+
const manifest = buildSlackManifest();
|
|
69
|
+
const manifestJson = JSON.stringify(manifest, null, 2);
|
|
70
|
+
|
|
71
|
+
step(stepTitle || 'Setting up Slack Bot (Sentinel Boss)…');
|
|
72
|
+
|
|
73
|
+
if (hasExisting) {
|
|
74
|
+
console.log(`
|
|
75
|
+
${chalk.bold('Update your existing Slack app manifest:')}
|
|
76
|
+
|
|
77
|
+
${chalk.white('1.')} Open: ${chalk.cyan('https://api.slack.com/apps')} → select your Sentinel app
|
|
78
|
+
${chalk.white('2.')} Click ${chalk.cyan('"App Manifest"')} in the left sidebar
|
|
79
|
+
${chalk.white('3.')} Replace the entire manifest with:
|
|
80
|
+
|
|
81
|
+
${manifestJson.split('\n').map(l => ' ' + l).join('\n')}
|
|
82
|
+
|
|
83
|
+
${chalk.white('4.')} Click ${chalk.green('"Save Changes"')} → reinstall to workspace if prompted
|
|
84
|
+
${chalk.white('5.')} Paste your tokens below ↓
|
|
85
|
+
`);
|
|
86
|
+
} else {
|
|
87
|
+
const createUrl = `https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
88
|
+
console.log(`
|
|
89
|
+
${chalk.bold('One-click Slack app setup:')}
|
|
90
|
+
|
|
91
|
+
${chalk.cyan(createUrl)}
|
|
92
|
+
|
|
93
|
+
${chalk.bold('Steps after clicking the link:')}
|
|
94
|
+
${chalk.white('1.')} Slack opens with all permissions pre-filled → click ${chalk.green('"Create App"')}
|
|
95
|
+
${chalk.white('2.')} Click ${chalk.green('"Install to Workspace"')} → Allow
|
|
96
|
+
${chalk.white('3.')} Copy ${chalk.yellow('Bot Token')} (OAuth & Permissions → Bot User OAuth Token → xoxb-...)
|
|
97
|
+
${chalk.white('4.')} Go to ${chalk.cyan('Settings → Basic Information → App-Level Tokens')}
|
|
98
|
+
→ ${chalk.green('"Generate Token and Scopes"')} name it anything, add scope: ${chalk.yellow('connections:write')}
|
|
99
|
+
→ Copy ${chalk.yellow('App-Level Token')} (xapp-...)
|
|
100
|
+
${chalk.white('5.')} Paste both tokens below ↓
|
|
101
|
+
`);
|
|
102
|
+
try {
|
|
103
|
+
const { execSync: _exec } = require('child_process');
|
|
104
|
+
const opener = process.platform === 'darwin' ? 'open'
|
|
105
|
+
: process.platform === 'win32' ? 'start'
|
|
106
|
+
: 'xdg-open';
|
|
107
|
+
_exec(`${opener} "${createUrl}"`, { stdio: 'ignore' });
|
|
108
|
+
console.log(chalk.green(' ✔') + ' Opened in your browser\n');
|
|
109
|
+
} catch (_) {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { buildSlackManifest, printSlackSetupGuide };
|
package/package.json
CHANGED
|
@@ -1,462 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const fs = require('fs-extra');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const { execSync, spawnSync } = require('child_process');
|
|
6
|
-
const prompts = require('prompts');
|
|
7
|
-
const chalk = require('chalk');
|
|
8
|
-
const { generateProjectScripts, generateWorkspaceScripts, writeExampleProject } = require('./generate');
|
|
9
|
-
const ok = msg => console.log(chalk.green(' ✔'), msg);
|
|
10
|
-
const info = msg => console.log(chalk.cyan(' →'), msg);
|
|
11
|
-
const warn = msg => console.log(chalk.yellow(' ⚠'), msg);
|
|
12
|
-
const step = msg => console.log('\n' + chalk.bold.white(msg));
|
|
13
|
-
module.exports = async function init() {
|
|
14
|
-
const defaultWorkspace = path.join(os.homedir(), 'sentinel');
|
|
15
|
-
const existing = readExistingConfig(defaultWorkspace);
|
|
16
|
-
if (Object.keys(existing).length) {
|
|
17
|
-
console.log(chalk.cyan('\n → Existing workspace config found — showing current values as defaults\n'));
|
|
18
|
-
}
|
|
19
|
-
const answers = await prompts([
|
|
20
|
-
{
|
|
21
|
-
type: 'text',
|
|
22
|
-
name: 'workspace',
|
|
23
|
-
message: 'Workspace directory (each project lives here as a subdirectory)',
|
|
24
|
-
initial: path.join(os.homedir(), 'sentinel'),
|
|
25
|
-
format: v => v.replace(/^~/, os.homedir()),
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
type: 'select',
|
|
29
|
-
name: 'authMode',
|
|
30
|
-
message: 'Claude authentication strategy',
|
|
31
|
-
hint: 'Boss = Slack AI; Fix Engine = autonomous code repair',
|
|
32
|
-
choices: [
|
|
33
|
-
{
|
|
34
|
-
title: 'Both (RECOMMENDED) — API key for Boss, Claude Pro for Fix Engine',
|
|
35
|
-
value: 'both',
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
title: 'API key only — full Boss tools; Fix Engine billed per token',
|
|
39
|
-
value: 'apikey',
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
title: 'Claude Pro / OAuth only — run `claude login`; Boss has limited tools',
|
|
43
|
-
value: 'oauth',
|
|
44
|
-
},
|
|
45
|
-
{ title: 'Skip (configure manually in sentinel.properties)', value: 'skip' },
|
|
46
|
-
],
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
type: prev => (prev === 'apikey' || prev === 'both') ? 'password' : null,
|
|
50
|
-
name: 'anthropicKey',
|
|
51
|
-
message: existing.ANTHROPIC_API_KEY
|
|
52
|
-
? 'Anthropic API key (press Enter to keep current)'
|
|
53
|
-
: 'Anthropic API key (sk-ant-...)',
|
|
54
|
-
validate: v => !v || v.startsWith('sk-ant-') ? true : 'Key should start with sk-ant-',
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
type: 'confirm',
|
|
58
|
-
name: 'example',
|
|
59
|
-
message: 'Create an example project to show how to configure?',
|
|
60
|
-
initial: true,
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
type: 'confirm',
|
|
64
|
-
name: 'systemd',
|
|
65
|
-
message: 'Set up systemd service for auto-start on reboot?',
|
|
66
|
-
initial: process.platform === 'linux',
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
type: 'text',
|
|
70
|
-
name: 'smtpUser',
|
|
71
|
-
message: 'SMTP sender address',
|
|
72
|
-
initial: existing.SMTP_USER || 'sentinel@yourdomain.com',
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
type: prev => prev ? 'password' : null,
|
|
76
|
-
name: 'smtpPassword',
|
|
77
|
-
message: existing.SMTP_PASSWORD ? 'SMTP password / app password (press Enter to keep current)' : 'SMTP password / app password',
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
type: prev => prev ? 'text' : null,
|
|
81
|
-
name: 'smtpHost',
|
|
82
|
-
message: 'SMTP host',
|
|
83
|
-
initial: existing.SMTP_HOST || 'smtp.gmail.com',
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
type: 'confirm',
|
|
87
|
-
name: 'setupSlack',
|
|
88
|
-
message: 'Set up Slack Bot (Sentinel Boss — conversational AI interface)?',
|
|
89
|
-
initial: !!(existing.SLACK_BOT_TOKEN),
|
|
90
|
-
},
|
|
91
|
-
], { onCancel: () => process.exit(0) });
|
|
92
|
-
if (answers.setupSlack && !existing.SLACK_BOT_TOKEN) {
|
|
93
|
-
printSlackSetupGuide();
|
|
94
|
-
}
|
|
95
|
-
const tokenAnswers = await prompts([
|
|
96
|
-
{
|
|
97
|
-
type: answers.setupSlack ? 'password' : null,
|
|
98
|
-
name: 'slackBotToken',
|
|
99
|
-
message: existing.SLACK_BOT_TOKEN ? 'Slack Bot Token (press Enter to keep current)' : 'Slack Bot Token (xoxb-...)',
|
|
100
|
-
validate: v => !v || v.startsWith('xoxb-') ? true : 'Should start with xoxb-',
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
type: answers.setupSlack ? 'password' : null,
|
|
104
|
-
name: 'slackAppToken',
|
|
105
|
-
message: existing.SLACK_APP_TOKEN ? 'Slack App-Level Token (press Enter to keep current)' : 'Slack App-Level Token (xapp-...)',
|
|
106
|
-
validate: v => !v || v.startsWith('xapp-') ? true : 'Should start with xapp-',
|
|
107
|
-
},
|
|
108
|
-
], { onCancel: () => process.exit(0) });
|
|
109
|
-
Object.assign(answers, tokenAnswers);
|
|
110
|
-
const { workspace, authMode, anthropicKey, example, systemd, smtpUser, smtpPassword, smtpHost, setupSlack, slackBotToken, slackAppToken } = answers;
|
|
111
|
-
const effectiveAnthropicKey = anthropicKey || existing.ANTHROPIC_API_KEY || '';
|
|
112
|
-
const effectiveSmtpPassword = smtpPassword || existing.SMTP_PASSWORD || '';
|
|
113
|
-
const effectiveSlackBotToken = slackBotToken || existing.SLACK_BOT_TOKEN || '';
|
|
114
|
-
const effectiveSlackAppToken = slackAppToken || existing.SLACK_APP_TOKEN || '';
|
|
115
|
-
const codeDir = path.join(workspace, 'code');
|
|
116
|
-
step('Checking npm permissions…');
|
|
117
|
-
ensureNpmUserPrefix();
|
|
118
|
-
step('Checking Python…');
|
|
119
|
-
const python = findPython();
|
|
120
|
-
if (!python) {
|
|
121
|
-
console.error(chalk.red(' ✖ python3 not found. Install it first:'));
|
|
122
|
-
console.error(' sudo apt-get install -y python3 python3-pip python3-venv');
|
|
123
|
-
process.exit(1);
|
|
124
|
-
}
|
|
125
|
-
ok(`Python: ${run(python, ['--version']).trim()}`);
|
|
126
|
-
step('Installing Sentinel code…');
|
|
127
|
-
const bundledPython = path.join(__dirname, '..', 'python');
|
|
128
|
-
if (!fs.existsSync(bundledPython)) {
|
|
129
|
-
console.error(chalk.red(' ✖ Bundled Python source not found (run npm publish from the Sentinel repo)'));
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}
|
|
132
|
-
fs.ensureDirSync(codeDir);
|
|
133
|
-
fs.copySync(bundledPython, codeDir, { overwrite: true });
|
|
134
|
-
ok(`Sentinel code → ${codeDir}`);
|
|
135
|
-
step('Setting up Python environment…');
|
|
136
|
-
const venv = path.join(codeDir, '.venv');
|
|
137
|
-
if (!fs.existsSync(venv)) {
|
|
138
|
-
info('Creating virtual environment…');
|
|
139
|
-
runLive(python, ['-m', 'venv', venv]);
|
|
140
|
-
}
|
|
141
|
-
const pip = path.join(venv, 'bin', 'pip3');
|
|
142
|
-
const pythonBin = path.join(venv, 'bin', 'python3');
|
|
143
|
-
info('Installing Python packages…');
|
|
144
|
-
runLive(pip, ['install', '--quiet', '--upgrade', 'pip']);
|
|
145
|
-
runLive(pip, ['install', '--quiet', '-r', path.join(codeDir, 'requirements.txt')]);
|
|
146
|
-
ok('Python packages installed');
|
|
147
|
-
step('Installing Node tools…');
|
|
148
|
-
installNpmGlobal('@misterhuydo/cairn-mcp', 'cairn');
|
|
149
|
-
step('Hooking Cairn MCP into Claude Code…');
|
|
150
|
-
runLive('cairn', ['install']);
|
|
151
|
-
ok('cairn install complete');
|
|
152
|
-
installNpmGlobal('@anthropic-ai/claude-code', 'claude');
|
|
153
|
-
step('Patching Claude Code permissions…');
|
|
154
|
-
ensureClaudePermissions();
|
|
155
|
-
step('Claude Code authentication…');
|
|
156
|
-
if (authMode === 'both' && effectiveAnthropicKey) {
|
|
157
|
-
ok('API key → Sentinel Boss (full tools, structured responses)');
|
|
158
|
-
ok('Claude Pro → Fix Engine + Ask Codebase (heavy coding, Pro subscription)');
|
|
159
|
-
info('Run `claude login` on this server now (or before starting projects)');
|
|
160
|
-
info('CLAUDE_PRO_FOR_TASKS=true written to workspace sentinel.properties');
|
|
161
|
-
} else if (authMode === 'apikey' && effectiveAnthropicKey) {
|
|
162
|
-
ok('API key → all Claude usage (Boss + Fix Engine billed to your API quota)');
|
|
163
|
-
info('CLAUDE_PRO_FOR_TASKS=false written — Fix Engine will use API key');
|
|
164
|
-
warn('Heavy fix tasks will consume API tokens. Claude Pro is cheaper for those.');
|
|
165
|
-
} else if (authMode === 'oauth') {
|
|
166
|
-
ok('Claude Pro / OAuth → Fix Engine + Ask Codebase');
|
|
167
|
-
warn('Boss will use CLI fallback — some tools unavailable without an API key');
|
|
168
|
-
info('Run `claude login` on this server to authenticate');
|
|
169
|
-
} else {
|
|
170
|
-
warn('No auth configured — add ANTHROPIC_API_KEY or run `claude login` before starting');
|
|
171
|
-
info('See: workspace sentinel.properties for full auth documentation');
|
|
172
|
-
}
|
|
173
|
-
if (effectiveSlackBotToken && effectiveSlackAppToken) {
|
|
174
|
-
step('Slack Bot (Sentinel Boss)…');
|
|
175
|
-
ok('Tokens will be written to workspace sentinel.properties');
|
|
176
|
-
info('Sentinel Boss starts automatically when the project starts');
|
|
177
|
-
} else if (setupSlack) {
|
|
178
|
-
warn('Slack tokens not provided — add them to sentinel.properties later');
|
|
179
|
-
info('Re-run sentinel init after creating the app to fill in the tokens');
|
|
180
|
-
}
|
|
181
|
-
step('Creating workspace…');
|
|
182
|
-
fs.ensureDirSync(workspace);
|
|
183
|
-
ok(`Workspace: ${workspace}`);
|
|
184
|
-
if (example) {
|
|
185
|
-
step('Creating example project…');
|
|
186
|
-
const exampleDir = path.join(workspace, 'my-project');
|
|
187
|
-
writeExampleProject(exampleDir, codeDir, pythonBin, effectiveAnthropicKey, { botToken: effectiveSlackBotToken, appToken: effectiveSlackAppToken });
|
|
188
|
-
ok(`Example project: ${exampleDir}`);
|
|
189
|
-
}
|
|
190
|
-
step('Generating scripts…');
|
|
191
|
-
const authConfig = {};
|
|
192
|
-
if (effectiveAnthropicKey) authConfig.apiKey = effectiveAnthropicKey;
|
|
193
|
-
if (authMode === 'both' || authMode === 'oauth') authConfig.claudeProForTasks = true;
|
|
194
|
-
if (authMode === 'apikey') authConfig.claudeProForTasks = false;
|
|
195
|
-
generateWorkspaceScripts(workspace, { host: smtpHost, user: smtpUser, password: effectiveSmtpPassword }, { botToken: effectiveSlackBotToken, appToken: effectiveSlackAppToken }, authConfig);
|
|
196
|
-
ok(`${workspace}/startAll.sh`);
|
|
197
|
-
ok(`${workspace}/stopAll.sh`);
|
|
198
|
-
if (systemd) {
|
|
199
|
-
step('Setting up systemd…');
|
|
200
|
-
setupSystemd(workspace);
|
|
201
|
-
}
|
|
202
|
-
console.log('\n' + chalk.bold.green('══ Done! ══') + '\n');
|
|
203
|
-
console.log(`${chalk.bold('Next steps:')}`);
|
|
204
|
-
if (example) {
|
|
205
|
-
console.log(`
|
|
206
|
-
1. Configure your first project:
|
|
207
|
-
${chalk.cyan(`${workspace}/my-project/config/sentinel.properties`)}
|
|
208
|
-
${chalk.cyan(`${workspace}/my-project/config/log-configs/`)}
|
|
209
|
-
${chalk.cyan(`${workspace}/my-project/config/repo-configs/`)}
|
|
210
|
-
2. Start all projects (Sentinel clones repos, indexes, and monitors automatically):
|
|
211
|
-
${chalk.cyan(`${workspace}/startAll.sh`)}
|
|
212
|
-
3. Stop all projects:
|
|
213
|
-
${chalk.cyan(`${workspace}/stopAll.sh`)}
|
|
214
|
-
`);
|
|
215
|
-
}
|
|
216
|
-
if (systemd) {
|
|
217
|
-
console.log(` Auto-start is enabled. To manage:
|
|
218
|
-
${chalk.cyan('sudo systemctl start sentinel')}
|
|
219
|
-
${chalk.cyan('sudo systemctl status sentinel')}
|
|
220
|
-
${chalk.cyan('journalctl -u sentinel -f')}
|
|
221
|
-
`);
|
|
222
|
-
}
|
|
223
|
-
console.log(` Add another project anytime:
|
|
224
|
-
${chalk.cyan('sentinel add <project-name>')}
|
|
225
|
-
`);
|
|
226
|
-
};
|
|
227
|
-
function readExistingConfig(workspace) {
|
|
228
|
-
const result = {};
|
|
229
|
-
_parsePropsInto(path.join(workspace, 'sentinel.properties'), result);
|
|
230
|
-
if (!result.SLACK_BOT_TOKEN || !result.SLACK_APP_TOKEN) {
|
|
231
|
-
try {
|
|
232
|
-
for (const entry of fs.readdirSync(workspace)) {
|
|
233
|
-
const p = path.join(workspace, entry, 'config', 'sentinel.properties');
|
|
234
|
-
const proj = {};
|
|
235
|
-
_parsePropsInto(p, proj);
|
|
236
|
-
if (!result.SLACK_BOT_TOKEN && proj.SLACK_BOT_TOKEN) result.SLACK_BOT_TOKEN = proj.SLACK_BOT_TOKEN;
|
|
237
|
-
if (!result.SLACK_APP_TOKEN && proj.SLACK_APP_TOKEN) result.SLACK_APP_TOKEN = proj.SLACK_APP_TOKEN;
|
|
238
|
-
if (result.SLACK_BOT_TOKEN && result.SLACK_APP_TOKEN) break;
|
|
239
|
-
}
|
|
240
|
-
} catch (_) {}
|
|
241
|
-
}
|
|
242
|
-
return result;
|
|
243
|
-
}
|
|
244
|
-
function _parsePropsInto(propsPath, result) {
|
|
245
|
-
if (!fs.existsSync(propsPath)) return;
|
|
246
|
-
try {
|
|
247
|
-
const lines = fs.readFileSync(propsPath, 'utf8').split(/\r?\n/);
|
|
248
|
-
for (const raw of lines) {
|
|
249
|
-
const line = raw.trim();
|
|
250
|
-
if (!line || line.startsWith('#')) continue;
|
|
251
|
-
const idx = line.indexOf('=');
|
|
252
|
-
if (idx === -1) continue;
|
|
253
|
-
result[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
254
|
-
}
|
|
255
|
-
} catch (_) {}
|
|
256
|
-
}
|
|
257
|
-
function findPython() {
|
|
258
|
-
for (const bin of ['python3', 'python']) {
|
|
259
|
-
try {
|
|
260
|
-
execSync(`${bin} --version`, { stdio: 'pipe' });
|
|
261
|
-
return bin;
|
|
262
|
-
} catch (_) {}
|
|
263
|
-
}
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
function run(bin, args) {
|
|
267
|
-
const r = spawnSync(bin, args, { encoding: 'utf8' });
|
|
268
|
-
return (r.stdout || '') + (r.stderr || '');
|
|
269
|
-
}
|
|
270
|
-
function runLive(bin, args) {
|
|
271
|
-
const r = spawnSync(bin, args, { stdio: 'inherit' });
|
|
272
|
-
if (r.status !== 0) {
|
|
273
|
-
console.error(chalk.red(` ✖ Command failed: ${bin} ${args.join(' ')}`));
|
|
274
|
-
process.exit(1);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
function installNpmGlobal(pkg, checkBin) {
|
|
278
|
-
try {
|
|
279
|
-
execSync(`${checkBin} --version`, { stdio: 'pipe' });
|
|
280
|
-
ok(`${pkg} already installed`);
|
|
281
|
-
} catch (_) {
|
|
282
|
-
info(`Installing ${pkg}…`);
|
|
283
|
-
runLive('npm', ['install', '-g', pkg]);
|
|
284
|
-
ok(`${pkg} installed`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
function ensureClaudePermissions() {
|
|
288
|
-
const settingsPath = require('path').join(require('os').homedir(), '.claude', 'settings.json');
|
|
289
|
-
const required = ['Read(**)', 'Write(**)', 'Edit(**)', 'Bash(**)'];
|
|
290
|
-
let settings = {};
|
|
291
|
-
try {
|
|
292
|
-
if (fs.existsSync(settingsPath)) {
|
|
293
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
294
|
-
}
|
|
295
|
-
} catch (e) {
|
|
296
|
-
warn('Could not read ' + settingsPath + ': ' + e.message);
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
if (!settings.permissions) settings.permissions = {};
|
|
300
|
-
if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = [];
|
|
301
|
-
const existing = new Set(settings.permissions.allow);
|
|
302
|
-
const added = [];
|
|
303
|
-
for (const perm of required) {
|
|
304
|
-
if (!existing.has(perm)) {
|
|
305
|
-
settings.permissions.allow.push(perm);
|
|
306
|
-
added.push(perm);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
if (added.length === 0) {
|
|
310
|
-
ok('Claude Code permissions already configured');
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
try {
|
|
314
|
-
fs.ensureDirSync(require('path').dirname(settingsPath));
|
|
315
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
316
|
-
ok('Claude Code permissions patched: ' + added.join(', '));
|
|
317
|
-
} catch (e) {
|
|
318
|
-
warn('Could not write ' + settingsPath + ': ' + e.message);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
function ensureNpmUserPrefix() {
|
|
322
|
-
try {
|
|
323
|
-
const currentPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
|
324
|
-
const homeDir = os.homedir();
|
|
325
|
-
if (currentPrefix.startsWith(homeDir)) {
|
|
326
|
-
ok(`npm prefix: ${currentPrefix} (user-owned, no sudo needed)`);
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
try { fs.accessSync(currentPrefix, fs.constants.W_OK); ok('npm prefix writable'); return; }
|
|
330
|
-
catch (_) {}
|
|
331
|
-
const userPrefix = path.join(homeDir, '.npm-global');
|
|
332
|
-
fs.ensureDirSync(userPrefix);
|
|
333
|
-
execSync(`npm config set prefix "${userPrefix}"`, { encoding: 'utf8' });
|
|
334
|
-
ok(`npm prefix set to ${userPrefix} (no sudo needed for upgrades)`);
|
|
335
|
-
const exportLine = `export PATH="${userPrefix}/bin:$PATH"`;
|
|
336
|
-
for (const rc of ['.bashrc', '.profile', '.zshrc']) {
|
|
337
|
-
const rcPath = path.join(homeDir, rc);
|
|
338
|
-
if (!fs.existsSync(rcPath)) continue;
|
|
339
|
-
const content = fs.readFileSync(rcPath, 'utf8');
|
|
340
|
-
if (!content.includes(userPrefix)) {
|
|
341
|
-
fs.appendFileSync(rcPath, `\n# Added by sentinel init\n${exportLine}\n`);
|
|
342
|
-
ok(`PATH updated in ~/${rc}`);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
warn(`Run: export PATH="${userPrefix}/bin:$PATH" (or open a new shell)`);
|
|
346
|
-
warn(`Then reinstall: npm install -g @misterhuydo/sentinel`);
|
|
347
|
-
} catch (e) {
|
|
348
|
-
warn(`Could not check npm prefix: ${e.message}`);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
function buildSlackManifest() {
|
|
352
|
-
return {
|
|
353
|
-
display_information: {
|
|
354
|
-
name: 'Sentinel',
|
|
355
|
-
description: 'Autonomous DevOps Agent — monitors logs, fixes bugs, manages deployments',
|
|
356
|
-
background_color: '#1a1a2e',
|
|
357
|
-
},
|
|
358
|
-
features: {
|
|
359
|
-
bot_user: {
|
|
360
|
-
display_name: 'Sentinel',
|
|
361
|
-
always_online: true,
|
|
362
|
-
},
|
|
363
|
-
},
|
|
364
|
-
oauth_config: {
|
|
365
|
-
scopes: {
|
|
366
|
-
bot: [
|
|
367
|
-
'app_mentions:read',
|
|
368
|
-
'channels:history',
|
|
369
|
-
'channels:read',
|
|
370
|
-
'chat:write',
|
|
371
|
-
'chat:write.customize',
|
|
372
|
-
'chat:write.public',
|
|
373
|
-
'files:read',
|
|
374
|
-
'files:write',
|
|
375
|
-
'groups:history',
|
|
376
|
-
'groups:read',
|
|
377
|
-
'im:history',
|
|
378
|
-
'im:read',
|
|
379
|
-
'im:write',
|
|
380
|
-
'mpim:history',
|
|
381
|
-
'mpim:read',
|
|
382
|
-
'reactions:write',
|
|
383
|
-
'users:read',
|
|
384
|
-
'users:read.email',
|
|
385
|
-
'usergroups:read',
|
|
386
|
-
],
|
|
387
|
-
},
|
|
388
|
-
},
|
|
389
|
-
settings: {
|
|
390
|
-
event_subscriptions: {
|
|
391
|
-
bot_events: [
|
|
392
|
-
'app_mention',
|
|
393
|
-
'message.channels',
|
|
394
|
-
'message.groups',
|
|
395
|
-
'message.im',
|
|
396
|
-
'message.mpim',
|
|
397
|
-
],
|
|
398
|
-
},
|
|
399
|
-
interactivity: { is_enabled: false },
|
|
400
|
-
org_deploy_enabled: false,
|
|
401
|
-
socket_mode_enabled: true,
|
|
402
|
-
token_rotation_enabled: false,
|
|
403
|
-
},
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
function printSlackSetupGuide() {
|
|
407
|
-
const manifest = buildSlackManifest();
|
|
408
|
-
const manifestJson = JSON.stringify(manifest);
|
|
409
|
-
const encoded = encodeURIComponent(manifestJson);
|
|
410
|
-
const createUrl = `https://api.slack.com/apps?new_app=1&manifest_json=${encoded}`;
|
|
411
|
-
step('Setting up Slack Bot (Sentinel Boss)…');
|
|
412
|
-
console.log(`
|
|
413
|
-
${chalk.bold('One-click Slack app setup:')}
|
|
414
|
-
${chalk.cyan(createUrl)}
|
|
415
|
-
${chalk.bold('Steps after clicking the link:')}
|
|
416
|
-
${chalk.white('1.')} Slack opens with all permissions pre-filled → click ${chalk.green('"Create App"')}
|
|
417
|
-
${chalk.white('2.')} Click ${chalk.green('"Install to Workspace"')} → Allow
|
|
418
|
-
${chalk.white('3.')} Copy ${chalk.yellow('Bot Token')} (OAuth & Permissions → Bot User OAuth Token → xoxb-...)
|
|
419
|
-
${chalk.white('4.')} Go to ${chalk.cyan('Settings → Basic Information → App-Level Tokens')}
|
|
420
|
-
→ ${chalk.green('"Generate Token and Scopes"')} name it anything, add scope: ${chalk.yellow('connections:write')}
|
|
421
|
-
→ Copy ${chalk.yellow('App-Level Token')} (xapp-...)
|
|
422
|
-
${chalk.white('5.')} Paste both tokens below ↓
|
|
423
|
-
`);
|
|
424
|
-
try {
|
|
425
|
-
const { execSync: _exec } = require('child_process');
|
|
426
|
-
const opener = process.platform === 'darwin' ? 'open'
|
|
427
|
-
: process.platform === 'win32' ? 'start'
|
|
428
|
-
: 'xdg-open';
|
|
429
|
-
_exec(`${opener} "${createUrl}"`, { stdio: 'ignore' });
|
|
430
|
-
console.log(chalk.green(' ✔') + ' Opened in your browser\n');
|
|
431
|
-
} catch (_) {
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
function setupSystemd(workspace) {
|
|
435
|
-
const user = os.userInfo().username;
|
|
436
|
-
const svc = `/etc/systemd/system/sentinel.service`;
|
|
437
|
-
const content = `[Unit]
|
|
438
|
-
Description=Sentinel — Autonomous DevOps Agent
|
|
439
|
-
After=network-online.target
|
|
440
|
-
Wants=network-online.target
|
|
441
|
-
[Service]
|
|
442
|
-
Type=forking
|
|
443
|
-
User=${user}
|
|
444
|
-
WorkingDirectory=${workspace}
|
|
445
|
-
ExecStart=${workspace}/startAll.sh
|
|
446
|
-
ExecStop=${workspace}/stopAll.sh
|
|
447
|
-
Restart=on-failure
|
|
448
|
-
RestartSec=10
|
|
449
|
-
[Install]
|
|
450
|
-
WantedBy=multi-user.target
|
|
451
|
-
`;
|
|
452
|
-
try {
|
|
453
|
-
fs.writeFileSync('/tmp/sentinel.service', content);
|
|
454
|
-
execSync(`sudo mv /tmp/sentinel.service ${svc}`);
|
|
455
|
-
execSync('sudo systemctl daemon-reload');
|
|
456
|
-
execSync('sudo systemctl enable sentinel');
|
|
457
|
-
ok('sentinel.service enabled');
|
|
458
|
-
} catch (e) {
|
|
459
|
-
warn(`Could not write systemd service (need sudo): ${e.message}`);
|
|
460
|
-
warn(`Manually create ${svc} to auto-start on reboot`);
|
|
461
|
-
}
|
|
462
|
-
}
|