Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
"url": "https://www.pgedge.com"
},
"plugins": [
{ "name": "pgedge", "source": "./" }
{
"name": "pgedge",
"source": {
"source": "git-subdir",
"url": "https://github.com/pgEdge/pgedge-cli.git",
"path": "plugins/pgedge",
"ref": "v0.5.0-beta.4"
}
}
]
}
5 changes: 4 additions & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{
"name": "pgedge",
"description": "Agent skills for the pgedge CLI: pgEdge Starfleet Managed and BYOC databases, Starfleet accounts, and a self-hosted pgEdge Control Plane. The skills install the CLI when it is missing.",
"version": "0.5.0-beta.4",
"author": {
"name": "pgEdge, Inc."
},
"homepage": "https://github.com/pgEdge/pgedge-cli",
"repository": "https://github.com/pgEdge/pgedge-cli",
"license": "PostgreSQL",
"keywords": ["postgres", "postgresql", "pgedge", "starfleet", "database", "cli"]
"keywords": ["postgres", "postgresql", "pgedge", "starfleet", "database", "cli"],
"logo": "plugins/pgedge/assets/pgedge-icon.png",
"skills": "./plugins/pgedge/skills"
}
17 changes: 17 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ jobs:
# KEY=VALUE lines are forwarded (comments/blanks are skipped).
- name: Load per-module versions
run: grep -E '^[A-Za-z_][A-Za-z0-9_]*=' versions.env >> "$GITHUB_ENV"
# The plugin marketplace installs the skills from this tag, so a
# release whose manifests name another one would install nothing.
- name: Check the plugin is pinned to this tag
run: |
want="${GITHUB_REF_NAME#v}"
for f in plugins/pgedge/.claude-plugin/plugin.json .cursor-plugin/plugin.json; do
got=$(jq -r .version "$f")
if [ "$got" != "$want" ]; then
echo "::error::$f has version $got; the tag needs $want"
exit 1
fi
done
ref=$(jq -r '.plugins[0].source.ref' .claude-plugin/marketplace.json)
if [ "$ref" != "$GITHUB_REF_NAME" ]; then
echo "::error::marketplace.json pins $ref; the tag is $GITHUB_REF_NAME"
exit 1
fi
- uses: goreleaser/goreleaser-action@a4f02ab5355a589de60d917a659ce617ed19b3de # v7
with:
# Pinned because cmd/caskpatch is tested against this
Expand Down
12 changes: 7 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ self-hosted Control Plane).
> for the index (global flags, config, profiles, env vars, exit codes,
> and a page per top-level command group, such as `pgedge llms
> inspect`), then `pgedge llms <module>` for the module you are
> working in. Also read `skills/pgedge/SKILL.md`. Do not improvise
> commands from `--help` output or fall back to raw curl.
> working in. Also read `plugins/pgedge/skills/pgedge/SKILL.md`. Do
> not improvise commands from `--help` output or fall back to raw
> curl.
>
> The command reference is **generated from the cobra tree**. Editing
> text between `<!-- BEGIN GENERATED: ... -->` and `<!-- END GENERATED
Expand All @@ -36,9 +37,10 @@ self-hosted Control Plane).
> branch without testing the read's own status — in prose, which no
> shell parser can see.
>
> Both walk `skills/` and every reference page (the root `llms.txt`,
> each module's `llms.txt` index and its `llms/**/*.txt` pages), so a
> new document is covered the day it lands. They read fenced blocks, including
> Both walk `plugins/pgedge/skills/` and every reference page (the
> root `llms.txt`, each module's `llms.txt` index and its
> `llms/**/*.txt` pages), so a new document is covered the day it
> lands. They read fenced blocks, including
> unlabelled ones, but not those labelled `json`, `yaml` or `text`,
> where a `pgedge` line is quoted rather than run. They do NOT read
> the 4-space indented blocks `README.md` and `docs/*.md` use, no rule
Expand Down
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,18 +248,18 @@ prefer to wire it up by hand.

## Installing the AI-Agent Skills

pgEdge ships agent skills in the `skills/` directory of this
repository. Each skill is a SKILL.md file in the Agent Skills open
standard. The directory holds five skills:

- `skills/pgedge`: an orientation to the pgedge CLI, and which module
to choose
- `skills/pgedge-starfleet`: pgEdge Starfleet authentication, invites
and team memberships
- `skills/pgedge-byoc`: pgEdge Starfleet BYOC resources
- `skills/pgedge-managed`: pgEdge Starfleet Managed databases, which
pgEdge hosts
- `skills/pgedge-controlplane`: pgEdge Control Plane resources
pgEdge ships agent skills in the `plugins/pgedge/skills/` directory
of this repository. Each skill is a SKILL.md file in the Agent Skills
open standard. The directory holds five skills:

- `pgedge`: an orientation to the pgedge CLI, and which module to
choose
- `pgedge-starfleet`: pgEdge Starfleet authentication, invites and
team memberships
- `pgedge-byoc`: pgEdge Starfleet BYOC resources
- `pgedge-managed`: pgEdge Starfleet Managed databases, which pgEdge
hosts
- `pgedge-controlplane`: pgEdge Control Plane resources

Skills are a layer above the CLI, so they install separately. The
install script offers to install them when it runs at a terminal.
Expand Down Expand Up @@ -305,7 +305,7 @@ any agent:

git clone https://github.com/pgEdge/pgedge-cli
mkdir -p .agents/skills
cp -r pgedge-cli/skills/* .agents/skills/
cp -r pgedge-cli/plugins/pgedge/skills/* .agents/skills/

To install for Claude Code alone, use `~/.claude/skills/` in place of
`.agents/skills/`.
Expand Down
10 changes: 5 additions & 5 deletions docs/ai-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ because it describes the binary that is installed.

### Installing the Agent Skills

pgEdge ships five agent skills in the `skills/` directory of the
repository. They follow the Agent Skills open standard. The following
table describes what each one covers:
pgEdge ships five agent skills in the `plugins/pgedge/skills/`
directory of the repository. They follow the Agent Skills open
standard. The following table describes what each one covers:

| Skill | Covers |
|---|---|
Expand Down Expand Up @@ -105,8 +105,8 @@ AI-Agent Skills" section covers three more cases:

Some agents read neither `.agents/skills/` nor a convention the
installer links. For such an agent, copy the directories under
`skills/` to the place that agent reads. Confirm that place in the
agent's own documentation. The rest of the setup stays the same.
`plugins/pgedge/skills/` to the place that agent reads. Confirm that
place in the agent's own documentation. The rest of the setup stays the same.

### Parsing Command Output

Expand Down
7 changes: 4 additions & 3 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,9 @@ on to one page per resource, such as `pgedge llms starfleet byoc
cluster`. Point an AI agent at these pages first, because they are
generated from the live command tree.

In a checkout, agents should also read `skills/pgedge/SKILL.md`. It is
the entry point for the bundled Claude Code skills.
In a checkout, agents should also read
`plugins/pgedge/skills/pgedge/SKILL.md`. It is the entry point for the
bundled Claude Code skills.

## Other Ways to Install

Expand Down Expand Up @@ -257,7 +258,7 @@ If you cannot write to `/usr/local/bin`, run the `install` command with

This build stamps `pgedge version` with the real commit and the
per-module versions. The checkout carries the AI-agent skills in
`skills/`. To update, run `git pull && make build`.
`plugins/pgedge/skills/`. To update, run `git pull && make build`.

## Keeping the CLI Up to Date

Expand Down
4 changes: 2 additions & 2 deletions embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
// tokens to look up one flag, and left module scope for the doc gates
// to infer rather than read off the file.
//
// Agent skills in skills/ are distributed by external installers (see
// the README), not embedded.
// Agent skills in plugins/pgedge/skills/ are distributed by external
// installers (see the README), not embedded.
package pgedgecli

import (
Expand Down
10 changes: 5 additions & 5 deletions internal/clitest/example_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ var exampleTableScopes = buildExampleTableScopes()
// report every table in that module's pages.
func buildExampleTableScopes() map[string][]string {
out := map[string][]string{
"../../skills/pgedge/SKILL.md": {"../cli", "../output"},
"../../skills/pgedge-starfleet/SKILL.md": {"../starfleet/account/cmd", "../output"},
"../../skills/pgedge-byoc/SKILL.md": {"../starfleet/byoc/cmd", "../output"},
"../../skills/pgedge-managed/SKILL.md": {
"../../plugins/pgedge/skills/pgedge/SKILL.md": {"../cli", "../output"},
"../../plugins/pgedge/skills/pgedge-starfleet/SKILL.md": {"../starfleet/account/cmd", "../output"},
"../../plugins/pgedge/skills/pgedge-byoc/SKILL.md": {"../starfleet/byoc/cmd", "../output"},
"../../plugins/pgedge/skills/pgedge-managed/SKILL.md": {
"../starfleet/managed/cmd", "../output",
},
"../../skills/pgedge-controlplane/SKILL.md": {"../controlplane/cmd", "../output"},
"../../plugins/pgedge/skills/pgedge-controlplane/SKILL.md": {"../controlplane/cmd", "../output"},
}
for _, f := range ReferenceFiles() {
if dirs, ok := referenceTableScopes[ReferenceModuleDir(f)]; ok {
Expand Down
14 changes: 7 additions & 7 deletions internal/clitest/flag_claims_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,10 @@ var externalToolFlags = map[string]struct {
map[string]bool{"../../llms.txt": true}},
"--global": {"the skills CLI (npx skills add)",
map[string]bool{
"../../README.md": true,
"../../skills/pgedge/SKILL.md": true,
"../../docs/getting-started.md": true,
"../../docs/ai-agents.md": true,
"../../README.md": true,
"../../plugins/pgedge/skills/pgedge/SKILL.md": true,
"../../docs/getting-started.md": true,
"../../docs/ai-agents.md": true,
}},
"--marketplace": {"Claude Code (/plugin install)",
map[string]bool{
Expand All @@ -141,8 +141,8 @@ var externalToolFlags = map[string]struct {
}},
"--skill": {"the skills CLI (npx skills add)",
map[string]bool{
"../../README.md": true,
"../../skills/pgedge/SKILL.md": true,
"../../README.md": true,
"../../plugins/pgedge/skills/pgedge/SKILL.md": true,
}},
"--fail": {"curl",
map[string]bool{
Expand Down Expand Up @@ -447,7 +447,7 @@ func TestFlagClaimCheckHonoursDeliberatelyWrongMarker(t *testing.T) {
func TestFlagClaimCheckExternalAllowlistIsFileScoped(t *testing.T) {
fixture := "Add `--global` to install for your user instead."

if got := checkFlagNameClaims(t, "../../skills/pgedge/SKILL.md",
if got := checkFlagNameClaims(t, "../../plugins/pgedge/skills/pgedge/SKILL.md",
docSection{module: "byoc", text: fixture}); len(got) != 0 {
t.Fatalf("--global was flagged in a file the allowlist names: %v",
got)
Expand Down
2 changes: 1 addition & 1 deletion internal/clitest/skills_docs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ var namedDocs = append([]string{
func scanDocFiles(t *testing.T) []string {
t.Helper()

skillMatches, err := filepath.Glob("../../skills/*/SKILL.md")
skillMatches, err := filepath.Glob("../../plugins/pgedge/skills/*/SKILL.md")
if err != nil {
t.Fatal(err)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/starfleet/managed/cmd/metricsample_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,8 +605,8 @@ func readFileOrFail(path string) func(t *testing.T) string {
// changed; the reference states what the code does now.
var staleNoteScanDocs = []scanDoc{
{referenceLabel, managedReferenceText},
{"../../../../skills/pgedge-managed/SKILL.md",
readFileOrFail("../../../../skills/pgedge-managed/SKILL.md")},
{"../../../../plugins/pgedge/skills/pgedge-managed/SKILL.md",
readFileOrFail("../../../../plugins/pgedge/skills/pgedge-managed/SKILL.md")},
}

// noteSpan is a half-open byte range within one line.
Expand Down
63 changes: 50 additions & 13 deletions plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,58 @@ package pgedgecli
import (
"encoding/json"
"os"
"path"
"path/filepath"
"testing"
)

const pluginDir = "plugins/pgedge"

type pluginManifest struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Icon string `json:"icon"`
Logo string `json:"logo"`
Skills string `json:"skills"`
}

func readJSON(t *testing.T, path string, v any) {
func readJSON(t *testing.T, file string, v any) {
t.Helper()
b, err := os.ReadFile(path)
b, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(b, v); err != nil {
t.Fatalf("%s: %v", path, err)
t.Fatalf("%s: %v", file, err)
}
}

func mustExist(t *testing.T, what, p string) {
t.Helper()
if _, err := os.Stat(p); err != nil {
t.Errorf("%s %q: %v", what, p, err)
}
}

// TestPluginManifestsAgree holds the Claude and Cursor manifests to one
// plugin. A version is refused because Claude Code then pins installs to
// it until someone remembers to bump it; without one, they track main.
// plugin, pinned to one release: the marketplace installs the skills from
// the tag of that release, so they describe the binary users have. The
// release workflow refuses a tag that doesn't match the version.
func TestPluginManifestsAgree(t *testing.T) {
var claude, cursor pluginManifest
readJSON(t, ".claude-plugin/plugin.json", &claude)
readJSON(t, filepath.Join(pluginDir, ".claude-plugin", "plugin.json"), &claude)
readJSON(t, ".cursor-plugin/plugin.json", &cursor)

var market struct {
Plugins []struct {
Name string `json:"name"`
Source string `json:"source"`
Source struct {
Source string `json:"source"`
URL string `json:"url"`
Path string `json:"path"`
Ref string `json:"ref"`
} `json:"source"`
} `json:"plugins"`
}
readJSON(t, ".claude-plugin/marketplace.json", &market)
Expand All @@ -47,12 +67,29 @@ func TestPluginManifestsAgree(t *testing.T) {
t.Errorf("descriptions differ or are empty:\nClaude: %q\nCursor: %q",
claude.Description, cursor.Description)
}
if claude.Version != "" || cursor.Version != "" {
t.Errorf("a manifest sets version (%q, %q)", claude.Version, cursor.Version)
if claude.Version == "" || claude.Version != cursor.Version {
t.Errorf("versions are %q (Claude) and %q (Cursor), want one release version",
claude.Version, cursor.Version)
}
if len(market.Plugins) != 1 {
t.Fatalf("marketplace.json has %d plugins, want 1", len(market.Plugins))
}
if len(market.Plugins) != 1 || market.Plugins[0].Name != claude.Name ||
market.Plugins[0].Source != "./" {
t.Errorf("marketplace.json plugins = %+v, want one %q entry with source ./",
market.Plugins, claude.Name)
entry := market.Plugins[0]
if entry.Name != claude.Name || entry.Source.Source != "git-subdir" ||
entry.Source.URL != "https://github.com/pgEdge/pgedge-cli.git" || entry.Source.Path != pluginDir ||
entry.Source.Ref != "v"+claude.Version {
t.Errorf("marketplace.json entry = %+v, want %q from git-subdir "+
"https://github.com/pgEdge/pgedge-cli.git path %s ref v%s",
entry, claude.Name, pluginDir, claude.Version)
}

// Claude resolves the icon from the plugin folder, Cursor resolves its
// paths from the repository root.
mustExist(t, "Claude icon", filepath.Join(pluginDir, claude.Icon))
mustExist(t, "Cursor logo", cursor.Logo)
if path.Clean(cursor.Skills) != pluginDir+"/skills" {
t.Errorf("Cursor skills = %q, want ./%s/skills", cursor.Skills, pluginDir)
}
mustExist(t, "README the Anthropic directory requires",
filepath.Join(pluginDir, "README.md"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
"name": "pgedge",
"displayName": "pgEdge",
"description": "Agent skills for the pgedge CLI: pgEdge Starfleet Managed and BYOC databases, Starfleet accounts, and a self-hosted pgEdge Control Plane. The skills install the CLI when it is missing.",
"version": "0.5.0-beta.4",
"author": {
"name": "pgEdge, Inc.",
"url": "https://www.pgedge.com"
},
"homepage": "https://github.com/pgEdge/pgedge-cli",
"repository": "https://github.com/pgEdge/pgedge-cli",
"license": "PostgreSQL",
"keywords": ["postgres", "postgresql", "pgedge", "starfleet", "database", "cli"]
"keywords": ["postgres", "postgresql", "pgedge", "starfleet", "database", "cli"],
"icon": "./assets/pgedge-icon.png"
}
Loading
Loading