sra-skills 0.20.2 → 0.20.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 (2) hide show
  1. package/index.mjs +76 -22
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -67,6 +67,7 @@ function writeIdentityCache(email, team, role, profileKey) {
67
67
  }, null, 2) + '\n');
68
68
  }
69
69
 
70
+ // Returns: { status: 'ok', team, role } | { status: 'not_found' } | { status: 'error' }
70
71
  async function fetchRoleFromApi(email) {
71
72
  try {
72
73
  const url = `${WHOIS_API_URL}?email=${encodeURIComponent(email)}`;
@@ -78,21 +79,27 @@ async function fetchRoleFromApi(email) {
78
79
  });
79
80
  clearTimeout(timeout);
80
81
  const result = await resp.json();
81
- if (!result.success) return null;
82
+ if (!result.success) return { status: 'not_found' };
82
83
  const data = result.data || {};
83
84
  const team = parseTeamFromDept(data.department_path || '');
84
85
  const role = (data.role || '').toLowerCase().trim();
85
- if (team && role) return [team, role];
86
- } catch {}
87
- return null;
86
+ if (team && role) return { status: 'ok', team, role };
87
+ return { status: 'not_found' };
88
+ } catch {
89
+ return { status: 'error' };
90
+ }
88
91
  }
89
92
 
90
- async function promptProfileSelection(profilesData) {
93
+ async function promptProfileSelection(profilesData, { email = null, reason = 'error' } = {}) {
91
94
  if (!process.stdin.isTTY || !profilesData?.profiles) return null;
92
95
  const profiles = profilesData.profiles;
93
96
  const keys = Object.keys(profiles);
94
97
  if (!keys.length) return null;
95
- console.log(`\n${C.y}⚠ Could not auto-detect your role (API unavailable). Please select manually:${C.x}`);
98
+ const identityHint = email ? ` (identity: ${C.d}${email}${C.x})` : '';
99
+ const msg = reason === 'not_found'
100
+ ? `\n${C.y}⚠ No matching role found for this identity${identityHint}. Please select manually:${C.x}`
101
+ : `\n${C.y}⚠ Role detection API unavailable${identityHint}. Please select manually:${C.x}`;
102
+ console.log(msg);
96
103
  const chosen = await select({
97
104
  message: 'Select your team/role profile',
98
105
  choices: keys.map(k => ({ name: `${k} — ${profiles[k].label || k}`, value: k })),
@@ -112,16 +119,15 @@ async function detectRole(email, opts = {}) {
112
119
  } catch {}
113
120
  }
114
121
  const result = await fetchRoleFromApi(email);
115
- if (result) {
116
- const [team, role] = result;
117
- const profileKey = `${team}/${role}`;
118
- writeIdentityCache(email, team, role, profileKey);
122
+ if (result.status === 'ok') {
123
+ const profileKey = `${result.team}/${result.role}`;
124
+ writeIdentityCache(email, result.team, result.role, profileKey);
119
125
  return profileKey;
120
126
  }
121
127
  if (cache && cache.email === email) return cache.profile_key || fallbackKey || 'other';
122
128
  if (fallbackKey) return fallbackKey;
123
129
  if (interactive && profilesData) {
124
- const chosen = await promptProfileSelection(profilesData);
130
+ const chosen = await promptProfileSelection(profilesData, { email, reason: result.status });
125
131
  if (chosen) {
126
132
  const parts = chosen.split('/');
127
133
  const team = parts[0] || '';
@@ -131,7 +137,10 @@ async function detectRole(email, opts = {}) {
131
137
  }
132
138
  }
133
139
  if (!interactive) {
134
- console.log(`${C.y}⚠ Could not auto-detect role (API unavailable, no cache). Using default profile "other".${C.x}`);
140
+ const hint = result.status === 'not_found' && email
141
+ ? `no matching role found for ${email}`
142
+ : result.status === 'not_found' ? 'no matching role found' : 'API unavailable';
143
+ console.log(`${C.y}⚠ Could not auto-detect role (${hint}, no cache). Using default profile "other".${C.x}`);
135
144
  }
136
145
  return 'other';
137
146
  }
@@ -479,6 +488,21 @@ if (cmd === 'add') {
479
488
  let profileIdentity = null; // email used for auto-detection
480
489
  let skills;
481
490
 
491
+ // --- Skill selection (three-state model) ---
492
+ // Skills have three states: enabled | disabled | available (implicit).
493
+ // enabled: installed, stored in manifest.skills
494
+ // disabled: user explicitly rejected, stored in manifest.disabled_skills
495
+ // available: exists in repo but user hasn't decided — not stored (implicit)
496
+ //
497
+ // `disabled` is populated ONLY when user explicitly rejects a recommended skill
498
+ // in an interactive prompt. Specifically:
499
+ // - Fresh install: disabled = profileSkillNames − selectedSet (rejected recommendations)
500
+ // - Update: disabled += rejected newProfile skills from promptSkillChanges
501
+ // - Non-interactive: never populates disabled (system choice, not user intent)
502
+ //
503
+ // In legacy mode (no profile), disabled_skills = discoveredNames − selectedNames
504
+ // (all uninstalled = disabled, since there's no profile-based update logic).
505
+
482
506
  if (skillsFilter) {
483
507
  const unknown = [...skillsFilter].filter(n => !discoveredNames.has(n));
484
508
  if (unknown.length) console.warn(`Warning: requested skill(s) not found in repo: ${unknown.join(', ')}`);
@@ -493,6 +517,7 @@ if (cmd === 'add') {
493
517
  } else {
494
518
  const profilesData = loadProfiles(repoPath);
495
519
  if (!profilesData) {
520
+ // Legacy path (no profiles.json): disabled_skills computed at manifest write time
496
521
  if (isRepeat) {
497
522
  skills = allDiscovered.filter(s => !disabled.has(s.name));
498
523
  } else if (skipPrompt) {
@@ -501,6 +526,7 @@ if (cmd === 'add') {
501
526
  skills = await selectSkills(allDiscovered);
502
527
  }
503
528
  } else {
529
+ // Profile path: resolve profile key
504
530
  if (explicitProfile) {
505
531
  profileKeyUsed = explicitProfile;
506
532
  profileSource = 'manual';
@@ -519,6 +545,7 @@ if (cmd === 'add') {
519
545
  profileSkillNames = validateProfileSkills(profileSkillNames, discoveredNames);
520
546
 
521
547
  if (isRepeat) {
548
+ // Three-way diff: profile vs enabled vs disabled
522
549
  const diff = computeSkillDiff(allDiscovered, profileSkillNames, enabled, disabled);
523
550
  const [toEnable, toDisable, prompted] = skipPrompt
524
551
  ? [diff.newProfile, new Set(), false]
@@ -533,11 +560,19 @@ if (cmd === 'add') {
533
560
  disabled = new Set([...disabled, ...toDisable].filter(n => !toEnable.has(n) && discoveredNames.has(n)));
534
561
  }
535
562
  } else {
563
+ // Fresh install with profile
536
564
  if (skipPrompt) {
565
+ // Non-interactive: install profile defaults, don't mark others as disabled
537
566
  skills = allDiscovered.filter(s => profileSkillNames.has(s.name));
538
567
  } else {
539
568
  [skills, profileKeyUsed] = await selectSkillsWithProfile(
540
569
  allDiscovered, profileSkillNames, profileKeyUsed, profilesData);
570
+ // disabled = only rejected recommendations (profile-recommended but user unchecked)
571
+ // Skills in "Other" left unchecked are "available", not disabled
572
+ if (process.stdin.isTTY) {
573
+ const selectedSet = new Set(skills.map(s => s.name));
574
+ disabled = new Set([...profileSkillNames].filter(n => !selectedSet.has(n)));
575
+ }
541
576
  }
542
577
  }
543
578
  }
@@ -557,6 +592,9 @@ if (cmd === 'add') {
557
592
  console.log(` Linked ${parts.join(', ')} [${tools.join(', ')}]`);
558
593
  runPostInstall(repoPath);
559
594
 
595
+ // Compute disabled_skills for manifest:
596
+ // - Profile mode: only user-rejected skills (from `disabled` set)
597
+ // - Legacy mode: all uninstalled skills (computed from discovered − selected)
560
598
  const selectedNames = new Set(skills.map(s => s.name));
561
599
  const disabledList = profileKeyUsed != null
562
600
  ? [...disabled].sort()
@@ -581,10 +619,24 @@ if (cmd === 'add') {
581
619
  console.log(`\n${C.b}Done.${C.x} ${name} ${C.g}${ver}${C.x}\n`);
582
620
  if (profileKeyUsed) {
583
621
  const srcLabel = profileSource === 'manual' ? 'manual' : 'auto';
584
- console.log(` ${C.b}Profile:${C.x} ${C.g}${profileKeyUsed}${C.x} (${srcLabel})`);
622
+ const identityPart = profileIdentity ? `, ${C.d}${profileIdentity}${C.x}` : '';
623
+ console.log(` ${C.b}Profile:${C.x} ${C.g}${profileKeyUsed}${C.x} (${srcLabel}${identityPart})`);
624
+ }
625
+ if (isLocal) {
626
+ console.log(` ${C.b}Mode:${C.x} ${C.y}local${C.x} → ${repoPath}`);
585
627
  }
586
628
  console.log(` ${C.b}Tools:${C.x} ${tools.join(', ')}`);
587
- console.log(` ${C.b}Skills:${C.x} ${C.g}${skills.length} enabled${C.x}, ${C.d}${disabledList.length} disabled${C.x}`);
629
+ const skillsSummary = disabledList.length
630
+ ? `${C.g}${skills.length} enabled${C.x}, ${C.d}${disabledList.length} disabled${C.x}`
631
+ : `${C.g}${skills.length} enabled${C.x}`;
632
+ console.log(` ${C.b}Skills:${C.x} ${skillsSummary}`);
633
+ if (isLocal) {
634
+ let localRemoteUrl = '';
635
+ try { localRemoteUrl = execSync('git remote get-url origin', { cwd: repoPath, encoding: 'utf8' }).trim(); } catch {}
636
+ console.log(`\n ${C.r}⚠ Local mode will NOT auto-update with new releases.${C.x}`);
637
+ console.log(` ${C.d}To switch to clone mode (recommended):${C.x}`);
638
+ console.log(` ${C.d} npx sra-skills add ${localRemoteUrl || '<git-url>'} --name ${name}${C.x}`);
639
+ }
588
640
  await promptSkillDeps([{ repoPath, skills }]);
589
641
  warnMissingCredentials(skills, credPath || DEFAULT_CRED_PATH, repoPath);
590
642
  console.log('');
@@ -692,15 +744,16 @@ if (cmd === 'add') {
692
744
  }
693
745
  console.log(` Updated ${prevRev.slice(0, 7)} → ${newRev.slice(0, 7)}`);
694
746
  } else {
695
- console.log(` ⚠ Local install detected — git update skipped.`);
696
- console.log(` ~/.sra/manifest.json is missing the "url" field for "${name}".`);
747
+ let localBranch = '';
748
+ try { localBranch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: repo.path, encoding: 'utf8' }).trim(); } catch {}
749
+ console.log(` ${C.y}⚠ Local mode → ${repo.path}${localBranch ? ` (branch: ${localBranch})` : ''}${C.x}`);
750
+ console.log(` ${C.r}Will NOT auto-update — skills stay at the version on disk.${C.x}`);
697
751
  let remoteUrl = '';
698
752
  try {
699
753
  remoteUrl = execSync('git remote get-url origin', { cwd: repo.path, encoding: 'utf8' }).trim();
700
754
  } catch {}
701
- console.log(` To enable auto-update, add "url" to the "${name}" entry in ~/.sra/manifest.json:`);
702
- console.log(` "url": "${remoteUrl || '<git-remote-url>'}",`);
703
- console.log(` Or: git -C ${repo.path} pull, then re-run ./scripts/install.sh`);
755
+ console.log(` ${C.d}To switch to clone mode (recommended):${C.x}`);
756
+ console.log(` ${C.d}npx sra-skills add ${remoteUrl || '<git-url>'} --name ${name}${C.x}`);
704
757
  repo.updated_at = new Date().toISOString();
705
758
  }
706
759
  const allSkills = discoverSkills(repo.path);
@@ -833,7 +886,8 @@ if (cmd === 'add') {
833
886
  }
834
887
  if (repo.profile) {
835
888
  const srcLabel = repo.profile_source === 'manual' ? 'manual' : 'auto';
836
- console.log(` ${C.b}Profile:${C.x} ${C.g}${repo.profile}${C.x} (${srcLabel})`);
889
+ const identityPart = repo.profile_identity ? `, ${C.d}${repo.profile_identity}${C.x}` : '';
890
+ console.log(` ${C.b}Profile:${C.x} ${C.g}${repo.profile}${C.x} (${srcLabel}${identityPart})`);
837
891
  }
838
892
 
839
893
  const totalEnabled = (repo.skills || []).length;
@@ -847,7 +901,7 @@ if (cmd === 'add') {
847
901
  if (removeCount) dp.push(`${C.y}-${removeCount}${C.x}`);
848
902
  skillsLine += ` (${dp.join(' ')})`;
849
903
  }
850
- skillsLine += `, ${C.d}${totalDisabled} disabled${C.x}`;
904
+ if (totalDisabled) skillsLine += `, ${C.d}${totalDisabled} disabled${C.x}`;
851
905
  console.log(` ${C.b}Skills:${C.x} ${skillsLine}`);
852
906
 
853
907
  const totalChanges = addCount + changes.removed.length + changes.stale.length;
@@ -859,7 +913,7 @@ if (cmd === 'add') {
859
913
  } else if (totalChanges > 5) {
860
914
  let changeLine = ` Changes: ${C.g}+${addCount}${C.x} ${C.y}-${changes.removed.length}${C.x}`;
861
915
  if (changes.stale.length) changeLine += ` ${C.r}✕${changes.stale.length}${C.x}`;
862
- changeLine += ` (run \`npx sra-skills skill list\` for details)`;
916
+ changeLine += ` (run \`npx sra-skills manage\` for details)`;
863
917
  console.log(changeLine);
864
918
  }
865
919
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sra-skills",
3
- "version": "0.20.2",
3
+ "version": "0.20.4",
4
4
  "description": "SRA agent skills installer — manage AI skill repos",
5
5
  "bin": {
6
6
  "sra": "index.mjs",