{"owner":"snyk","repo":"cli","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"skills":{"AGENTS.md":"# Agent Instructions for snyk/cli\n\n## Mental Model: The CLI as a Binary Container\n\nThis repo builds a **host binary**, not a self-contained application. The Snyk CLI is a Go executable that **composes product features at compile time** by importing extensions from separate repositories. Almost all product/feature logic lives in those external `cli-extension-*` repos — not here.\n\nWhat **this repo** owns:\n\n- **The Go host process** (`cliv2/`) — startup, Cobra command routing, networking, proxy, analytics, error handling, teardown\n- **Extension registration** — `cliv2/pkg/core/workflows.go` is the manifest of all compiled-in extensions\n- **The legacy TypeScript CLI** (`src/`) — older commands that haven't migrated to Go workflows yet\n- **Build & release infrastructure** — Makefile, CI/CD, signing, packaging\n\nWhat **this repo does NOT contain**:\n\n- Product logic for `snyk test`, `snyk code test`, `snyk iac test`, `snyk container test`, etc. — these live in extension repos\n- The Go Application Framework (GAF) itself — that's `go-application-framework`\n\n**Implication for agents**: If you're investigating a product behavior or bug, the code is almost certainly in an extension repo, not here. See [Extension → Command Mapping](#extension--command-mapping) and [Where Does the Bug Live?](#where-does-the-bug-live) below.\n\n### Command routing\n\nAt startup, every registered workflow is turned into a Cobra command dynamically (`createCommandsForWorkflows` in `cliv2/pkg/core/main.go`). When a user runs a command:\n\n1. **Cobra matches** → the command is dispatched to the corresponding Go workflow via `engine.Invoke()`\n2. **Cobra can't match** (\"unknown command\") → the command **falls back to the legacy TypeScript CLI** via `defaultCmd()` → `basic_workflows.WORKFLOWID_LEGACY_CLI`\n\nThis fallback is why many commands still work without a Go workflow — they're silently dispatched to the embedded TS binary. A few commands have special wiring (e.g., `code test`, `auth`, `secrets test`) in `cliv2/pkg/core/main.go`.\n\n### Extension → command mapping\n\nThe canonical source is `cliv2/pkg/core/workflows.go`. Current mapping:\n\n| User command                        | Extension repo             | Init                                |\n| ----------------------------------- | -------------------------- | ----------------------------------- |\n| `snyk test`, `snyk monitor`         | `cli-extension-os-flows`   | `osflows.Init`                      |\n| `snyk code test`                    | `code-client-go`           | `code.Init`                         |\n| `snyk iac test`                     | `cli-extension-iac`        | `iac.Init`                          |\n| `snyk iac capture`                  | `snyk-iac-capture`         | `capture.Init`                      |\n| `snyk iac rules`                    | `cli-extension-iac-rules`  | `iacrules.Init`                     |\n| `snyk container test`               | `container-cli`            | `container.Init`                    |\n| `snyk sbom`                         | `cli-extension-sbom`       | `sbom.Init`                         |\n| `snyk secrets test`                 | `cli-extension-secrets`    | `secrets.Init`                      |\n| `snyk agent-scan` / `snyk mcp-scan` | `cli-extension-agent-scan` | `agentscan.Init`                    |\n| `snyk aibom test`                   | `cli-extension-ai-bom`     | `aibom.Init`                        |\n| dep-graph                           | `cli-extension-dep-graph`  | `depgraph.Init`                     |\n| MCP server                          | `studio-mcp`               | `mcp.Init`                          |\n| Language server                     | `snyk-ls`                  | `ls_extension.Init`                 |\n| `snyk ignore`                       | GAF built-in               | `ignore_workflow.Init`              |\n| Connectivity check                  | GAF built-in               | `connectivity_check_extension.Init` |\n| _Unrecognized commands_             | Legacy TS CLI (`src/`)     | Fallback via `defaultCmd()`         |\n\nThe private build (`cliv2-private/`) additionally registers `remy-cli-extension` via `WithAdditionalExtensions`.\n\n### Where does the bug live?\n\n- **Product bug** (wrong scan results, missing output fields, incorrect behavior for `snyk test|code|iac|container|sbom`) → **the extension repo** listed above. Clone it, use `go.mod replace` to test locally.\n- **CLI plumbing** (auth, proxy, analytics, networking, configuration) → **`cliv2/`** or **`go-application-framework`**.\n- **Output formatting** (JSON shape, SARIF, human-readable output) → likely the **output workflow pipeline** in GAF (`local_workflows/output_workflow`), or the extension's content type.\n- **Legacy TS command** (commands that fall back to the TypeScript CLI) → **`src/`**.\n- **Build/CI issue** → **`.circleci/config.yml`**, **`Makefile`**, or **`cliv2/Makefile`**.\n\n## Development\n\n### Setting up the Developer Environment\n\n- Applies to macOS and linux\n- Install homebrew (https://brew.sh/) (used for installing all other dependencies)\n- Do not install anything directly, use the install script\n\n```sh\n./scripts/install-dev-dependencies.sh\n```\n\n### Changing the Code\n\n- The Snyk binary connects multiple repositories to build a single binary.\n- Code changes might be required in different repositories.\n- For local development, use `go mod replace` to point to local code.\n- For CI/CD verification, use temporary commit shas to point to the desired code versions.\n- Use `make clean` to build a binary from scratch, this will remove all build artifacts and dependencies and increases the build time, so only do this if really required for example to build for another platform.\n- Only build the binary with `make build` (add `BUILD_MODE=public` without private-repo access), everything else is error prone.\n- Add tests - see [Testing Strategy](#testing-strategy)\n- Test the binary — see [Running Tests](#running-tests).\n\n### Before Committing (pre-commit)\n\nRun these before every commit — they mirror CI, which also fails if any tracked file is left uncommitted.\n\n1. **Format**: `make format` (TypeScript + Go, runs `make tidy`)\n2. **Lint**: `make lint` (TypeScript + Go)\n3. **Verify no drift**: `git diff --name-only` must be empty. Stage anything the steps above changed.\n\n## Project Structure\n\nA **hybrid TypeScript + Go** project:\n\n- **`src/`** — TypeScript CLI source (CLIv1, legacy CLI). Manifest `package.json`; resolved-dep source of truth `package-lock.json`\n- **`cliv2/`** — Go CLI wrapper (public runtime) that embeds the TypeScript binary. Module `cliv2/go.mod`; public extensions registered in `cliv2/pkg/core/workflows.go`\n- **`cliv2-private/`** — private Go runtime; entrypoint adds private extensions (e.g. `github.com/snyk/remy-cli-extension`). Module `cliv2-private/go.mod` may not resolve without private GitHub access / `GOPRIVATE`, but static parsing of `go.mod` still works\n- **`packages/`** — npm workspaces (`@snyk/fix`, `@snyk/protect`)\n- **`ts-binary-wrapper/`** — npm package that downloads and runs released CLI binaries\n- **`release-scripts/`, `scripts/`, `.circleci/`** — release and CI tooling\n- **`binary-releases/`** — build output (gitignored)\n\n### Dependency landmarks\n\nThe authoritative dependency lists live in `go.mod`/`go.sum` (Go) and `package-lock.json` (npm) — read them for exact names and versions rather than trusting any list here. These roles orient you to which dependencies usually matter:\n\n- **Core framework** — `go-application-framework` (GAF): config, networking, workflow engine\n- **Language server** — `snyk-ls`\n- **Feature logic** — the `cli-extension-*` repos (one per product area); public set registered in `cliv2/pkg/core/workflows.go`, private set in `cliv2-private/`\n- **CLIv1 (TypeScript) plugins** — the `snyk-*-plugin` / `@snyk/*` packages\n\n### Investigating an issue (impact assessment)\n\nFirst, determine **where the bug lives** — see [Where Does the Bug Live?](#where-does-the-bug-live) and the [Extension → Command Mapping](#extension--command-mapping) table. Most product bugs are in extension repos, not here.\n\nMethod, not memorized data — resolve specifics from source each time:\n\n- **Real versions**: read `go.mod` / `package-lock.json`.\n- **Blast radius** (who depends on a package): `go mod why <module>` and `go mod graph` in `cliv2/`; for npm, `npm ls <pkg>`.\n- **Pull down an extension repo**: these are separate GitHub repos — `gh repo clone snyk/<name>` (private ones need `GOPRIVATE` / auth). Use `go.mod replace` to test local changes against the CLI build.\n\n## Testing Strategy\n\nThe CLI follows a layered testing pyramid. Each layer has a different goal, system under test (SUT), and execution context.\n\n### Unit & Component Tests (Open Box)\n\n- **Goal**: Verify correct implementation of individual functions and components.\n- **SUT**: CLI/Plugin/Extension logic (not the built binary).\n- **Properties**: Uses mocks to simulate external components. No network calls.\n- **Locations**: `test/jest/unit/**/*.spec.ts` (TypeScript), `cliv2/**/*_test.go` (Go).\n- **Runs on**: every CLI branch push, and in plugin/extension CI/CD pipelines.\n- **Note**: Tests and logic should be in the same repo as the code they test.\n\n### Integration Tests (Grey Box)\n\n- **Goal**: Verify correct handling of edge cases in component interaction and integration.\n- **SUT**: The built CLI binary.\n- **Properties**: Uses a fake server (`test/acceptance/fake-server.ts`) to simulate the Snyk API. No real external calls.\n- **Locations**: `test/jest/acceptance/**/*.spec.ts` (most files — they use fake-server), `test/tap/*.test.ts` (legacy Tap tests).\n- **Runs on**: CLI branch pushes (the `acceptance-tests` CI job, across multiple OS/arch combinations).\n- **Note**: Legacy Tap tests (`test/tap/`) exist at this layer. New tests should be Jest (`*.spec.ts`) in `test/jest/acceptance/`.\n\n### User Journey Tests (Grey Box)\n\n- **Goal**: Verify that end-to-end user journeys work and API contracts are met.\n- **SUT**: The built CLI binary.\n- **Properties**: End-to-end tests against a configurable (real) Snyk instance. Includes contract tests (CLI arguments, JSON output shape) and enforcement testing.\n- **Runs on**: plugin/extension CI/CD pipelines and environment testing (on-demand/scheduled). A small number of acceptance tests that use `TEST_SNYK_TOKEN` instead of fake-server belong to this layer.\n- **Note**: These tests should cover also features from extensions to ensure that the user experience through the surface remains intact.\n\n### System Tests (Closed Box)\n\n- **Goal**: Verify deployment artifacts work correctly across target environments.\n- **SUT**: Deployment artifacts (downloaded binaries, not locally built).\n- **Properties**: Installs the CLI from the release download URL and runs basic smoke commands (`snyk whoami`, `snyk woof`) on the target platform.\n- **Locations**: The `test-release` and `test-release-static` CI jobs in `.circleci/config.yml`.\n- **Runs on**: release/deployment branches and `*e2e*` branches. Covers Docker, Alpine, macOS, Windows, Linux (multiple distros), FIPS, and scratch containers.\n\n### Unfocused / Post-Fix Tests (Closed Box)\n\n- **Goal**: Detect regressions for unspecified behavior and implicit contracts with users.\n- **SUT**: The CLI binary (preview or release candidate).\n- **Properties**: Exploratory and regression testing that is not covered by other layers.\n- **Methods**: Snyk-internal preview-release testing (\"Snyk for Snyk\"), explorative testing, regression testing against multiple open-source projects, canary deployments.\n- **Runs on**: on-demand/scheduled, typically before a GA release.\n\n### Which test type should I write?\n\n- **Changing internal logic** (a function, a parser, a formatter) → **unit test** in `test/jest/unit/` or `cliv2/**/*_test.go`.\n- **Changing CLI behavior** (command output, flag handling, API interaction) → **integration test** in `test/jest/acceptance/` using fake-server.\n- **E2E and system tests** are managed by the CI pipeline and are not typically written by feature contributors.\n\n## Running Tests\n\n```sh\n# TypeScript unit tests (some suites validate credentials, so a token is required)\nTEST_SNYK_TOKEN=<token> npm run test:unit\n\n# TypeScript acceptance/user journey tests (requires a built binary)\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm run test:acceptance\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm jest --runInBand test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts\n\n# Go tests\ncd cliv2 && make test\n\n# A single TS test file\nnpx jest --runInBand test/jest/unit/path/to/test.spec.ts\n```\n\n`SNYK_TOKEN` is **not** an alternative to `TEST_SNYK_TOKEN` — `test/setup.js` removes `SNYK_TOKEN` (and `SNYK_API_KEY`) from the environment when either is set, and writes `TEST_SNYK_TOKEN` into the CLI user config so tests run against a known configuration.\n\n## Running the CLI Locally\n\n```sh\nmake build\n./binary-releases/snyk-macos-arm64 --version   # adjust for your platform\n```\n\nTypeScript-only, without building the full binary:\n\n```sh\nnpm run dev -- test --all-projects\n```\n\n## Build Modes\n\nAuto-detected, but forceable:\n\n```sh\nmake build BUILD_MODE=public    # OSS-only build (external contributors)\nmake build BUILD_MODE=private   # full build, requires cliv2-private access\n```\n\n**Keep public/private differences explicit.** The public build must not require private-module access — never make `BUILD_MODE=public` depend on `cliv2-private/` or other private repos. When changing dependencies, verify the narrowest affected build/test path and update lockfiles / module files intentionally.\n\n## Commit Message Format\n\n[Conventional Commits](https://www.conventionalcommits.org/): `type: summary`, with an optional body explaining the reasoning.\n\nTypes: `feat`, `fix`, `chore`, `test`, `refactor`, `docs`, `revert`.\n**No breaking changes** — never use `BREAKING CHANGE` or `!`.\n\nKeep the first line under 72 characters. This format is enforced on every commit (and, when a branch has more than one commit, on the PR title — it becomes the squash message).\n\n## Pull Request Checks\n\nPR conventions are enforced by **Danger** (`dangerfile.js` is authoritative). To pass first time:\n\n- **Squash to a single commit** before merging — multiple commits are flagged.\n- **Commit/PR-title format** must follow [Commit Message Format](#commit-message-format) above (the only _blocking_ check).\n- **Update tests alongside `src/` changes** — touching `src/` with no `test/` change is flagged.\n- **New tests go under `test/jest/` as Jest** (`*.spec.ts`); avoid adding Tap-style tests (`*.test.ts`) elsewhere.\n- **Use ES6 `import`/`export`** in `.ts` files — not `require()` / `module.exports`.\n- **CLI `help/` text** is edited in Gitbook, not here — it syncs in automatically.\n\n## Updating Go Dependencies\n\n```sh\ngo run ./scripts/upgrade-snyk-go-dependencies.go -name=go-application-framework\nmake tidy\n```\n\n## Building with Local Dependencies\n\n**Go** — add to `cliv2/go.mod`:\n\n```go\nreplace github.com/snyk/cli-extension-foo => ../../cli-extension-foo\n```\n\n**TypeScript** — update `package.json`, then `npm install` and temporarily commit:\n\n```json\n\"snyk-foo\": \"file:../snyk-foo\",\n```\n\n## Architecture\n\nSee [Mental Model: The CLI as a Binary Container](#mental-model-the-cli-as-a-binary-container) for the high-level picture of how this repo fits together.\n\n### Binary structure\n\nThe shipped CLI is a **Go executable** (`cliv2/`) that embeds the TypeScript CLI binary via `go:embed`. At runtime, registered Go workflows become Cobra commands; unrecognized commands fall back to the embedded TS binary (the `legacycli` workflow), proxying stdin/stdout/stderr and the exit code. See [Command Routing](#command-routing) for details.\n\n### What typically changes in this repo\n\n- **`cliv2/pkg/core/workflows.go`** — add/remove extension registrations\n- **`cliv2/go.mod`** — bump extension or GAF versions\n- **`cliv2/pkg/core/main.go`** — startup, Cobra wiring, special-case command handlers, error handling\n- **`cliv2/internal/`** — proxy, debug logging, constants, help routing\n- **`src/`** — legacy TypeScript commands (shrinking as commands migrate to Go workflows)\n- **`.circleci/config.yml`**, **`Makefile`** — CI/CD and build pipeline\n\n### Go Application Framework (GAF)\n\nBuilt on `go-application-framework` (GAF). Commands are **workflows** registered with the engine. Key packages: `pkg/workflow` (engine), `pkg/configuration`, `pkg/networking`, `pkg/auth`, `pkg/local_workflows` (built-ins like auth, whoami).\n\nA workflow receives an `InvocationContext` and input `Data`, and returns output `Data`:\n\n```go\nfunc myWorkflow(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) {\n    config := invocation.GetConfiguration()\n    logger := invocation.GetEnhancedLogger()\n    // ... do work ...\n    return output, nil\n}\n```\n\n`InvocationContext` exposes `GetConfiguration()`, `GetEnhancedLogger()` (zerolog), `GetNetworkAccess().GetHttpClient()` (auth/proxy-configured), and `GetAnalytics()`.\n\nRegistering a workflow is a three-step pattern — define a `WorkflowIdentifier`, write an `Init` that builds a flagset and calls `engine.Register(...)`, and wire it in via `engine.AddExtensionInitializer(...)`. See an existing extension (e.g. `cli-extension-sbom`) for the canonical shape.\n\n### Extensions\n\nFeature logic lives in separate **`cli-extension-*`** repos, registered with GAF at startup via `ExtensionInit`. The full list is in the [Extension → Command Mapping](#extension--command-mapping) table. Suggested layout:\n\n```\nextension/\n├── init.go       # ExtensionInit + Register + config defaults\n├── workflow.go   # Callback (thin shell)\n└── domain/       # Business logic, clients, types (no GAF imports)\n```\n\n**Key principle**: the workflow callback is a **thin integration shell** — read config/client/context out of `InvocationContext`, hand concrete values to domain code, package the result back into `[]Data`.\n\n**Anti-patterns**:\n\n- ❌ Domain logic inside the callback — extract into domain packages\n- ❌ Passing `InvocationContext` into domain code — pass concrete values\n- ❌ Deep workflow call chains — keep composition flat\n"},"files":{"AGENTS.md":"# Agent Instructions for snyk/cli\n\n## Mental Model: The CLI as a Binary Container\n\nThis repo builds a **host binary**, not a self-contained application. The Snyk CLI is a Go executable that **composes product features at compile time** by importing extensions from separate repositories. Almost all product/feature logic lives in those external `cli-extension-*` repos — not here.\n\nWhat **this repo** owns:\n\n- **The Go host process** (`cliv2/`) — startup, Cobra command routing, networking, proxy, analytics, error handling, teardown\n- **Extension registration** — `cliv2/pkg/core/workflows.go` is the manifest of all compiled-in extensions\n- **The legacy TypeScript CLI** (`src/`) — older commands that haven't migrated to Go workflows yet\n- **Build & release infrastructure** — Makefile, CI/CD, signing, packaging\n\nWhat **this repo does NOT contain**:\n\n- Product logic for `snyk test`, `snyk code test`, `snyk iac test`, `snyk container test`, etc. — these live in extension repos\n- The Go Application Framework (GAF) itself — that's `go-application-framework`\n\n**Implication for agents**: If you're investigating a product behavior or bug, the code is almost certainly in an extension repo, not here. See [Extension → Command Mapping](#extension--command-mapping) and [Where Does the Bug Live?](#where-does-the-bug-live) below.\n\n### Command routing\n\nAt startup, every registered workflow is turned into a Cobra command dynamically (`createCommandsForWorkflows` in `cliv2/pkg/core/main.go`). When a user runs a command:\n\n1. **Cobra matches** → the command is dispatched to the corresponding Go workflow via `engine.Invoke()`\n2. **Cobra can't match** (\"unknown command\") → the command **falls back to the legacy TypeScript CLI** via `defaultCmd()` → `basic_workflows.WORKFLOWID_LEGACY_CLI`\n\nThis fallback is why many commands still work without a Go workflow — they're silently dispatched to the embedded TS binary. A few commands have special wiring (e.g., `code test`, `auth`, `secrets test`) in `cliv2/pkg/core/main.go`.\n\n### Extension → command mapping\n\nThe canonical source is `cliv2/pkg/core/workflows.go`. Current mapping:\n\n| User command                        | Extension repo             | Init                                |\n| ----------------------------------- | -------------------------- | ----------------------------------- |\n| `snyk test`, `snyk monitor`         | `cli-extension-os-flows`   | `osflows.Init`                      |\n| `snyk code test`                    | `code-client-go`           | `code.Init`                         |\n| `snyk iac test`                     | `cli-extension-iac`        | `iac.Init`                          |\n| `snyk iac capture`                  | `snyk-iac-capture`         | `capture.Init`                      |\n| `snyk iac rules`                    | `cli-extension-iac-rules`  | `iacrules.Init`                     |\n| `snyk container test`               | `container-cli`            | `container.Init`                    |\n| `snyk sbom`                         | `cli-extension-sbom`       | `sbom.Init`                         |\n| `snyk secrets test`                 | `cli-extension-secrets`    | `secrets.Init`                      |\n| `snyk agent-scan` / `snyk mcp-scan` | `cli-extension-agent-scan` | `agentscan.Init`                    |\n| `snyk aibom test`                   | `cli-extension-ai-bom`     | `aibom.Init`                        |\n| dep-graph                           | `cli-extension-dep-graph`  | `depgraph.Init`                     |\n| MCP server                          | `studio-mcp`               | `mcp.Init`                          |\n| Language server                     | `snyk-ls`                  | `ls_extension.Init`                 |\n| `snyk ignore`                       | GAF built-in               | `ignore_workflow.Init`              |\n| Connectivity check                  | GAF built-in               | `connectivity_check_extension.Init` |\n| _Unrecognized commands_             | Legacy TS CLI (`src/`)     | Fallback via `defaultCmd()`         |\n\nThe private build (`cliv2-private/`) additionally registers `remy-cli-extension` via `WithAdditionalExtensions`.\n\n### Where does the bug live?\n\n- **Product bug** (wrong scan results, missing output fields, incorrect behavior for `snyk test|code|iac|container|sbom`) → **the extension repo** listed above. Clone it, use `go.mod replace` to test locally.\n- **CLI plumbing** (auth, proxy, analytics, networking, configuration) → **`cliv2/`** or **`go-application-framework`**.\n- **Output formatting** (JSON shape, SARIF, human-readable output) → likely the **output workflow pipeline** in GAF (`local_workflows/output_workflow`), or the extension's content type.\n- **Legacy TS command** (commands that fall back to the TypeScript CLI) → **`src/`**.\n- **Build/CI issue** → **`.circleci/config.yml`**, **`Makefile`**, or **`cliv2/Makefile`**.\n\n## Development\n\n### Setting up the Developer Environment\n\n- Applies to macOS and linux\n- Install homebrew (https://brew.sh/) (used for installing all other dependencies)\n- Do not install anything directly, use the install script\n\n```sh\n./scripts/install-dev-dependencies.sh\n```\n\n### Changing the Code\n\n- The Snyk binary connects multiple repositories to build a single binary.\n- Code changes might be required in different repositories.\n- For local development, use `go mod replace` to point to local code.\n- For CI/CD verification, use temporary commit shas to point to the desired code versions.\n- Use `make clean` to build a binary from scratch, this will remove all build artifacts and dependencies and increases the build time, so only do this if really required for example to build for another platform.\n- Only build the binary with `make build` (add `BUILD_MODE=public` without private-repo access), everything else is error prone.\n- Add tests - see [Testing Strategy](#testing-strategy)\n- Test the binary — see [Running Tests](#running-tests).\n\n### Before Committing (pre-commit)\n\nRun these before every commit — they mirror CI, which also fails if any tracked file is left uncommitted.\n\n1. **Format**: `make format` (TypeScript + Go, runs `make tidy`)\n2. **Lint**: `make lint` (TypeScript + Go)\n3. **Verify no drift**: `git diff --name-only` must be empty. Stage anything the steps above changed.\n\n## Project Structure\n\nA **hybrid TypeScript + Go** project:\n\n- **`src/`** — TypeScript CLI source (CLIv1, legacy CLI). Manifest `package.json`; resolved-dep source of truth `package-lock.json`\n- **`cliv2/`** — Go CLI wrapper (public runtime) that embeds the TypeScript binary. Module `cliv2/go.mod`; public extensions registered in `cliv2/pkg/core/workflows.go`\n- **`cliv2-private/`** — private Go runtime; entrypoint adds private extensions (e.g. `github.com/snyk/remy-cli-extension`). Module `cliv2-private/go.mod` may not resolve without private GitHub access / `GOPRIVATE`, but static parsing of `go.mod` still works\n- **`packages/`** — npm workspaces (`@snyk/fix`, `@snyk/protect`)\n- **`ts-binary-wrapper/`** — npm package that downloads and runs released CLI binaries\n- **`release-scripts/`, `scripts/`, `.circleci/`** — release and CI tooling\n- **`binary-releases/`** — build output (gitignored)\n\n### Dependency landmarks\n\nThe authoritative dependency lists live in `go.mod`/`go.sum` (Go) and `package-lock.json` (npm) — read them for exact names and versions rather than trusting any list here. These roles orient you to which dependencies usually matter:\n\n- **Core framework** — `go-application-framework` (GAF): config, networking, workflow engine\n- **Language server** — `snyk-ls`\n- **Feature logic** — the `cli-extension-*` repos (one per product area); public set registered in `cliv2/pkg/core/workflows.go`, private set in `cliv2-private/`\n- **CLIv1 (TypeScript) plugins** — the `snyk-*-plugin` / `@snyk/*` packages\n\n### Investigating an issue (impact assessment)\n\nFirst, determine **where the bug lives** — see [Where Does the Bug Live?](#where-does-the-bug-live) and the [Extension → Command Mapping](#extension--command-mapping) table. Most product bugs are in extension repos, not here.\n\nMethod, not memorized data — resolve specifics from source each time:\n\n- **Real versions**: read `go.mod` / `package-lock.json`.\n- **Blast radius** (who depends on a package): `go mod why <module>` and `go mod graph` in `cliv2/`; for npm, `npm ls <pkg>`.\n- **Pull down an extension repo**: these are separate GitHub repos — `gh repo clone snyk/<name>` (private ones need `GOPRIVATE` / auth). Use `go.mod replace` to test local changes against the CLI build.\n\n## Testing Strategy\n\nThe CLI follows a layered testing pyramid. Each layer has a different goal, system under test (SUT), and execution context.\n\n### Unit & Component Tests (Open Box)\n\n- **Goal**: Verify correct implementation of individual functions and components.\n- **SUT**: CLI/Plugin/Extension logic (not the built binary).\n- **Properties**: Uses mocks to simulate external components. No network calls.\n- **Locations**: `test/jest/unit/**/*.spec.ts` (TypeScript), `cliv2/**/*_test.go` (Go).\n- **Runs on**: every CLI branch push, and in plugin/extension CI/CD pipelines.\n- **Note**: Tests and logic should be in the same repo as the code they test.\n\n### Integration Tests (Grey Box)\n\n- **Goal**: Verify correct handling of edge cases in component interaction and integration.\n- **SUT**: The built CLI binary.\n- **Properties**: Uses a fake server (`test/acceptance/fake-server.ts`) to simulate the Snyk API. No real external calls.\n- **Locations**: `test/jest/acceptance/**/*.spec.ts` (most files — they use fake-server), `test/tap/*.test.ts` (legacy Tap tests).\n- **Runs on**: CLI branch pushes (the `acceptance-tests` CI job, across multiple OS/arch combinations).\n- **Note**: Legacy Tap tests (`test/tap/`) exist at this layer. New tests should be Jest (`*.spec.ts`) in `test/jest/acceptance/`.\n\n### User Journey Tests (Grey Box)\n\n- **Goal**: Verify that end-to-end user journeys work and API contracts are met.\n- **SUT**: The built CLI binary.\n- **Properties**: End-to-end tests against a configurable (real) Snyk instance. Includes contract tests (CLI arguments, JSON output shape) and enforcement testing.\n- **Runs on**: plugin/extension CI/CD pipelines and environment testing (on-demand/scheduled). A small number of acceptance tests that use `TEST_SNYK_TOKEN` instead of fake-server belong to this layer.\n- **Note**: These tests should cover also features from extensions to ensure that the user experience through the surface remains intact.\n\n### System Tests (Closed Box)\n\n- **Goal**: Verify deployment artifacts work correctly across target environments.\n- **SUT**: Deployment artifacts (downloaded binaries, not locally built).\n- **Properties**: Installs the CLI from the release download URL and runs basic smoke commands (`snyk whoami`, `snyk woof`) on the target platform.\n- **Locations**: The `test-release` and `test-release-static` CI jobs in `.circleci/config.yml`.\n- **Runs on**: release/deployment branches and `*e2e*` branches. Covers Docker, Alpine, macOS, Windows, Linux (multiple distros), FIPS, and scratch containers.\n\n### Unfocused / Post-Fix Tests (Closed Box)\n\n- **Goal**: Detect regressions for unspecified behavior and implicit contracts with users.\n- **SUT**: The CLI binary (preview or release candidate).\n- **Properties**: Exploratory and regression testing that is not covered by other layers.\n- **Methods**: Snyk-internal preview-release testing (\"Snyk for Snyk\"), explorative testing, regression testing against multiple open-source projects, canary deployments.\n- **Runs on**: on-demand/scheduled, typically before a GA release.\n\n### Which test type should I write?\n\n- **Changing internal logic** (a function, a parser, a formatter) → **unit test** in `test/jest/unit/` or `cliv2/**/*_test.go`.\n- **Changing CLI behavior** (command output, flag handling, API interaction) → **integration test** in `test/jest/acceptance/` using fake-server.\n- **E2E and system tests** are managed by the CI pipeline and are not typically written by feature contributors.\n\n## Running Tests\n\n```sh\n# TypeScript unit tests (some suites validate credentials, so a token is required)\nTEST_SNYK_TOKEN=<token> npm run test:unit\n\n# TypeScript acceptance/user journey tests (requires a built binary)\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm run test:acceptance\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm jest --runInBand test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts\n\n# Go tests\ncd cliv2 && make test\n\n# A single TS test file\nnpx jest --runInBand test/jest/unit/path/to/test.spec.ts\n```\n\n`SNYK_TOKEN` is **not** an alternative to `TEST_SNYK_TOKEN` — `test/setup.js` removes `SNYK_TOKEN` (and `SNYK_API_KEY`) from the environment when either is set, and writes `TEST_SNYK_TOKEN` into the CLI user config so tests run against a known configuration.\n\n## Running the CLI Locally\n\n```sh\nmake build\n./binary-releases/snyk-macos-arm64 --version   # adjust for your platform\n```\n\nTypeScript-only, without building the full binary:\n\n```sh\nnpm run dev -- test --all-projects\n```\n\n## Build Modes\n\nAuto-detected, but forceable:\n\n```sh\nmake build BUILD_MODE=public    # OSS-only build (external contributors)\nmake build BUILD_MODE=private   # full build, requires cliv2-private access\n```\n\n**Keep public/private differences explicit.** The public build must not require private-module access — never make `BUILD_MODE=public` depend on `cliv2-private/` or other private repos. When changing dependencies, verify the narrowest affected build/test path and update lockfiles / module files intentionally.\n\n## Commit Message Format\n\n[Conventional Commits](https://www.conventionalcommits.org/): `type: summary`, with an optional body explaining the reasoning.\n\nTypes: `feat`, `fix`, `chore`, `test`, `refactor`, `docs`, `revert`.\n**No breaking changes** — never use `BREAKING CHANGE` or `!`.\n\nKeep the first line under 72 characters. This format is enforced on every commit (and, when a branch has more than one commit, on the PR title — it becomes the squash message).\n\n## Pull Request Checks\n\nPR conventions are enforced by **Danger** (`dangerfile.js` is authoritative). To pass first time:\n\n- **Squash to a single commit** before merging — multiple commits are flagged.\n- **Commit/PR-title format** must follow [Commit Message Format](#commit-message-format) above (the only _blocking_ check).\n- **Update tests alongside `src/` changes** — touching `src/` with no `test/` change is flagged.\n- **New tests go under `test/jest/` as Jest** (`*.spec.ts`); avoid adding Tap-style tests (`*.test.ts`) elsewhere.\n- **Use ES6 `import`/`export`** in `.ts` files — not `require()` / `module.exports`.\n- **CLI `help/` text** is edited in Gitbook, not here — it syncs in automatically.\n\n## Updating Go Dependencies\n\n```sh\ngo run ./scripts/upgrade-snyk-go-dependencies.go -name=go-application-framework\nmake tidy\n```\n\n## Building with Local Dependencies\n\n**Go** — add to `cliv2/go.mod`:\n\n```go\nreplace github.com/snyk/cli-extension-foo => ../../cli-extension-foo\n```\n\n**TypeScript** — update `package.json`, then `npm install` and temporarily commit:\n\n```json\n\"snyk-foo\": \"file:../snyk-foo\",\n```\n\n## Architecture\n\nSee [Mental Model: The CLI as a Binary Container](#mental-model-the-cli-as-a-binary-container) for the high-level picture of how this repo fits together.\n\n### Binary structure\n\nThe shipped CLI is a **Go executable** (`cliv2/`) that embeds the TypeScript CLI binary via `go:embed`. At runtime, registered Go workflows become Cobra commands; unrecognized commands fall back to the embedded TS binary (the `legacycli` workflow), proxying stdin/stdout/stderr and the exit code. See [Command Routing](#command-routing) for details.\n\n### What typically changes in this repo\n\n- **`cliv2/pkg/core/workflows.go`** — add/remove extension registrations\n- **`cliv2/go.mod`** — bump extension or GAF versions\n- **`cliv2/pkg/core/main.go`** — startup, Cobra wiring, special-case command handlers, error handling\n- **`cliv2/internal/`** — proxy, debug logging, constants, help routing\n- **`src/`** — legacy TypeScript commands (shrinking as commands migrate to Go workflows)\n- **`.circleci/config.yml`**, **`Makefile`** — CI/CD and build pipeline\n\n### Go Application Framework (GAF)\n\nBuilt on `go-application-framework` (GAF). Commands are **workflows** registered with the engine. Key packages: `pkg/workflow` (engine), `pkg/configuration`, `pkg/networking`, `pkg/auth`, `pkg/local_workflows` (built-ins like auth, whoami).\n\nA workflow receives an `InvocationContext` and input `Data`, and returns output `Data`:\n\n```go\nfunc myWorkflow(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) {\n    config := invocation.GetConfiguration()\n    logger := invocation.GetEnhancedLogger()\n    // ... do work ...\n    return output, nil\n}\n```\n\n`InvocationContext` exposes `GetConfiguration()`, `GetEnhancedLogger()` (zerolog), `GetNetworkAccess().GetHttpClient()` (auth/proxy-configured), and `GetAnalytics()`.\n\nRegistering a workflow is a three-step pattern — define a `WorkflowIdentifier`, write an `Init` that builds a flagset and calls `engine.Register(...)`, and wire it in via `engine.AddExtensionInitializer(...)`. See an existing extension (e.g. `cli-extension-sbom`) for the canonical shape.\n\n### Extensions\n\nFeature logic lives in separate **`cli-extension-*`** repos, registered with GAF at startup via `ExtensionInit`. The full list is in the [Extension → Command Mapping](#extension--command-mapping) table. Suggested layout:\n\n```\nextension/\n├── init.go       # ExtensionInit + Register + config defaults\n├── workflow.go   # Callback (thin shell)\n└── domain/       # Business logic, clients, types (no GAF imports)\n```\n\n**Key principle**: the workflow callback is a **thin integration shell** — read config/client/context out of `InvocationContext`, hand concrete values to domain code, package the result back into `[]Data`.\n\n**Anti-patterns**:\n\n- ❌ Domain logic inside the callback — extract into domain packages\n- ❌ Passing `InvocationContext` into domain code — pass concrete values\n- ❌ Deep workflow call chains — keep composition flat\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# Agent Instructions for snyk/cli\n\n## Mental Model: The CLI as a Binary Container\n\nThis repo builds a **host binary**, not a self-contained application. The Snyk CLI is a Go executable that **composes product features at compile time** by importing extensions from separate repositories. Almost all product/feature logic lives in those external `cli-extension-*` repos — not here.\n\nWhat **this repo** owns:\n\n- **The Go host process** (`cliv2/`) — startup, Cobra command routing, networking, proxy, analytics, error handling, teardown\n- **Extension registration** — `cliv2/pkg/core/workflows.go` is the manifest of all compiled-in extensions\n- **The legacy TypeScript CLI** (`src/`) — older commands that haven't migrated to Go workflows yet\n- **Build & release infrastructure** — Makefile, CI/CD, signing, packaging\n\nWhat **this repo does NOT contain**:\n\n- Product logic for `snyk test`, `snyk code test`, `snyk iac test`, `snyk container test`, etc. — these live in extension repos\n- The Go Application Framework (GAF) itself — that's `go-application-framework`\n\n**Implication for agents**: If you're investigating a product behavior or bug, the code is almost certainly in an extension repo, not here. See [Extension → Command Mapping](#extension--command-mapping) and [Where Does the Bug Live?](#where-does-the-bug-live) below.\n\n### Command routing\n\nAt startup, every registered workflow is turned into a Cobra command dynamically (`createCommandsForWorkflows` in `cliv2/pkg/core/main.go`). When a user runs a command:\n\n1. **Cobra matches** → the command is dispatched to the corresponding Go workflow via `engine.Invoke()`\n2. **Cobra can't match** (\"unknown command\") → the command **falls back to the legacy TypeScript CLI** via `defaultCmd()` → `basic_workflows.WORKFLOWID_LEGACY_CLI`\n\nThis fallback is why many commands still work without a Go workflow — they're silently dispatched to the embedded TS binary. A few commands have special wiring (e.g., `code test`, `auth`, `secrets test`) in `cliv2/pkg/core/main.go`.\n\n### Extension → command mapping\n\nThe canonical source is `cliv2/pkg/core/workflows.go`. Current mapping:\n\n| User command                        | Extension repo             | Init                                |\n| ----------------------------------- | -------------------------- | ----------------------------------- |\n| `snyk test`, `snyk monitor`         | `cli-extension-os-flows`   | `osflows.Init`                      |\n| `snyk code test`                    | `code-client-go`           | `code.Init`                         |\n| `snyk iac test`                     | `cli-extension-iac`        | `iac.Init`                          |\n| `snyk iac capture`                  | `snyk-iac-capture`         | `capture.Init`                      |\n| `snyk iac rules`                    | `cli-extension-iac-rules`  | `iacrules.Init`                     |\n| `snyk container test`               | `container-cli`            | `container.Init`                    |\n| `snyk sbom`                         | `cli-extension-sbom`       | `sbom.Init`                         |\n| `snyk secrets test`                 | `cli-extension-secrets`    | `secrets.Init`                      |\n| `snyk agent-scan` / `snyk mcp-scan` | `cli-extension-agent-scan` | `agentscan.Init`                    |\n| `snyk aibom test`                   | `cli-extension-ai-bom`     | `aibom.Init`                        |\n| dep-graph                           | `cli-extension-dep-graph`  | `depgraph.Init`                     |\n| MCP server                          | `studio-mcp`               | `mcp.Init`                          |\n| Language server                     | `snyk-ls`                  | `ls_extension.Init`                 |\n| `snyk ignore`                       | GAF built-in               | `ignore_workflow.Init`              |\n| Connectivity check                  | GAF built-in               | `connectivity_check_extension.Init` |\n| _Unrecognized commands_             | Legacy TS CLI (`src/`)     | Fallback via `defaultCmd()`         |\n\nThe private build (`cliv2-private/`) additionally registers `remy-cli-extension` via `WithAdditionalExtensions`.\n\n### Where does the bug live?\n\n- **Product bug** (wrong scan results, missing output fields, incorrect behavior for `snyk test|code|iac|container|sbom`) → **the extension repo** listed above. Clone it, use `go.mod replace` to test locally.\n- **CLI plumbing** (auth, proxy, analytics, networking, configuration) → **`cliv2/`** or **`go-application-framework`**.\n- **Output formatting** (JSON shape, SARIF, human-readable output) → likely the **output workflow pipeline** in GAF (`local_workflows/output_workflow`), or the extension's content type.\n- **Legacy TS command** (commands that fall back to the TypeScript CLI) → **`src/`**.\n- **Build/CI issue** → **`.circleci/config.yml`**, **`Makefile`**, or **`cliv2/Makefile`**.\n\n## Development\n\n### Setting up the Developer Environment\n\n- Applies to macOS and linux\n- Install homebrew (https://brew.sh/) (used for installing all other dependencies)\n- Do not install anything directly, use the install script\n\n```sh\n./scripts/install-dev-dependencies.sh\n```\n\n### Changing the Code\n\n- The Snyk binary connects multiple repositories to build a single binary.\n- Code changes might be required in different repositories.\n- For local development, use `go mod replace` to point to local code.\n- For CI/CD verification, use temporary commit shas to point to the desired code versions.\n- Use `make clean` to build a binary from scratch, this will remove all build artifacts and dependencies and increases the build time, so only do this if really required for example to build for another platform.\n- Only build the binary with `make build` (add `BUILD_MODE=public` without private-repo access), everything else is error prone.\n- Add tests - see [Testing Strategy](#testing-strategy)\n- Test the binary — see [Running Tests](#running-tests).\n\n### Before Committing (pre-commit)\n\nRun these before every commit — they mirror CI, which also fails if any tracked file is left uncommitted.\n\n1. **Format**: `make format` (TypeScript + Go, runs `make tidy`)\n2. **Lint**: `make lint` (TypeScript + Go)\n3. **Verify no drift**: `git diff --name-only` must be empty. Stage anything the steps above changed.\n\n## Project Structure\n\nA **hybrid TypeScript + Go** project:\n\n- **`src/`** — TypeScript CLI source (CLIv1, legacy CLI). Manifest `package.json`; resolved-dep source of truth `package-lock.json`\n- **`cliv2/`** — Go CLI wrapper (public runtime) that embeds the TypeScript binary. Module `cliv2/go.mod`; public extensions registered in `cliv2/pkg/core/workflows.go`\n- **`cliv2-private/`** — private Go runtime; entrypoint adds private extensions (e.g. `github.com/snyk/remy-cli-extension`). Module `cliv2-private/go.mod` may not resolve without private GitHub access / `GOPRIVATE`, but static parsing of `go.mod` still works\n- **`packages/`** — npm workspaces (`@snyk/fix`, `@snyk/protect`)\n- **`ts-binary-wrapper/`** — npm package that downloads and runs released CLI binaries\n- **`release-scripts/`, `scripts/`, `.circleci/`** — release and CI tooling\n- **`binary-releases/`** — build output (gitignored)\n\n### Dependency landmarks\n\nThe authoritative dependency lists live in `go.mod`/`go.sum` (Go) and `package-lock.json` (npm) — read them for exact names and versions rather than trusting any list here. These roles orient you to which dependencies usually matter:\n\n- **Core framework** — `go-application-framework` (GAF): config, networking, workflow engine\n- **Language server** — `snyk-ls`\n- **Feature logic** — the `cli-extension-*` repos (one per product area); public set registered in `cliv2/pkg/core/workflows.go`, private set in `cliv2-private/`\n- **CLIv1 (TypeScript) plugins** — the `snyk-*-plugin` / `@snyk/*` packages\n\n### Investigating an issue (impact assessment)\n\nFirst, determine **where the bug lives** — see [Where Does the Bug Live?](#where-does-the-bug-live) and the [Extension → Command Mapping](#extension--command-mapping) table. Most product bugs are in extension repos, not here.\n\nMethod, not memorized data — resolve specifics from source each time:\n\n- **Real versions**: read `go.mod` / `package-lock.json`.\n- **Blast radius** (who depends on a package): `go mod why <module>` and `go mod graph` in `cliv2/`; for npm, `npm ls <pkg>`.\n- **Pull down an extension repo**: these are separate GitHub repos — `gh repo clone snyk/<name>` (private ones need `GOPRIVATE` / auth). Use `go.mod replace` to test local changes against the CLI build.\n\n## Testing Strategy\n\nThe CLI follows a layered testing pyramid. Each layer has a different goal, system under test (SUT), and execution context.\n\n### Unit & Component Tests (Open Box)\n\n- **Goal**: Verify correct implementation of individual functions and components.\n- **SUT**: CLI/Plugin/Extension logic (not the built binary).\n- **Properties**: Uses mocks to simulate external components. No network calls.\n- **Locations**: `test/jest/unit/**/*.spec.ts` (TypeScript), `cliv2/**/*_test.go` (Go).\n- **Runs on**: every CLI branch push, and in plugin/extension CI/CD pipelines.\n- **Note**: Tests and logic should be in the same repo as the code they test.\n\n### Integration Tests (Grey Box)\n\n- **Goal**: Verify correct handling of edge cases in component interaction and integration.\n- **SUT**: The built CLI binary.\n- **Properties**: Uses a fake server (`test/acceptance/fake-server.ts`) to simulate the Snyk API. No real external calls.\n- **Locations**: `test/jest/acceptance/**/*.spec.ts` (most files — they use fake-server), `test/tap/*.test.ts` (legacy Tap tests).\n- **Runs on**: CLI branch pushes (the `acceptance-tests` CI job, across multiple OS/arch combinations).\n- **Note**: Legacy Tap tests (`test/tap/`) exist at this layer. New tests should be Jest (`*.spec.ts`) in `test/jest/acceptance/`.\n\n### User Journey Tests (Grey Box)\n\n- **Goal**: Verify that end-to-end user journeys work and API contracts are met.\n- **SUT**: The built CLI binary.\n- **Properties**: End-to-end tests against a configurable (real) Snyk instance. Includes contract tests (CLI arguments, JSON output shape) and enforcement testing.\n- **Runs on**: plugin/extension CI/CD pipelines and environment testing (on-demand/scheduled). A small number of acceptance tests that use `TEST_SNYK_TOKEN` instead of fake-server belong to this layer.\n- **Note**: These tests should cover also features from extensions to ensure that the user experience through the surface remains intact.\n\n### System Tests (Closed Box)\n\n- **Goal**: Verify deployment artifacts work correctly across target environments.\n- **SUT**: Deployment artifacts (downloaded binaries, not locally built).\n- **Properties**: Installs the CLI from the release download URL and runs basic smoke commands (`snyk whoami`, `snyk woof`) on the target platform.\n- **Locations**: The `test-release` and `test-release-static` CI jobs in `.circleci/config.yml`.\n- **Runs on**: release/deployment branches and `*e2e*` branches. Covers Docker, Alpine, macOS, Windows, Linux (multiple distros), FIPS, and scratch containers.\n\n### Unfocused / Post-Fix Tests (Closed Box)\n\n- **Goal**: Detect regressions for unspecified behavior and implicit contracts with users.\n- **SUT**: The CLI binary (preview or release candidate).\n- **Properties**: Exploratory and regression testing that is not covered by other layers.\n- **Methods**: Snyk-internal preview-release testing (\"Snyk for Snyk\"), explorative testing, regression testing against multiple open-source projects, canary deployments.\n- **Runs on**: on-demand/scheduled, typically before a GA release.\n\n### Which test type should I write?\n\n- **Changing internal logic** (a function, a parser, a formatter) → **unit test** in `test/jest/unit/` or `cliv2/**/*_test.go`.\n- **Changing CLI behavior** (command output, flag handling, API interaction) → **integration test** in `test/jest/acceptance/` using fake-server.\n- **E2E and system tests** are managed by the CI pipeline and are not typically written by feature contributors.\n\n## Running Tests\n\n```sh\n# TypeScript unit tests (some suites validate credentials, so a token is required)\nTEST_SNYK_TOKEN=<token> npm run test:unit\n\n# TypeScript acceptance/user journey tests (requires a built binary)\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm run test:acceptance\nTEST_SNYK_COMMAND=./binary-releases/snyk-macos-arm64 npm jest --runInBand test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts\n\n# Go tests\ncd cliv2 && make test\n\n# A single TS test file\nnpx jest --runInBand test/jest/unit/path/to/test.spec.ts\n```\n\n`SNYK_TOKEN` is **not** an alternative to `TEST_SNYK_TOKEN` — `test/setup.js` removes `SNYK_TOKEN` (and `SNYK_API_KEY`) from the environment when either is set, and writes `TEST_SNYK_TOKEN` into the CLI user config so tests run against a known configuration.\n\n## Running the CLI Locally\n\n```sh\nmake build\n./binary-releases/snyk-macos-arm64 --version   # adjust for your platform\n```\n\nTypeScript-only, without building the full binary:\n\n```sh\nnpm run dev -- test --all-projects\n```\n\n## Build Modes\n\nAuto-detected, but forceable:\n\n```sh\nmake build BUILD_MODE=public    # OSS-only build (external contributors)\nmake build BUILD_MODE=private   # full build, requires cliv2-private access\n```\n\n**Keep public/private differences explicit.** The public build must not require private-module access — never make `BUILD_MODE=public` depend on `cliv2-private/` or other private repos. When changing dependencies, verify the narrowest affected build/test path and update lockfiles / module files intentionally.\n\n## Commit Message Format\n\n[Conventional Commits](https://www.conventionalcommits.org/): `type: summary`, with an optional body explaining the reasoning.\n\nTypes: `feat`, `fix`, `chore`, `test`, `refactor`, `docs`, `revert`.\n**No breaking changes** — never use `BREAKING CHANGE` or `!`.\n\nKeep the first line under 72 characters. This format is enforced on every commit (and, when a branch has more than one commit, on the PR title — it becomes the squash message).\n\n## Pull Request Checks\n\nPR conventions are enforced by **Danger** (`dangerfile.js` is authoritative). To pass first time:\n\n- **Squash to a single commit** before merging — multiple commits are flagged.\n- **Commit/PR-title format** must follow [Commit Message Format](#commit-message-format) above (the only _blocking_ check).\n- **Update tests alongside `src/` changes** — touching `src/` with no `test/` change is flagged.\n- **New tests go under `test/jest/` as Jest** (`*.spec.ts`); avoid adding Tap-style tests (`*.test.ts`) elsewhere.\n- **Use ES6 `import`/`export`** in `.ts` files — not `require()` / `module.exports`.\n- **CLI `help/` text** is edited in Gitbook, not here — it syncs in automatically.\n\n## Updating Go Dependencies\n\n```sh\ngo run ./scripts/upgrade-snyk-go-dependencies.go -name=go-application-framework\nmake tidy\n```\n\n## Building with Local Dependencies\n\n**Go** — add to `cliv2/go.mod`:\n\n```go\nreplace github.com/snyk/cli-extension-foo => ../../cli-extension-foo\n```\n\n**TypeScript** — update `package.json`, then `npm install` and temporarily commit:\n\n```json\n\"snyk-foo\": \"file:../snyk-foo\",\n```\n\n## Architecture\n\nSee [Mental Model: The CLI as a Binary Container](#mental-model-the-cli-as-a-binary-container) for the high-level picture of how this repo fits together.\n\n### Binary structure\n\nThe shipped CLI is a **Go executable** (`cliv2/`) that embeds the TypeScript CLI binary via `go:embed`. At runtime, registered Go workflows become Cobra commands; unrecognized commands fall back to the embedded TS binary (the `legacycli` workflow), proxying stdin/stdout/stderr and the exit code. See [Command Routing](#command-routing) for details.\n\n### What typically changes in this repo\n\n- **`cliv2/pkg/core/workflows.go`** — add/remove extension registrations\n- **`cliv2/go.mod`** — bump extension or GAF versions\n- **`cliv2/pkg/core/main.go`** — startup, Cobra wiring, special-case command handlers, error handling\n- **`cliv2/internal/`** — proxy, debug logging, constants, help routing\n- **`src/`** — legacy TypeScript commands (shrinking as commands migrate to Go workflows)\n- **`.circleci/config.yml`**, **`Makefile`** — CI/CD and build pipeline\n\n### Go Application Framework (GAF)\n\nBuilt on `go-application-framework` (GAF). Commands are **workflows** registered with the engine. Key packages: `pkg/workflow` (engine), `pkg/configuration`, `pkg/networking`, `pkg/auth`, `pkg/local_workflows` (built-ins like auth, whoami).\n\nA workflow receives an `InvocationContext` and input `Data`, and returns output `Data`:\n\n```go\nfunc myWorkflow(invocation workflow.InvocationContext, input []workflow.Data) ([]workflow.Data, error) {\n    config := invocation.GetConfiguration()\n    logger := invocation.GetEnhancedLogger()\n    // ... do work ...\n    return output, nil\n}\n```\n\n`InvocationContext` exposes `GetConfiguration()`, `GetEnhancedLogger()` (zerolog), `GetNetworkAccess().GetHttpClient()` (auth/proxy-configured), and `GetAnalytics()`.\n\nRegistering a workflow is a three-step pattern — define a `WorkflowIdentifier`, write an `Init` that builds a flagset and calls `engine.Register(...)`, and wire it in via `engine.AddExtensionInitializer(...)`. See an existing extension (e.g. `cli-extension-sbom`) for the canonical shape.\n\n### Extensions\n\nFeature logic lives in separate **`cli-extension-*`** repos, registered with GAF at startup via `ExtensionInit`. The full list is in the [Extension → Command Mapping](#extension--command-mapping) table. Suggested layout:\n\n```\nextension/\n├── init.go       # ExtensionInit + Register + config defaults\n├── workflow.go   # Callback (thin shell)\n└── domain/       # Business logic, clients, types (no GAF imports)\n```\n\n**Key principle**: the workflow callback is a **thin integration shell** — read config/client/context out of `InvocationContext`, hand concrete values to domain code, package the result back into `[]Data`.\n\n**Anti-patterns**:\n\n- ❌ Domain logic inside the callback — extract into domain packages\n- ❌ Passing `InvocationContext` into domain code — pass concrete values\n- ❌ Deep workflow call chains — keep composition flat\n","category":"root","tokens":4546}]}