@falldownthesystem/devtool 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md ADDED
@@ -0,0 +1,54 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ `devtool` — a Node.js CLI (`dev`) for interacting with Bitbucket pull requests and Jira tickets from the terminal. Uses ES modules, commander for CLI parsing, and the `conf` package for persistent config storage.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ # Run the CLI
13
+ node src/index.js <command>
14
+
15
+ # Install globally for `dev` command
16
+ npm link
17
+ ```
18
+
19
+ No build step, no tests, no linter configured yet.
20
+
21
+ ## Architecture
22
+
23
+ ```
24
+ src/
25
+ index.js # Entry point — commander program, registers subcommands, auto-setup hook
26
+ config/store.js # Conf-based config (persisted to OS config dir via `conf` package)
27
+ clients/ # API clients (bitbucket.js uses fetch + Basic auth, jira.js uses jira.js SDK + Bearer token)
28
+ commands/ # Commander subcommands (config, pr, jira)
29
+ formatters/ # Output formatting — each has three variants: rich (colored tables), plain (compact text), JSON (curated objects)
30
+ utils/
31
+ output.js # Shared UI primitives: spinner, table (cli-table3), heading, field, divider, mask
32
+ errors.js # handleError — maps HTTP status codes and connection errors to user-friendly messages
33
+ mode.js # getOutputMode — reads --json/--plain global flags, returns 'json'|'plain'|'rich'
34
+ ```
35
+
36
+ ### Output Mode Pattern
37
+
38
+ Every command supports three output modes via global flags `--json` and `--plain`:
39
+ - **rich** (default): colored output with tables (cli-table3) and spinners
40
+ - **plain**: no colors, no tables, compact text — suitable for piping
41
+ - **json**: curated JSON (not raw API responses) via `curate*Json` functions in formatters
42
+
43
+ Commands follow this pattern: check mode → conditionally show spinner → fetch data → format via mode-specific function. The `withMode` helper in `commands/pr.js` abstracts this for simple cases.
44
+
45
+ ### Config
46
+
47
+ Stored via `conf` package (projectName: `devtool`). Interactive setup runs automatically on first use (preAction hook in index.js). Config keys:
48
+ - `bitbucket.{workspace, defaultRepo, email, token}`
49
+ - `jira.{baseUrl, token, defaultProject}`
50
+
51
+ ### API Clients
52
+
53
+ - **Bitbucket** (`clients/bitbucket.js`): Raw fetch against `api.bitbucket.org/2.0`, Basic auth, manual pagination via `next` links
54
+ - **Jira** (`clients/jira.js`): Uses `jira.js` SDK (Version2Client) with Bearer token auth (overrides SDK's built-in Basic auth via request interceptor)
package/README.md CHANGED
@@ -5,7 +5,8 @@ A terminal-first CLI for working with Bitbucket PRs, Jira tickets, and Confluenc
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install -g @falldownthesystem/devtool
8
+ npm install
9
+ npm link
9
10
  ```
10
11
 
11
12
  This gives you the `dev` command globally. On first run, it'll walk you through connecting your Bitbucket, Jira, and Confluence accounts.
@@ -22,6 +23,7 @@ dev pr activity 42 # approvals, updates, comments
22
23
  dev jira view PROJ-123 # ticket details
23
24
  dev jira comments PROJ-123 # ticket comments
24
25
  dev jira search "sprint = 5" # JQL search
26
+ dev jira children PROJ-123 # epic children or subtasks (auto-detect)
25
27
  dev jira mine # your open tickets
26
28
 
27
29
  dev confluence spaces # list spaces
package/package.json CHANGED
@@ -1,34 +1,17 @@
1
1
  {
2
2
  "name": "@falldownthesystem/devtool",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for Bitbucket PRs, Jira tickets, and Confluence pages",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "dev": "src/index.js"
8
8
  },
9
- "files": [
10
- "src/"
11
- ],
12
9
  "scripts": {
13
10
  "start": "node src/index.js"
14
11
  },
15
- "keywords": [
16
- "cli",
17
- "bitbucket",
18
- "jira",
19
- "confluence",
20
- "pull-request",
21
- "devtools"
22
- ],
23
- "author": "FallDownTheSystem",
12
+ "keywords": [],
13
+ "author": "",
24
14
  "license": "ISC",
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/FallDownTheSystem/devtool.git"
28
- },
29
- "engines": {
30
- "node": ">=18"
31
- },
32
15
  "dependencies": {
33
16
  "@clack/prompts": "^1.0.0",
34
17
  "cli-table3": "^0.6.5",
@@ -59,3 +59,40 @@ export async function getMyIssues() {
59
59
  : `assignee = currentUser() AND status != Done ORDER BY updated DESC`;
60
60
  return searchIssues(jql);
61
61
  }
62
+
63
+ export async function getIssueChildren(issueKey) {
64
+ const issue = await getIssue(issueKey);
65
+ const type = issue.fields.issuetype?.name;
66
+ const parent = { key: issue.key, type, summary: issue.fields.summary };
67
+
68
+ if (type === 'Epic') {
69
+ const response = await getClient().instance.get(
70
+ `/rest/agile/1.0/epic/${encodeURIComponent(issueKey)}/issue`,
71
+ {
72
+ params: {
73
+ fields: 'summary,status,assignee,priority,updated,issuetype',
74
+ maxResults: 100,
75
+ },
76
+ }
77
+ );
78
+ const data = response.data;
79
+ return {
80
+ issues: data.issues || [],
81
+ total: data.total ?? (data.issues || []).length,
82
+ startAt: data.startAt || 0,
83
+ maxResults: data.maxResults || 100,
84
+ parent,
85
+ source: 'epic',
86
+ };
87
+ }
88
+
89
+ const subtasks = issue.fields.subtasks || [];
90
+ return {
91
+ issues: subtasks,
92
+ total: subtasks.length,
93
+ startAt: 0,
94
+ maxResults: subtasks.length,
95
+ parent,
96
+ source: 'subtasks',
97
+ };
98
+ }
@@ -4,11 +4,13 @@ import {
4
4
  getIssueComments,
5
5
  searchIssues,
6
6
  getMyIssues,
7
+ getIssueChildren,
7
8
  } from '../clients/jira.js';
8
9
  import {
9
10
  formatIssueView, formatIssueComments, formatIssueSearch,
10
11
  plainIssueView, plainIssueComments, plainIssueSearch,
11
12
  curateIssueJson, curateCommentsJson, curateSearchJson,
13
+ formatIssueChildren, plainIssueChildren, curateChildrenJson,
12
14
  } from '../formatters/jira.js';
13
15
  import { spinner } from '../utils/output.js';
14
16
  import { handleError } from '../utils/errors.js';
@@ -74,6 +76,24 @@ jira.command('search <jql>')
74
76
  }
75
77
  });
76
78
 
79
+ jira.command('children <key>')
80
+ .alias('subs')
81
+ .description('List epic children or issue subtasks')
82
+ .action(async function (key) {
83
+ const mode = getOutputMode(this);
84
+ const s = mode === 'rich' ? spinner(`Fetching children of ${key}...`).start() : null;
85
+ try {
86
+ const result = await getIssueChildren(key);
87
+ if (s) s.success({ text: `${result.total} ${result.source === 'epic' ? 'issue(s) in epic' : 'subtask(s)'}` });
88
+ if (mode === 'json') outputJson(curateChildrenJson(result));
89
+ else if (mode === 'plain') plainIssueChildren(result);
90
+ else formatIssueChildren(result);
91
+ } catch (err) {
92
+ if (s) s.error({ text: `Failed to fetch children of ${key}` });
93
+ handleError(err, `jira children ${key}`);
94
+ }
95
+ });
96
+
77
97
  jira.command('mine')
78
98
  .alias('my')
79
99
  .description('Show my open issues')
@@ -109,6 +109,23 @@ export function curateSearchJson(result) {
109
109
  };
110
110
  }
111
111
 
112
+ export function curateChildrenJson(result) {
113
+ return {
114
+ parent: result.parent,
115
+ source: result.source,
116
+ total: result.total,
117
+ issues: result.issues.map((issue) => ({
118
+ key: issue.key,
119
+ summary: issue.fields?.summary || null,
120
+ status: issue.fields?.status?.name || null,
121
+ type: issue.fields?.issuetype?.name || null,
122
+ assignee: issue.fields?.assignee?.displayName || null,
123
+ priority: issue.fields?.priority?.name || null,
124
+ updated: issue.fields?.updated || null,
125
+ })),
126
+ };
127
+ }
128
+
112
129
  // --- Plain formatters ---
113
130
 
114
131
  export function plainIssueView(issue) {
@@ -149,6 +166,27 @@ export function plainIssueSearch(result) {
149
166
  if (result.total > showing) console.log(`Showing ${result.startAt + 1}-${showing} of ${result.total}`);
150
167
  }
151
168
 
169
+ export function plainIssueChildren(result) {
170
+ const label = result.source === 'epic' ? 'epic children' : 'subtasks';
171
+ console.log(`${result.parent.key} (${result.parent.type}) "${result.parent.summary}" - ${result.total} ${label}`);
172
+ if (result.issues.length === 0) { console.log('None.'); return; }
173
+ for (const issue of result.issues) {
174
+ const f = issue.fields || {};
175
+ const parts = [
176
+ `${issue.key}`,
177
+ f.status?.name ? `[${f.status.name}]` : null,
178
+ f.issuetype?.name ? `(${f.issuetype.name})` : null,
179
+ f.summary ? `"${f.summary}"` : null,
180
+ ].filter(Boolean);
181
+ const meta = [
182
+ f.assignee?.displayName || null,
183
+ f.priority?.name || null,
184
+ f.updated ? relativeTime(f.updated) : null,
185
+ ].filter(Boolean).join(', ');
186
+ console.log(`${parts.join(' ')}${meta ? ` - ${meta}` : ''}`);
187
+ }
188
+ }
189
+
152
190
  // --- Rich formatters (original) ---
153
191
 
154
192
  export function formatIssueView(issue) {
@@ -234,3 +272,36 @@ export function formatIssueSearch(result) {
234
272
  }
235
273
  console.log('');
236
274
  }
275
+
276
+ export function formatIssueChildren(result) {
277
+ const { issues, parent, source } = result;
278
+ const label = source === 'epic' ? 'children' : 'subtasks';
279
+
280
+ heading(`${parent.key} ${parent.summary}`);
281
+ console.log(pc.dim(` ${parent.type} · ${result.total} ${label}`));
282
+ divider();
283
+
284
+ if (issues.length === 0) {
285
+ console.log(pc.dim(' None.'));
286
+ console.log('');
287
+ return;
288
+ }
289
+
290
+ const rows = issues.map((issue) => {
291
+ const f = issue.fields || {};
292
+ return [
293
+ pc.bold(issue.key),
294
+ (f.summary || '').length > 50
295
+ ? f.summary.slice(0, 47) + '...'
296
+ : f.summary || '',
297
+ f.issuetype?.name || pc.dim('—'),
298
+ statusColor(f.status?.name),
299
+ f.assignee?.displayName || pc.dim('Unassigned'),
300
+ priorityColor(f.priority?.name),
301
+ f.updated ? relativeTime(f.updated) : pc.dim('—'),
302
+ ];
303
+ });
304
+
305
+ console.log(table(['Key', 'Summary', 'Type', 'Status', 'Assignee', 'Priority', 'Updated'], rows));
306
+ console.log('');
307
+ }
package/LICENSE DELETED
@@ -1,15 +0,0 @@
1
- ISC License
2
-
3
- Copyright (c) 2025 FallDownTheSystem
4
-
5
- Permission to use, copy, modify, and/or distribute this software for any
6
- purpose with or without fee is hereby granted, provided that the above
7
- copyright notice and this permission notice appear in all copies.
8
-
9
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
- PERFORMANCE OF THIS SOFTWARE.