{"id":50486773,"url":"https://github.com/vimkim/cubrid-jira","last_synced_at":"2026-06-01T23:02:18.557Z","repository":{"id":344826747,"uuid":"1183249179","full_name":"vimkim/cubrid-jira","owner":"vimkim","description":"CUBRID JIRA CLI: cache-first reads and dry-run-default writes (create / comment / link / transition / assign).","archived":false,"fork":false,"pushed_at":"2026-05-22T10:40:46.000Z","size":4888,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T14:35:20.995Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vimkim.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-16T12:24:39.000Z","updated_at":"2026-05-22T10:40:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/vimkim/cubrid-jira","commit_stats":null,"previous_names":["vimkim/cubrid-jira-fetcher"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/vimkim/cubrid-jira","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vimkim%2Fcubrid-jira","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vimkim%2Fcubrid-jira/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vimkim%2Fcubrid-jira/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vimkim%2Fcubrid-jira/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vimkim","download_url":"https://codeload.github.com/vimkim/cubrid-jira/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vimkim%2Fcubrid-jira/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33797128,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-01T23:02:17.385Z","updated_at":"2026-06-01T23:02:18.549Z","avatar_url":"https://github.com/vimkim.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cubrid-jira\n\nA CUBRID JIRA CLI for `http://jira.cubrid.org` with three workflow buckets:\n\n* **cache-first reads** (`search`) — markdown to stdout, no network on a cache hit.\n* **field writes** (`create`, `comment`, `comment-list`, `comment-update`, `comment-delete`, `link`, `transition`, `assign`, `update`) — dry-run by default; `--yes` to send.\n* **structural writes** (`convert-to-issue`, `convert-to-subtask`, `reparent`) — drive the JIRA Convert wizard for the operations REST silently no-ops on; same dry-run contract.\n\nDesigned to be driven by AI agents, slash commands, and shell pipelines.\n\n---\n\n## For AI agents — 30-second contract\n\nIf you are an autonomous agent running in a shell, this is everything you need:\n\n```text\nCanonical command   : cubrid-jira \u003csubcommand\u003e [args…]\nSubcommands         : read              search\n                      field-write       create | comment | comment-list | comment-update | comment-delete |\n                                        link | transition | assign | update\n                      structural-write  convert-to-issue | convert-to-subtask | reparent\nCredentials         : env  CUBRID_JIRA_USER  +  CUBRID_JIRA_PASSWORD\n                      (no interactive prompt; falls back to ~/.netrc)\nOutput contract     : markdown / JSON   → stdout\n                      status / progress → stderr\n                      (safe to pipe stdout)\nMachine-readable    : add `--output json` to any write subcommand;\n                      stdout becomes exactly one JSON object.\nDry-run is default  : ALL writes are dry-run unless you pass `--yes`.\n                      This includes the structural writes.\nCAPTCHA lockout     : on HTTP 401 the tool exits 2 immediately and does\n                      NOT retry. Jira Server locks the account and\n                      forces a web-UI CAPTCHA after repeated failures.\nExit codes          : 0 ok | 1 generic | 2 401 | 3 403 | 4 404 | 5 400\n                      (see \"Error contract\" below for what they mean)\n```\n\n`cubrid-jira search CBRD-XXXXX` is the agent-friendly read; use it freely.\nAny write subcommand without `--yes` is **safe to invoke** — it only prints the planned request.\n\n---\n\n![Demo](./demo.gif)\n\n\u003e The demo gif shows the **read-only** flow (`cubrid-jira search`). The write subcommands shipped after the demo was recorded.\n\n---\n\n## Install\n\n`uv tool install` is the right tool: isolated env, binary on `$PATH`, easy uninstall. `pipx` is an equivalent fallback.\n\n```sh\n# Recommended:\nuv tool install git+https://github.com/vimkim/cubrid-jira.git\n\n# pipx (equivalent, slower):\npipx install git+https://github.com/vimkim/cubrid-jira.git\n\n# From a local clone:\ncd cubrid-jira \u0026\u0026 uv tool install . || pipx install .\n```\n\nInstalls three binaries on `$PATH`:\n\n| Binary | Status |\n|---|---|\n| `cubrid-jira` | **canonical** — use this |\n| `cubrid-jira-search` | deprecated alias for `cubrid-jira search` |\n| `cubrid-jira-fetch` | deprecated bulk-fetch tool — the original CLI before the v1.0 rename |\n\n\u003e Do **not** use `pip install -e .` to install — that mode is only for editing this repo's source.\n\n## Prerequisites\n\n- **Python 3.14+**\n- **[pandoc](https://pandoc.org/)** — converts Jira wiki markup to markdown\n\n```sh\nbrew install pandoc          # macOS / Linuxbrew\nsudo apt install pandoc      # Debian / Ubuntu\nsudo dnf install pandoc      # Fedora / RHEL\n```\n\nOptional: [`uv`](https://github.com/astral-sh/uv), [`just`](https://github.com/casey/just).\n\n---\n\n## Read flow — `cubrid-jira search`\n\nPrints one issue's markdown to **stdout**; progress goes to stderr, so piping stays clean.\n\n```sh\ncubrid-jira search CBRD-26463\ncubrid-jira search http://jira.cubrid.org/browse/CBRD-26463\ncubrid-jira search CBRD-26463 --force         # bypass cache\ncubrid-jira search CBRD-26463 --no-recurse    # don't walk related on miss\ncubrid-jira search CBRD-26463 --dir /tmp/jira # override cache directory\n```\n\nHow it works:\n\n1. Look for `CBRD-26463*.md` in the cache directory.\n2. **Cache hit** → print it. No network.\n3. **Cache miss** → fetch the issue (+ 1 level of related issues) into the cache, then print.\n4. Exit non-zero on fetch failure.\n\n### Cache directory\n\nResolved in order (first match wins):\n\n1. `--dir DIR`\n2. `$CUBRID_JIRA_DIR`\n3. `~/.local/share/cubrid-jira/issues/` (default)\n\nRecommended one-time setup:\n\n```sh\necho 'export CUBRID_JIRA_DIR=\"$HOME/.local/share/cubrid-jira/issues\"' \u003e\u003e ~/.bashrc\n```\n\n---\n\n## Field-write flow — `create / comment / comment-list / comment-update / comment-delete / link / transition / assign / update`\n\nThese edit fields on an existing or new issue. Same dry-run-by-default contract as the structural writes ([next section](#structural-write-flow--convert--reparent)); pass `--yes` to actually send.\n\n```sh\n# Read first — review the planned request:\ncubrid-jira create --project CBRD --type Bug --summary \"...\"\n# Then commit with --yes:\ncubrid-jira create --project CBRD --type Bug --summary \"...\" --yes\n```\n\n### Subcommand reference\n\n```sh\ncubrid-jira create     --project CBRD --type Bug --summary \"...\" \\\n                       [--description-file path] [--priority Major] [--assignee user] \\\n                       [--label l1 --label l2] [--component sql] \\\n                       [--link-relates CBRD-Y] [--link-blocks CBRD-Z] \\\n                       [--field \"QA Scenario={\\\"value\\\":\\\"Not Required\\\"}\"] [--field customfield_NNN=...]\ncubrid-jira comment    CBRD-XXXXX --body-file note.md\ncubrid-jira comment-list   CBRD-XXXXX [--limit N]                  # list comments (read-only)\ncubrid-jira comment-update CBRD-XXXXX --id \u003cCOMMENT-ID\u003e --body-file note.md\ncubrid-jira comment-delete CBRD-XXXXX --id \u003cCOMMENT-ID\u003e            # irreversible — prints a one-line warning\ncubrid-jira link       CBRD-A --type Relates --to CBRD-B   # also Blocks | Cloners | Duplicate\ncubrid-jira transition CBRD-A [--to \"In Progress\"]         # omit --to to list available\ncubrid-jira assign     CBRD-A --to \u003cusername\u003e              # --to \"\" to unassign\ncubrid-jira update     CBRD-A [--summary \"...\"] [--description-file path] \\\n                       [--priority Major] [--label l1 --label l2] [--component sql] \\\n                       [--field \"QA Scenario={\\\"value\\\":\\\"Complete\\\"}\"] [--field customfield_NNN=...]\n```\n\n`update` edits an existing issue's fields. At least one of `--summary`, `--description-file`, `--priority`, `--label`, `--component`, or `--field` is required. **`--label` and `--component` replace the full list** — they are not additive (Jira REST `fields` semantics). `--description-file -` reads from stdin.\n\n### `--field FIELD=VALUE` — arbitrary custom fields\n\nRepeat `--field` to set any JIRA custom field (the canonical use case is project-required fields like CUBRID's `QA Scenario`, which gates every `create` against `CBRD`).\n\n- `FIELD` may be a raw id (`customfield_210565`) or a display name (`\"QA Scenario\"`). Names are resolved against `/rest/api/2/field` on first use and cached at `\u003ccache_dir\u003e/field-map.json`; subsequent calls skip the lookup.\n- `VALUE` starting with `{` or `[` is JSON-decoded — needed for **single-select**, **cascading-select**, **multi-select**, **user**, and **date** fields. Anything else is sent as a raw string (text/textarea fields).\n- Ambiguous display names (two custom fields with the same `name`) error out and ask you to disambiguate by id — silently picking one would risk writing to the wrong field.\n\n```sh\n# Text field — bare string is fine.\ncubrid-jira update CBRD-XXXXX --field \"Some Free Text=hello\" --yes\n\n# Single-select field — wrap value in JSON.\ncubrid-jira create --project CBRD --type Task --summary \"...\" \\\n  --field 'QA Scenario={\"value\":\"Not Required\"}' --yes\n\n# Discover valid option labels for a required select field:\ncurl -u \"$CUBRID_JIRA_USER:$CUBRID_JIRA_PASSWORD\" \\\n  'http://jira.cubrid.org/rest/api/2/issue/createmeta?projectKeys=CBRD\u0026issuetypeNames=Task\u0026expand=projects.issuetypes.fields' \\\n  | jq '.projects[0].issuetypes[0].fields[\"customfield_210565\"].allowedValues'\n```\n\nGlobal flags on every write subcommand:\n\n| Flag | Default | Description |\n|---|---|---|\n| `--dry-run` | (always on unless `--yes`) | Print the resolved URL, masked headers, and JSON body. Don't send. |\n| `--yes` | off | Required to actually perform the live write. |\n| `--server URL` | `http://jira.cubrid.org` | JIRA base URL. |\n| `-d`, `--dir DIR` | shared cache | Cache directory for post-write cache updates. |\n| `--output {text,json}` | `text` | Machine-readable output mode; see below. |\n\n### Cache interaction on writes\n\n- `create` (live): the new issue is fetched and saved into the cache, so `cubrid-jira search NEW-KEY` is an immediate hit.\n- `comment`, `comment-update`, `comment-delete`, `link`, `transition`, `assign`, `update` (live): cached markdown for the affected issue key(s) is **deleted**, so the next read re-fetches. `link` invalidates both sides. `comment-list` is read-only and never invalidates.\n\n---\n\n## Structural-write flow — `convert-to-issue / convert-to-subtask / reparent`\n\nThree subcommands change the **structural type** of an issue rather than editing its fields:\n\n```sh\ncubrid-jira convert-to-issue   CBRD-XXXXX [--type Task]                 # Sub-task → Task (drops parent)\ncubrid-jira convert-to-subtask CBRD-XXXXX --to CBRD-YYYYY               # Task → Sub-task of YYYYY\ncubrid-jira reparent           CBRD-XXXXX --to CBRD-YYYYY               # move a Sub-task under YYYYY\n```\n\n`reparent` composes the other two for the common case of changing a sub-task's parent.\n\n### Why these aren't just a REST `PUT`\n\nOn `jira.cubrid.org` (JIRA Server 7.7.1), `PUT /rest/api/2/issue/{KEY}` with `{\"fields\":{\"parent\":{\"key\":\"X\"}}}` **returns HTTP 204 but does not mutate the parent field** — the CBRD project's Field Configuration Scheme doesn't put `parent` on the Sub-task Edit screen, so the API silently strips it. These three subcommands drive the same **Convert wizard** the web UI uses (`/secure/ConvertSubTask.jspa` and `/secure/ConvertIssue.jspa`) via session cookies and form POSTs, including the `X-Atlassian-Token: no-check` header required to bypass JIRA's XSRF gate on non-browser clients.\n\nFull technical rationale, the trap list (8 of them), and a curl-only smoke test recipe live in [`docs/reparent-subtasks-via-convert-wizard.md`](./docs/reparent-subtasks-via-convert-wizard.md).\n\n### What they do\n\n| Subcommand | Pre-flight | Cache invalidates | Notes |\n|---|---|---|---|\n| `convert-to-issue` | refuses if not Sub-task | issue + previous parent | Drops the parent; default `--type Task`. |\n| `convert-to-subtask` | refuses if already Sub-task, or if `--to` is itself a Sub-task | issue + new parent | Default `--type Sub-task`. |\n| `reparent` | refuses if not Sub-task, if `--to` equals current parent, or if `--to` is a Sub-task | issue + old parent + new parent | Two-phase: Sub-task → Task, then Task → Sub-task. |\n\n### Atomicity on `reparent`\n\n`reparent` runs the forward wizard (Sub-task → Task), verifies the intermediate state, **then** runs the reverse wizard (Task → Sub-task under `--to`). If the reverse half fails after the forward half succeeded, the issue is left as a Task with no parent — a worse state than it started in. The command does **not** swallow this:\n\n* Prints a loud `!!! ATOMICITY WARNING` to stderr with the exact recovery command (`cubrid-jira convert-to-subtask KEY --to NEW --yes`).\n* Exits non-zero (1).\n* Does **not** invalidate the cache, so the next `search` re-fetches and surfaces the actual state.\n\n### Issuetype IDs are resolved at runtime\n\nNumeric issuetype IDs (Sub-task, Task, Bug, …) vary per JIRA install. None of the three subcommands hard-codes them; each parses the `\u003cselect name=\"issuetype\"\u003e` block from the wizard page on every live run and matches by display name. Pointing `--server` at a different JIRA Server picks up that server's IDs automatically.\n\n### Dry-run safety\n\nLike every other write subcommand, all three default to **dry-run**. Without `--yes` they:\n\n1. Fetch the issue's current metadata over the same unauthenticated REST endpoint `search` uses.\n2. Run the pre-flight refusals.\n3. Print the planned wizard POSTs with `atl_token=\u003cextracted-at-runtime\u003e`, `guid=\u003cextracted-at-runtime\u003e`, and `issuetype=\u003cresolved-at-runtime\u003e` placeholders.\n4. Never log in, never contact the wizard endpoints, never touch credentials.\n\nSo `cubrid-jira reparent CBRD-1 --to CBRD-2` is safe to run as a preview any time, even without creds set.\n\n---\n\n## Error contract\n\nApplies to every subcommand — read, field-write, and structural-write alike. 401 in particular can fire on a wizard step, not just on the field-write POST.\n\n| Exit | Cause |\n|---|---|\n| 0 | Success (or dry-run completed). |\n| 1 | Generic error: parse failure, network exhaustion, unknown link type, atomicity rollback warning on `reparent`, … |\n| 2 | **HTTP 401 — auth failed.** Hard exit, no retry. CAPTCHA-lockout warning printed; the wizard's session login surfaces 401 the same way. |\n| 3 | HTTP 403 — authenticated but missing permission. |\n| 4 | HTTP 404 — issue key not found. |\n| 5 | HTTP 400 — validation; server's `errors` / `errorMessages` payload printed verbatim. |\n\n5xx and transient network errors get one short retry with backoff, then exit 1. Wizard XSRF rejections (atl_token went stale mid-flow) exit 1 with a message naming the failing step.\n\n---\n\n## Output formats\n\nEvery **write** subcommand supports `--output {text,json}`.\n\n### `text` (default)\n\nHuman-readable status to stderr; the JSON request body (in dry-run) goes to stdout. Suitable for piping to a TTY or to a log.\n\n### `json`\n\nExactly **one** JSON object on stdout, nothing else. Status/errors still go to stderr. Suitable for `jq`, agent pipelines, and CI gates.\n\n| Subcommand | Live success shape | Dry-run shape |\n|---|---|---|\n| `create` | `{\"key\": \"CBRD-9999\", \"url\": \"...\"}` | `{\"dry_run\": true, \"requests\": [POST issue, POST issueLink, …]}` |\n| `comment` | `{\"issue\": \"CBRD-1\", \"comment_id\": \"42\"}` | `{\"dry_run\": true, \"requests\": [POST .../comment]}` |\n| `comment-list` | `{\"issue\": \"CBRD-1\", \"total\": N, \"comments\": [...]}` | (same — listing is a GET) |\n| `comment-update` | `{\"issue\": \"CBRD-1\", \"comment_id\": \"42\", \"updated\": true}` | `{\"dry_run\": true, \"requests\": [PUT .../comment/42]}` |\n| `comment-delete` | `{\"issue\": \"CBRD-1\", \"comment_id\": \"42\", \"deleted\": true}` | `{\"dry_run\": true, \"requests\": [DELETE .../comment/42]}` |\n| `link` | `{\"inward\": \"CBRD-1\", \"outward\": \"CBRD-2\", \"type\": \"Relates\"}` | `{\"dry_run\": true, \"requests\": [POST issueLink]}` |\n| `transition` (with `--to`) | `{\"issue\": \"CBRD-1\", \"transition_id\": \"21\", \"to\": \"In Progress\"}` | `{\"dry_run\": true, \"requests\": [POST transitions]}` |\n| `transition` (list mode) | `{\"issue\": \"CBRD-1\", \"transitions\": [...]}` | (same — listing is a GET) |\n| `assign` (set) | `{\"issue\": \"CBRD-1\", \"assignee\": \"vimkim\"}` | `{\"dry_run\": true, \"requests\": [PUT assignee]}` |\n| `assign` (clear) | `{\"issue\": \"CBRD-1\", \"assignee\": null}` | (same) |\n| `convert-to-issue` | `{\"issue\": \"CBRD-1\", \"type\": \"Task\", \"previous_parent\": \"CBRD-X\"}` | `{\"dry_run\": true, \"requests\": [POST step1, step3, step4]}` |\n| `convert-to-subtask` | `{\"issue\": \"CBRD-1\", \"parent\": \"CBRD-Y\", \"type\": \"Sub-task\"}` | `{\"dry_run\": true, \"requests\": [POST step1, step3, step4]}` |\n| `reparent` | `{\"issue\": \"CBRD-1\", \"from_parent\": \"CBRD-X\", \"to_parent\": \"CBRD-Y\"}` | `{\"dry_run\": true, \"requests\": [6 POSTs: forward + reverse]}` |\n\nThe dry-run `requests` field captures **every** mutation the live run would send (so `create --link-relates X --link-blocks Y` returns the 3-request plan), with `method`, `url`, and `body` per request.\n\n---\n\n## Credentials\n\n### ⚠️ Cleartext + CAPTCHA-lockout warnings\n\n- **Cleartext.** `jira.cubrid.org` is HTTP-only; basic-auth headers travel unencrypted. Use a trusted network and a JIRA-only password.\n- **CAPTCHA.** Jira Server 7.7.1 locks an account and forces a web-UI CAPTCHA after a small number of failed basic-auth attempts. The CLI **never retries on 401** to avoid triggering this. If you see `Error: Auth failed (HTTP 401)`, log into `http://jira.cubrid.org` in a browser, solve the CAPTCHA, fix your credentials, and try again.\n\n### Resolution order\n\n1. `CUBRID_JIRA_USER` + `CUBRID_JIRA_PASSWORD` env vars (preferred for agents).\n2. `~/.netrc` entry for `jira.cubrid.org`.\n3. Hard error with an instructive message — no interactive prompt.\n\nExample `~/.netrc`:\n\n```\nmachine jira.cubrid.org\n  login your-jira-username\n  password your-jira-password\n```\n\n```sh\nchmod 600 ~/.netrc\n```\n\n---\n\n## Worked examples\n\n### Create a bug related to `CBRD-26517`\n\n```sh\n# 1) Dry-run JSON plan — review what would be sent.\ncubrid-jira create \\\n    --project CBRD \\\n    --type Bug \\\n    --summary \"OOS: heap_record_replace crashes when …\" \\\n    --priority Major \\\n    --description-file ./bug-notes.md \\\n    --link-relates CBRD-26517 \\\n    --output json\n# → {\"dry_run\": true, \"requests\": [POST /rest/api/2/issue, POST /rest/api/2/issueLink]}\n\n# 2) Same command with --yes — actually creates the issue + link.\nKEY=$(cubrid-jira create \\\n    --project CBRD \\\n    --type Bug \\\n    --summary \"OOS: heap_record_replace crashes when …\" \\\n    --priority Major \\\n    --description-file ./bug-notes.md \\\n    --link-relates CBRD-26517 \\\n    --yes --output json | jq -r .key)\necho \"Created $KEY\"\ncubrid-jira search \"$KEY\"   # immediate cache hit, no extra fetch\n```\n\n### Revise the description of an existing issue\n\n```sh\n# 1) Dry-run — review the PUT body.\ncubrid-jira update CBRD-26517 --description-file ./new-notes.md\n# → DRY RUN PUT /rest/api/2/issue/CBRD-26517  {\"fields\": {\"description\": \"...\"}}\n\n# 2) Commit. Cached markdown for CBRD-26517 is deleted, so the next\n#    `cubrid-jira search` re-fetches the live issue.\ncubrid-jira update CBRD-26517 \\\n    --summary \"OOS: heap_record_replace — updated repro\" \\\n    --description-file ./new-notes.md \\\n    --yes --output json\n# → {\"issue\": \"CBRD-26517\", \"updated_fields\": [\"description\", \"summary\"]}\n```\n\n### Edit your own comment\n\n```sh\n# 1) Capture the comment ID via comment-list --output json.\nID=$(cubrid-jira comment-list CBRD-26517 --output json \\\n       | jq -r '.comments[] | select(.author==\"vimkim\") | .id' | tail -1)\necho \"Editing comment $ID\"\n\n# 2) Dry-run the edit — review the PUT body.\necho \"updated text\" \u003e /tmp/edit.md\ncubrid-jira comment-update CBRD-26517 --id \"$ID\" --body-file /tmp/edit.md\n# → DRY RUN PUT /rest/api/2/issue/CBRD-26517/comment/$ID  {\"body\": \"updated text\\n\"}\n\n# 3) Commit. Cache for CBRD-26517 is invalidated, so the next search re-fetches.\ncubrid-jira comment-update CBRD-26517 --id \"$ID\" --body-file /tmp/edit.md \\\n    --yes --output json\n# → {\"issue\": \"CBRD-26517\", \"comment_id\": \"$ID\", \"updated\": true}\n```\n\n`comment-delete` follows the same shape; it prints a one-line `# About to DELETE comment ...` warning to stderr in live mode before sending.\n\n### Move a sub-task to a new parent\n\n```sh\n# 1) Preview the 6-POST wizard plan (no credentials needed).\ncubrid-jira reparent CBRD-26660 --to CBRD-26835 --output json\n# → {\"dry_run\": true, \"requests\": [...3 forward POSTs..., ...3 reverse POSTs...]}\n\n# 2) Commit. Same call, plus --yes.\ncubrid-jira reparent CBRD-26660 --to CBRD-26835 --yes\n# Reparented CBRD-26660: CBRD-26583 -\u003e CBRD-26835; cache invalidated for all three keys.\n```\n\nIf the reverse half fails after the forward half succeeded, `reparent` prints the recovery command and exits 1 — it never silently leaves the issue stranded. See **Atomicity on `reparent`** above.\n\n---\n\n## Caching behavior\n\nThe cache directory is shared by `cubrid-jira search` and the legacy `cubrid-jira-fetch` bulk tool. Both honour the same precedence (`--dir`, `$CUBRID_JIRA_DIR`, default). A markdown file written by one is served immediately by the other.\n\n- `cubrid-jira search KEY` — cache hit prints from disk with no network; cache miss fetches one issue plus one level of related issues (`--no-recurse` disables the walk).\n- `cubrid-jira-fetch KEY --depth N` *(deprecated)* — bulk-fetch a transitive closure up to depth `N`. Already-saved files are skipped, so a later `--depth 2` run extends a prior `--depth 1` run; pass `--force` to re-download.\n- Field-write commands invalidate the affected key(s) (`link` invalidates both sides); structural-write commands invalidate 2–3 keys per the table above.\n\n---\n\n## Development\n\n```sh\ngit clone https://github.com/vimkim/cubrid-jira.git\ncd cubrid-jira\nuv sync --dev\nuv run pytest                # ~78 unit + mocked-integration tests in ~0.25s\nuv run pytest -m live        # also hits the real jira.cubrid.org (read-only)\nuv run cubrid-jira search CBRD-1\n```\n\nWith `just`:\n\n```sh\njust test\njust search CBRD-26463\n```\n\n### Module layout (`src/cubrid_jira/`)\n\n| File | Role |\n|---|---|\n| `cli.py` | Parent `cubrid-jira` argparse + dispatch. |\n| `http.py` | `JiraClient` (basic-auth, dry-run, retries, 401 hard-fail) + `fetch_issue` read helper. **Layering rule: no `subprocess` imports.** |\n| `session.py` | `SessionClient` for the Convert wizard — manages `JSESSIONID` via `http.cookiejar.CookieJar` and adds `X-Atlassian-Token: no-check` on every mutating POST. Same dry-run semantics as `JiraClient`. **Layering rule: no `subprocess` imports.** |\n| `wizard.py` | Pure HTML parsing (`atl_token` / `guid` / `\u003cselect name=\"issuetype\"\u003e` extraction, XSRF-rejection detection) + payload builders for the six wizard form POSTs. **Layering rule: no `urllib` imports.** |\n| `markdown.py` | Pure rendering (Jira wiki → markdown via pandoc) and `extract_related_keys`. **Layering rule: no `urllib` imports.** |\n| `walk.py` | Recursive related-issue walking + on-disk cache writes. |\n| `auth.py` | Credential resolution: env → netrc → error. |\n| `cache.py` | Cache directory resolution + prefix-safe invalidation. |\n| `legacy.py` | Deprecation shims for the old `cubrid-jira-search` / `cubrid-jira-fetch` binaries. |\n\nThe `cubrid_jira_fetcher` import path remains as a deprecation shim that re-exports `cubrid_jira` and emits a `DeprecationWarning`. New code should `import cubrid_jira` directly.\n\n---\n\n## Troubleshooting\n\n- **`command not found: cubrid-jira`** — install dir isn't on `$PATH`. With `uv tool install`, run `uv tool update-shell`. With `pipx`, run `pipx ensurepath`. Restart the shell.\n- **`pandoc: command not found`** — install pandoc (see Prerequisites). Without pandoc, descriptions/comments fall through as plain text.\n- **`Error: Auth failed (HTTP 401)`** — do NOT retry. See [CAPTCHA-lockout warning](#-cleartext--captcha-lockout-warnings). Solve the CAPTCHA via the JIRA web UI, then fix your credentials.\n- **Redirect loop / HTTPS errors** — JIRA responses are expected over plain HTTP; do not force HTTPS at the proxy level.\n- **Stale cache** — `cubrid-jira search CBRD-XXXXX --force`, or just delete the cache directory.\n- **Deprecation warning when importing `cubrid_jira_fetcher`** — expected; rename your import to `cubrid_jira`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvimkim%2Fcubrid-jira","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvimkim%2Fcubrid-jira","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvimkim%2Fcubrid-jira/lists"}