Skip to content
Open
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
101 changes: 101 additions & 0 deletions .claude/skills/new-command/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
name: new-command
description: Scaffold a new Kosli CLI command or subcommand (kosli <verb> <noun>) in this repo. Use when adding a CLI command, e.g. "add a command", "new CLI command", "scaffold a command", "create a kosli subcommand". Interviews for archetype, endpoint, flags, args, and beta/hidden status, then generates the command file, test skeleton, flag constants, and registration.
---

## Overview

This skill scaffolds a new Kosli CLI command following the repo's `kosli <verb> <noun> [flags]`
convention. It interviews you to collect the command path, archetype, API endpoint (where
applicable), positional args, flags, lifecycle status, and help-text descriptions, then
generates the command file, test skeleton, flag constants, and registration wiring - leaving
the codebase in a compiling, `--help`-renderable state with zero manual wiring remaining.

## Interview

Run these steps in order. Collect all answers before generating any files.

**Step 1 - Command path.** Ask for the verb and noun (e.g. `get trail`, `create deployment`).
Scan `cmd/kosli/` for an existing `new<Verb>Cmd` factory and the `AddCommand` block in
`root.go` to determine whether the verb already exists. If the verb is new, note that a
parent verb file (`cmd/kosli/<verb>.go`) and `root.go` wiring will also be created.

**Step 2 - Archetype.** Pre-suggest from the verb using this mapping:

| Verb(s) | Suggested archetype |
|---|---|
| `get` | read-single |
| `list` | read-list |
| `create`, `report`, `request` | create-mutate |
| `attest` | attest |
| `fingerprint`, `version`, `config` | local |
| anything else | prompt (default: generic-action if API, else local) |

The developer may override. Choosing `local` skips steps 3 (endpoint) and omits the
global-flag requirement (`RequireGlobalFlags`) entirely - no Org/ApiToken needed.

**Step 3 - API endpoint (API archetypes only; skip for `local`).** See
`references/openapi.md`. Fetch the relevant path from the OpenAPI spec, confirm the method
and path with the developer, derive `url.JoinPath` segments, and pre-fill the `Payload`
struct (for create-mutate/attest) and flag candidates (for read/list).

**Step 4 - Positional args.** Ask for names and count (mapped to `cobra.ExactArgs` or
`CustomMaximumNArgs`). For API archetypes, suggest from OpenAPI path params not covered by
global flags. For `local`, derive from the command's inputs.

**Step 5 - Flags.** For each flag collect: name, shorthand, Go type
(`String`/`Bool`/`StringSlice`/`StringToString`/`Int`...), default, requirement (required /
conditional / mutually-exclusive), and description. Suggest reusing shared helpers based on
archetype: `addDryRunFlag` (create-mutate), `addListFlags` (read-list),
`addAttestationFlags` + `addFingerprintFlags` (attest). Propose flags derived from the
OpenAPI request body or query params.

**Step 6 - Lifecycle.** Ask: beta? (adds `betaCLIAnnotation`); incubating/hidden? (adds
`Hidden: true` and `docgen.DocHiddenAnnotation`). Deprecated is out of scope for new
commands.

**Step 7 - Descriptions.** Draft `Short`, `Long`, and `Example` from the gathered
information. Use repo conventions: `^carets^` for inline code; `# title` and
backslash-continuation accordion format for examples. Present drafts for the developer to
edit before generating files.

## Routing table

After completing the interview, read the archetype-specific reference file, then `references/wiring.md`.

| Archetype | Read |
|---|---|
| local | `references/archetype-local.md` |
| read-single / read-list | `references/archetype-read.md` |
| create-mutate / generic-action | `references/archetype-mutate.md` |
| attest | `references/archetype-attest.md` |

For API archetypes, also read `references/openapi.md` for the endpoint/payload step.
For wiring (registration, flag constants, lifecycle annotations, test skeleton), read
`references/wiring.md`.

## Verification

After generating all files, run these checks in order:

1. `go build ./...` - must succeed with no errors.
2. `golangci-lint run ./cmd/kosli/...` - fallback: `go vet ./cmd/kosli/...`.
3. `go run . <verb> <noun> --help` - confirms wiring and that help renders (including the
beta banner if marked beta).
4. Tests:
- API archetypes: requires a local server (`make test_setup`); run
`make test_integration_single TARGET=<Suite>`.
- `local` archetype: typically runs without a server; run
`go test ./cmd/kosli/ -run <Suite>` directly.

## Non-goals

- **No docs MDX.** Reference docs regenerate downstream in `kosli-dev/docs` on the next
CLI release. Note that a release is required for docs to appear, and that beta/hidden
commands surface via the lifecycle-notices mechanism.
- **No golden files.** Leave `golden: ""` / minimal assertions; capture golden output after
the first real run against a server.
- **Never invent API endpoints or fields.** Consult the OpenAPI spec; if unreachable or the
endpoint is not yet published, fall back to manual entry and explicitly flag the payload
as unverified.
- **No deprecation handling.** Irrelevant for newly created commands.
82 changes: 82 additions & 0 deletions .claude/skills/new-command/references/archetype-attest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Archetype: attest

Canonical example: `cmd/kosli/attestCustom.go` — read it in full and adapt.

## Deltas

**Payload struct**
- Embed `*CommonAttestationPayload` plus any type-specific fields.
- Include a `TypeName string \`json:"type_name"\`` field (or equivalent identifier for the attestation type).

```go
type <Noun>AttestationPayload struct {
*CommonAttestationPayload
TypeName string `json:"type_name"`
// ... type-specific fields
}
```

**Options struct**
- Embed `*CommonAttestationOptions`; hold a `payload <Noun>AttestationPayload`.

```go
type attest<Noun>Options struct {
*CommonAttestationOptions
payload <Noun>AttestationPayload
}
```

**Factory initialisation**
- Initialise both embedded structs explicitly:
```go
o := &attest<Noun>Options{
CommonAttestationOptions: &CommonAttestationOptions{
fingerprintOptions: &fingerprintOptions{},
},
payload: <Noun>AttestationPayload{
CommonAttestationPayload: &CommonAttestationPayload{},
},
}
```

**`PreRunE`**
1. `CustomMaximumNArgs(1, args)` — attestation commands allow 0 or 1 positional artifact arg.
2. `RequireGlobalFlags(global, []string{"Org", "ApiToken"})` wrapped in `ErrorBeforePrintingUsage`.
3. `MuXRequiredFlags(cmd, []string{"fingerprint", "artifact-type"}, false)` — only one of these may be supplied.
4. `ValidateSliceValues(o.redactedCommitInfo, allowedCommitRedactionValues)` if the type exposes `--redact-commit-info`.
5. `ValidateAttestationArtifactArg(args, o.fingerprintOptions.artifactType, o.payload.ArtifactFingerprint)`.
6. `ValidateRegistryFlags(cmd, o.fingerprintOptions)`.

**Flags**
- Call `addAttestationFlags(cmd, o.CommonAttestationOptions, o.payload.CommonAttestationPayload, ci)` where `ci := WhichCI()` — this adds `--flow`, `--trail`, `--name`, `--fingerprint`, `--artifact-type`, commit flags, and more.
- Add type-specific flags after `addAttestationFlags`.
- `RequireFlags(cmd, []string{"flow", "trail", "name", ...})` for type-specific required flags.

**`RunE`**
- Capture `o.repoURLExplicit = cmd.Flags().Changed("repo-url")` before delegating.

**`run` method**
- Build the URL: `url.JoinPath(global.Host, "api/v2/attestations", global.Org, o.flowName, "trail", o.trailName, "<type-slug>")`.
- Call `o.CommonAttestationOptions.run(args, o.payload.CommonAttestationPayload)` to resolve fingerprint and commit info.
- Load any type-specific data (e.g. a JSON file) and populate `o.payload` fields.
- Call `prepareAttestationForm(o.payload, o.attachments)` to build the multipart form; handle `cleanupNeeded` with a `defer os.Remove`.
- POST: `kosliClient.Do(&requests.RequestParams{Method: http.MethodPost, URL: url, Form: form, DryRun: global.DryRun, Token: global.ApiToken})`.
- Log success: `logger.Info("<type>:%s attestation '%s' is reported to trail: %s", o.payload.TypeName, o.payload.AttestationName, o.trailName)`.
- Return `wrapAttestationError(err)` — never return `err` directly.

**No `Args:` field on the `cobra.Command`**
- The comment in `attestCustom.go` explains why: `CustomMaximumNArgs` handles this in `PreRunE` instead, so `Args` is intentionally omitted.

## Test setup

`SetupTest` must:
1. Set `global = &GlobalOpts{...}` as usual.
2. Call `CreateFlow(...)` to ensure the flow exists.
3. Call `BeginTrail(...)` to ensure the trail exists.

See `cmd/kosli/getFlow_test.go` for the global setup pattern and `cmd/kosli/testHelpers.go` for `CreateFlow`/`BeginTrail` helpers.

## Where to look next

- Registration, flag constants, lifecycle annotations, full test skeleton: `references/wiring.md`.
- Endpoint path: `references/openapi.md` (path pattern is `POST /attestations/{org}/{flow_name}/trail/{trail_name}/<type-slug>`).
41 changes: 41 additions & 0 deletions .claude/skills/new-command/references/archetype-local.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Archetype: local (no-API)

Canonical example: `cmd/kosli/fingerprint.go` — read it in full and adapt.

## Deltas from the canonical example

These are the things that differ for a generic local command vs. `fingerprint.go`:

**Options struct**
- Define a `<verbNoun>Options` struct with only the fields the command needs locally (no `Payload`, no API-related fields).

**`PreRunE`**
- Do local flag validation only (e.g. `RequireFlags`, `ValidateRegistryFlags` if reading an image, `MuXRequiredFlags` for mutually exclusive flags).
- **No `RequireGlobalFlags`** — local commands do not need `Org` or `ApiToken`.

**`RunE` → `o.run(args, out)`**
- Delegate to `o.run(args, out)` exactly as in `fingerprint.go`.

**`run` method**
- Does local work: reads files, computes values, calls internal packages.
- Prints results via `logger.Info(...)` or writes to `out`.
- **No `url.JoinPath`**, **no `kosliClient.Do`**, **no `DryRun`**, **no `Payload`**.

**Flags**
- Add only the flags this command needs; no `addDryRunFlag`.
- Use `RequireFlags` for mandatory ones.

**Imports**
- Typically only `"io"` and `"github.com/spf13/cobra"` at a minimum; add internal packages as needed.
- No `"net/http"`, `"net/url"`, or `requests` import.

## What stays the same

- `new<VerbNoun>Cmd(out io.Writer) *cobra.Command` factory with description consts.
- `Args: cobra.ExactArgs(N)` or `cobra.NoArgs` as appropriate.
- String consts for `Short`, `Long`, `Example` above the factory.

## Where to look next

- Registration, flag constants, lifecycle annotations, test skeleton: `references/wiring.md`.
- For a simple example of the `run` pattern without API calls, read `cmd/kosli/fingerprint.go` alongside `cmd/kosli/version.go`.
60 changes: 60 additions & 0 deletions .claude/skills/new-command/references/archetype-mutate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Archetype: create-mutate / generic-action

Two sub-shapes. Use create-mutate for straightforward PUT/POST commands that send a JSON payload. Use generic-action when the command gathers data (e.g. from the local environment or another service) before calling the API and does not fit the read or attest shapes.

## create-mutate

Canonical example: `cmd/kosli/createFlow.go` — read it in full and adapt.

### Deltas

**Payload struct**
- Define a `<Noun>Payload` struct with fields and `json:` tags derived from the OpenAPI request body schema (see `references/openapi.md`).
- Embed it in the options struct: `payload <Noun>Payload`.

**Options struct**
- Holds `payload <Noun>Payload` and any extra fields that are command-local (e.g. a file path that is processed before being added to the payload).

**`PreRunE`**
- `RequireGlobalFlags(global, []string{"Org", "ApiToken"})`.
- Add `MuXRequiredFlags` calls for any mutually exclusive flag pairs (see `createFlow.go` for the template/template-file example).
- Add `RequireFlags` for any flags that are always required (beyond global flags).

**Flags**
- Bind each payload field to a flag: `cmd.Flags().StringVar(&o.payload.FieldName, "flag-name", "", flagConstant)`.
- Call `addDryRunFlag(cmd)` — dry-run support is expected on all mutate commands.

**`run` method**
- Build the URL: `url.JoinPath(global.Host, "api/v2/<resource>", global.Org, ...)`.
- Send with `kosliClient.Do(&requests.RequestParams{Method: http.MethodPut, URL: url, Payload: o.payload, DryRun: global.DryRun, Token: global.ApiToken})`.
- For multipart form uploads (e.g. when a file is involved), build a `[]requests.FormItem` and use `Form: form` instead of `Payload`.
- Log success: `if err == nil && !global.DryRun { logger.Info("<noun> '%s' was created", ...) }`.

**Imports**
- `"net/http"`, `neturl "net/url"` (alias to avoid collision with `url` variable), `requests`, `cobra`, `"io"`.

---

## generic-action fallback

Use when the command does significant local work (snapshot, diff, report) before or after the API call. There is no single canonical file — look at the closest example in `cmd/kosli/` for the verb you are implementing.

Key differences from create-mutate:
- May have no `Payload` struct if the body is built dynamically.
- `PreRunE` may be heavier (e.g. `ConditionallyRequiredFlags`, environment-specific validation).
- `run` may call multiple internal packages before building `reqParams`.

For everything else (global flags, dry-run, logging, error handling), follow the create-mutate pattern.

---

## What stays the same

- `new<VerbNoun>Cmd(out io.Writer) *cobra.Command` factory with description consts.
- `ErrorBeforePrintingUsage` wrapping for `RequireGlobalFlags` errors.
- `addDryRunFlag(cmd)` is always called.

## Where to look next

- Endpoint path and payload field derivation: `references/openapi.md`.
- Registration, flag constants, lifecycle annotations, test skeleton: `references/wiring.md`.
69 changes: 69 additions & 0 deletions .claude/skills/new-command/references/archetype-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Archetype: read (single and list)

Two sub-shapes; choose based on whether the command returns one object or many.

## read-single

Canonical example: `cmd/kosli/getFlow.go` — read it in full and adapt.

### Deltas

**Options struct**
- Include an `output string` field; no `Payload`.

**`PreRunE`**
- `RequireGlobalFlags(global, []string{"Org", "ApiToken"})` wrapped in `ErrorBeforePrintingUsage`.
- No other required-flag validation unless the command has additional required flags.

**`Args`**
- `cobra.ExactArgs(1)` — the single positional name (e.g. the flow name).

**Flags**
- `cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag)` — no other shared helpers.

**`run` method**
- Build the URL: `url.JoinPath(global.Host, "api/v2/<resource>", global.Org, args[0])`.
- `GET` via `kosliClient.Do(&requests.RequestParams{Method: http.MethodGet, URL: url, Token: global.ApiToken})`.
- Print with `output.FormattedPrint(response.Body, o.output, out, 0, map[string]output.FormatOutputFunc{"table": print<Noun>AsTable, "json": output.PrintJson})`.
- Define a `print<Noun>AsTable(raw string, out io.Writer, page int) error` helper that unmarshals and renders.

---

## read-list

Two patterns exist; pick based on whether the endpoint is paginated.

- **Paginated (most lists):** canonical `cmd/kosli/listArtifacts.go` (or `listTrails.go`). Embed `listOptions` in your options struct and call `addListFlags(cmd, &o.listOptions)` (defined in `flags.go`) — it adds `--output`, `--page`, and `--page-limit`. Pass an optional custom page-limit as a third arg, e.g. `addListFlags(cmd, &o.listOptions, 20)` (see `listTrails.go`).
- **Simple (non-paginated):** canonical `cmd/kosli/listFlows.go` — adds `--output` (and filter flags like `--name`, `--ignore-case`) directly with `StringVarP`/`BoolVarP`, no `addListFlags`.

Read whichever canonical file matches and adapt.

### Deltas vs read-single

**`Args`**
- `cobra.NoArgs` (no positional argument).

**Flags**
- Paginated: `addListFlags(cmd, &o.listOptions)` as above.
- Simple: `--output` plus any filter flags added directly.

**`run` method**
- Build the base URL into a `base` variable (not `url` — `listArtifacts.go` does this so the `net/url` package stays in scope), then append query params via `url.Values{}` and `params.Encode()`.
- Same `GET` + `output.FormattedPrint` pattern as read-single.
- The `print<Noun>sAsTable` helper unmarshals to a `[]map[string]interface{}` and handles the empty-list case with `logger.Info("No <nouns> were found.")`.

**`RunE` signature**
- `return o.run(out)` (no `args` needed when there are no positional args).

---

## What stays the same across both sub-shapes

- `new<VerbNoun>Cmd(out io.Writer) *cobra.Command` factory with description consts.
- `ErrorBeforePrintingUsage` wrapping for `RequireGlobalFlags` errors.
- Imports: `"net/http"`, `"net/url"`, `"io"`, `output`, `requests`, `cobra`.

## Where to look next

- Endpoint path and query-param suggestions: `references/openapi.md`.
- Registration, flag constants, lifecycle annotations, test skeleton: `references/wiring.md`.
Loading
Loading