reskill 1.16.0 → 1.17.1

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/dist/cli/index.js CHANGED
@@ -3473,6 +3473,7 @@ class RegistryClient {
3473
3473
  formData.append('metadata', JSON.stringify(metadata));
3474
3474
  if (payload.skillMd?.allowedTools) formData.append('allowed_tools', payload.skillMd.allowedTools.join(' '));
3475
3475
  if (options.tag) formData.append('tag', options.tag);
3476
+ if (options.groupPath) formData.append('group_path', options.groupPath);
3476
3477
  // Append tarball as Blob
3477
3478
  const tarballBlob = new Blob([
3478
3479
  tarball
@@ -3490,6 +3491,186 @@ class RegistryClient {
3490
3491
  if (!response.ok) throw new RegistryError(data.error || `Publish failed: ${response.status}`, response.status, data);
3491
3492
  return data;
3492
3493
  }
3494
+ // ============================================================================
3495
+ // Group Methods
3496
+ // ============================================================================
3497
+ /**
3498
+ * Resolve a human-readable group path to its details.
3499
+ *
3500
+ * @param groupPath - Human-readable path (e.g., "kanyun/frontend")
3501
+ * @returns Group detail with current_user_role if authenticated
3502
+ * @throws RegistryError if not found or request failed
3503
+ */ async resolveGroup(groupPath) {
3504
+ const params = new URLSearchParams({
3505
+ path: groupPath
3506
+ });
3507
+ const url = `${this.getApiBase()}/skill-groups/resolve?${params.toString()}`;
3508
+ const response = await fetch(url, {
3509
+ method: 'GET',
3510
+ headers: this.getAuthHeaders()
3511
+ });
3512
+ if (!response.ok) {
3513
+ const data = await response.json();
3514
+ throw new RegistryError(data.error || `Group not found: ${groupPath}`, response.status, data);
3515
+ }
3516
+ const body = await response.json();
3517
+ return body.data;
3518
+ }
3519
+ /**
3520
+ * List groups visible to the current user.
3521
+ *
3522
+ * @param options - Filter options (parent_id, visibility)
3523
+ * @returns Array of groups
3524
+ */ async listGroups(options = {}) {
3525
+ const params = new URLSearchParams();
3526
+ if (options.parentId) params.set('parent_id', options.parentId);
3527
+ if (options.visibility) params.set('visibility', options.visibility);
3528
+ if (options.flat) params.set('flat', 'true');
3529
+ const qs = params.toString();
3530
+ const url = `${this.getApiBase()}/skill-groups${qs ? `?${qs}` : ''}`;
3531
+ const response = await fetch(url, {
3532
+ method: 'GET',
3533
+ headers: this.getAuthHeaders()
3534
+ });
3535
+ if (!response.ok) {
3536
+ const data = await response.json();
3537
+ throw new RegistryError(data.error || `Failed to list groups: ${response.status}`, response.status, data);
3538
+ }
3539
+ const body = await response.json();
3540
+ return body.data;
3541
+ }
3542
+ /**
3543
+ * Create a new skill group.
3544
+ *
3545
+ * @param input - Group creation parameters
3546
+ * @returns Created group
3547
+ */ async createGroup(input) {
3548
+ const url = `${this.getApiBase()}/skill-groups`;
3549
+ const response = await fetch(url, {
3550
+ method: 'POST',
3551
+ headers: {
3552
+ ...this.getAuthHeaders(),
3553
+ 'Content-Type': 'application/json'
3554
+ },
3555
+ body: JSON.stringify(input)
3556
+ });
3557
+ if (!response.ok) {
3558
+ const data = await response.json();
3559
+ throw new RegistryError(data.error || `Failed to create group: ${response.status}`, response.status, data);
3560
+ }
3561
+ const body = await response.json();
3562
+ return body.data;
3563
+ }
3564
+ /**
3565
+ * Delete a skill group.
3566
+ *
3567
+ * @param groupId - Group UUID
3568
+ * @param dryRun - If true, only preview what would be deleted
3569
+ * @returns Deletion result (affected skills count in dry-run mode)
3570
+ */ async deleteGroup(groupId, dryRun = false) {
3571
+ const params = dryRun ? '?dry_run=true' : '';
3572
+ const encodedGroupId = encodeURIComponent(groupId);
3573
+ const url = `${this.getApiBase()}/skill-groups/${encodedGroupId}${params}`;
3574
+ const response = await fetch(url, {
3575
+ method: 'DELETE',
3576
+ headers: this.getAuthHeaders()
3577
+ });
3578
+ if (!response.ok) {
3579
+ const data = await response.json();
3580
+ throw new RegistryError(data.error || `Failed to delete group: ${response.status}`, response.status, data);
3581
+ }
3582
+ const body = await response.json();
3583
+ return body.data;
3584
+ }
3585
+ /**
3586
+ * List members of a group.
3587
+ *
3588
+ * @param groupId - Group UUID
3589
+ * @returns Array of members
3590
+ */ async listGroupMembers(groupId) {
3591
+ const encodedGroupId = encodeURIComponent(groupId);
3592
+ const url = `${this.getApiBase()}/skill-groups/${encodedGroupId}/members`;
3593
+ const response = await fetch(url, {
3594
+ method: 'GET',
3595
+ headers: this.getAuthHeaders()
3596
+ });
3597
+ if (!response.ok) {
3598
+ const data = await response.json();
3599
+ throw new RegistryError(data.error || `Failed to list members: ${response.status}`, response.status, data);
3600
+ }
3601
+ const body = await response.json();
3602
+ return body.data;
3603
+ }
3604
+ /**
3605
+ * Add members to a group.
3606
+ *
3607
+ * @param groupId - Group UUID
3608
+ * @param userIds - Array of user IDs to add
3609
+ * @param role - Role to assign (defaults to 'developer')
3610
+ */ async addGroupMembers(groupId, userIds, role = 'developer') {
3611
+ const encodedGroupId = encodeURIComponent(groupId);
3612
+ const url = `${this.getApiBase()}/skill-groups/${encodedGroupId}/members`;
3613
+ const response = await fetch(url, {
3614
+ method: 'POST',
3615
+ headers: {
3616
+ ...this.getAuthHeaders(),
3617
+ 'Content-Type': 'application/json'
3618
+ },
3619
+ body: JSON.stringify({
3620
+ user_ids: userIds,
3621
+ role
3622
+ })
3623
+ });
3624
+ if (!response.ok) {
3625
+ const data = await response.json();
3626
+ throw new RegistryError(data.error || `Failed to add members: ${response.status}`, response.status, data);
3627
+ }
3628
+ }
3629
+ /**
3630
+ * Remove a member from a group.
3631
+ *
3632
+ * @param groupId - Group UUID
3633
+ * @param userId - User ID to remove
3634
+ */ async removeGroupMember(groupId, userId) {
3635
+ const params = new URLSearchParams({
3636
+ user_id: userId
3637
+ });
3638
+ const encodedGroupId = encodeURIComponent(groupId);
3639
+ const url = `${this.getApiBase()}/skill-groups/${encodedGroupId}/members?${params.toString()}`;
3640
+ const response = await fetch(url, {
3641
+ method: 'DELETE',
3642
+ headers: this.getAuthHeaders()
3643
+ });
3644
+ if (!response.ok) {
3645
+ const data = await response.json();
3646
+ throw new RegistryError(data.error || `Failed to remove member: ${response.status}`, response.status, data);
3647
+ }
3648
+ }
3649
+ /**
3650
+ * Update a member's role in a group.
3651
+ *
3652
+ * @param groupId - Group UUID
3653
+ * @param userId - User ID to update
3654
+ * @param role - New role to assign
3655
+ */ async updateGroupMemberRole(groupId, userId, role) {
3656
+ const encodedGroupId = encodeURIComponent(groupId);
3657
+ const url = `${this.getApiBase()}/skill-groups/${encodedGroupId}/members`;
3658
+ const response = await fetch(url, {
3659
+ method: 'PATCH',
3660
+ headers: {
3661
+ ...this.getAuthHeaders(),
3662
+ 'Content-Type': 'application/json'
3663
+ },
3664
+ body: JSON.stringify({
3665
+ user_id: userId,
3666
+ role
3667
+ })
3668
+ });
3669
+ if (!response.ok) {
3670
+ const data = await response.json();
3671
+ throw new RegistryError(data.error || `Failed to update member role: ${response.status}`, response.status, data);
3672
+ }
3673
+ }
3493
3674
  }
3494
3675
  /**
3495
3676
  * Tarball Extractor (Step 3.6)
@@ -4784,15 +4965,15 @@ class RegistryResolver {
4784
4965
  /**
4785
4966
  * Install a web-published skill.
4786
4967
  *
4787
- * Web-published skills do not support versioning. Branches to different
4968
+ * Web-published skills (except local) do not support versioning. Branches to different
4788
4969
  * installation logic based on source_type:
4789
4970
  * - github/gitlab: reuses installToAgentsFromGit
4790
4971
  * - oss_url/custom_url: reuses installToAgentsFromHttp
4791
4972
  * - local: downloads tarball via Registry API
4792
4973
  */ async installFromWebPublished(skillInfo, parsed, targetAgents, options = {}) {
4793
4974
  const { source_type, source_url } = skillInfo;
4794
- // Web-published skills do not support version specifiers
4795
- if (parsed.version && 'latest' !== parsed.version) throw new Error(`Version specifier not supported for web-published skills.\n'${parsed.fullName}' was published via web and does not support versioning.\nUse: reskill install ${parsed.fullName}`);
4975
+ // Web-published skills (except local) do not support version specifiers
4976
+ if ('local' !== source_type && parsed.version && 'latest' !== parsed.version) throw new Error(`Version specifier not supported for web-published skills.\n'${parsed.fullName}' was published via web and does not support versioning.\nUse: reskill install ${parsed.fullName}`);
4796
4977
  if (!source_url) throw new Error(`Missing source_url for web-published skill: ${parsed.fullName}`);
4797
4978
  logger_logger["package"](`Installing ${parsed.fullName} from ${source_type} source...`);
4798
4979
  // Build registry context so downstream methods use the registry name
@@ -4883,12 +5064,13 @@ class RegistryResolver {
4883
5064
  const { save = true, mode = 'symlink' } = options;
4884
5065
  const registryUrl = await this.resolveRegistryUrl(parsed.fullName, options.registry);
4885
5066
  const shortName = getShortName(parsed.fullName);
4886
- const version = 'latest';
4887
- // Download tarball via RegistryClient (handles auth + 302 redirect to signed URL)
5067
+ // Resolve version via dist-tags (supports @latest, @1.0.0, etc.)
4888
5068
  const client = new RegistryClient({
4889
5069
  registry: registryUrl,
4890
5070
  token: options.token
4891
5071
  });
5072
+ const version = await client.resolveVersion(parsed.fullName, parsed.version);
5073
+ // Download tarball via RegistryClient (handles auth + 302 redirect to signed URL)
4892
5074
  const { tarball } = await client.downloadSkill(parsed.fullName, version);
4893
5075
  logger_logger["package"](`Installing ${shortName} from ${registryUrl} to ${targetAgents.length} agent(s)...`);
4894
5076
  // Extract tarball to temp directory (clean stale files first)
@@ -6419,6 +6601,406 @@ class AuthManager {
6419
6601
  // Command Definition
6420
6602
  // ============================================================================
6421
6603
  const findCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('find').alias('search').description('Search for skills in the registry').argument('<query>', 'Search query').option('-r, --registry <url>', 'Registry URL (or set RESKILL_REGISTRY env var, or defaults.publishRegistry in skills.json)').option('-l, --limit <n>', 'Maximum number of results', '10').option('-j, --json', 'Output as JSON').action(findAction);
6604
+ /**
6605
+ * Group path utilities — normalization, slug generation, and validation.
6606
+ *
6607
+ * Shared by the `group` and `publish` CLI commands.
6608
+ */ const SLUG_REGEX = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
6609
+ const MAX_GROUP_DEPTH = 3;
6610
+ const MAX_SEGMENT_LENGTH = 64;
6611
+ /**
6612
+ * Normalize a group path for API usage.
6613
+ *
6614
+ * Rules from spec §13.2:
6615
+ * - Strip leading/trailing slashes and whitespace
6616
+ * - Collapse consecutive slashes
6617
+ * - Lowercase
6618
+ */ function normalizeGroupPath(raw) {
6619
+ return raw.trim().replace(/\/+/g, '/').replace(/^\/|\/$/g, '').toLowerCase();
6620
+ }
6621
+ /**
6622
+ * Generate a URL-safe slug from a human-readable name.
6623
+ *
6624
+ * Spec §13.4:
6625
+ * - Lowercase, trim, replace spaces/underscores with hyphens
6626
+ * - Strip non-alphanumeric characters (except hyphens)
6627
+ * - Collapse consecutive hyphens, strip leading/trailing hyphens
6628
+ * - Truncate to MAX_SEGMENT_LENGTH characters
6629
+ */ function generateSlug(name) {
6630
+ return name.toLowerCase().trim().replace(/[\s_]+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, MAX_SEGMENT_LENGTH).replace(/-$/g, '');
6631
+ }
6632
+ /**
6633
+ * Validate a normalized group path.
6634
+ *
6635
+ * Spec §13.2:
6636
+ * - Segment slug must match SLUG_REGEX
6637
+ * - Segment length <= 64
6638
+ * - Max depth <= 3
6639
+ */ function validateGroupPath(path) {
6640
+ if (!path) return {
6641
+ valid: false,
6642
+ error: 'Group path cannot be empty'
6643
+ };
6644
+ const segments = path.split('/');
6645
+ if (segments.length > MAX_GROUP_DEPTH) return {
6646
+ valid: false,
6647
+ error: `Group path depth cannot exceed ${MAX_GROUP_DEPTH} segments`
6648
+ };
6649
+ for (const segment of segments){
6650
+ if (segment.length > MAX_SEGMENT_LENGTH) return {
6651
+ valid: false,
6652
+ error: `Group path segment "${segment}" exceeds ${MAX_SEGMENT_LENGTH} characters`
6653
+ };
6654
+ if (!SLUG_REGEX.test(segment)) return {
6655
+ valid: false,
6656
+ error: `Invalid group path segment "${segment}". Segments must match ${SLUG_REGEX}`
6657
+ };
6658
+ }
6659
+ return {
6660
+ valid: true
6661
+ };
6662
+ }
6663
+ /**
6664
+ * group command - Manage skill groups
6665
+ *
6666
+ * Provides subcommands for listing, creating, inspecting, and deleting
6667
+ * skill groups, as well as managing group membership.
6668
+ *
6669
+ * Usage:
6670
+ * reskill group list # List visible groups
6671
+ * reskill group create <name> # Create a group
6672
+ * reskill group info <path> # Show group details
6673
+ * reskill group delete <path> # Delete a group
6674
+ * reskill group member list <path> # List members
6675
+ * reskill group member add <path> <users...> # Add members
6676
+ * reskill group member remove <path> <user> # Remove a member
6677
+ * reskill group member role <path> <user> <role> # Change member role
6678
+ */ // ============================================================================
6679
+ // Constants
6680
+ // ============================================================================
6681
+ const VALID_ROLES = [
6682
+ 'owner',
6683
+ 'maintainer',
6684
+ 'developer'
6685
+ ];
6686
+ /**
6687
+ * Validate that a role string is one of the allowed values.
6688
+ */ function validateRole(role) {
6689
+ return VALID_ROLES.includes(role);
6690
+ }
6691
+ function assertValidGroupPath(path) {
6692
+ const validation = validateGroupPath(path);
6693
+ if (!validation.valid) {
6694
+ logger_logger.error(validation.error);
6695
+ process.exit(1);
6696
+ }
6697
+ }
6698
+ // ============================================================================
6699
+ // Client Factory
6700
+ // ============================================================================
6701
+ function createClient(registry) {
6702
+ const authManager = new AuthManager();
6703
+ const token = authManager.getToken(registry);
6704
+ if (!token) {
6705
+ logger_logger.error('Authentication required');
6706
+ logger_logger.newline();
6707
+ logger_logger.log("Run 'reskill login' to authenticate.");
6708
+ process.exit(1);
6709
+ }
6710
+ return new RegistryClient({
6711
+ registry,
6712
+ token
6713
+ });
6714
+ }
6715
+ // ============================================================================
6716
+ // Display Helpers
6717
+ // ============================================================================
6718
+ function getGroupIndentLevel(group) {
6719
+ if ('number' == typeof group.level && Number.isFinite(group.level)) return Math.max(0, group.level - 1);
6720
+ return Math.max(0, group.path.split('/').length - 1);
6721
+ }
6722
+ function displayGroupList(groups, json, tree = false) {
6723
+ if (json) {
6724
+ console.log(JSON.stringify(groups, null, 2));
6725
+ return;
6726
+ }
6727
+ if (0 === groups.length) {
6728
+ logger_logger.warn('No groups found');
6729
+ return;
6730
+ }
6731
+ logger_logger.newline();
6732
+ logger_logger.log(`Found ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(String(groups.length))} group${1 === groups.length ? '' : 's'}:`);
6733
+ logger_logger.newline();
6734
+ if (tree) {
6735
+ const sortedGroups = [
6736
+ ...groups
6737
+ ].sort((a, b)=>a.path.localeCompare(b.path));
6738
+ for (const group of sortedGroups){
6739
+ const vis = 'private' === group.visibility ? __WEBPACK_EXTERNAL_MODULE_chalk__["default"].yellow(' (private)') : '';
6740
+ const desc = group.description ? __WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(` - ${group.description}`) : '';
6741
+ const indent = ' '.repeat(getGroupIndentLevel(group));
6742
+ const nodeLabel = group.path.split('/').pop() || group.path;
6743
+ logger_logger.log(` ${indent}└─ ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold.cyan(nodeLabel)}${vis}${desc} ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(`(${group.path})`)}`);
6744
+ }
6745
+ logger_logger.newline();
6746
+ return;
6747
+ }
6748
+ for (const group of groups){
6749
+ const vis = 'private' === group.visibility ? __WEBPACK_EXTERNAL_MODULE_chalk__["default"].yellow(' (private)') : '';
6750
+ const desc = group.description ? __WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(` - ${group.description}`) : '';
6751
+ logger_logger.log(` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold.cyan(group.path)}${vis}${desc}`);
6752
+ const meta = [];
6753
+ if (void 0 !== group.skill_count) meta.push(`${group.skill_count} skill(s)`);
6754
+ if (void 0 !== group.member_count) meta.push(`${group.member_count} member(s)`);
6755
+ if (meta.length > 0) logger_logger.log(` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(meta.join(' · '))}`);
6756
+ }
6757
+ logger_logger.newline();
6758
+ }
6759
+ function displayGroupDetail(detail, json) {
6760
+ if (json) {
6761
+ console.log(JSON.stringify(detail, null, 2));
6762
+ return;
6763
+ }
6764
+ logger_logger.newline();
6765
+ logger_logger.log(`${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(detail.name)} ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(`(${detail.path})`)}`);
6766
+ if (detail.description) logger_logger.log(` ${detail.description}`);
6767
+ logger_logger.newline();
6768
+ const vis = 'private' === detail.visibility ? __WEBPACK_EXTERNAL_MODULE_chalk__["default"].yellow('private') : __WEBPACK_EXTERNAL_MODULE_chalk__["default"].green('public');
6769
+ logger_logger.log(` Visibility: ${vis}`);
6770
+ logger_logger.log(` Level: ${detail.level}`);
6771
+ if (void 0 !== detail.skill_count) logger_logger.log(` Skills: ${detail.skill_count}`);
6772
+ if (void 0 !== detail.member_count) logger_logger.log(` Members: ${detail.member_count}`);
6773
+ if (detail.current_user_role) logger_logger.log(` Your role: ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(detail.current_user_role)}`);
6774
+ if (detail.children && detail.children.length > 0) {
6775
+ logger_logger.newline();
6776
+ logger_logger.log(' Sub groups:');
6777
+ for (const child of detail.children)logger_logger.log(` • ${child.path}`);
6778
+ }
6779
+ logger_logger.newline();
6780
+ }
6781
+ function displayMemberList(members, groupPath, json) {
6782
+ if (json) {
6783
+ console.log(JSON.stringify(members, null, 2));
6784
+ return;
6785
+ }
6786
+ if (0 === members.length) {
6787
+ logger_logger.warn(`No members in group "${groupPath}"`);
6788
+ return;
6789
+ }
6790
+ logger_logger.newline();
6791
+ logger_logger.log(`${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(String(members.length))} member${1 === members.length ? '' : 's'} in ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(groupPath)}:`);
6792
+ logger_logger.newline();
6793
+ for (const member of members){
6794
+ const role = __WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(member.role);
6795
+ const handle = member.handle || member.user_id;
6796
+ logger_logger.log(` ${handle} ${role}`);
6797
+ }
6798
+ logger_logger.newline();
6799
+ }
6800
+ // ============================================================================
6801
+ // Subcommand Actions
6802
+ // ============================================================================
6803
+ async function listAction(options) {
6804
+ const registry = resolveRegistry(options.registry);
6805
+ const client = createClient(registry);
6806
+ try {
6807
+ const groups = await client.listGroups({
6808
+ flat: Boolean(options.tree)
6809
+ });
6810
+ displayGroupList(groups, options.json || false, Boolean(options.tree));
6811
+ } catch (error) {
6812
+ logger_logger.error(`Failed to list groups: ${error.message}`);
6813
+ process.exit(1);
6814
+ }
6815
+ }
6816
+ async function createAction(name, options) {
6817
+ const registry = resolveRegistry(options.registry);
6818
+ const slug = generateSlug(name);
6819
+ if (!slug) {
6820
+ logger_logger.error('Name must contain at least one ASCII alphanumeric character to generate a valid slug');
6821
+ process.exit(1);
6822
+ }
6823
+ if (!SLUG_REGEX.test(slug)) {
6824
+ logger_logger.error(`Generated slug "${slug}" is invalid. Name must produce a slug matching ${SLUG_REGEX.source}`);
6825
+ process.exit(1);
6826
+ }
6827
+ let parentId;
6828
+ let client;
6829
+ if (options.parent) {
6830
+ const normalizedParent = normalizeGroupPath(options.parent);
6831
+ assertValidGroupPath(normalizedParent);
6832
+ client = createClient(registry);
6833
+ try {
6834
+ const parentGroup = await client.resolveGroup(normalizedParent);
6835
+ parentId = parentGroup.id;
6836
+ } catch (error) {
6837
+ if (error instanceof RegistryError) logger_logger.error(`Parent group "${options.parent}" not found`);
6838
+ else logger_logger.error(`Failed to resolve parent: ${error.message}`);
6839
+ process.exit(1);
6840
+ }
6841
+ }
6842
+ try {
6843
+ const ensuredClient = client ?? createClient(registry);
6844
+ const group = await ensuredClient.createGroup({
6845
+ name,
6846
+ slug,
6847
+ description: options.description,
6848
+ visibility: options.visibility,
6849
+ parent_id: parentId
6850
+ });
6851
+ if (options.json) console.log(JSON.stringify(group, null, 2));
6852
+ else {
6853
+ logger_logger.newline();
6854
+ logger_logger.success(`Group created: ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(group.path)}`);
6855
+ logger_logger.log(` ID: ${group.id}`);
6856
+ logger_logger.log(` Visibility: ${group.visibility}`);
6857
+ logger_logger.newline();
6858
+ }
6859
+ } catch (error) {
6860
+ logger_logger.error(`Failed to create group: ${error.message}`);
6861
+ process.exit(1);
6862
+ }
6863
+ }
6864
+ async function infoAction(groupPath, options) {
6865
+ const normalized = normalizeGroupPath(groupPath);
6866
+ assertValidGroupPath(normalized);
6867
+ const registry = resolveRegistry(options.registry);
6868
+ const client = createClient(registry);
6869
+ try {
6870
+ const detail = await client.resolveGroup(normalized);
6871
+ displayGroupDetail(detail, options.json || false);
6872
+ } catch (error) {
6873
+ if (error instanceof RegistryError) {
6874
+ if (404 === error.statusCode) logger_logger.error(`Group "${groupPath}" not found`);
6875
+ else logger_logger.error(`Failed to get group info: ${error.message}`);
6876
+ } else logger_logger.error(`Failed to get group info: ${error.message}`);
6877
+ process.exit(1);
6878
+ }
6879
+ }
6880
+ async function deleteAction(groupPath, options) {
6881
+ const normalized = normalizeGroupPath(groupPath);
6882
+ assertValidGroupPath(normalized);
6883
+ const registry = resolveRegistry(options.registry);
6884
+ const client = createClient(registry);
6885
+ try {
6886
+ const detail = await client.resolveGroup(normalized);
6887
+ if (options.dryRun) {
6888
+ const result = await client.deleteGroup(detail.id, true);
6889
+ logger_logger.newline();
6890
+ logger_logger.log(`Dry run: deleting group "${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(groupPath)}"`);
6891
+ if (void 0 !== result.affected_skills) logger_logger.log(` Affected skills: ${result.affected_skills}`);
6892
+ logger_logger.log('No changes made (--dry-run)');
6893
+ logger_logger.newline();
6894
+ return;
6895
+ }
6896
+ if (!options.yes) {
6897
+ const { createInterface } = await import("node:readline");
6898
+ const rl = createInterface({
6899
+ input: process.stdin,
6900
+ output: process.stdout
6901
+ });
6902
+ const answer = await new Promise((resolve)=>{
6903
+ rl.question(`\n? Delete group "${groupPath}" and all its contents? (y/N) `, resolve);
6904
+ rl.once('close', ()=>resolve(''));
6905
+ });
6906
+ rl.close();
6907
+ if ('y' !== answer.trim().toLowerCase() && 'yes' !== answer.trim().toLowerCase()) {
6908
+ logger_logger.log('Cancelled.');
6909
+ return;
6910
+ }
6911
+ }
6912
+ await client.deleteGroup(detail.id, false);
6913
+ logger_logger.success(`Group "${groupPath}" deleted`);
6914
+ } catch (error) {
6915
+ if (error instanceof RegistryError) {
6916
+ if (404 === error.statusCode) logger_logger.error(`Group "${groupPath}" not found`);
6917
+ else logger_logger.error(`Failed to delete group: ${error.message}`);
6918
+ } else logger_logger.error(`Failed to delete group: ${error.message}`);
6919
+ process.exit(1);
6920
+ }
6921
+ }
6922
+ // ============================================================================
6923
+ // Member Subcommand Actions
6924
+ // ============================================================================
6925
+ async function memberListAction(groupPath, options) {
6926
+ const normalized = normalizeGroupPath(groupPath);
6927
+ assertValidGroupPath(normalized);
6928
+ const registry = resolveRegistry(options.registry);
6929
+ const client = createClient(registry);
6930
+ try {
6931
+ const detail = await client.resolveGroup(normalized);
6932
+ const members = await client.listGroupMembers(detail.id);
6933
+ displayMemberList(members, groupPath, options.json || false);
6934
+ } catch (error) {
6935
+ logger_logger.error(`Failed to list members: ${error.message}`);
6936
+ process.exit(1);
6937
+ }
6938
+ }
6939
+ async function memberAddAction(groupPath, userIds, options) {
6940
+ const normalized = normalizeGroupPath(groupPath);
6941
+ assertValidGroupPath(normalized);
6942
+ const registry = resolveRegistry(options.registry);
6943
+ const client = createClient(registry);
6944
+ const role = options.role || 'developer';
6945
+ if (!validateRole(role)) {
6946
+ logger_logger.error(`Invalid role "${role}". Must be one of: ${VALID_ROLES.join(', ')}`);
6947
+ process.exit(1);
6948
+ }
6949
+ try {
6950
+ const detail = await client.resolveGroup(normalized);
6951
+ await client.addGroupMembers(detail.id, userIds, role);
6952
+ logger_logger.success(`Added ${userIds.length} member(s) to "${groupPath}" as ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(role)}`);
6953
+ } catch (error) {
6954
+ logger_logger.error(`Failed to add members: ${error.message}`);
6955
+ process.exit(1);
6956
+ }
6957
+ }
6958
+ async function memberRemoveAction(groupPath, userId, options) {
6959
+ const normalized = normalizeGroupPath(groupPath);
6960
+ assertValidGroupPath(normalized);
6961
+ const registry = resolveRegistry(options.registry);
6962
+ const client = createClient(registry);
6963
+ try {
6964
+ const detail = await client.resolveGroup(normalized);
6965
+ await client.removeGroupMember(detail.id, userId);
6966
+ logger_logger.success(`Removed "${userId}" from "${groupPath}"`);
6967
+ } catch (error) {
6968
+ logger_logger.error(`Failed to remove member: ${error.message}`);
6969
+ process.exit(1);
6970
+ }
6971
+ }
6972
+ async function memberRoleAction(groupPath, userId, role, options) {
6973
+ const normalized = normalizeGroupPath(groupPath);
6974
+ assertValidGroupPath(normalized);
6975
+ const registry = resolveRegistry(options.registry);
6976
+ const client = createClient(registry);
6977
+ if (!validateRole(role)) {
6978
+ logger_logger.error(`Invalid role "${role}". Must be one of: ${VALID_ROLES.join(', ')}`);
6979
+ process.exit(1);
6980
+ }
6981
+ try {
6982
+ const detail = await client.resolveGroup(normalized);
6983
+ await client.updateGroupMemberRole(detail.id, userId, role);
6984
+ logger_logger.success(`Updated role of "${userId}" in "${groupPath}" to ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(role)}`);
6985
+ } catch (error) {
6986
+ logger_logger.error(`Failed to update role: ${error.message}`);
6987
+ process.exit(1);
6988
+ }
6989
+ }
6990
+ // ============================================================================
6991
+ // Command Definitions
6992
+ // ============================================================================
6993
+ const memberCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('member').description('Manage group members');
6994
+ memberCommand.command('list <path>').description('List members of a group').option('-r, --registry <url>', 'Registry URL').option('-j, --json', 'Output as JSON').action(memberListAction);
6995
+ memberCommand.command('add <path> <users...>').description('Add members to a group').option('-r, --registry <url>', 'Registry URL').option('--role <role>', 'Role to assign (owner|maintainer|developer)', 'developer').action(memberAddAction);
6996
+ memberCommand.command('remove <path> <user>').description('Remove a member from a group').option('-r, --registry <url>', 'Registry URL').action(memberRemoveAction);
6997
+ memberCommand.command('role <path> <user> <role>').description("Change a member's role").option('-r, --registry <url>', 'Registry URL').action(memberRoleAction);
6998
+ const groupCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('group').description('Manage skill groups');
6999
+ groupCommand.command('list').description('List visible groups').option('-r, --registry <url>', 'Registry URL').option('--tree', 'Render groups as a tree (requests flat group list)').option('-j, --json', 'Output as JSON').action(listAction);
7000
+ groupCommand.command('create <name>').description('Create a new group').option('-r, --registry <url>', 'Registry URL').option('-d, --description <text>', 'Group description').option('--visibility <level>', 'Visibility: public or private', 'public').option('--parent <path>', 'Parent group path (for sub groups)').option('-j, --json', 'Output as JSON').action(createAction);
7001
+ groupCommand.command('info <path>').description('Show group details').option('-r, --registry <url>', 'Registry URL').option('-j, --json', 'Output as JSON').action(infoAction);
7002
+ groupCommand.command('delete <path>').description('Delete a group').option('-r, --registry <url>', 'Registry URL').option('-n, --dry-run', 'Preview deletion without executing').option('-y, --yes', 'Skip confirmation').action(deleteAction);
7003
+ groupCommand.addCommand(memberCommand);
6422
7004
  /**
6423
7005
  * info command - Show skill details
6424
7006
  */ const infoCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('info').description('Show skill details').argument('<skill>', 'Skill name').option('-j, --json', 'Output as JSON').action((skillName, options)=>{
@@ -7650,6 +8232,7 @@ const SNIPPET_MAX_LENGTH = 120;
7650
8232
  }
7651
8233
  },
7652
8234
  // Rule 3: Content Obfuscation (high) — scans ALL content including safe zones
8235
+ // Zero-width chars and base64 are suspicious everywhere (even inside code blocks).
7653
8236
  {
7654
8237
  id: 'obfuscation',
7655
8238
  level: 'high',
@@ -7670,6 +8253,18 @@ const SNIPPET_MAX_LENGTH = 120;
7670
8253
  line: i + 1,
7671
8254
  snippet: 'Suspicious base64-encoded block detected'
7672
8255
  });
8256
+ return matches;
8257
+ }
8258
+ },
8259
+ // Rule 3b: Large HTML Comments (high) — respects safe zones (code blocks, etc.)
8260
+ // HTML comments inside fenced code blocks are normal code examples, not obfuscation.
8261
+ {
8262
+ id: 'obfuscation',
8263
+ level: 'high',
8264
+ message: 'Detected content obfuscation',
8265
+ skipSafeZones: true,
8266
+ check: (content)=>{
8267
+ const matches = [];
7673
8268
  // Large HTML comments (>200 chars of content)
7674
8269
  const commentRegex = /<!--([\s\S]{200,}?)-->/g;
7675
8270
  let match;
@@ -8760,6 +9355,7 @@ async function publishAction(skillPath, options) {
8760
9355
  const absolutePath = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(skillPath);
8761
9356
  // Use cwd() as project root to find skills.json, not the skill path
8762
9357
  const registry = resolveRegistry(options.registry, process.cwd());
9358
+ let normalizedGroupPath;
8763
9359
  // Validate registry is not a blocked public registry
8764
9360
  validateRegistry(registry);
8765
9361
  // Check directory exists
@@ -8767,6 +9363,14 @@ async function publishAction(skillPath, options) {
8767
9363
  logger_logger.error(`Directory not found: ${skillPath}`);
8768
9364
  process.exit(1);
8769
9365
  }
9366
+ if (options.group) {
9367
+ normalizedGroupPath = normalizeGroupPath(options.group);
9368
+ const validation = validateGroupPath(normalizedGroupPath);
9369
+ if (!validation.valid) {
9370
+ logger_logger.error(validation.error);
9371
+ process.exit(1);
9372
+ }
9373
+ }
8770
9374
  const validator = new SkillValidator();
8771
9375
  const publisher = new Publisher();
8772
9376
  try {
@@ -8852,6 +9456,11 @@ async function publishAction(skillPath, options) {
8852
9456
  displayFiles(absolutePath, skill.files, publisher);
8853
9457
  // Display metadata
8854
9458
  displayMetadata(skill);
9459
+ // Display group info
9460
+ if (normalizedGroupPath) {
9461
+ logger_logger.newline();
9462
+ logger_logger.log(`Group: ${normalizedGroupPath}`);
9463
+ }
8855
9464
  // 8. Dry run mode ends here
8856
9465
  if (options.dryRun && payload) {
8857
9466
  displayDryRunSummary(payload);
@@ -8897,7 +9506,8 @@ async function publishAction(skillPath, options) {
8897
9506
  process.exit(1);
8898
9507
  }
8899
9508
  const result = await client.publish(skillName, payload, absolutePath, {
8900
- tag: options.tag
9509
+ tag: options.tag,
9510
+ groupPath: normalizedGroupPath
8901
9511
  });
8902
9512
  if (!result.success || !result.data) {
8903
9513
  logger_logger.error(result.error || 'Publish failed');
@@ -8931,7 +9541,7 @@ async function publishAction(skillPath, options) {
8931
9541
  // ============================================================================
8932
9542
  // Command Definition
8933
9543
  // ============================================================================
8934
- const publishCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('publish').alias('pub').description('Publish a skill to the registry').argument('[path]', 'Path to skill directory', '.').option('-r, --registry <url>', 'Registry URL (or set RESKILL_REGISTRY env var, or defaults.publishRegistry in skills.json)').option('-t, --tag <tag>', 'Git tag to publish').option('--access <level>', 'Access level: public or restricted', 'public').option('-n, --dry-run', 'Validate without publishing').option('-y, --yes', 'Skip confirmation prompts').action(publishAction);
9544
+ const publishCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('publish').alias('pub').description('Publish a skill to the registry').argument('[path]', 'Path to skill directory', '.').option('-r, --registry <url>', 'Registry URL (or set RESKILL_REGISTRY env var, or defaults.publishRegistry in skills.json)').option('-t, --tag <tag>', 'Git tag to publish').option('--access <level>', 'Access level: public or restricted', 'public').option('-n, --dry-run', 'Validate without publishing').option('-y, --yes', 'Skip confirmation prompts').option('-g, --group <path>', 'Publish skill into a group (e.g., "kanyun/frontend")').action(publishAction);
8935
9545
  /**
8936
9546
  * uninstall command - Uninstall one or more skills
8937
9547
  */ const uninstallCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('uninstall').alias('un').alias('remove').alias('rm').description('Uninstall one or more skills').argument('<skills...>', 'Skill names to uninstall').option('-g, --global', 'Uninstall from global installation (~/.claude/skills)').option('-y, --yes', 'Skip confirmation prompts').action(async (skillNames, options)=>{
@@ -9107,6 +9717,7 @@ program.addCommand(publishCommand);
9107
9717
  program.addCommand(loginCommand);
9108
9718
  program.addCommand(logoutCommand);
9109
9719
  program.addCommand(whoamiCommand);
9720
+ program.addCommand(groupCommand);
9110
9721
  program.addCommand(completionCommand);
9111
9722
  program.addCommand(doctorCommand);
9112
9723
  // Start update check in background (non-blocking)