From ee50fd6b8f448eddc017640fca93cf69f0e70597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Fri, 19 Jun 2026 21:53:06 +0200 Subject: [PATCH 01/14] feat(skill): add new-command skill router and interview --- .claude/skills/new-command/SKILL.md | 101 ++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 .claude/skills/new-command/SKILL.md diff --git a/.claude/skills/new-command/SKILL.md b/.claude/skills/new-command/SKILL.md new file mode 100644 index 000000000..692dd9169 --- /dev/null +++ b/.claude/skills/new-command/SKILL.md @@ -0,0 +1,101 @@ +--- +name: new-command +description: Scaffold a new Kosli CLI command or subcommand (kosli ) 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 [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 `newCmd` 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/.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 . --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=`. + - `local` archetype: typically runs without a server; run + `go test ./cmd/kosli/ -run ` 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. From 6a71f2f3f8d7692c340ef2e50f48bce1eba6e13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Fri, 19 Jun 2026 21:55:42 +0200 Subject: [PATCH 02/14] feat(skill): add wiring reference (registration, flags, lifecycle, tests) --- .../skills/new-command/references/wiring.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 .claude/skills/new-command/references/wiring.md diff --git a/.claude/skills/new-command/references/wiring.md b/.claude/skills/new-command/references/wiring.md new file mode 100644 index 000000000..426c93fb1 --- /dev/null +++ b/.claude/skills/new-command/references/wiring.md @@ -0,0 +1,128 @@ +# Wiring reference: registration, flags, lifecycle, tests + +This file covers the four cross-cutting steps that apply to every new command, regardless of archetype. Archetype-specific logic lives in the `archetype-*.md` files. + +--- + +## 1. Registration + +Add `newCmd(out)` to the parent verb's `AddCommand(...)` block. + +Canonical examples to read: `cmd/kosli/create.go` (verb with multiple subcommands), `cmd/kosli/attest.go` (same pattern). + +```go +cmd.AddCommand( + newCreateFlowCmd(out), + newCreateFooCmd(out), // add the new command here +) +``` + +**New verb:** If the verb does not yet exist, create `cmd/kosli/.go` with a factory following the same shape as `create.go`: + +```go +func newFooCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{Use: "foo", Short: fooDesc, Long: fooDesc} + cmd.AddCommand(newFooBarCmd(out)) + return cmd +} +``` + +Then add `newFooCmd(out)` to the `AddCommand` block in `cmd/kosli/root.go` (around line 407 — the block that lists `newGetCmd`, `newCreateCmd`, etc.). + +--- + +## 2. Flag constants + +Append new flag description constants to `cmd/kosli/root.go` in the `const (...)` block (around line 97). Use the `[optional]`/`[conditional]`/`[defaulted]` prefix convention: + +```go +fooNameFlag = "[optional] The name of the foo." +fooTimeoutFlag = "[defaulted] Timeout in seconds for the foo operation." +``` + +- `[optional]` - flag is never required +- `[conditional]` - required only under certain conditions (document in the description) +- `[defaulted]` - always has a meaningful default; mention it in the description +- No prefix - unconditionally required + +Reference the existing block in `root.go` (lines 97-160+) for naming and formatting conventions. + +--- + +## 3. Lifecycle annotations + +**Beta** - add to the `cobra.Command` literal: + +```go +Annotations: map[string]string{betaCLIAnnotation: ""}, +``` + +`betaCLIAnnotation` is defined in `root.go` as `"betaCLI"`. No import needed. + +**Hidden/incubating** - combine `Hidden: true` with `docgen.DocHiddenAnnotation` to suppress from both Cobra help listing and generated docs: + +```go +Hidden: true, +Annotations: map[string]string{docgen.DocHiddenAnnotation: "", betaCLIAnnotation: ""}, +``` + +Import: `"github.com/kosli-dev/cli/internal/docgen"` + +Canonical examples: `cmd/kosli/evaluate.go` (beta only, parent verb), `cmd/kosli/attestDecision.go` (hidden + beta, leaf command). + +--- + +## 4. Test skeleton + +The dominant test pattern in `cmd/kosli/` is a testify suite. Read `cmd/kosli/getFlow_test.go` for the full shape; the key elements are: + +**Suite struct and setup:** + +```go +type FooBarCommandTestSuite struct { + suite.Suite + defaultKosliArguments string +} + +func (suite *FooBarCommandTestSuite) SetupTest() { + global = &GlobalOpts{ + ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY", + Org: "docs-cmd-test-user", + Host: "http://localhost:8001", + } + suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken) + CreateFlow(suite.flowName, suite.T()) // attest archetype also calls BeginTrail +} +``` + +The test API token and org are the standard test values used across all suites - copy verbatim from `getFlow_test.go`. + +**Two minimal test cases:** + +```go +func (suite *FooBarCommandTestSuite) TestFooBarCmd() { + tests := []cmdTestCase{ + { + wantError: true, + name: "missing required flag fails", + cmd: fmt.Sprintf(`foo bar %s`, suite.defaultKosliArguments), + golden: "Error: ...\n", + }, + { + name: "happy path works", + cmd: fmt.Sprintf(`foo bar --some-flag value %s`, suite.defaultKosliArguments), + }, + } + runTestCmd(suite.T(), tests) +} +``` + +**Suite entrypoint:** + +```go +func TestFooBarCommandTestSuite(t *testing.T) { + suite.Run(t, new(FooBarCommandTestSuite)) +} +``` + +Leave `golden: ""` on the happy-path case; capture actual output after the first run against a real server and paste it in. See `cmd/kosli/testHelpers.go` for `runTestCmd`, `CreateFlow`, `BeginTrail`, and other setup helpers. From 5ed425cbac054c12b07d9835888b7c1518c8b5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Fri, 19 Jun 2026 21:55:47 +0200 Subject: [PATCH 03/14] feat(skill): add OpenAPI-driven endpoint/payload reference --- .../skills/new-command/references/openapi.md | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 .claude/skills/new-command/references/openapi.md diff --git a/.claude/skills/new-command/references/openapi.md b/.claude/skills/new-command/references/openapi.md new file mode 100644 index 000000000..49c56a0d1 --- /dev/null +++ b/.claude/skills/new-command/references/openapi.md @@ -0,0 +1,102 @@ +# OpenAPI-driven endpoint and payload derivation + +Use this file for all API archetypes (read-single, read-list, create-mutate, attest, generic-action). Skip for `local`. + +--- + +## 1. Source + +`https://app.kosli.com/api/v2/openapi.json` - public, no auth required, OpenAPI 3.1.0, ~80 paths. + +**Fetch only what you need** - never dump the whole spec into context. Use `jq` to extract the single path or schema you care about. + +--- + +## 2. Find the endpoint and method + +Match the command to a spec path. Confirm the match with the developer before proceeding. + +Example - extract a single path: + +```bash +curl -s https://app.kosli.com/api/v2/openapi.json | jq '.paths["/flows/{org}"]' +``` + +List all paths to discover candidates: + +```bash +curl -s https://app.kosli.com/api/v2/openapi.json | jq '.paths | keys[]' +``` + +Extract the schema for a specific request body: + +```bash +curl -s https://app.kosli.com/api/v2/openapi.json \ + | jq '.paths["/flows/{org}"].put.requestBody.content["application/json"].schema' +``` + +--- + +## 3. Path mapping to `url.JoinPath` + +Spec paths are relative to `/api/v2`. Map each segment: + +| Spec path segment | CLI mapping | +|---|---| +| `/flows/{org}` | `url.JoinPath(global.Host, "api/v2/flows", global.Org)` | +| `{flow_name}` | positional arg or `--flow` flag value | +| `{trail_name}` | `--trail` flag value | +| `{environment_name}` | positional arg or `--environment` flag value | + +Path params covered by global flags (`{org}` → `global.Org`, `{host}` → `global.Host`) are never exposed as CLI flags. All other path params become positional args or named flags - decide with the developer. + +Full example for `PUT /flows/{org}`: + +```go +url, err := url.JoinPath(global.Host, "api/v2/flows", global.Org) +``` + +--- + +## 4. Payload struct (create-mutate and attest) + +Derive the `Payload` struct fields and `json:` tags directly from the request body's component schema. Do not invent field names. + +If the schema references a `$ref`, resolve it: + +```bash +curl -s https://app.kosli.com/api/v2/openapi.json \ + | jq '.components.schemas.FlowRequest' +``` + +Then write the struct with exact field names from the spec: + +```go +type FooPayload struct { + Name string `json:"name"` + Description string `json:"description"` +} +``` + +For the attest archetype the payload embeds `*CommonAttestationPayload` and adds a `type_name` field - see `cmd/kosli/attestDecision.go` for the exact shape. + +--- + +## 5. Flag suggestions + +- **read/list** - query params from the spec `parameters` array become flag candidates. +- **create-mutate** - body fields from the schema become flag candidates; required body fields map to required flags (no `[optional]` prefix). +- Shared helpers to prefer over custom flags: `addDryRunFlag`, `addListFlags`, `addAttestationFlags`, `addFingerprintFlags` - check `cmd/kosli/flags.go` before adding a custom flag. + +--- + +## 6. Fallback - never fabricate + +If the spec is unreachable or the endpoint is not yet published (common when the API change is still in flight): + +1. Tell the developer the spec could not be reached or the path was not found. +2. Ask the developer to supply the endpoint path, method, and payload fields manually. +3. Mark every field in the generated `Payload` struct with a comment `// UNVERIFIED - confirm against spec`. +4. Do not invent field names, types, or JSON tags. + +This upholds the project rule: never assume API response structures or field names. From 2b0c6f831064edcb86b367c4ad0e65b74c0c7283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Fri, 19 Jun 2026 21:58:29 +0200 Subject: [PATCH 04/14] feat(skill): add local (no-API) archetype reference --- .../new-command/references/archetype-local.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .claude/skills/new-command/references/archetype-local.md diff --git a/.claude/skills/new-command/references/archetype-local.md b/.claude/skills/new-command/references/archetype-local.md new file mode 100644 index 000000000..ad2502192 --- /dev/null +++ b/.claude/skills/new-command/references/archetype-local.md @@ -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 `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 + +- `newCmd(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`. From e2bccfd72863fbf124d527082dc144d4c41e40d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Fri, 19 Jun 2026 21:59:40 +0200 Subject: [PATCH 05/14] feat(skill): add read (single/list) archetype reference --- .../new-command/references/archetype-read.md | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .claude/skills/new-command/references/archetype-read.md diff --git a/.claude/skills/new-command/references/archetype-read.md b/.claude/skills/new-command/references/archetype-read.md new file mode 100644 index 000000000..efbda2878 --- /dev/null +++ b/.claude/skills/new-command/references/archetype-read.md @@ -0,0 +1,64 @@ +# 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/", 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": printAsTable, "json": output.PrintJson})`. +- Define a `printAsTable(raw string, out io.Writer, page int) error` helper that unmarshals and renders. + +--- + +## read-list + +Canonical example: `cmd/kosli/listFlows.go` — read it in full and adapt. + +### Deltas vs read-single + +**`Args`** +- `cobra.NoArgs` (no positional argument). + +**Flags** +- Same `--output` flag as read-single. +- Add any filter flags (e.g. `--name`, `--ignore-case`) as `StringVarP`/`BoolVarP` directly — `addListFlags` is **not** a shared helper in this repo; look at `listFlows.go` for the actual pattern. + +**`run` method** +- Build the base URL, then append query params via `url.Values{}` and `params.Encode()`. +- Same `GET` + `output.FormattedPrint` pattern as read-single. +- The `printsAsTable` helper unmarshals to a `[]map[string]interface{}` and handles the empty-list case with `logger.Info("No 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 + +- `newCmd(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`. From fe03b769cfc1c0a34ee35f7ad748eb7b7028a814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Fri, 19 Jun 2026 22:00:58 +0200 Subject: [PATCH 06/14] feat(skill): add create-mutate/generic-action archetype reference --- .../references/archetype-mutate.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .claude/skills/new-command/references/archetype-mutate.md diff --git a/.claude/skills/new-command/references/archetype-mutate.md b/.claude/skills/new-command/references/archetype-mutate.md new file mode 100644 index 000000000..7ded088ad --- /dev/null +++ b/.claude/skills/new-command/references/archetype-mutate.md @@ -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 `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 Payload`. + +**Options struct** +- Holds `payload 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/", 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(" '%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 + +- `newCmd(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`. From ad9e3814475173e76fc40741c6c56c6e9a360be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Fri, 19 Jun 2026 22:02:31 +0200 Subject: [PATCH 07/14] feat(skill): add attest archetype reference --- .../references/archetype-attest.md | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .claude/skills/new-command/references/archetype-attest.md diff --git a/.claude/skills/new-command/references/archetype-attest.md b/.claude/skills/new-command/references/archetype-attest.md new file mode 100644 index 000000000..fdd4b5862 --- /dev/null +++ b/.claude/skills/new-command/references/archetype-attest.md @@ -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 AttestationPayload struct { + *CommonAttestationPayload + TypeName string `json:"type_name"` + // ... type-specific fields + } + ``` + +**Options struct** +- Embed `*CommonAttestationOptions`; hold a `payload AttestationPayload`. + + ```go + type attestOptions struct { + *CommonAttestationOptions + payload AttestationPayload + } + ``` + +**Factory initialisation** +- Initialise both embedded structs explicitly: + ```go + o := &attestOptions{ + CommonAttestationOptions: &CommonAttestationOptions{ + fingerprintOptions: &fingerprintOptions{}, + }, + payload: 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, "")`. +- 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(":%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}/`). From 6653a403773024395a20c9a66d4c754f1841bbe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Fri, 19 Jun 2026 22:05:47 +0200 Subject: [PATCH 08/14] docs: point CONTRIBUTING at the new-command skill --- CONTRIBUTING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b5bf5fff7..f9a634c14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,4 +34,8 @@ The version number is not generated automatically and must be decided manually. We are using semantic versioning (ie: 2.3.2). ``` make release tag=v -``` \ No newline at end of file +``` + +## Adding a command + +New CLI commands are scaffolded with the `new-command` skill - invoke `/new-command` in Claude Code, or ask Claude to "add a command". The skill interviews you for the command name, archetype (local, read, mutate, attest), endpoint details, and flags, then generates the command file, flag constants, registration wiring, lifecycle annotations (beta/hidden), and a test skeleton. \ No newline at end of file From d38ee1dbce24f956ed137b2c9e1d1eff809a95be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Fri, 19 Jun 2026 22:07:59 +0200 Subject: [PATCH 09/14] fix(skill): correct addListFlags guidance in read archetype --- .../skills/new-command/references/archetype-read.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.claude/skills/new-command/references/archetype-read.md b/.claude/skills/new-command/references/archetype-read.md index efbda2878..d1cd3c252 100644 --- a/.claude/skills/new-command/references/archetype-read.md +++ b/.claude/skills/new-command/references/archetype-read.md @@ -31,7 +31,12 @@ Canonical example: `cmd/kosli/getFlow.go` — read it in full and adapt. ## read-list -Canonical example: `cmd/kosli/listFlows.go` — read it in full and adapt. +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:84`) — 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 @@ -39,8 +44,8 @@ Canonical example: `cmd/kosli/listFlows.go` — read it in full and adapt. - `cobra.NoArgs` (no positional argument). **Flags** -- Same `--output` flag as read-single. -- Add any filter flags (e.g. `--name`, `--ignore-case`) as `StringVarP`/`BoolVarP` directly — `addListFlags` is **not** a shared helper in this repo; look at `listFlows.go` for the actual pattern. +- Paginated: `addListFlags(cmd, &o.listOptions)` as above. +- Simple: `--output` plus any filter flags added directly. **`run` method** - Build the base URL, then append query params via `url.Values{}` and `params.Encode()`. From ac39a4627beb0e8eac880d7be19b1a25a14df1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Mon, 22 Jun 2026 09:58:58 +0200 Subject: [PATCH 10/14] docs: note the new-command skill in CLAUDE.md --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index b511e97a1..3917a12e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,6 +46,8 @@ Some tests are skipped without these env vars: `KOSLI_GITHUB_TOKEN`, `KOSLI_GITL - **`cmd/kosli/*.go`** — ~80+ command files, each following the pattern `newCmd()` factory function returning a `*cobra.Command` - **`GlobalOpts`** struct in root.go holds shared config (ApiToken, Org, Host, HttpProxy, DryRun, MaxAPIRetries, etc.) +> **Adding a command?** Use the `new-command` skill (invoke `/new-command`, or ask "add a command"). It interviews for archetype, endpoint (OpenAPI-driven), flags, args, and beta/hidden status, then scaffolds the command file, test skeleton, flag constants, and registration. See `.claude/skills/new-command/`. + ### Internal Packages (`internal/`) | Package | Purpose | From 77af1227e69c6440ab2ec7b0dd000158d59b364f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Mon, 22 Jun 2026 10:12:10 +0200 Subject: [PATCH 11/14] docs(skill): drop pinned line numbers in references, rely on symbols/landmarks --- .claude/skills/new-command/references/archetype-read.md | 2 +- .claude/skills/new-command/references/wiring.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude/skills/new-command/references/archetype-read.md b/.claude/skills/new-command/references/archetype-read.md index d1cd3c252..3bfebce9d 100644 --- a/.claude/skills/new-command/references/archetype-read.md +++ b/.claude/skills/new-command/references/archetype-read.md @@ -33,7 +33,7 @@ Canonical example: `cmd/kosli/getFlow.go` — read it in full and adapt. 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:84`) — 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`). +- **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. diff --git a/.claude/skills/new-command/references/wiring.md b/.claude/skills/new-command/references/wiring.md index 426c93fb1..96a4daf4e 100644 --- a/.claude/skills/new-command/references/wiring.md +++ b/.claude/skills/new-command/references/wiring.md @@ -27,13 +27,13 @@ func newFooCmd(out io.Writer) *cobra.Command { } ``` -Then add `newFooCmd(out)` to the `AddCommand` block in `cmd/kosli/root.go` (around line 407 — the block that lists `newGetCmd`, `newCreateCmd`, etc.). +Then add `newFooCmd(out)` to the `AddCommand` block in `cmd/kosli/root.go` (the block that lists `newGetCmd`, `newCreateCmd`, etc.). --- ## 2. Flag constants -Append new flag description constants to `cmd/kosli/root.go` in the `const (...)` block (around line 97). Use the `[optional]`/`[conditional]`/`[defaulted]` prefix convention: +Append new flag description constants to `cmd/kosli/root.go` in the large `const (...)` block that holds the flag-description strings (e.g. `apiTokenFlag`, `flowNameFlag`). Use the `[optional]`/`[conditional]`/`[defaulted]` prefix convention: ```go fooNameFlag = "[optional] The name of the foo." @@ -45,7 +45,7 @@ fooTimeoutFlag = "[defaulted] Timeout in seconds for the foo operation." - `[defaulted]` - always has a meaningful default; mention it in the description - No prefix - unconditionally required -Reference the existing block in `root.go` (lines 97-160+) for naming and formatting conventions. +Reference the existing flag-description `const` block in `root.go` for naming and formatting conventions. --- From 0140fa3080df4bbd47cc7bfb1e6a829a22639ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Mon, 22 Jun 2026 10:31:04 +0200 Subject: [PATCH 12/14] docs(skill): warn against shadowing net/url in read-list run method --- .claude/skills/new-command/references/archetype-read.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/new-command/references/archetype-read.md b/.claude/skills/new-command/references/archetype-read.md index 3bfebce9d..d6267afad 100644 --- a/.claude/skills/new-command/references/archetype-read.md +++ b/.claude/skills/new-command/references/archetype-read.md @@ -48,7 +48,7 @@ Read whichever canonical file matches and adapt. - Simple: `--output` plus any filter flags added directly. **`run` method** -- Build the base URL, then append query params via `url.Values{}` and `params.Encode()`. +- 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 `printsAsTable` helper unmarshals to a `[]map[string]interface{}` and handles the empty-list case with `logger.Info("No were found.")`. From 08504faf87a453aec40f8703ac658e09b64b3285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Mon, 22 Jun 2026 10:39:23 +0200 Subject: [PATCH 13/14] docs(skill): align attest payload pointer with archetype reference --- .claude/skills/new-command/references/openapi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/new-command/references/openapi.md b/.claude/skills/new-command/references/openapi.md index 49c56a0d1..2f46c4f84 100644 --- a/.claude/skills/new-command/references/openapi.md +++ b/.claude/skills/new-command/references/openapi.md @@ -78,7 +78,7 @@ type FooPayload struct { } ``` -For the attest archetype the payload embeds `*CommonAttestationPayload` and adds a `type_name` field - see `cmd/kosli/attestDecision.go` for the exact shape. +For the attest archetype the payload embeds `*CommonAttestationPayload` and adds a `type_name` field - see `references/archetype-attest.md` (canonical example `cmd/kosli/attestCustom.go`) for the exact shape. --- From ac80b090c3561e103dd2307fdf4049250d5e3b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20Gr=C3=B8ndahl?= Date: Mon, 22 Jun 2026 15:47:46 +0200 Subject: [PATCH 14/14] docs: clarify command-adding section heading mentions AI skill --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f9a634c14..07cef7c4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,6 @@ We are using semantic versioning (ie: 2.3.2). make release tag=v ``` -## Adding a command +## Adding a command using AI skill New CLI commands are scaffolded with the `new-command` skill - invoke `/new-command` in Claude Code, or ask Claude to "add a command". The skill interviews you for the command name, archetype (local, read, mutate, attest), endpoint details, and flags, then generates the command file, flag constants, registration wiring, lifecycle annotations (beta/hidden), and a test skeleton. \ No newline at end of file