cmd/qctl/ # Main entry point
internal/
api/
client.gen.go # Auto-generated OpenAPI client (NEVER modify)
oapi-codegen.yaml # Code generation config
client/
client.go # HTTP client wrapper (bearerTransport, loggingTransport, curl logging)
cmdutil/
bootstrap.go # Bootstrap() — resolves config/server/creds/org into CommandContext
flags.go # ValidateConflictingFlags, RequireOneOfFlags, ParseIntArgs
prompt.go # ConfirmYesNo, ConfirmYesNoDefault, PromptString
resolve.go # ResolveTable, ResolveCloudSource
commands/
root/root_test.go
apply/ # apply.go (parent + generic dispatch), generic.go, env.go, dataset.go,
# destination.go, cloud_source.go, rules.go (smart apply), table_rules.go (patch/instantiate)
submit/ # submit.go (parent), rules.go (Python file submission)
get/ # datasets.go, dataset.go, ingestion_jobs.go, files.go, alerts.go,
# rule_families.go, rule_revisions.go, rule.go, table_rules.go, etc.
describe/ # destination.go, alert.go, file.go, ingestion_job.go, rule.go, etc.
delete/ # delete.go (parent), file.go, error_incident.go, rule.go, rules.go
enable/ # enable.go (parent), rule.go — set rule state to "enabled"
disable/ # disable.go (parent), rule.go — set rule state to "disabled"
set_default/ # set_default.go (parent), rule.go — set is_default on rule revision
run/ # run.go (parent), ingestion_job.go, profiling_job.go
kill/ # kill.go (parent), ingestion_job.go, profiling_job.go
create/ # dry_run_job.go
inspect/ # dry_run_job.go
upload/download/undelete/ # file operations
explain/ # Schema documentation from OpenAPI spec
auth/ # login.go, logout.go, switch_org.go, me.go
config/ # config.go, set_context.go, use_context.go, etc.
completion/ # completion.go, install.go
version/ # version.go
docs/ # docs.go
config/ # Config loading, server/org resolution
auth/ # Credential storage (keyring + plaintext fallback)
org/ # Organization resolver (name/UUID/prefix/fuzzy matching)
datasets/ # Client + types + apply + manifest for tables
destinations/ # Client + types + manifest
ingestion/ # Client + manifest
cloud_sources/ # Client + apply + manifest
stored_items/ # Client + manifest
warnings/ # Client
alerts/ # Client
errorincidents/ # Client + types
profiling/ # Client + manifest
dry_runs/ # Client + manifest
rule_families/ # Client + types + display (list rule families for `get rules`)
rule_versions/ # Client + resolver + describe + display + types (full domain package)
dataset_rules/ # Client + resolver + display + types (full domain package)
ws/ # WebSocket client
schema/ # OpenAPI schema cache + metadata for `explain`
output/ # Older printer (output.NewPrinterFromCmd)
pkg/
printer/ # Printer: printer.NewPrinterFromCmd, NewPrinterFromCmdWithMarkdown
tableui/ # Lipgloss-styled table printer: tableui.PrintFromCmd (used by rule commands)
manifest/ # Manifest loading: LoadFile, LoadBytes, StrictUnmarshal, APIVersionV1
timeutil/ # FormatRelative, FormatRelativePtr
tags/ # tags.Build(pairs...) — comma-separated tag strings for table columns
logs/ # Log helpers
secrets/ # Secret field injection for env vars
jsonschemautil/ # ValidateParams — validates JSON params against JSON Schema (table rule apply)
markdown/ # Markdown/ANSI rendering, ProcessFieldsPlain
apierror/ # HandleHTTPError, HandleHTTPErrorFromBytes
errors/ # Exit code mapping (FromHTTPStatus)
testutil/
config.go # NewTestEnv, SetupConfigWithOrg, SetupCredential
http.go # NewMockAPIServer, RespondJSON, RespondError, RespondText,
# MockPaginatedHandler, MockUnauthorizedHandler, MockNotFoundHandler,
# MockInternalServerErrorHandler, RespondWithCookie
fixtures.go # IntPtr, StringPtr, BoolPtr
version/ # Version info
pkg/
filters/ # Filter parsing
table/ # Table formatting
qctl
├── get tables, table, files, ingestion-jobs, profiling-jobs, alerts, warnings,
│ cloud-sources, destinations, error-incidents, rules, rule-revisions,
│ rule, table-rules, table-rule, dry-run-job, dry-run-jobs, job-activity
├── describe table, file, ingestion-job, profiling-job, alert, warning,
│ cloud-source, destination, error-incident, rule, table-rule, dry-run-job
├── apply -f <file> (generic dispatch by kind), table, destination, cloud-source
├── submit rules
├── delete file, error-incident, rule, rules
├── enable rule
├── disable rule
├── set-default rule
├── run ingestion-job, profiling-job
├── kill ingestion-job, profiling-job
├── create dry-run-job
├── inspect dry-run-job
├── upload file
├── download file
├── undelete file
├── explain RESOURCE[.FIELD] (schema docs from OpenAPI)
├── auth login, logout, switch-org, me
├── config set-context, use-context, delete-context, list-contexts, current-context
├── completion bash, zsh, fish, powershell (+install subcommand)
├── version
└── docs
--config string Config file (default $HOME/.qctl/config)
-o, --output string Output format: table|json|yaml|name (default "table")
-O, --org string Organization ID or name
-v, --verbose count Verbosity: 7=structured HTTP, 8=curl redacted, 9=curl full
--server string API server URL override
--user string User email override
--no-headers Omit table headers
--columns string Comma-separated column list (table format)
--max-column-width int Max column width, 0=unlimited (default 80)
--allow-plaintext-secrets Show secrets in json/yaml output
--allow-insecure-http Allow non-localhost http://
--request-timeout duration Request timeout
--retries int Retry count
File: internal/commands/get/<resource>.go
package get
func NewXxxCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "<cli-name>", // e.g., "tables", "ingestion-jobs"
Short: "List <resources>",
Long: `List all <resources> in the current organization.`,
RunE: func(cmd *cobra.Command, args []string) error {
// 1. Bootstrap auth context
ctx, err := cmdutil.Bootstrap(cmd)
if err != nil { return err }
// 2. Validate conflicting flags (if any)
if err = cmdutil.ValidateConflictingFlags(cmd, []string{"state", "states"}); err != nil {
return err
}
// 3. Parse params from flags
params, err := parseXxxParams(cmd)
if err != nil { return err }
// 4. Create domain client and fetch
client := xxx.NewClient(ctx.ServerURL, ctx.OrganizationID, ctx.Verbosity)
resp, err := client.GetXxx(ctx.Credential.AccessToken, params)
if err != nil { return fmt.Errorf("failed to get <resources>: %w", err) }
// 5. Print results using printer
setDefaultColumns(cmd, "col1,col2,col3")
printer, err := output.NewPrinterFromCmd(cmd) // or printer.NewPrinterFromCmd
if err != nil { return fmt.Errorf("failed to create output printer: %w", err) }
return printer.Print(resp.Results)
},
}
addXxxFlags(cmd)
return cmd
}Key details:
- Use
setDefaultColumns(cmd, "...")for table format defaults (defined iningestion_jobs.go) - Pagination hint: if
resp.Next != nil, print tocmd.ErrOrStderr() - Three printer options exist (see "Printer Packages" section below). Use whichever the surrounding code uses.
File: internal/commands/describe/<resource>.go
func NewXxxCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "<resource> <id>",
Short: "Show details of a specific <resource>",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := strconv.Atoi(args[0])
// ... or resolve by name
ctx, err := cmdutil.Bootstrap(cmd)
client := xxx.NewClient(ctx.ServerURL, ctx.OrganizationID, ctx.Verbosity)
result, err := client.GetXxx(ctx.Credential.AccessToken, id)
// Output format: describe defaults to YAML
outputFormat, _ := cmd.Flags().GetString("output")
if !cmd.Flags().Changed("output") {
outputFormat = "yaml"
}
switch outputFormat {
case "json":
encoder := json.NewEncoder(cmd.OutOrStdout())
encoder.SetIndent("", " ")
return encoder.Encode(result)
case "table":
p, err := printer.NewPrinterFromCmd(cmd)
return p.Print(manifest)
default: // yaml
encoder := yaml.NewEncoder(cmd.OutOrStdout())
encoder.SetIndent(2)
defer encoder.Close()
return encoder.Encode(manifest)
}
},
}
return cmd
}Key details:
- Describe commands default to YAML output (not table)
- Often convert API response to manifest format for round-trip
applycompatibility - Verbosity levels (-v, -vv) control detail tiers in some describe commands (alerts, rules)
- Exception:
describe ruleoutputs plain text by default (not YAML manifest) — see "Rule Commands" section
File: internal/commands/apply/<resource>.go
func NewXxxCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "<resource>",
Short: "Apply a <resource> configuration from a file",
RunE: func(cmd *cobra.Command, args []string) error {
filePath, _ := cmd.Flags().GetString("filename")
if filePath == "" { return fmt.Errorf("filename is required (-f or --filename)") }
// Load and validate manifest
manifest, err := loadXxxManifest(filePath)
if errs := manifest.Validate(); len(errs) > 0 { ... }
ctx, err := cmdutil.Bootstrap(cmd)
client := xxx.NewClient(ctx.ServerURL, ctx.OrganizationID, ctx.Verbosity)
result, err := client.Apply(ctx.Credential.AccessToken, manifest)
// Success output
fmt.Fprintf(cmd.OutOrStdout(), "<resource>/%s %s\n", manifest.Metadata.Name, result.Action)
return nil
},
}
cmd.Flags().StringP("filename", "f", "", "Path to the YAML manifest file (required)")
_ = cmd.MarkFlagRequired("filename")
return cmd
}apply -f <file> (no subcommand) reads the kind field from the manifest and dispatches automatically:
- File:
internal/commands/apply/generic.go—genericApply()routes toapplyTable,applyDestination,applyCloudSource,applyRuleYAML,applyTableRuleYAML .pyfiles are rejected with a hint to useqctl submit rules- Supports multi-document YAML (
---separators) - Flag:
--fail-fast— stop on first document failure - File:
internal/commands/apply/env.go—expandEnvVars(s)expands${VAR}patterns in manifests
File: internal/commands/apply/rules.go — applyRuleYAML()
Not a simple create/update — implements a smart diff against live state:
- Fetches live state via
GetRuleRevisionDetails - Compares field by field using JSON normalization (
fieldEqualsviajson.Marshal) - Outcomes per document:
"patched","unchanged", or"failed" - Output:
rule/<name> (<short-id>) patched|unchanged|failed
Immutable fields (changes rejected → user told to use qctl submit rules):
release, code, input_columns, validates_columns, corrects_columns, enriches_columns, param_schema, is_builtin, is_caf
Patchable fields: description (from spec), state (from spec or status), is_default (from spec or status)
File: internal/commands/apply/table_rules.go
Two modes based on manifest content:
- Patch (
metadata.idpresent): patchesinstance_name,is_enabled,treat_as_alert,params,column_mappingon existing rule - Instantiate (
spec.rule_revision_idpresent, nometadata.id): creates new dataset rule viaclient.InstantiateRule() - Both modes validate
paramsagainst the rule revision'sparam_schemausingjsonschemautil.ValidateParams()when params are provided
Manifest structure:
apiVersion: qluster.ai/v1
kind: Table|Destination|CloudSource|DryRunJob|Rule|TableRule
metadata:
name: <string>
spec:
...Loading: pkgmanifest.LoadBytes(data) → check APIVersion + Kind → pkgmanifest.StrictUnmarshal(data, &typedManifest)
// Accepts IDs as positional args OR --filter for bulk operations
// Uses --dry-list to preview, --yes to skip confirmation
cmd := &cobra.Command{
Use: "<resource> [id...]",
RunE: func(cmd *cobra.Command, args []string) error { ... },
}
cmd.Flags().String("filter", "", "Filter format: key1=val1,key2=val2")
cmd.Flags().Bool("dry-list", false, "Preview without acting")
cmd.Flags().Bool("yes", false, "Skip confirmation")Files: internal/commands/enable/rule.go, internal/commands/disable/rule.go, internal/commands/set_default/rule.go
All three follow the same pattern:
- Resolve rule by name/ID/short-ID using
rule_versions.ResolveRule() - Patch the rule revision via
client.PatchRuleRevision() - Print confirmation
func NewRuleCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "rule <name-or-id>",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx, err := cmdutil.Bootstrap(cmd)
client, err := rule_versions.NewClient(ctx.ServerURL, ctx.OrganizationID, ctx.Verbosity)
// Fetch all revisions for resolution
revisions, err := client.GetRuleRevisions(ctx.Credential.AccessToken, ...)
// Resolve to a single revision
releaseFilter, _ := cmd.Flags().GetString("release")
resolved, err := rule_versions.ResolveRule(revisions, args[0], releaseFilter)
// Patch
_, err = client.PatchRuleRevision(ctx.Credential.AccessToken, resolved.ID, body)
fmt.Fprintf(cmd.OutOrStdout(), "rule/%s (%s) enabled\n", resolved.Name, shortID)
return nil
},
}
cmd.Flags().String("release", "", "Disambiguate when rule has multiple releases")
return cmd
}Flag: --release — required to disambiguate when a rule has multiple releases.
File: internal/<domain>/client.go
type Client struct {
baseURL string
organizationID openapi_types.UUID
verbosity int
timeout time.Duration
}
func NewClient(baseURL, organizationID string, verbosity int) *Client {
orgUUID, _ := uuid.Parse(organizationID)
return &Client{
baseURL: baseURL,
organizationID: openapi_types.UUID(orgUUID),
verbosity: verbosity,
timeout: 30 * time.Second,
}
}
// Type aliases from generated client
type XxxFull = api.XxxSchemaFull
type XxxTiny = api.XxxTinySchema
func (c *Client) GetXxx(accessToken string, params ...) (*XxxPage, error) {
apiClient, err := client.New(client.Config{
BaseURL: c.baseURL,
AccessToken: accessToken,
Timeout: c.timeout,
Verbosity: c.verbosity,
})
// ... call apiClient.API.XxxWithResponse(...)
// ... check resp.StatusCode(), use apierror.HandleHTTPErrorFromBytes on error
}rule_versions.NewClient and dataset_rules.NewClient return (*Client, error) — they validate the UUID at construction time:
func NewClient(baseURL, organizationID string, verbosity int) (*Client, error) {
orgUUID, err := uuid.Parse(organizationID)
if err != nil { return nil, fmt.Errorf("invalid organization ID: %w", err) }
return &Client{...}, nil
}Key: Domain clients use client.New() to create the generated API client wrapper, then call the generated methods.
Client for listing rule families (grouped by name). Used by get rules.
Files: client.go, display.go, types.go
NewClient(baseURL, organizationID, verbosity)— returns*Client(standard pattern)GetRuleFamilies(accessToken, GetRuleFamiliesParams)— params:SearchQuery,ExcludeBuiltin,OrderBy,Page,Limit,ReverseToDisplayList(families)— converts to[]RuleFamilyDisplay, emits 1 row per primary revision + optional 2nd row for secondary (newer-than-default) revisionRuleFamilyDisplaystruct:Name,Release,State,Tags,Author,ShortID,CreatedAt,UpdatedAt
Full domain client for individual rule revisions. Used by get rule, get rule-revisions, describe rule, enable/disable/set-default rule, apply (kind: Rule), submit rules, delete rule(s).
Files: client.go, resolver.go, describe.go, display.go, types.go
Client methods:
GetRuleRevisions(accessToken, params)— list revisionsGetRuleRevisionAllReleases(accessToken, ruleRevisionID)— all releases for a familyGetRuleRevisionDetails(accessToken, ruleRevisionID)— full detail including codePatchRuleRevision(accessToken, ruleRevisionID, body)— patch state, is_default, descriptionDeleteRuleRevision(accessToken, ruleRevisionID)— deleteSubmitRuleVersion(accessToken, req)— submit Python codeUnsubmitRuleVersion(accessToken, fileText)— reverse of submitResolveRuleIDAny(accessToken, input)— resolves name/short-ID/UUID to rule revision UUID; lenient (picks first on multi-release)ResolveRuleFull(accessToken, input, release)— resolves to fullResolvedRulestruct; strict (errors on multi-release without--release)
Resolver (resolver.go):
ResolveRule(rules, input, releaseFilter)— strict: errors on multi-release ambiguityResolveRuleAny(rules, input)— lenient: silently picks first on multi-releaseShortID(uuid)— strips dashes and returns first 8 hex chars- Resolution order: full UUID (fast path) → exact name → UUID prefix/short-ID → fuzzy substring
Describe (describe.go):
FormatDescribe(detail, showCode)— produces human-readable plain text (NOT a YAML manifest)- Shows: Identity, Flags (State/Default/Built-in/CAF), Description, Columns, Param Schema, Provenance, Code (15-line preview by default)
Display (display.go):
RuleRevisionDisplaystruct withTagsandShortIDcomputed fieldsToDisplayList(rules)— builds tags: Default, Built-in, CAF, Update available, Newer than default
Types (types.go):
RuleManifest/RuleRawManifest— for describe output (includesstatus, NOT apply-compatible)RuleGetManifest/RuleGetSpec/RuleGetStatus— forget rule -o yaml(apply-compatible, kind: Rule)RuleFamilyManifest/RuleFamilyRawManifest— for multi-release output (kind: RuleFamily)RuleApplyManifest/PatchManifest— for parsingapply -f rule.yamlinputFullResponseToGetManifest(resp)— convertsRuleRevisionFulltoRuleGetManifest(apply-compatible)
Full domain client for table rules (dataset rules). Used by get table-rules, describe table-rule, apply (kind: TableRule).
Files: client.go, resolver.go, display.go, types.go
Client methods:
GetDatasetRules(accessToken, datasetID, params)— list rules for a table; params includeInstanceNamefor server-side filteringGetDatasetRuleDetail(accessToken, datasetRuleID)— single rule detail (flat endpoint, no datasetID needed)ResolveDatasetRuleID(accessToken, datasetID, input)— resolves name/short-ID/UUID to full UUID; full UUID fast-paths without API call, names use server-sideInstanceNamefilter, short IDs fall back to listing allPatchDatasetRule(accessToken, datasetID, datasetRuleID, body)— patchInstantiateRule(accessToken, ruleRevisionID, body)— create dataset rule from revision (POST, 201)
Resolver: ResolveDatasetRule(rules, input) — in-memory resolution by instance_name or UUID/short-ID prefix (used internally by ResolveDatasetRuleID)
Display (display.go):
DatasetRuleDisplaystruct: ID, ShortID, InstanceName, Release, Position, State (title-cased), Severity ("Blocker"/"Warning"), CreatedAt, UpdatedAtToDisplayList(rules)— converts list ofDatasetRuleTinyto display structsDetailToDisplay(detail)— converts singleDatasetRuleDetailto display struct
Types (types.go):
TableRuleManifest/TableRuleRawManifest— for describe outputTableRuleApplyManifest— for apply input; hasIsPatch()andIsInstantiate()methodsAPIResponseToManifest(resp, verbosity)— converts API detail toTableRuleManifest; verbosity 0=essential fields + params + column_mapping, 1=adds dataset_field_namesAPIResponseToRawManifest(resp)— converts API detail toTableRuleRawManifestfor -vv raw dump
Lists rule families (one row per rule name, not individual revisions).
- Uses
rule_families.NewClientandtableui.PrintFromCmd - Default columns:
name,release,state,tags,author,short_id - Flags:
--limit(default 1000),--page,--order-by(defaultimpact_score),--reverse,--search,--exclude-builtin
Lists all individual revisions (flat list).
- Default columns:
name,release,state,tags,short_id; with-v: addsid,description,input_columns - Flags:
--limit,--page,--order-by,--reverse,--state,--search,--only-default,--has-upgrade-available
Fetches a single rule (or all releases of a rule family).
- Flag:
--release— pins to a single revision; omitted → shows all releases - Output formats:
table(default):tableui.PrintFromCmdwith display structscode:qctl get rule foo -o code— prints raw Python source only (viawriteCode())yaml/json: single release →kind: Rulemanifest viaFullResponseToGetManifest()(apply-compatible); multi-release without--release→ error
- Helper:
encodeStructured(cmd, format, data)— shared JSON/YAML encoder (also used byget table-rule)
Fetches a single table rule (dataset rule) by instance name, short ID, or full UUID.
- Required flag:
--table(table/dataset ID) - Uses
dataset_rules.NewClient()(error-returning constructor) - Resolves via
client.ResolveDatasetRuleID()→client.GetDatasetRuleDetail() - Output:
table(default):tableui.PrintFromCmdwithdataset_rules.DetailToDisplay(); default columns:instance_name,release,state,severity,short_id;-vaddsid,created_at,updated_atjson/yaml:dataset_rules.APIResponseToManifest(detail, verbosity)— produces kind: TableRule manifest
Shows details of a table rule with YAML manifest output (default).
- Required flag:
--table(table/dataset ID) - Default output: YAML (standard describe pattern)
- Verbosity tiers:
- (default): Essential fields including params and column_mapping (round-trip apply compatible)
-v: Addsdataset_field_names-vv: Raw API response dump viaAPIResponseToRawManifest()
- Supports
-o json/-o yaml
Exception to standard describe pattern: default output is plain text (not YAML manifest).
- Uses
rule_versions.FormatDescribe()for human-readable non-round-trippable output - Flags:
--release,--show-code(show full code; default is 15-line preview) - With
-o yaml/-o json: outputs raw API response - Users should use
get rule -o yamlfor apply-compatible output
-fflag isStringArrayVar— accepts multiple files- Validates all files are
.pybefore bootstrapping auth - File size limit: 500 KB
- Flags:
--force(resubmit existing versions),--yes - Shows "Unchanged rule(s):" section for rules already at latest version
- Accepts
-f <file.py>(Python files, not YAML) - Calls
client.UnsubmitRuleVersion(token, fileText)— server finds matching rule versions - Result in three sections: Deleted, Not found, Skipped (with reason)
Three printer packages exist:
| Package | Function | Used by | Notes |
|---|---|---|---|
internal/output |
output.NewPrinterFromCmd(cmd) |
Older commands (datasets, files, etc.) | Reflection-based struct-to-table |
internal/pkg/printer |
printer.NewPrinterFromCmd(cmd) |
Newer commands | Same API as output |
internal/pkg/tableui |
tableui.PrintFromCmd(cmd, data, defaultColumns) |
Rule commands (get rules, get rule, etc.) |
Lipgloss-styled tables; delegates to output for json/yaml/name formats |
tableui uppercases headers and replaces underscores with dashes (e.g., short_id → SHORT-ID).
Use whichever the surrounding code uses. For new rule-related commands, prefer tableui.PrintFromCmd.
File: internal/commands/<verb>/<resource>_test.go
const testOrgID = "b2c3d4e5-f6a7-8901-bcde-f23456789012"
func setupTestCommand() *cobra.Command {
rootCmd := &cobra.Command{Use: "qctl"}
// Add global flags matching root command
rootCmd.PersistentFlags().String("server", "", "API server URL")
rootCmd.PersistentFlags().String("user", "", "User email")
rootCmd.PersistentFlags().StringP("output", "o", "table", "output format")
rootCmd.PersistentFlags().Bool("no-headers", false, "")
rootCmd.PersistentFlags().String("columns", "", "")
rootCmd.PersistentFlags().Int("max-column-width", 80, "")
rootCmd.PersistentFlags().Bool("allow-plaintext-secrets", false, "")
rootCmd.PersistentFlags().Bool("allow-insecure-http", false, "")
rootCmd.PersistentFlags().CountP("verbose", "v", "") // NOTE: CountP, not BoolP!
// Add the command under test
parentCmd := &cobra.Command{Use: "<verb>"}
parentCmd.AddCommand(NewXxxCommand())
rootCmd.AddCommand(parentCmd)
return rootCmd
}
func TestXxxCommand(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
wantOutputContains []string
}{ ... }
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 1. Setup test env (temp HOME, plaintext tokens)
env := testutil.NewTestEnv(t)
defer env.Cleanup()
// 2. Create mock API server
mock := testutil.NewMockAPIServer()
defer mock.Close()
// 3. Setup config and credentials
endpointKey, _ := config.NormalizeEndpointKey(mock.Server.URL)
env.SetupConfigWithOrg(mock.Server.URL, "test@example.com", testOrgID)
env.SetupCredential(endpointKey, testOrgID, "test-token")
// 4. Register mock handlers
mock.RegisterHandler("GET", "/api/orgs/"+testOrgID+"/...", func(w http.ResponseWriter, r *http.Request) {
testutil.RespondJSON(w, http.StatusOK, responseData)
})
// 5. Create command, capture output, execute
cmd := setupTestCommand()
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
cmd.SetArgs(append([]string{"<verb>", "<resource>"}, tt.args...))
err := cmd.Execute()
// 6. Assert
if (err != nil) != tt.wantErr { t.Errorf(...) }
for _, want := range tt.wantOutputContains {
if !strings.Contains(buf.String(), want) { t.Errorf(...) }
}
})
}
}CRITICAL: The verbose flag MUST use CountP("verbose", "v", "") not BoolP. Bootstrap reads it with GetCount("verbose").
| Function | Location | Purpose |
|---|---|---|
cmdutil.Bootstrap(cmd) |
internal/cmdutil/bootstrap.go |
Resolve config → server → creds → org → verbosity into CommandContext |
cmdutil.BootstrapWithoutAuth(cmd) |
same | For commands that don't need creds (e.g., login) |
cmdutil.ValidateConflictingFlags(cmd, sets...) |
internal/cmdutil/flags.go |
Reject mutually exclusive flags |
cmdutil.RequireOneOfFlags(cmd, flags) |
same | Require at least one from set |
cmdutil.ParseIntArgs(args, name) |
same | Parse positional int args |
cmdutil.ConfirmYesNo(prompt) |
internal/cmdutil/prompt.go |
y/N confirmation |
cmdutil.ResolveTable(ctx, tableID, tableName) |
internal/cmdutil/resolve.go |
Resolve table by ID or name |
cmdutil.ResolveCloudSource(ctx, ...) |
same | Resolve cloud source by ID, name, or auto-detect |
printer.NewPrinterFromCmd(cmd) |
internal/pkg/printer/helpers.go |
Create printer from cmd flags |
printer.NewPrinterFromCmdWithMarkdown(cmd, fields) |
same | Printer with markdown rendering |
tableui.PrintFromCmd(cmd, data, defaultColumns) |
internal/pkg/tableui/table.go |
Lipgloss-styled table printer (rule commands) |
output.NewPrinterFromCmd(cmd) |
internal/output/helpers.go |
Older printer (same API) |
setDefaultColumns(cmd, cols) |
internal/commands/get/ingestion_jobs.go |
Set default columns for table format |
tags.Build(pairs...) |
internal/pkg/tags/tags.go |
Build comma-separated tag string from label/bool pairs |
timeutil.FormatRelative(t) |
internal/pkg/timeutil/relative.go |
"5 minutes ago" formatting |
timeutil.FormatRelativePtr(t) |
same | Nil-safe version |
manifest.LoadFile(path) |
internal/pkg/manifest/manifest.go |
Load + validate generic manifest |
manifest.StrictUnmarshal(data, v) |
same | YAML parse with unknown-field rejection |
manifest.APIVersionV1 |
same | Constant: "qluster.ai/v1" |
apierror.HandleHTTPError(resp, msg) |
internal/apierror/http.go |
Convert HTTP errors to user-friendly errors |
apierror.HandleHTTPErrorFromBytes(code, body, msg) |
same | Same, from pre-read body |
jsonschemautil.ValidateParams(params, schema) |
internal/pkg/jsonschemautil/validate.go |
Validate JSON params against JSON Schema (table rule apply) |
encodeStructured(cmd, format, data) |
internal/commands/get/rule.go |
Shared JSON/YAML encoder for structured output |
testutil.NewTestEnv(t) |
internal/testutil/config.go |
Create temp HOME with isolated config |
testutil.NewMockAPIServer() |
internal/testutil/http.go |
Mock HTTP transport (no real sockets) |
testutil.RespondJSON(w, status, data) |
same | Write JSON mock response |
testutil.RespondError(w, status, msg) |
same | Write error mock response |
testutil.RespondText(w, status, text) |
same | Write plain text mock response |
testutil.MockPaginatedHandler(cb) |
same | Paginated response helper |
testutil.MockUnauthorizedHandler() |
same | Returns 401 handler |
testutil.MockNotFoundHandler(msg) |
same | Returns 404 handler |
testutil.MockInternalServerErrorHandler() |
same | Returns 500 handler |
testutil.RespondWithCookie(w, status, data, name, val) |
same | JSON response with cookie |
cmdutil.Bootstrap(cmd) returns *CommandContext:
type CommandContext struct {
Config *config.Config
Credential *auth.Credential
ServerURL string
Verbosity int
OrganizationID string
OrganizationName string // human-readable org name (used in confirmations)
}All API endpoints follow: /api/orgs/{orgID}/<resource>
Examples:
GET /api/orgs/{orgID}/datasets— list tablesGET /api/orgs/{orgID}/datasets/{id}— get tableGET /api/orgs/{orgID}/stored_items— list filesGET /api/orgs/{orgID}/data_sources— list cloud sourcesGET /api/orgs/{orgID}/destinations— list destinationsGET /api/orgs/{orgID}/ingestion_jobs— list ingestion jobsGET /api/orgs/{orgID}/alert_items— list alertsGET /api/orgs/{orgID}/warnings— list warningsGET /api/orgs/{orgID}/error_incidents— list error incidentsGET /api/orgs/{orgID}/rule_families— list rule familiesGET /api/orgs/{orgID}/rule_revisions— list rule revisionsGET /api/orgs/{orgID}/rule_revisions/{id}— get rule revision detailPATCH /api/orgs/{orgID}/rule_revisions/{id}— patch rule revisionDELETE /api/orgs/{orgID}/rule_revisions/{id}— delete rule revisionPOST /api/orgs/{orgID}/rule_revisions/submit— submit rulePOST /api/orgs/{orgID}/rule_revisions/unsubmit— unsubmit ruleGET /api/orgs/{orgID}/datasets/{id}/dataset-rules— list table rules (nested under dataset)GET /api/orgs/{orgID}/dataset-rules/{ruleID}— get table rule detail (flat path, no dataset ID)PATCH /api/orgs/{orgID}/datasets/{id}/dataset-rules/{ruleID}— patch table rule (nested)POST /api/orgs/{orgID}/rule_revisions/{id}/instantiate— instantiate table rule
getcommands: default output istableformatget rule -o code: outputs raw Python source code (special format for rules only)describecommands: default output isyamlformat (override with-o)describe rule: exception — default output is plain text (not YAML); useget rule -o yamlfor apply-compatible outputapplycommands: print<resource>/<name> <action>on success (e.g.,table/orders created)applyrules: printrule/<name> (<short-id>) patched|unchanged|faileddelete/run/killcommands: print confirmation message to stdoutenable/disable/set-default: printrule/<name> (<short-id>) enabled|disabled|set as default
| Backend | CLI | Go package |
|---|---|---|
| dataset | table | datasets |
| stored item | file | stored_items |
| data source | cloud source | cloud_sources |
| alert_item | alert | alerts |
| data_source_model | cloud source | (within cloud_sources) |
| rule_family | rule (in list context) | rule_families |
| rule_revision | rule (in detail context) | rule_versions |
| dataset_rule | table-rule | dataset_rules |