happyskills 0.30.0 → 0.31.0

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.
@@ -688,3 +688,264 @@ describe('CLI — delete command', () => {
688
688
  }
689
689
  })
690
690
  })
691
+
692
+ // ---------------------------------------------------------------------------
693
+ // Enable / Disable
694
+ // ---------------------------------------------------------------------------
695
+
696
+ /**
697
+ * Helper: scaffold a project directory with a lock file and skill dirs that
698
+ * mimic a real HappySkills install. Returns { project_root, cleanup }.
699
+ *
700
+ * Structure:
701
+ * <tmp>/
702
+ * ├── skills-lock.json
703
+ * ├── .agents/skills/<skill_name>/SKILL.md (canonical)
704
+ * └── .claude/skills/<skill_name> → symlink (agent link)
705
+ */
706
+ const scaffold_project = (skills) => {
707
+ const root = make_tmp()
708
+ const lock_skills = {}
709
+
710
+ for (const s of skills) {
711
+ const canonical = path.join(root, '.agents', 'skills', s.short)
712
+ fs.mkdirSync(canonical, { recursive: true })
713
+ fs.writeFileSync(path.join(canonical, 'SKILL.md'), `---\nname: ${s.short}\ndescription: test skill\n---\nTest`)
714
+
715
+ if (s.enabled !== false) {
716
+ const agent_dir = path.join(root, '.claude', 'skills')
717
+ fs.mkdirSync(agent_dir, { recursive: true })
718
+ fs.symlinkSync(canonical, path.join(agent_dir, s.short), 'junction')
719
+ }
720
+
721
+ lock_skills[s.full] = {
722
+ version: s.version || '1.0.0',
723
+ type: 'skill',
724
+ ref: `refs/tags/v${s.version || '1.0.0'}`,
725
+ commit: 'abc123',
726
+ integrity: null,
727
+ base_commit: 'abc123',
728
+ base_integrity: null,
729
+ requested_by: ['__root__'],
730
+ dependencies: {}
731
+ }
732
+ }
733
+
734
+ fs.writeFileSync(path.join(root, 'skills-lock.json'), JSON.stringify({
735
+ lockVersion: 2,
736
+ generatedAt: new Date().toISOString(),
737
+ skills: lock_skills
738
+ }, null, '\t'))
739
+
740
+ return { root, cleanup: () => fs.rmSync(root, { recursive: true, force: true }) }
741
+ }
742
+
743
+ describe('enable / disable commands', () => {
744
+ it('enable --help exits 0', () => {
745
+ const { code, stdout } = run(['enable', '--help'])
746
+ assert.strictEqual(code, 0)
747
+ assert.ok(stdout.includes('Usage: happyskills enable'))
748
+ })
749
+
750
+ it('disable --help exits 0', () => {
751
+ const { code, stdout } = run(['disable', '--help'])
752
+ assert.strictEqual(code, 0)
753
+ assert.ok(stdout.includes('Usage: happyskills disable'))
754
+ })
755
+
756
+ it('"on" alias resolves to enable', () => {
757
+ const { code, stdout } = run(['on', '--help'])
758
+ assert.strictEqual(code, 0)
759
+ assert.ok(stdout.includes('Usage: happyskills enable'))
760
+ })
761
+
762
+ it('"off" alias resolves to disable', () => {
763
+ const { code, stdout } = run(['off', '--help'])
764
+ assert.strictEqual(code, 0)
765
+ assert.ok(stdout.includes('Usage: happyskills disable'))
766
+ })
767
+
768
+ it('disable without arguments exits with usage error', () => {
769
+ const { code, stderr } = run(['disable', '--json'])
770
+ assert.strictEqual(code, 2)
771
+ })
772
+
773
+ it('enable without arguments exits with usage error', () => {
774
+ const { code, stderr } = run(['enable', '--json'])
775
+ assert.strictEqual(code, 2)
776
+ })
777
+
778
+ it('disable removes agent symlink, enable restores it', () => {
779
+ const { root, cleanup: clean } = scaffold_project([
780
+ { full: 'acme/deploy-aws', short: 'deploy-aws' }
781
+ ])
782
+ try {
783
+ const link_path = path.join(root, '.claude', 'skills', 'deploy-aws')
784
+
785
+ // Verify symlink exists before disable
786
+ assert.ok(fs.lstatSync(link_path).isSymbolicLink())
787
+
788
+ // Disable — force agents to claude only (auto-detect won't find .claude in tmp)
789
+ const d = run(['disable', 'acme/deploy-aws', '--agents', 'claude'], {}, { cwd: root })
790
+ assert.strictEqual(d.code, 0, `disable failed: ${d.stderr}`)
791
+ assert.ok(!fs.existsSync(link_path), 'symlink should be removed after disable')
792
+
793
+ // Canonical dir must still exist
794
+ assert.ok(fs.existsSync(path.join(root, '.agents', 'skills', 'deploy-aws')))
795
+
796
+ // Enable
797
+ const e = run(['enable', 'acme/deploy-aws', '--agents', 'claude'], {}, { cwd: root })
798
+ assert.strictEqual(e.code, 0, `enable failed: ${e.stderr}`)
799
+ assert.ok(fs.lstatSync(link_path).isSymbolicLink(), 'symlink should be restored after enable')
800
+ } finally {
801
+ clean()
802
+ }
803
+ })
804
+
805
+ it('disable an already-disabled skill warns but does not fail', () => {
806
+ const { root, cleanup: clean } = scaffold_project([
807
+ { full: 'acme/deploy-aws', short: 'deploy-aws', enabled: false }
808
+ ])
809
+ try {
810
+ const { code, stderr } = run(['disable', 'acme/deploy-aws', '--agents', 'claude'], {}, { cwd: root })
811
+ assert.strictEqual(code, 0)
812
+ assert.ok(stderr.includes('already disabled'))
813
+ } finally {
814
+ clean()
815
+ }
816
+ })
817
+
818
+ it('enable an already-enabled skill warns but does not fail', () => {
819
+ const { root, cleanup: clean } = scaffold_project([
820
+ { full: 'acme/deploy-aws', short: 'deploy-aws', enabled: true }
821
+ ])
822
+ try {
823
+ const { code, stderr } = run(['enable', 'acme/deploy-aws', '--agents', 'claude'], {}, { cwd: root })
824
+ assert.strictEqual(code, 0)
825
+ assert.ok(stderr.includes('already enabled'))
826
+ } finally {
827
+ clean()
828
+ }
829
+ })
830
+
831
+ it('disable multiple skills at once', () => {
832
+ const { root, cleanup: clean } = scaffold_project([
833
+ { full: 'acme/deploy-aws', short: 'deploy-aws' },
834
+ { full: 'acme/monitoring', short: 'monitoring' }
835
+ ])
836
+ try {
837
+ const { code } = run(['disable', 'acme/deploy-aws', 'acme/monitoring', '--agents', 'claude'], {}, { cwd: root })
838
+ assert.strictEqual(code, 0)
839
+
840
+ assert.ok(!fs.existsSync(path.join(root, '.claude', 'skills', 'deploy-aws')))
841
+ assert.ok(!fs.existsSync(path.join(root, '.claude', 'skills', 'monitoring')))
842
+ } finally {
843
+ clean()
844
+ }
845
+ })
846
+
847
+ it('disable accepts short names', () => {
848
+ const { root, cleanup: clean } = scaffold_project([
849
+ { full: 'acme/deploy-aws', short: 'deploy-aws' }
850
+ ])
851
+ try {
852
+ const { code } = run(['disable', 'deploy-aws', '--agents', 'claude'], {}, { cwd: root })
853
+ assert.strictEqual(code, 0)
854
+ assert.ok(!fs.existsSync(path.join(root, '.claude', 'skills', 'deploy-aws')))
855
+ } finally {
856
+ clean()
857
+ }
858
+ })
859
+
860
+ it('enable accepts short names', () => {
861
+ const { root, cleanup: clean } = scaffold_project([
862
+ { full: 'acme/deploy-aws', short: 'deploy-aws', enabled: false }
863
+ ])
864
+ try {
865
+ const { code } = run(['enable', 'deploy-aws', '--agents', 'claude'], {}, { cwd: root })
866
+ assert.strictEqual(code, 0)
867
+ assert.ok(fs.lstatSync(path.join(root, '.claude', 'skills', 'deploy-aws')).isSymbolicLink())
868
+ } finally {
869
+ clean()
870
+ }
871
+ })
872
+
873
+ it('disable a non-existent skill warns but does not fail', () => {
874
+ const { root, cleanup: clean } = scaffold_project([])
875
+ try {
876
+ const { code, stderr } = run(['disable', 'acme/nope', '--agents', 'claude'], {}, { cwd: root })
877
+ assert.strictEqual(code, 0)
878
+ assert.ok(stderr.includes('not a HappySkills-managed skill'))
879
+ } finally {
880
+ clean()
881
+ }
882
+ })
883
+
884
+ it('disable --json returns structured results', () => {
885
+ const { root, cleanup: clean } = scaffold_project([
886
+ { full: 'acme/deploy-aws', short: 'deploy-aws' }
887
+ ])
888
+ try {
889
+ const { code, stdout } = run(['disable', 'acme/deploy-aws', '--agents', 'claude', '--json'], {}, { cwd: root })
890
+ assert.strictEqual(code, 0)
891
+ const out = parse_json_output(stdout, 'disable --json')
892
+ assert.ok(out.data)
893
+ assert.ok(Array.isArray(out.data.results))
894
+ assert.strictEqual(out.data.results[0].skill, 'acme/deploy-aws')
895
+ assert.strictEqual(out.data.results[0].status, 'disabled')
896
+ } finally {
897
+ clean()
898
+ }
899
+ })
900
+
901
+ it('enable --json returns structured results', () => {
902
+ const { root, cleanup: clean } = scaffold_project([
903
+ { full: 'acme/deploy-aws', short: 'deploy-aws', enabled: false }
904
+ ])
905
+ try {
906
+ const { code, stdout } = run(['enable', 'acme/deploy-aws', '--agents', 'claude', '--json'], {}, { cwd: root })
907
+ assert.strictEqual(code, 0)
908
+ const out = parse_json_output(stdout, 'enable --json')
909
+ assert.ok(out.data)
910
+ assert.ok(Array.isArray(out.data.results))
911
+ assert.strictEqual(out.data.results[0].skill, 'acme/deploy-aws')
912
+ assert.strictEqual(out.data.results[0].status, 'enabled')
913
+ } finally {
914
+ clean()
915
+ }
916
+ })
917
+ })
918
+
919
+ describe('list — enabled column', () => {
920
+ it('list --json includes enabled field for managed skills', () => {
921
+ const { root, cleanup: clean } = scaffold_project([
922
+ { full: 'acme/deploy-aws', short: 'deploy-aws', enabled: true },
923
+ { full: 'acme/monitoring', short: 'monitoring', enabled: false }
924
+ ])
925
+ try {
926
+ const { code, stdout } = run(['list', '--json', '--agents', 'claude'], {}, { cwd: root })
927
+ assert.strictEqual(code, 0)
928
+ const out = parse_json_output(stdout, 'list --json')
929
+ assert.strictEqual(out.data.skills['acme/deploy-aws'].enabled, true)
930
+ assert.strictEqual(out.data.skills['acme/monitoring'].enabled, false)
931
+ } finally {
932
+ clean()
933
+ }
934
+ })
935
+
936
+ it('list table output shows enabled/disabled labels', () => {
937
+ const { root, cleanup: clean } = scaffold_project([
938
+ { full: 'acme/deploy-aws', short: 'deploy-aws', enabled: true },
939
+ { full: 'acme/monitoring', short: 'monitoring', enabled: false }
940
+ ])
941
+ try {
942
+ const { code, stdout } = run(['list', '--agents', 'claude'], {}, { cwd: root })
943
+ assert.strictEqual(code, 0)
944
+ assert.ok(stdout.includes('Enabled'), 'table should have Enabled header')
945
+ assert.ok(stdout.includes('enabled'), 'should show enabled label')
946
+ assert.ok(stdout.includes('disabled'), 'should show disabled label')
947
+ } finally {
948
+ clean()
949
+ }
950
+ })
951
+ })