Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions skills/Jira/CliReference.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Prompts for: Atlassian instance URL, email, API token (from https://id.atlassian

| Flag | Description |
|------|-------------|
| `--fulltext` | Disable truncation of descriptions and comments (`--no-truncate` is a deprecated alias kept during migration) |
| `--fulltext` | Disable truncation of descriptions and comments without changing list output shape |
| `--id` | Emit only the primary identifier (useful for scripting). Takes precedence over `--fulltext` |
| `--no-color` | Disable colored output |
| `-v, --verbose` | Enable verbose output |
Expand Down Expand Up @@ -150,7 +150,7 @@ Common transition names: "To Do", "In Progress", "In Review", "Done" (instance-d

## Sprints

`--board` and sprint positional arguments accept either a numeric ID or a name (resolved via cache — see SKILL.md Cache Warming).
`--board` and sprint positional arguments accept either a numeric ID or a name (resolved via cache — see SKILL.md Cache Warming). Sprint names must resolve uniquely; ambiguity reports candidate IDs, while unresolved names require a cache refresh or numeric ID.

| Command | Description |
|---------|-------------|
Expand Down
2 changes: 1 addition & 1 deletion skills/Jira/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Use `jtk refresh --status` to inspect cache freshness without fetching anything.
`jtk` commands follow a text-first output model (per the repo's [Output Artifact Contract](https://github.com/open-cli-collective/atlassian-cli/blob/main/docs/ARTIFACT_CONTRACT.md)):

- **`--fields <csv>`** — explicitly selects supported fields on commands that expose field projection.
- **`--fulltext`** — disables truncation of descriptions and comments. Use when the user needs full body content (e.g., "show the full description"). `--no-truncate` is a deprecated alias kept during the migration; prefer `--fulltext`.
- **`--fulltext`** — disables truncation of descriptions and comments without changing list output shape. Use when the user needs full body content (e.g., "show the full description").
- **`--id`** — emits only the primary identifier (issue key, account ID, etc.) and takes precedence over `--fulltext` and `--fields` (projection is bypassed entirely). Stdout is identifier-only, one per line, safe to pipe; any continuation notice goes to stderr.

`automation export` is the only resource command that emits JSON (`set-credential --json` is the control-plane exception). For every other command, use `--id` for scripting composition.
Expand Down
4 changes: 2 additions & 2 deletions skills/Jira/Workflows/ManageComments.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ View and add comments on Jira issues.
jtk comments list PROJ-123
```

Optional: `--max N` to control page size (default 50). Use `--fulltext` global flag to disable truncation of long comment bodies.
Optional: `--max N` to control page size (default 50). Use `--fulltext` to disable truncation of long comment bodies without changing the table shape.

### Add Comment

Expand Down Expand Up @@ -55,7 +55,7 @@ The `COMMENT_ID` is a numeric ID (e.g., `12345`). If the user refers to the comm

## Output Format

- **List comments:** Show each comment with author, timestamp, and body
- **List comments:** A table with ID, author, timestamp, and body; `--fulltext` preserves these columns
- **Add comment:** Confirm comment was added with issue key and a snippet of the comment text

## Post-Action
Expand Down
1 change: 1 addition & 0 deletions tools/jtk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **Breaking:** Removed global `--extended` and `issues list/search --all-fields`. Use explicit `--fields`; `--fulltext` controls truncation only. ([#455](https://github.com/open-cli-collective/atlassian-cli/issues/455))
- **Breaking:** Pagination and continuation prose now goes to stderr, keeping `--id` stdout identifier-only. `--max` is a positive single-page size; issue list/search no longer auto-exhaust pages. ([#455](https://github.com/open-cli-collective/atlassian-cli/issues/455))
- **Breaking:** `comments list --fulltext` now preserves table shape and only disables truncation; deprecated `--no-truncate` aliases were removed. Sprint-name filters now fail closed on ambiguous or unresolved names. Automation export now validates and deterministically formats API JSON. ([#455](https://github.com/open-cli-collective/atlassian-cli/issues/455))
- **Breaking:** Short alias for `--file` renamed from `-f` to `-F` on `attachments add`, `automation create`, and `automation update`. `-f` continues to mean `--field` on field-setting commands (`issues create`/`update`, `transitions do`). No back-compat alias. ([#339](https://github.com/open-cli-collective/atlassian-cli/issues/339))
- Default page size for paginated commands converged to 50: `issues list` and `issues search` were 25, `users search` was 10. `users search` and `dashboards list` also gain the `-m` short alias for `--max`. ([#340](https://github.com/open-cli-collective/atlassian-cli/issues/340))
- `links types`, `issues types`, `boards list`, `sprints list`, and `users get` now serve from the local instance cache by default — removes per-command API calls in the most common paths. Run `jtk refresh` to update. ([#328](https://github.com/open-cli-collective/atlassian-cli/pull/328), [#329](https://github.com/open-cli-collective/atlassian-cli/pull/329), [#330](https://github.com/open-cli-collective/atlassian-cli/pull/330))
Expand Down
7 changes: 6 additions & 1 deletion tools/jtk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ jtk issues list --project MYPROJECT --fields summary,status,customfield_10005
| `--fields` | | | Comma-separated display columns (headers, Jira field IDs, or human names) |
| `--next-page-token` | | | Token for next page of results |

Sprint names must resolve uniquely from the cache; ambiguous names report candidate IDs, and unresolved names require a cache refresh or numeric sprint ID.

---

### `jtk issues get <issue-key> [issue-key...]`
Expand Down Expand Up @@ -757,6 +759,8 @@ jtk comments list PROJ-123 --fields ID,AUTHOR
| `--fulltext` | | `false` | Show full comment bodies without truncation (global) |
| `--fields` | | | Comma-separated display fields |

`--fulltext` preserves the table columns and row shape; it only disables body truncation.

**Arguments:**
- `<issue-key>` - The issue key (**required**)

Expand Down Expand Up @@ -1215,12 +1219,13 @@ jtk automation export 123 > rule-backup.json

| Flag | Default | Description |
|------|---------|-------------|
| `--compact` | `false` | Output minified JSON |
| `--compact` | `false` | Output whitespace-normalized minified JSON |

**Arguments:**
- `<rule-id>` - The rule ID (**required**)

> Note: Output is always JSON — this is the only resource command that emits JSON directly (the control-plane `set-credential --json` envelope is the other exception).
> Invalid JSON returned by the API fails with empty stdout.

---

Expand Down
21 changes: 10 additions & 11 deletions tools/jtk/internal/cmd/OUTPUT_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ All entity-reference flags (`--assignee`, `--project`, `--board`, `--sprint`, li
- No match + looks like a raw ID → pass through unchanged
- No match + looks like a name → fail with suggestion to `jtk refresh <resource>`

Sprint names must resolve to exactly one cached sprint ID. Ambiguous names list candidate IDs; unresolved names fail with refresh and numeric-ID guidance. `issues list --sprint` never falls back to broader name-based JQL.

```
$ jtk issues assign MON-4820 "John Smith"
Ambiguous user "John Smith" — 3 matches:
Expand Down Expand Up @@ -295,18 +297,11 @@ ID | AUTHOR | CREATED | BODY
21242 | Aaron Wong | 2026-04-16 | Short audit conclusion after the current code changes: The major source-level accessibility findings on CapOne-specific surfaces appear to be addressed or materially improv...
```

**`comments list MON-4810 --fulltext`:** one block per comment:
**`comments list MON-4810 --fulltext`:** the same table with body truncation disabled:
```
ID: 21242
Author: Aaron Wong
Created: 2026-04-16
Body:
Short audit conclusion after the current code changes:
The major source-level accessibility findings on CapOne-specific surfaces
appear to be addressed or materially improved:
- loading / redirect states now expose accessible status messaging
- the unsupported-package modal now exposes both title and description correctly
...
ID | AUTHOR | CREATED | BODY
21242 | Aaron Wong | 2026-04-16 | Short audit conclusion after the current code changes:
The major source-level accessibility findings on CapOne-specific surfaces appear to be addressed or materially improved: ...

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --fulltext example still ends the body with ..., even though the preceding text now says truncation is disabled. The renderer only appends an ellipsis when fulltext is false, so this example teaches the opposite behavior. Show the complete sample body (or clearly mark omitted illustrative text outside the command-output block).

Reply inline to this comment.

```

### `links`
Expand Down Expand Up @@ -406,7 +401,11 @@ Description: Creates Tasks when a new Onboarding Epic is created

**`automation get <id> --show-components`:** dumps the full component tree as indented text (trigger → conditions → actions).

<<<<<<< HEAD
**`automation export <id>`:** validates and emits the rule definition as pretty-printed JSON to stdout. This is the round-trip format consumed by `automation create --from-file`. `--compact` emits whitespace-normalized minified JSON. Invalid API JSON is an error and emits nothing. This command bypasses the global flag system.
=======
**`automation export <id>`:** emits the rule definition as pretty-printed JSON to stdout. This is the round-trip format consumed by `automation create --file`. `--compact` minifies. This command bypasses the global flag system.
>>>>>>> 455-jtk-pagination

### `dashboards`

Expand Down
19 changes: 8 additions & 11 deletions tools/jtk/internal/cmd/automation/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ func newExportCmd(opts *root.Options) *cobra.Command {
Short: "Export automation rule as JSON",
Long: `Export the full automation rule definition as JSON.

This outputs the exact JSON returned by the API, suitable for editing
and re-importing via 'jtk auto update'. Output is always JSON.
This validates and formats the JSON returned by the API, suitable for
editing and re-importing via 'jtk auto update'. Output is always JSON.

RECOMMENDED WORKFLOW:
jtk auto export <rule-id> > rule.json
Expand Down Expand Up @@ -51,17 +51,14 @@ func runExport(ctx context.Context, opts *root.Options, ruleID string, compact b
return err
}

var buf bytes.Buffer
if compact {
_, err = fmt.Fprintln(opts.Stdout, string(raw))
return err
err = json.Compact(&buf, raw)
} else {
err = json.Indent(&buf, raw, "", " ")
}

// Pretty-print the JSON
var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", " "); err != nil {
// If indenting fails, output raw
_, err = fmt.Fprintln(opts.Stdout, string(raw))
return err
if err != nil {
return fmt.Errorf("formatting automation rule JSON: %w", err)
}

_, err = fmt.Fprintln(opts.Stdout, buf.String())
Expand Down
78 changes: 78 additions & 0 deletions tools/jtk/internal/cmd/automation/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package automation

import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/open-cli-collective/atlassian-go/testutil"

"github.com/open-cli-collective/jira-ticket-cli/api"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/root"
)

func TestRunExportFormatsJSONDeterministically(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
compact bool
want string
}{
{"pretty input defaults to pretty", "{\n \"name\": \"rule\",\n \"enabled\": true\n}", false, "{\n \"name\": \"rule\",\n \"enabled\": true\n}\n"},
{"compact input defaults to pretty", `{"name":"rule","enabled":true}`, false, "{\n \"name\": \"rule\",\n \"enabled\": true\n}\n"},
{"compact normalizes whitespace", " { \"name\" : \"rule\", \"enabled\" : true } ", true, `{"name":"rule","enabled":true}` + "\n"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := automationExportServer(tt.raw)
defer server.Close()

opts, stdout := exportTestOptions(t, server)
testutil.RequireNoError(t, runExport(context.Background(), opts, "rule-1", tt.compact))
if stdout.String() != tt.want {
t.Fatalf("output mismatch:\n got %q\nwant %q", stdout.String(), tt.want)
}
})
}
}

func TestRunExportMalformedJSONLeavesStdoutEmpty(t *testing.T) {
t.Parallel()
for _, compact := range []bool{false, true} {
server := automationExportServer(`{"broken":`)
opts, stdout := exportTestOptions(t, server)
err := runExport(context.Background(), opts, "rule-1", compact)
server.Close()

if err == nil {
t.Fatalf("compact=%v: expected malformed JSON error", compact)
}
if stdout.Len() != 0 {
t.Fatalf("compact=%v: stdout must stay empty, got %q", compact, stdout.String())
}
}
}

func automationExportServer(raw string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/_edge/tenant_info" {
_, _ = w.Write([]byte(`{"cloudId":"test-cloud"}`))
return
}
_, _ = w.Write([]byte(raw))
}))
}

func exportTestOptions(t *testing.T, server *httptest.Server) (*root.Options, *bytes.Buffer) {
t.Helper()
client, err := api.New(api.ClientConfig{URL: server.URL, Email: "t@x.com", APIToken: "tok"})
testutil.RequireNoError(t, err)
stdout := &bytes.Buffer{}
opts := &root.Options{Stdout: stdout, Stderr: &bytes.Buffer{}}
opts.SetAPIClient(client)
return opts, stdout
}
42 changes: 10 additions & 32 deletions tools/jtk/internal/cmd/comments/comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (

"github.com/spf13/cobra"

"github.com/open-cli-collective/atlassian-go/present"

"github.com/open-cli-collective/jira-ticket-cli/api"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/root"
jtkpresent "github.com/open-cli-collective/jira-ticket-cli/internal/present"
Expand Down Expand Up @@ -42,7 +40,6 @@ func Register(parent *cobra.Command, opts *root.Options) {
func newListCmd(opts *root.Options) *cobra.Command {
var maxResults int
var nextPageToken string
var noTruncate bool
var fieldsFlag string

cmd := &cobra.Command{
Expand All @@ -61,14 +58,12 @@ func newListCmd(opts *root.Options) *cobra.Command {
return jtkpresent.ValidateMax(maxResults)
},
RunE: func(cmd *cobra.Command, args []string) error {
return runList(cmd.Context(), opts, args[0], maxResults, nextPageToken, noTruncate || opts.IsFullText(), fieldsFlag)
return runList(cmd.Context(), opts, args[0], maxResults, nextPageToken, opts.IsFullText(), fieldsFlag)
},
}

cmd.Flags().IntVarP(&maxResults, "max", "m", 50, "Page size")
cmd.Flags().StringVar(&nextPageToken, "next-page-token", "", "Decimal startAt for the next page")
cmd.Flags().BoolVar(&noTruncate, "no-truncate", false, "Show full comment bodies without truncation")
_ = cmd.Flags().MarkDeprecated("no-truncate", "use --fulltext instead")
cmd.Flags().StringVar(&fieldsFlag, "fields", "", "Comma-separated display fields (labels)")

return cmd
Expand All @@ -92,9 +87,6 @@ func runList(ctx context.Context, opts *root.Options, issueKey string, maxResult
var selected []projection.ColumnSpec
var projected bool
spec := jtkpresent.CommentListSpec
if noTruncate {
spec = jtkpresent.CommentDetailSpec
}
if !idOnly {
selected, projected, err = projection.Resolve(
ctx,
Expand Down Expand Up @@ -133,33 +125,19 @@ func runList(ctx context.Context, opts *root.Options, issueKey string, maxResult
return jtkpresent.Emit(opts, model)
}

optional := projection.HasOptionalFields(selected, spec)
var model *present.OutputModel
if noTruncate {
model = jtkpresent.CommentPresenter{}.PresentListFullWithPagination(result.Comments, optional, hasMore, nextToken)
if projected {
projectAllDetailSectionsInModel(model, selected)
}
} else {
model = jtkpresent.CommentPresenter{}.PresentListWithPagination(result.Comments, optional, hasMore, nextToken)
if projected {
projection.ApplyToTableInModel(model, selected)
}
model := jtkpresent.CommentPresenter{}.PresentListWithPagination(
result.Comments,
projection.HasOptionalFields(selected, spec),
noTruncate,
hasMore,
nextToken,
)
if projected {
projection.ApplyToTableInModel(model, selected)
}
return jtkpresent.Emit(opts, model)
}

// projectAllDetailSectionsInModel rewrites every DetailSection of model
// to the selected fields, leaving non-Detail sections (e.g. the
// pagination MessageSection) untouched.
func projectAllDetailSectionsInModel(model *present.OutputModel, selected []projection.ColumnSpec) {
for i, s := range model.Sections {
if ds, ok := s.(*present.DetailSection); ok {
model.Sections[i] = projection.ProjectDetail(ds, selected)
}
}
}

// commentsHasMore computes pagination using the authoritative API metadata,
// falling back to a full-page heuristic when Total is unavailable (Jira Cloud
// occasionally returns Total=0).
Expand Down
Loading
Loading