Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
6c0d4f4
feat(skillhub-cli): add skillhub CLI with interactive multi-agent sup…
Rsweater Apr 14, 2026
c8e58e0
fix(resolve): strip 'v' prefix from version for API compatibility
Rsweater Apr 14, 2026
a85beea
fix(install): change install method from multiselect to single select
Rsweater Apr 14, 2026
ffdf8ae
fix(skillhub-cli): improve publish output with default namespace hint…
Rsweater Apr 14, 2026
c86d99f
feat(explore): add --sort parameter for hot/newest/downloads ordering
Rsweater Apr 14, 2026
a7b0485
fix(cli package.json): package name changed from @motovis/skillhub to…
Rsweater Apr 14, 2026
e15ad7b
feat(explore): add --sort parameter and fix k key input issue
Rsweater Apr 14, 2026
679333c
feat(cli): show tags and changelog after successful publish
Rsweater Apr 14, 2026
98b0425
fix: change package name to @motovis/skillhub for npm org scope
Rsweater Apr 15, 2026
de7be0f
fix: add .npmignore to exclude tests and tmp from published package
Rsweater Apr 15, 2026
8c87cc5
fix: add shebang to cli.ts for npm bin to work properly
Rsweater Apr 15, 2026
af6ebf3
fix(cli): handle 302 redirect in download and fix --skill-version opt…
Rsweater Apr 16, 2026
c9644e8
chore: bump version to 1.0.2
Rsweater Apr 16, 2026
626490e
feat(cli): support SKILLHUB_REGISTRY env var for registry URL
Rsweater Apr 16, 2026
c970a76
chore: bump version to 1.0.3
Rsweater Apr 16, 2026
616319f
fix(cli): replace execSync unzip with unzipper for cross-platform sup…
Rsweater Apr 16, 2026
601644b
fix(cli): fix addToLock namespace doubling, update fallback path, and…
Rsweater Apr 16, 2026
ed21388
chore: bump version to 1.0.6
Rsweater Apr 16, 2026
5cf44c5
feat(cli): custom grouped help page with aliases and examples
Rsweater Apr 16, 2026
e93adaa
feat(web): replace clawhub with skillhub CLI in frontend
Rsweater Apr 16, 2026
90809c4
fix(cli): stop spinner before version selection to prevent UI conflict
Rsweater Apr 17, 2026
39db503
fix(cli): improve install spinner messages and fix interactive search…
Rsweater Apr 20, 2026
ea4e206
chore(cli): update .npmignore and add release scripts
Rsweater Apr 20, 2026
32395ee
refactor(cli): use dynamic isUniversalForScope instead of static isUn…
Rsweater Apr 20, 2026
3353953
fix(cli): determine scope before agent selection for correct universa…
Rsweater Apr 20, 2026
aaf80db
fix(cli): sort agent lists alphabetically and fix spinner residue
Rsweater Apr 20, 2026
86b8de1
feat(cli): merge install results by path for cleaner output
Rsweater Apr 20, 2026
b6301cb
chore(cli): bump version to 1.1.0
Rsweater Apr 20, 2026
e5da739
refactor(cli): sort all list outputs and agent names alphabetically
Rsweater Apr 20, 2026
04bc62f
feat(cli): add scope-aware universal grouping and improve output form…
Rsweater Apr 20, 2026
3cf4d64
chore(cli): bump version to 1.1.2
Rsweater Apr 20, 2026
ee18161
feat(cli): improve list and check commands with better filtering
Rsweater Apr 20, 2026
d6b9f02
revert(web): restore vite.config.ts to use port 3000
Rsweater Apr 20, 2026
2543cd5
fix(cli): make --registry parameter work for all commands
Rsweater Apr 20, 2026
6a6df19
chore(cli): bump version to 1.1.3 for --registry parameter fix
Rsweater Apr 20, 2026
b332d0b
Merge remote-tracking branch 'upstream/main' into feat/skillhub-cli-v2
Rsweater Apr 21, 2026
f5caed9
chore: remove local scripts from git tracking
Rsweater Apr 21, 2026
8815c15
fix(cli): human-readable API error messages
Rsweater Apr 21, 2026
d96ef86
fix(cli): replace process.exit(1) with process.exitCode = 1
Rsweater Apr 21, 2026
d4ea485
fix(cli): fix registry config priority for login and all commands
Rsweater Apr 21, 2026
acea441
feat(cli): enhance inspect/install commands and reorganize help
Rsweater Apr 21, 2026
a1fb6d2
feat(cli): simplify config command and enhance inspect/install
Rsweater Apr 22, 2026
3349182
fix(cli): fix show-env-instructions output format
Rsweater Apr 22, 2026
2fffc2b
fix(cli): remove unsupported rating sort, add stars sort, clean up ve…
Rsweater Apr 22, 2026
4a7ebb4
feat(cli): implement comprehensive sort strategy with backend and cli…
Rsweater Apr 22, 2026
f33a9e0
feat(cli): add inspect hint after explore selection
Rsweater Apr 22, 2026
c7d4f82
feat(cli): enhance inspect with interactive selection and reorganize …
Rsweater Apr 22, 2026
08ce3d5
feat(cli): 优化帮助界面和命令结构
Rsweater Apr 23, 2026
3239b4d
fix(inspect): 修复版本选项与全局 --version 冲突
Rsweater Apr 23, 2026
58ee1a5
fix(install): 修复帮助文本中的循环引用
Rsweater Apr 23, 2026
273e14a
feat(cli): 添加 --namespace 选项和智能 namespace 解析
Rsweater Apr 23, 2026
f4752f2
feat(cli): 优化 download 和 update 命令,添加友好错误提示
Rsweater Apr 23, 2026
803540b
feat(auth): 优化权限配置,扩展 SKILL_ADMIN 和 AUDITOR 权限
Rsweater Apr 23, 2026
aa9fd5f
feat(cli): add command aliases and unhide command
Rsweater Apr 24, 2026
84a7288
fix(cli): remove debug logging from api-client
Rsweater Apr 24, 2026
898325a
fix(cli): restore -v short option for --skill-version
Rsweater Apr 24, 2026
67c0726
fix(cli): enhance 503 error handling in download command
Rsweater Apr 24, 2026
bc9b3d1
fix(cli): rename loop variable to avoid conflict with info function
Rsweater Apr 24, 2026
d96fb00
feat(cli): enhance error handling and auto-create output directory
Rsweater Apr 24, 2026
c14b844
refactor(cli): merge list/check commands, unify skill status display
Rsweater Apr 24, 2026
1ba1c90
feat(api): unify permission checks for hide/unhide/archive operations
Rsweater Apr 24, 2026
d273cac
feat(cli): update hide/unhide commands and reorganize help categories
Rsweater Apr 24, 2026
0bfcb02
feat(cli): move namespaces to me subcommand and reorganize help categ…
Rsweater Apr 24, 2026
5caf0b3
feat(cli): reorganize help categories - merge Skill Lifecycle into Pu…
Rsweater Apr 24, 2026
b25bbf5
fix(api): resolve API token authentication issues for skill operations
Rsweater Apr 24, 2026
0bef6fd
feat(api): enhance permission checks for skill lifecycle operations
Rsweater Apr 24, 2026
189b3e7
chore(cli): bump version to 1.2.11
Rsweater Apr 24, 2026
4ae9518
fix: update test files for platformRoles parameter in lifecycle gover…
Rsweater Apr 24, 2026
ac2d4eb
feat(auth): add skill:manage scope for lifecycle governance API token…
Rsweater Apr 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# OS files
.DS_Store
Thumbs.db
.nfs*

# Editors / IDEs / local tooling
.claude/
Expand All @@ -14,6 +15,7 @@ Thumbs.db
*.iml
*.swp
*.swo
*.code-workspace

# Logs
*.log
Expand Down Expand Up @@ -86,3 +88,7 @@ CLAUDE.md

# Local config file
.mcp.json

# Local scripts (personal utilities)
scripts/notify-feishu.sh
scripts/release-cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ public ApiResponse<TokenCreateResponse> create(
@Valid @RequestBody TokenCreateRequest request) {
String scopeJson;
if (request.scopes() == null || request.scopes().isEmpty()) {
scopeJson = "[\"skill:read\",\"skill:publish\"]";
scopeJson = "[\"skill:read\",\"skill:publish\",\"skill:manage\"]";
} else {
try {
scopeJson = objectMapper.writeValueAsString(request.scopes());
} catch (JsonProcessingException e) {
scopeJson = "[\"skill:read\",\"skill:publish\"]";
scopeJson = "[\"skill:read\",\"skill:publish\",\"skill:manage\"]";
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.iflytek.skillhub.controller.admin;

import com.iflytek.skillhub.controller.BaseApiController;
import com.iflytek.skillhub.dto.ApiResponse;
import com.iflytek.skillhub.dto.ApiResponseFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* Auditor-specific endpoints for read-only access to platform data.
* AUDITOR role can view all data for compliance and auditing purposes.
*/
@RestController
@RequestMapping("/api/v1/admin/auditor")
public class AdminAuditorController extends BaseApiController {

public AdminAuditorController(ApiResponseFactory responseFactory) {
super(responseFactory);
}

// TODO: Implement auditor-specific endpoints
// Examples:
// - GET /all-skills - View all skills including hidden
// - GET /review-history - View review history
// - GET /user-activity - View user activity logs
// - GET /statistics - View platform statistics

@GetMapping("/status")
@PreAuthorize("hasAnyRole('AUDITOR', 'SUPER_ADMIN')")
public ApiResponse<String> getStatus() {
return ok("response.success", "Auditor API is ready. More endpoints coming soon.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ public AdminSkillController(ApiResponseFactory responseFactory,
}

@PostMapping("/{skillId}/hide")
@PreAuthorize("hasRole('SUPER_ADMIN')")
@PreAuthorize("hasAnyRole('SKILL_ADMIN', 'SUPER_ADMIN')")
public ApiResponse<AdminSkillMutationResponse> hideSkill(@PathVariable Long skillId,
@RequestBody(required = false) AdminSkillActionRequest request,
@AuthenticationPrincipal PlatformPrincipal principal,
HttpServletRequest httpRequest) {
var skill = skillGovernanceService.hideSkill(
var skill = skillGovernanceService.hideSkillAsAdmin(
skillId,
principal.userId(),
httpRequest.getRemoteAddr(),
Expand All @@ -49,11 +49,11 @@ public ApiResponse<AdminSkillMutationResponse> hideSkill(@PathVariable Long skil
}

@PostMapping("/{skillId}/unhide")
@PreAuthorize("hasRole('SUPER_ADMIN')")
@PreAuthorize("hasAnyRole('SKILL_ADMIN', 'SUPER_ADMIN')")
public ApiResponse<AdminSkillMutationResponse> unhideSkill(@PathVariable Long skillId,
@AuthenticationPrincipal PlatformPrincipal principal,
HttpServletRequest httpRequest) {
var skill = skillGovernanceService.unhideSkill(
var skill = skillGovernanceService.unhideSkillAsAdmin(
skillId,
principal.userId(),
httpRequest.getRemoteAddr(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public SkillDeleteController(SkillDeleteAppService skillDeleteAppService,
}

@DeleteMapping("/id/{skillId}")
@PreAuthorize("hasRole('SUPER_ADMIN')")
@PreAuthorize("hasAnyRole('SKILL_ADMIN', 'SUPER_ADMIN')")
public ApiResponse<SkillDeleteResponse> deleteSkillById(@PathVariable Long skillId,
@AuthenticationPrincipal PlatformPrincipal principal,
HttpServletRequest request) {
Expand All @@ -50,7 +50,7 @@ public ApiResponse<SkillDeleteResponse> deleteSkillById(@PathVariable Long skill
}

@DeleteMapping("/{namespace}/{slug}")
@PreAuthorize("hasRole('SUPER_ADMIN')")
@PreAuthorize("hasAnyRole('SKILL_ADMIN', 'SUPER_ADMIN')")
public ApiResponse<SkillDeleteResponse> deleteSkill(@PathVariable String namespace,
@PathVariable String slug,
@RequestParam(required = false) String ownerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import jakarta.validation.Valid;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Set;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand Down Expand Up @@ -44,6 +45,7 @@ public ApiResponse<SkillLifecycleMutationResponse> archiveSkill(@PathVariable St
@RequestBody(required = false) AdminSkillActionRequest request,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
@RequestAttribute(value = "platformRoles", required = false) Set<String> platformRoles,
HttpServletRequest httpRequest) {
return ok("response.success.updated",
governanceWorkflowAppService.archiveSkill(
Expand All @@ -52,6 +54,7 @@ public ApiResponse<SkillLifecycleMutationResponse> archiveSkill(@PathVariable St
request,
userId,
userNsRoles,
platformRoles,
AuditRequestContext.from(httpRequest)));
}

Expand All @@ -60,13 +63,15 @@ public ApiResponse<SkillLifecycleMutationResponse> unarchiveSkill(@PathVariable
@PathVariable String slug,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
@RequestAttribute(value = "platformRoles", required = false) Set<String> platformRoles,
HttpServletRequest httpRequest) {
return ok("response.success.updated",
governanceWorkflowAppService.unarchiveSkill(
namespace,
slug,
userId,
userNsRoles,
platformRoles,
AuditRequestContext.from(httpRequest)));
}

Expand All @@ -76,6 +81,7 @@ public ApiResponse<SkillLifecycleMutationResponse> deleteVersion(@PathVariable S
@PathVariable String version,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
@RequestAttribute(value = "platformRoles", required = false) Set<String> platformRoles,
HttpServletRequest httpRequest) {
return ok("response.success.deleted",
governanceWorkflowAppService.deleteVersion(
Expand All @@ -84,6 +90,7 @@ public ApiResponse<SkillLifecycleMutationResponse> deleteVersion(@PathVariable S
version,
userId,
userNsRoles,
platformRoles,
AuditRequestContext.from(httpRequest)));
}

Expand Down Expand Up @@ -141,11 +148,11 @@ public ApiResponse<SkillLifecycleMutationResponse> submitForReview(@PathVariable

@PostMapping("/{namespace}/{slug}/confirm-publish")
public ApiResponse<SkillLifecycleMutationResponse> confirmPublish(@PathVariable String namespace,
@PathVariable String slug,
@Valid @RequestBody ConfirmPublishRequest request,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
HttpServletRequest httpRequest) {
@PathVariable String slug,
@Valid @RequestBody ConfirmPublishRequest request,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
HttpServletRequest httpRequest) {
return ok("response.success.updated",
governanceWorkflowAppService.confirmPublish(
namespace,
Expand All @@ -155,4 +162,40 @@ public ApiResponse<SkillLifecycleMutationResponse> confirmPublish(@PathVariable
userNsRoles,
AuditRequestContext.from(httpRequest)));
}

@PostMapping("/{namespace}/{slug}/hide")
public ApiResponse<SkillLifecycleMutationResponse> hideSkill(@PathVariable String namespace,
@PathVariable String slug,
@RequestBody(required = false) AdminSkillActionRequest request,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
@RequestAttribute(value = "platformRoles", required = false) Set<String> platformRoles,
HttpServletRequest httpRequest) {
return ok("response.success.updated",
governanceWorkflowAppService.hideSkill(
namespace,
slug,
request,
userId,
userNsRoles,
platformRoles,
AuditRequestContext.from(httpRequest)));
}

@PostMapping("/{namespace}/{slug}/unhide")
public ApiResponse<SkillLifecycleMutationResponse> unhideSkill(@PathVariable String namespace,
@PathVariable String slug,
@RequestAttribute("userId") String userId,
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
@RequestAttribute(value = "platformRoles", required = false) Set<String> platformRoles,
HttpServletRequest httpRequest) {
return ok("response.success.updated",
governanceWorkflowAppService.unhideSkill(
namespace,
slug,
userId,
userNsRoles,
platformRoles,
AuditRequestContext.from(httpRequest)));
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
package com.iflytek.skillhub.controller.portal;

import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
import com.iflytek.skillhub.controller.BaseApiController;
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
import com.iflytek.skillhub.dto.ApiResponse;
import com.iflytek.skillhub.dto.ApiResponseFactory;
import com.iflytek.skillhub.dto.SkillRatingRequest;
import com.iflytek.skillhub.dto.SkillRatingStatusResponse;
import com.iflytek.skillhub.domain.social.SkillRatingService;
import jakarta.validation.Valid;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;

/**
* Endpoints for reading and mutating the current user's rating on a skill.
*/
@RestController
@RequestMapping({"/api/v1/skills", "/api/web/skills"})
public class SkillRatingController extends BaseApiController {
Expand All @@ -31,19 +27,22 @@ public SkillRatingController(ApiResponseFactory responseFactory,
public ApiResponse<Void> rateSkill(
@PathVariable Long skillId,
@Valid @RequestBody SkillRatingRequest request,
@AuthenticationPrincipal PlatformPrincipal principal) {
skillRatingService.rate(skillId, principal.userId(), request.score());
@RequestAttribute("userId") String userId) {
if (userId == null) {
throw new DomainForbiddenException("error.auth.required");
}
skillRatingService.rate(skillId, userId, request.score());
return ok("response.success.updated", null);
}

@GetMapping("/{skillId}/rating")
public ApiResponse<SkillRatingStatusResponse> getUserRating(
@PathVariable Long skillId,
@AuthenticationPrincipal PlatformPrincipal principal) {
if (principal == null) {
@RequestAttribute(value = "userId", required = false) String userId) {
if (userId == null) {
return ok("response.success.read", new SkillRatingStatusResponse((short) 0, false));
}
Optional<Short> rating = skillRatingService.getUserRating(skillId, principal.userId());
Optional<Short> rating = skillRatingService.getUserRating(skillId, userId);
return ok(
"response.success.read",
new SkillRatingStatusResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
package com.iflytek.skillhub.controller.portal;

import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
import com.iflytek.skillhub.controller.BaseApiController;
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
import com.iflytek.skillhub.dto.ApiResponse;
import com.iflytek.skillhub.dto.ApiResponseFactory;
import com.iflytek.skillhub.domain.social.SkillStarService;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

/**
* Endpoints for starring, unstarring, and checking star state on a skill.
*/
@RestController
@RequestMapping({"/api/v1/skills", "/api/web/skills"})
public class SkillStarController extends BaseApiController {
Expand All @@ -26,27 +22,33 @@ public SkillStarController(ApiResponseFactory responseFactory,
@PutMapping("/{skillId}/star")
public ApiResponse<Void> starSkill(
@PathVariable Long skillId,
@AuthenticationPrincipal PlatformPrincipal principal) {
skillStarService.star(skillId, principal.userId());
@RequestAttribute("userId") String userId) {
if (userId == null) {
throw new DomainForbiddenException("error.auth.required");
}
skillStarService.star(skillId, userId);
return ok("response.success.updated", null);
}

@DeleteMapping("/{skillId}/star")
public ApiResponse<Void> unstarSkill(
@PathVariable Long skillId,
@AuthenticationPrincipal PlatformPrincipal principal) {
skillStarService.unstar(skillId, principal.userId());
@RequestAttribute("userId") String userId) {
if (userId == null) {
throw new DomainForbiddenException("error.auth.required");
}
skillStarService.unstar(skillId, userId);
return ok("response.success.updated", null);
}

@GetMapping("/{skillId}/star")
public ApiResponse<Boolean> checkStarred(
@PathVariable Long skillId,
@AuthenticationPrincipal PlatformPrincipal principal) {
if (principal == null) {
@RequestAttribute(value = "userId", required = false) String userId) {
if (userId == null) {
return ok("response.success.read", false);
}
boolean starred = skillStarService.isStarred(skillId, principal.userId());
boolean starred = skillStarService.isStarred(skillId, userId);
return ok("response.success.read", starred);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,15 @@ public ApiAccessDeniedHandler(ObjectMapper objectMapper,
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
logger.info(
"Forbidden API request [requestId={}, method={}, path={}, reason={}]",
String userId = request.getAttribute("userId") != null ? request.getAttribute("userId").toString() : "anonymous";
logger.warn(
"Forbidden API request [requestId={}, method={}, path={}, userId={}, reason={}, message={}]",
MDC.get("requestId"),
request.getMethod(),
sensitiveLogSanitizer.sanitizeRequestTarget(request),
accessDeniedException.getClass().getSimpleName()
userId,
accessDeniedException.getClass().getSimpleName(),
accessDeniedException.getMessage()
);
ApiResponse<Void> body = apiResponseFactory.error(403, "error.forbidden");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
Expand Down
Loading
Loading