@misterhuydo/sentinel 1.4.70 → 1.4.72

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-26T18:21:14.192Z
1
+ 2026-03-26T18:52:08.498Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-03-26T18:22:50.768Z",
3
- "checkpoint_at": "2026-03-26T18:22:50.770Z",
2
+ "message": "Auto-checkpoint at 2026-03-26T18:49:49.152Z",
3
+ "checkpoint_at": "2026-03-26T18:49:49.153Z",
4
4
  "active_files": [],
5
5
  "notes": [],
6
6
  "mtime_snapshot": {}
package/lib/add.js CHANGED
@@ -313,13 +313,20 @@ async function addFromGit(gitUrl, workspace) {
313
313
  const projectDir = path.join(workspace, name);
314
314
  step(`[2/3] Scanning repo-configs in ${repoSlug}…`);
315
315
 
316
+ const sshEnv = keyFile
317
+ ? gitEnv({ GIT_SSH_COMMAND: `ssh -i ${keyFile} -o StrictHostKeyChecking=no -o BatchMode=yes` })
318
+ : gitEnv({});
316
319
  if (!fs.existsSync(projectDir)) {
317
- const cloneEnv = keyFile
318
- ? gitEnv({ GIT_SSH_COMMAND: `ssh -i ${keyFile} -o StrictHostKeyChecking=no -o BatchMode=yes` })
319
- : gitEnv({});
320
320
  spawnSync(gitBin(), ['clone', '--depth', '1', gitUrl, projectDir], {
321
321
  stdio: 'inherit',
322
- env: cloneEnv,
322
+ env: sshEnv,
323
+ });
324
+ } else {
325
+ // Pull latest so we get up-to-date sentinel.properties (e.g. GIT_ACCESS setting)
326
+ spawnSync(gitBin(), ['pull', '--rebase'], {
327
+ cwd: projectDir,
328
+ stdio: 'inherit',
329
+ env: sshEnv,
323
330
  });
324
331
  }
325
332
 
@@ -337,6 +344,23 @@ async function addFromGit(gitUrl, workspace) {
337
344
  }
338
345
 
339
346
 
347
+ // Read project-level GIT_ACCESS from the cloned sentinel.properties
348
+ const sentinelPropsPath = path.join(projectDir, 'config', 'sentinel.properties');
349
+ let projectGitAccess = '';
350
+ let projectGitSshKey = '';
351
+ if (fs.existsSync(sentinelPropsPath)) {
352
+ const lines = fs.readFileSync(sentinelPropsPath, 'utf8').split('\n');
353
+ for (const line of lines) {
354
+ const trimmed = line.trim();
355
+ if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
356
+ const [k, ...rest] = trimmed.split('=');
357
+ const v = rest.join('=').split('#')[0].trim();
358
+ if (k.trim() === 'GIT_ACCESS') projectGitAccess = v;
359
+ if (k.trim() === 'GIT_SSH_KEY') projectGitSshKey = v.replace(/^~/, require('os').homedir());
360
+ }
361
+ }
362
+ const useSshUserKey = projectGitAccess === 'ssh_user_key';
363
+
340
364
  // Classify each discovered repo
341
365
  const privateRepos = [];
342
366
  const publicRepos = [];
@@ -355,50 +379,69 @@ async function addFromGit(gitUrl, workspace) {
355
379
  }
356
380
  }
357
381
 
358
- // ── 3. Generate deploy keys for all private repos (batch) ─────────────────
382
+ // ── 3. Set up access for all private repos ─────────────────────────────────
359
383
  if (privateRepos.length > 0) {
360
- step(`[3/3] Deploy keys needed for ${privateRepos.length} private repo(s)`);
361
-
362
- for (const r of privateRepos) {
363
- const { keyFile: rKey } = generateDeployKey(r.slug, projectDir);
364
- r.keyFile = rKey;
365
- const rOrgRepo = gitUrlToOrgRepo(r.url);
366
- printDeployKeyInstructions(rOrgRepo, rKey);
367
- }
368
-
369
- if (publicRepos.length > 0) {
370
- console.log(chalk.green(' ✔ Public repos (no deploy key needed):'));
384
+ if (useSshUserKey) {
385
+ // Project uses a personal SSH key — validate access, no deploy keys needed
386
+ step(`[3/3] Validating access to ${privateRepos.length} private repo(s) via SSH user key…`);
387
+ const sshKeyForValidation = projectGitSshKey || null;
388
+ for (const r of privateRepos) {
389
+ const v = validateAccess(r.url, sshKeyForValidation);
390
+ if (!v.ok) {
391
+ console.error(chalk.red(` ✖ ${r.slug}: cannot reach ${r.url}`));
392
+ if (v.stderr) console.error(chalk.red(' ' + v.stderr));
393
+ console.error(chalk.yellow(` Check that GIT_SSH_KEY (${projectGitSshKey || 'SSH agent'}) has access, then re-run.`));
394
+ process.exit(1);
395
+ }
396
+ ok(`${r.slug}: reachable`);
397
+ }
371
398
  for (const r of publicRepos) {
372
- console.log(chalk.green(` ${r.slug}`));
399
+ ok(`${r.slug}: public, no key needed`);
400
+ }
401
+ } else {
402
+ // Default: generate a deploy key per private repo
403
+ step(`[3/3] Deploy keys needed for ${privateRepos.length} private repo(s)`);
404
+
405
+ for (const r of privateRepos) {
406
+ const { keyFile: rKey } = generateDeployKey(r.slug, projectDir);
407
+ r.keyFile = rKey;
408
+ const rOrgRepo = gitUrlToOrgRepo(r.url);
409
+ printDeployKeyInstructions(rOrgRepo, rKey);
373
410
  }
374
- console.log('');
375
- }
376
411
 
377
- await prompts({
378
- type: 'text', name: '_', format: () => '',
379
- message: chalk.bold(`Press Enter once you've added all ${privateRepos.length} deploy key(s) to GitHub…`),
380
- }, { onCancel: () => process.exit(0) });
412
+ if (publicRepos.length > 0) {
413
+ console.log(chalk.green(' Public repos (no deploy key needed):'));
414
+ for (const r of publicRepos) {
415
+ console.log(chalk.green(` ${r.slug}`));
416
+ }
417
+ console.log('');
418
+ }
381
419
 
382
- // Validate each private repo
383
- step('Validating repository access…');
384
- for (const r of privateRepos) {
385
- const v = validateAccess(r.url, r.keyFile);
386
- if (!v.ok) {
387
- console.error(chalk.red(` ✖ ${r.slug}: cannot reach ${r.url}`));
388
- if (v.stderr) console.error(chalk.red(' ' + v.stderr));
389
- console.error(chalk.yellow(' Fix access then re-run sentinel add.'));
390
- process.exit(1);
420
+ await prompts({
421
+ type: 'text', name: '_', format: () => '',
422
+ message: chalk.bold(`Press Enter once you've added all ${privateRepos.length} deploy key(s) to GitHub…`),
423
+ }, { onCancel: () => process.exit(0) });
424
+
425
+ // Validate each private repo
426
+ step('Validating repository access…');
427
+ for (const r of privateRepos) {
428
+ const v = validateAccess(r.url, r.keyFile);
429
+ if (!v.ok) {
430
+ console.error(chalk.red(` ✖ ${r.slug}: cannot reach ${r.url}`));
431
+ if (v.stderr) console.error(chalk.red(' ' + v.stderr));
432
+ console.error(chalk.yellow(' Fix access then re-run sentinel add.'));
433
+ process.exit(1);
434
+ }
435
+ ok(`${r.slug}: reachable`);
436
+ }
437
+ for (const r of publicRepos) {
438
+ ok(`${r.slug}: public, no key needed`);
391
439
  }
392
- ok(`${r.slug}: reachable`);
393
- }
394
- for (const r of publicRepos) {
395
- ok(`${r.slug}: public, no key needed`);
396
- }
397
440
 
398
- // Keys stored at <projectDir>/<slug>.key — config_loader.py auto-discovers them.
399
- // SSH_KEY_FILE is NOT written to git-tracked .properties files.
400
- for (const r of privateRepos) {
401
- info(`Key stored at ${r.keyFile} (auto-discovered, not committed to git)`);
441
+ // Keys stored at <projectDir>/<slug>.key — config_loader.py auto-discovers them.
442
+ for (const r of privateRepos) {
443
+ info(`Key stored at ${r.keyFile} (auto-discovered, not committed to git)`);
444
+ }
402
445
  }
403
446
  } else if (discovered.length > 0) {
404
447
  step('[3/3] All repos are public — no deploy keys needed');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.4.70",
3
+ "version": "1.4.72",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -109,6 +109,7 @@ class RepoConfig:
109
109
  cicd_user: str = "" # Jenkins username for Basic auth (defaults to "sentinel")
110
110
  health_url: str = "" # optional: HTTP endpoint returning {"Status": "true"}
111
111
  cicd_token: str = ""
112
+ git_access: str = "" # ssh_user_key | ssh_deploy_key | https_token | https_pat
112
113
  ssh_key_file: str = "" # path to SSH private key; sets GIT_SSH_COMMAND when present
113
114
 
114
115
 
@@ -226,6 +227,13 @@ class ConfigLoader:
226
227
  repos_dir = self.config_dir / "repo-configs"
227
228
  if not repos_dir.exists():
228
229
  return
230
+
231
+ # Project-level defaults from sentinel.properties
232
+ sentinel_path = self.config_dir / "sentinel.properties"
233
+ proj_d: dict[str, str] = _parse_properties(str(sentinel_path)) if sentinel_path.exists() else {}
234
+ default_git_access = proj_d.get("GIT_ACCESS", "")
235
+ default_git_ssh_key = os.path.expanduser(proj_d.get("GIT_SSH_KEY", ""))
236
+
229
237
  self.repos = {}
230
238
  for path in sorted(repos_dir.glob("*.properties")):
231
239
  if path.name.startswith("_"):
@@ -242,8 +250,11 @@ class ConfigLoader:
242
250
  r.cicd_user = d.get("CICD_USER", "")
243
251
  r.cicd_token = d.get("CICD_TOKEN", "")
244
252
  r.health_url = d.get("HEALTH_URL", "")
245
- r.ssh_key_file = os.path.expanduser(d.get("SSH_KEY_FILE", ""))
246
- # Auto-discover key in project dir if not set in properties
253
+ r.git_access = d.get("GIT_ACCESS", default_git_access)
254
+ # GIT_SSH_KEY (preferred) or legacy SSH_KEY_FILE, then project default
255
+ raw_key = d.get("GIT_SSH_KEY", "") or d.get("SSH_KEY_FILE", "")
256
+ r.ssh_key_file = os.path.expanduser(raw_key) if raw_key else default_git_ssh_key
257
+ # Auto-discover deploy key in project dir if no key configured
247
258
  if not r.ssh_key_file:
248
259
  auto_key = Path(self.config_dir).parent / f"{r.repo_name}.key"
249
260
  if auto_key.exists():
@@ -18,6 +18,16 @@ MAILS=you@yourdomain.com
18
18
  # Uncomment here only if this project needs a different token.
19
19
  # GITHUB_TOKEN=<github-pat>
20
20
 
21
+ # Default SSH access method for all repos in this project.
22
+ # Per-repo configs (config/repo-configs/*.properties) can override these.
23
+ #
24
+ # Options:
25
+ # ssh_user_key — personal key at GIT_SSH_KEY path (full account access, recommended)
26
+ # ssh_deploy_key — per-repo deploy key generated by sentinel add (default if not set)
27
+ #
28
+ # GIT_ACCESS=ssh_user_key
29
+ # GIT_SSH_KEY=~/.ssh/your-key
30
+
21
31
  # Fix confirmation: hours of silence after a fix marker appears in production logs before
22
32
  # the fix is declared confirmed. Increase for services that deploy infrequently.
23
33
  # MARKER_CONFIRM_HOURS=24