@misterhuydo/sentinel 1.3.1 → 1.3.3

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 CHANGED
@@ -1 +1 @@
1
- 2026-03-23T17:50:08.240Z
1
+ 2026-03-23T18:20:09.151Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-23T17:39:48.232Z",
3
- "checkpoint_at": "2026-03-23T17:39:48.233Z",
2
+ "message": "Auto-checkpoint at 2026-03-23T18:08:05.159Z",
3
+ "checkpoint_at": "2026-03-23T18:08:05.160Z",
4
4
  "active_files": [],
5
5
  "notes": [],
6
6
  "mtime_snapshot": {}
package/lib/add.js CHANGED
@@ -129,7 +129,7 @@ function ensureKnownHosts() {
129
129
  const content = fs.existsSync(knownHosts) ? fs.readFileSync(knownHosts, 'utf8') : '';
130
130
  if (content.includes('github.com')) return;
131
131
  info('Adding GitHub to known_hosts…');
132
- const r = spawnSync('ssh-keyscan', ['github.com'], { encoding: 'utf8', timeout: 10000 });
132
+ const r = spawnSync('ssh-keyscan', ['github.com'], { encoding: 'utf8', timeout: 10000, env: gitEnv() });
133
133
  if (r.stdout) fs.appendFileSync(knownHosts, r.stdout);
134
134
  }
135
135
 
@@ -175,6 +175,15 @@ function printDeployKeyInstructions(orgRepo, keyFile) {
175
175
  console.log('');
176
176
  }
177
177
 
178
+ // Ensure git/ssh are findable regardless of how Node was launched
179
+ function gitEnv(extra = {}) {
180
+ const PATH = [
181
+ process.env.PATH || '',
182
+ '/usr/bin', '/usr/local/bin', '/bin', '/usr/sbin',
183
+ ].filter(Boolean).join(':');
184
+ return { ...process.env, PATH, GIT_TERMINAL_PROMPT: '0', ...extra };
185
+ }
186
+
178
187
  // ── repo discovery helpers ────────────────────────────────────────────────────
179
188
 
180
189
  function gitUrlToOrgRepo(gitUrl) {
@@ -191,16 +200,18 @@ function toHttpsUrl(gitUrl) {
191
200
  function isPublicRepo(gitUrl) {
192
201
  const r = spawnSync('git', ['ls-remote', '--heads', toHttpsUrl(gitUrl)], {
193
202
  encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'],
194
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
203
+ env: gitEnv(),
195
204
  });
196
205
  return r.status === 0;
197
206
  }
198
207
 
199
208
  function validateAccess(repoUrl, keyFile) {
200
- const env = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
201
- if (keyFile) env.GIT_SSH_COMMAND = `ssh -i ${keyFile} -o StrictHostKeyChecking=no -o BatchMode=yes`;
209
+ const extra = keyFile
210
+ ? { GIT_SSH_COMMAND: `ssh -i ${keyFile} -o StrictHostKeyChecking=no -o BatchMode=yes` }
211
+ : {};
202
212
  const r = spawnSync('git', ['ls-remote', '--heads', repoUrl], {
203
- encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'], env,
213
+ encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
214
+ env: gitEnv(extra),
204
215
  });
205
216
  return { ok: r.status === 0, stderr: (r.stderr || r.error?.message || '').trim() };
206
217
  }
@@ -258,10 +269,10 @@ async function addFromGit(gitUrl, workspace) {
258
269
  step(`[2/3] Scanning repo-configs in ${repoSlug}…`);
259
270
 
260
271
  if (!fs.existsSync(localPath)) {
261
- const cloneEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0',
262
- GIT_SSH_COMMAND: `ssh -i ${keyFile} -o StrictHostKeyChecking=no -o BatchMode=yes` };
263
- spawnSync('git', ['clone', '--depth', '1', gitUrl, localPath],
264
- { stdio: 'inherit', env: cloneEnv });
272
+ spawnSync('git', ['clone', '--depth', '1', gitUrl, localPath], {
273
+ stdio: 'inherit',
274
+ env: gitEnv({ GIT_SSH_COMMAND: `ssh -i ${keyFile} -o StrictHostKeyChecking=no -o BatchMode=yes` }),
275
+ });
265
276
  }
266
277
 
267
278
  const discovered = discoverReposFromClone(localPath);
@@ -364,6 +375,51 @@ async function addFromGit(gitUrl, workspace) {
364
375
  warn('AUTO_PUBLISH=true: fixes push directly to main. Ensure CI blocks bad pushes.');
365
376
  }
366
377
 
378
+ // ── GitHub token ───────────────────────────────────────────────────────────
379
+ // AUTO_PUBLISH=false → required (open PRs via API)
380
+ // AUTO_PUBLISH=true → optional (CI/CD triggers only)
381
+ const workspaceProps = path.join(workspace, 'sentinel.properties');
382
+ const existingToken = fs.existsSync(workspaceProps)
383
+ ? (fs.readFileSync(workspaceProps, 'utf8').match(/^GITHUB_TOKEN\s*=\s*(.+)$/m) || [])[1]?.trim()
384
+ : '';
385
+
386
+ if (!autoPublish) {
387
+ // PR mode — token is required
388
+ console.log('');
389
+ console.log(chalk.bold(' GitHub Personal Access Token (classic) — required for opening PRs'));
390
+ console.log(chalk.cyan(' github.com/settings/tokens/new → Tokens (classic)'));
391
+ console.log(chalk.cyan(' Note: "Expiration → No expiration" Scope: ✓ repo'));
392
+ console.log('');
393
+ }
394
+
395
+ const { githubToken } = await prompts({
396
+ type: (!autoPublish || !existingToken) ? 'password' : null,
397
+ name: 'githubToken',
398
+ message: existingToken
399
+ ? 'GitHub token (press Enter to keep current)'
400
+ : autoPublish
401
+ ? 'GitHub token (classic, repo scope) — optional, press Enter to skip'
402
+ : 'GitHub token (classic, repo scope)',
403
+ validate: v => {
404
+ if (!autoPublish && !v && !existingToken) return 'Token is required for PR mode';
405
+ if (v && !v.startsWith('ghp_') && !v.startsWith('github_pat_')) return 'Should start with ghp_ or github_pat_';
406
+ return true;
407
+ },
408
+ }, { onCancel: () => process.exit(0) });
409
+
410
+ const effectiveToken = githubToken || existingToken || '';
411
+ if (effectiveToken && fs.existsSync(workspaceProps)) {
412
+ let props = fs.readFileSync(workspaceProps, 'utf8');
413
+ if (/^#?\s*GITHUB_TOKEN\s*=/m.test(props))
414
+ props = props.replace(/^#?\s*GITHUB_TOKEN\s*=.*/m, `GITHUB_TOKEN=${effectiveToken}`);
415
+ else
416
+ props = props.trimEnd() + `\nGITHUB_TOKEN=${effectiveToken}\n`;
417
+ fs.writeFileSync(workspaceProps, props);
418
+ ok('GITHUB_TOKEN saved to workspace sentinel.properties');
419
+ } else if (effectiveToken) {
420
+ info('GITHUB_TOKEN will be written when project files are created');
421
+ }
422
+
367
423
  // ── Preview + confirm ──────────────────────────────────────────────────────
368
424
  const projectDir = path.join(workspace, name);
369
425
 
@@ -426,7 +482,7 @@ async function addFromGit(gitUrl, workspace) {
426
482
  });
427
483
  const example = path.join(repoDir, '_example.properties');
428
484
  if (fs.existsSync(example)) fs.removeSync(example);
429
- generateWorkspaceScripts(workspace);
485
+ generateWorkspaceScripts(workspace, {}, {}, {}, effectiveToken);
430
486
  ok(`Project "${name}" created at ${projectDir}`);
431
487
  printNextSteps(projectDir, autoPublish);
432
488
  }
package/lib/generate.js CHANGED
@@ -96,7 +96,7 @@ rm -f "$PID_FILE"
96
96
 
97
97
  // ── Workspace-level startAll / stopAll ────────────────────────────────────────
98
98
 
99
- function generateWorkspaceScripts(workspace, smtpConfig = {}, slackConfig = {}, authConfig = {}) {
99
+ function generateWorkspaceScripts(workspace, smtpConfig = {}, slackConfig = {}, authConfig = {}, githubToken = '') {
100
100
  // Write shared sentinel.properties once (never overwrite existing)
101
101
  const workspaceProps = path.join(workspace, 'sentinel.properties');
102
102
  if (!fs.existsSync(workspaceProps)) {
@@ -126,6 +126,15 @@ function generateWorkspaceScripts(workspace, smtpConfig = {}, slackConfig = {},
126
126
  }
127
127
  fs.writeFileSync(workspaceProps, props);
128
128
  }
129
+ // Always upsert GitHub token so re-runs persist it
130
+ if (githubToken) {
131
+ let props = fs.readFileSync(workspaceProps, 'utf8');
132
+ if (/^#?\s*GITHUB_TOKEN=/m.test(props))
133
+ props = props.replace(/^#?\s*GITHUB_TOKEN=.*/mg, 'GITHUB_TOKEN=' + githubToken);
134
+ else
135
+ props = props.trimEnd() + '\nGITHUB_TOKEN=' + githubToken + '\n';
136
+ fs.writeFileSync(workspaceProps, props);
137
+ }
129
138
  // Always upsert Slack tokens so re-runs persist them
130
139
  if (slackConfig.botToken || slackConfig.appToken) {
131
140
  let props = fs.readFileSync(workspaceProps, 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"