diff --git a/.notes/note-1769283562795-n834eh806.json b/.notes/note-1769283562795-n834eh806.json index bd233a39bf..5232d10eab 100644 --- a/.notes/note-1769283562795-n834eh806.json +++ b/.notes/note-1769283562795-n834eh806.json @@ -1,7 +1,7 @@ { "id": "note-1769283562795-n834eh806", "title": "translate", - "content": " \u0022packages.visibility\u0022: \u0022Visibility\u0022,\n \u0022packages.settings.visibility.private.text\u0022: \u0022This package is currently private. Make it public to allow anyone to access it.\u0022,\n \u0022packages.settings.visibility.private.button\u0022: \u0022Make Private\u0022,\n \u0022packages.settings.visibility.private.bullet_title\u0022: \u0022You are about to make this package private.\u0022,\n \u0022packages.settings.visibility.private.bullet_one\u0022: \u0022Only users with appropriate permissions will be able to access this package.\u0022,\n \u0022packages.settings.visibility.private.success\u0022: \u0022Package is now private.\u0022,\n \u0022packages.settings.visibility.public.text\u0022: \u0022This package is currently public. Make it private to restrict access.\u0022,\n \u0022packages.settings.visibility.public.button\u0022: \u0022Make Public\u0022,\n \u0022packages.settings.visibility.public.bullet_title\u0022: \u0022You are about to make this package public.\u0022,\n \u0022packages.settings.visibility.public.bullet_one\u0022: \u0022Anyone will be able to access and download this package.\u0022,\n \u0022packages.settings.visibility.public.success\u0022: \u0022Package is now public.\u0022,\n \u0022packages.settings.visibility.error\u0022: \u0022Failed to update package visibility.\u0022,\n\n \u0022secrets.update.value_optional\u0022: \u0022Leave empty to keep the existing value and only update the description.\u0022,\n \u0022secrets.global_secrets\u0022: \u0022Global Secrets\u0022,\n \u0022secrets.global_secrets.description\u0022: \u0022These secrets are configured by system administrators and are available to all workflows. They cannot be modified here.\u0022,\n \u0022secrets.read_only\u0022: \u0022Read-only\u0022,", + "content": " \u0022packages.visibility\u0022: \u0022Visibility\u0022,\n \u0022packages.settings.visibility.private.text\u0022: \u0022This package is currently private. Make it public to allow anyone to access it.\u0022,\n \u0022packages.settings.visibility.private.button\u0022: \u0022Make Private\u0022,\n \u0022packages.settings.visibility.private.bullet_title\u0022: \u0022You are about to make this package private.\u0022,\n \u0022packages.settings.visibility.private.bullet_one\u0022: \u0022Only users with appropriate permissions will be able to access this package.\u0022,\n \u0022packages.settings.visibility.private.success\u0022: \u0022Package is now private.\u0022,\n \u0022packages.settings.visibility.public.text\u0022: \u0022This package is currently public. Make it private to restrict access.\u0022,\n \u0022packages.settings.visibility.public.button\u0022: \u0022Make Public\u0022,\n \u0022packages.settings.visibility.public.bullet_title\u0022: \u0022You are about to make this package public.\u0022,\n \u0022packages.settings.visibility.public.bullet_one\u0022: \u0022Anyone will be able to access and download this package.\u0022,\n \u0022packages.settings.visibility.public.success\u0022: \u0022Package is now public.\u0022,\n \u0022packages.settings.visibility.error\u0022: \u0022Failed to update package visibility.\u0022,\n\n \u0022secrets.update.value_optional\u0022: \u0022Leave empty to keep the existing value and only update the description.\u0022,\n \u0022secrets.global_secrets\u0022: \u0022Global Secrets\u0022,\n \u0022secrets.global_secrets.description\u0022: \u0022These secrets are configured by system administrators and are available to all workflows. They cannot be modified here.\u0022,\n \u0022secrets.read_only\u0022: \u0022Read-only\u0022,\n\n \u0022explore.packages\u0022: \u0022Packages\u0022,\n \u0022explore.packages.empty.description\u0022: \u0022No public or global packages are available yet.\u0022,\n \u0022admin.config.enable_explore_packages\u0022: \u0022Enable Explore Packages\u0022,\n \u0022admin.config.enable_explore_packages_desc\u0022: \u0022Show a Packages tab in the Explore menu to browse public and global packages\u0022,", "createdAt": 1769283562793, - "updatedAt": 1769284645347 + "updatedAt": 1769286635539 } \ No newline at end of file diff --git a/.notes/note-1769285434594-8owbl45ah.json b/.notes/note-1769285434594-8owbl45ah.json new file mode 100644 index 0000000000..3c530920e2 --- /dev/null +++ b/.notes/note-1769285434594-8owbl45ah.json @@ -0,0 +1,8 @@ +{ + "id": "note-1769285434594-8owbl45ah", + "title": "Packages MCP", + "content": " New MCP Tool: list_packages\n\n GitCaddy Server (routers/api/v2/mcp.go)\n\n Tool Definition:\n {\n Name: \"list_packages\",\n Description: \"List packages for an owner or globally. Shows package name, type, version info, and visibility.\",\n InputSchema: {\n \"owner\": \"Package owner (user or organization). If omitted, lists global packages.\",\n \"type\": \"Filter by package type (e.g., nuget, npm, container, generic)\",\n \"limit\": \"Maximum number of packages to return (default 50)\"\n }\n }\n\n Tool Implementation: toolListPackages() function that:\n - If owner is omitted, lists global packages only (packages with IsGlobal=true)\n - If owner is specified, lists that user/org's packages\n - Can filter by package type (nuget, npm, container, etc.)\n - Returns package info including:\n - Name, type, latest version\n - Download count\n - Visibility (private/public, global)\n - Owner info\n\n Example Response:\n {\n \"packages\": [\n {\n \"id\": 123,\n \"name\": \"MyLibrary\",\n \"type\": \"nuget\",\n \"type_name\": \"NuGet\",\n \"latest_version\": \"1.2.3\",\n \"is_private\": false,\n \"is_global\": true,\n \"download_count\": 456,\n \"owner\": \"_\",\n \"owner_type\": \"global\",\n \"created_at\": \"2026-01-24T...\"\n }\n ],\n \"count\": 1,\n \"scope\": \"global\"\n }", + "createdAt": 1769285434592, + "updatedAt": 1769285438575, + "tags": [] +} \ No newline at end of file diff --git a/models/packages/package_version.go b/models/packages/package_version.go index b1d7d94c47..b7399041ea 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -194,6 +194,8 @@ type PackageSearchOptions struct { Properties map[string]string // only results are found which contain all listed version properties with the specific value IsInternal optional.Option[bool] IsPrivate optional.Option[bool] // filter by private/public packages + IsGlobal optional.Option[bool] // filter by global packages + PublicOrGlobal bool // if true, shows public packages OR global packages (for explore page) HasFileWithName string // only results are found which are associated with a file with the specific name HasFiles optional.Option[bool] // only results are found which have associated files Sort VersionSort @@ -220,8 +222,19 @@ func (opts *PackageSearchOptions) ToConds() builder.Cond { if opts.PackageID != 0 { cond = cond.And(builder.Eq{"package.id": opts.PackageID}) } - if opts.IsPrivate.Has() { - cond = cond.And(builder.Eq{"package.is_private": opts.IsPrivate.Value()}) + if opts.PublicOrGlobal { + // For explore page: show packages that are either public OR global + cond = cond.And(builder.Or( + builder.Eq{"package.is_private": false}, + builder.Eq{"package.is_global": true}, + )) + } else { + if opts.IsPrivate.Has() { + cond = cond.And(builder.Eq{"package.is_private": opts.IsPrivate.Value()}) + } + if opts.IsGlobal.Has() { + cond = cond.And(builder.Eq{"package.is_global": opts.IsGlobal.Value()}) + } } if opts.Name.Value != "" { if opts.Name.ExactMatch { diff --git a/modules/setting/config.go b/modules/setting/config.go index b95d0c9f4b..184874094d 100644 --- a/modules/setting/config.go +++ b/modules/setting/config.go @@ -56,6 +56,7 @@ type RepositoryStruct struct { type ThemeStruct struct { DisableRegistration *config.Value[bool] HideExploreUsers *config.Value[bool] + EnableExplorePackages *config.Value[bool] HelpURL *config.Value[string] CustomSiteIconURL *config.Value[string] CustomHomeLogoURL *config.Value[string] @@ -92,6 +93,7 @@ func initDefaultConfig() { Theme: &ThemeStruct{ DisableRegistration: config.ValueJSON[bool]("theme.disable_registration").WithFileConfig(config.CfgSecKey{Sec: "service", Key: "DISABLE_REGISTRATION"}), HideExploreUsers: config.ValueJSON[bool]("theme.hide_explore_users").WithDefault(false), + EnableExplorePackages: config.ValueJSON[bool]("theme.enable_explore_packages").WithFileConfig(config.CfgSecKey{Sec: "service.explore", Key: "ENABLE_PACKAGES_PAGE"}), HelpURL: config.ValueJSON[string]("theme.help_url").WithDefault(""), CustomSiteIconURL: config.ValueJSON[string]("theme.custom_site_icon_url").WithDefault(""), CustomHomeLogoURL: config.ValueJSON[string]("theme.custom_home_logo_url").WithDefault(""), diff --git a/modules/setting/service.go b/modules/setting/service.go index 44fc1bbac6..b3802755f5 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -97,6 +97,7 @@ var Service = struct { DisableUsersPage bool `ini:"DISABLE_USERS_PAGE"` DisableOrganizationsPage bool `ini:"DISABLE_ORGANIZATIONS_PAGE"` DisableCodePage bool `ini:"DISABLE_CODE_PAGE"` + EnablePackagesPage bool `ini:"ENABLE_PACKAGES_PAGE"` } `ini:"service.explore"` QoS struct { diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 47c07f9ba7..5b83804114 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -362,6 +362,8 @@ "explore.repos": "Repositories", "explore.users": "Users", "explore.organizations": "Organizations", + "explore.packages": "Packages", + "explore.packages.empty.description": "No public or global packages are available yet.", "explore.go_to": "Go to", "explore.code": "Code", "explore.code_last_indexed_at": "Last indexed %s", @@ -3955,6 +3957,8 @@ "admin.config.help_url_help": "URL for the Help link in the navigation. Leave empty to hide the Help link.", "admin.config.hide_explore_users": "Hide Explore Users", "admin.config.hide_explore_users_desc": "Hide the Users tab from the Explore menu", + "admin.config.enable_explore_packages": "Enable Explore Packages", + "admin.config.enable_explore_packages_desc": "Show a Packages tab in the Explore menu to browse public and global packages", "admin.config.custom_home_title": "Homepage Title", "admin.config.custom_home_title_placeholder": "Leave empty to use app name", "admin.config.custom_home_title_help": "Custom title displayed on the homepage. Leave empty to use the default app name.", diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index d9111f07b0..14b50d7c97 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -243,6 +243,7 @@ func ChangeConfig(ctx *context.Context) { cfg.Theme.APIHeaderURL.DynKey(): marshalString(""), cfg.Theme.HelpURL.DynKey(): marshalString(""), cfg.Theme.HideExploreUsers.DynKey(): marshalBool, + cfg.Theme.EnableExplorePackages.DynKey(): marshalBool, cfg.Theme.CustomHomeTitle.DynKey(): marshalString(""), cfg.Theme.CustomHomeTagline.DynKey(): marshalString(""), cfg.Theme.PinnedOrgDisplayFormat.DynKey(): marshalString("condensed"), diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 2c207e733b..d09e250a09 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -30,6 +30,7 @@ func Code(ctx *context.Context) { ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage || setting.Config().Theme.HideExploreUsers.Value(ctx) ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage + ctx.Data["PackagesPageIsEnabled"] = setting.Service.Explore.EnablePackagesPage || setting.Config().Theme.EnableExplorePackages.Value(ctx) ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["Title"] = ctx.Tr("explore_title") ctx.Data["PageIsExplore"] = true diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go index 26882bd8c7..fd029f2891 100644 --- a/routers/web/explore/org.go +++ b/routers/web/explore/org.go @@ -21,6 +21,7 @@ func Organizations(ctx *context.Context) { ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage || setting.Config().Theme.HideExploreUsers.Value(ctx) ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage + ctx.Data["PackagesPageIsEnabled"] = setting.Service.Explore.EnablePackagesPage || setting.Config().Theme.EnableExplorePackages.Value(ctx) ctx.Data["Title"] = ctx.Tr("explore_title") ctx.Data["PageIsExplore"] = true ctx.Data["PageIsExploreOrganizations"] = true diff --git a/routers/web/explore/package.go b/routers/web/explore/package.go new file mode 100644 index 0000000000..1ad4997788 --- /dev/null +++ b/routers/web/explore/package.go @@ -0,0 +1,97 @@ +// Copyright 2025 The GitCaddy Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package explore + +import ( + "net/http" + + "code.gitcaddy.com/server/v3/models/db" + packages_model "code.gitcaddy.com/server/v3/models/packages" + access_model "code.gitcaddy.com/server/v3/models/perm/access" + "code.gitcaddy.com/server/v3/modules/optional" + "code.gitcaddy.com/server/v3/modules/setting" + "code.gitcaddy.com/server/v3/modules/templates" + "code.gitcaddy.com/server/v3/services/context" +) + +const tplExplorePackages templates.TplName = "explore/packages" + +// Packages render explore packages page +func Packages(ctx *context.Context) { + if !setting.Service.Explore.EnablePackagesPage && !setting.Config().Theme.EnableExplorePackages.Value(ctx) { + ctx.Redirect(setting.AppSubURL + "/explore") + return + } + + ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage || setting.Config().Theme.HideExploreUsers.Value(ctx) + ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage + ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage + ctx.Data["PackagesPageIsEnabled"] = true + ctx.Data["Title"] = ctx.Tr("packages.title") + ctx.Data["PageIsExplore"] = true + ctx.Data["PageIsExplorePackages"] = true + ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled + + page := max(ctx.FormInt("page"), 1) + query := ctx.FormTrim("q") + packageType := ctx.FormTrim("type") + + // Search for public packages and global packages + searchOpts := &packages_model.PackageSearchOptions{ + Paginator: &db.ListOptions{ + PageSize: setting.UI.PackagesPagingNum, + Page: page, + }, + Type: packages_model.Type(packageType), + Name: packages_model.SearchValue{Value: query}, + IsInternal: optional.Some(false), + PublicOrGlobal: true, // Show public packages OR global packages + } + + pvs, total, err := packages_model.SearchLatestVersions(ctx, searchOpts) + if err != nil { + ctx.ServerError("SearchLatestVersions", err) + return + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + ctx.ServerError("GetPackageDescriptors", err) + return + } + + repositoryAccessMap := make(map[int64]bool) + for _, pd := range pds { + if pd.Repository == nil { + continue + } + if _, has := repositoryAccessMap[pd.Repository.ID]; has { + continue + } + + permission, err := access_model.GetUserRepoPermission(ctx, pd.Repository, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + repositoryAccessMap[pd.Repository.ID] = permission.HasAnyUnitAccess() + } + + // Check if there are any packages available for browsing + hasPackages := total > 0 + + ctx.Data["Query"] = query + ctx.Data["PackageType"] = packageType + ctx.Data["AvailableTypes"] = packages_model.TypeList + ctx.Data["HasPackages"] = hasPackages + ctx.Data["PackageDescriptors"] = pds + ctx.Data["Total"] = total + ctx.Data["RepositoryAccessMap"] = repositoryAccessMap + + pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) + pager.AddParamFromRequest(ctx.Req) + ctx.Data["Page"] = pager + + ctx.HTML(http.StatusOK, tplExplorePackages) +} diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index 21185ce189..cbca085c67 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -153,6 +153,7 @@ func Repos(ctx *context.Context) { ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage || setting.Config().Theme.HideExploreUsers.Value(ctx) ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage + ctx.Data["PackagesPageIsEnabled"] = setting.Service.Explore.EnablePackagesPage || setting.Config().Theme.EnableExplorePackages.Value(ctx) ctx.Data["Title"] = ctx.Tr("explore_title") ctx.Data["PageIsExplore"] = true ctx.Data["ShowRepoOwnerOnList"] = true diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index db1b963eef..6384a8464b 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -165,6 +165,7 @@ func Users(ctx *context.Context) { } ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage + ctx.Data["PackagesPageIsEnabled"] = setting.Service.Explore.EnablePackagesPage || setting.Config().Theme.EnableExplorePackages.Value(ctx) ctx.Data["Title"] = ctx.Tr("explore_title") ctx.Data["PageIsExplore"] = true ctx.Data["PageIsExploreUsers"] = true diff --git a/routers/web/web.go b/routers/web/web.go index 837073f856..689479d58c 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -554,6 +554,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/users", explore.Users) m.Get("/users/sitemap-{idx}.xml", sitemapEnabled, explore.Users) m.Get("/organizations", explore.Organizations) + m.Get("/packages", explore.Packages) m.Get("/code", func(ctx *context.Context) { if unit.TypeCode.UnitGlobalDisabled() { ctx.NotFound(nil) diff --git a/templates/admin/config_settings/theme.tmpl b/templates/admin/config_settings/theme.tmpl index 6bb113160e..5bb478c350 100644 --- a/templates/admin/config_settings/theme.tmpl +++ b/templates/admin/config_settings/theme.tmpl @@ -17,6 +17,13 @@
+