@falldownthesystem/devtool 0.4.0 → 0.5.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.
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { getAll, set, runSetup, configPath, isConfigured } from '../config/store.js';
2
+ import { getAll, set, runSetup, configPath, schema } from '../config/store.js';
3
3
  import { heading, field, mask, success, error, divider } from '../utils/output.js';
4
4
 
5
5
  const config = new Command('config')
@@ -17,29 +17,15 @@ config
17
17
  .alias('get')
18
18
  .description('Show current configuration')
19
19
  .action(() => {
20
- if (!isConfigured()) {
21
- error('Not configured yet. Run: dev config setup');
22
- return;
23
- }
24
-
25
20
  const all = getAll();
26
21
 
27
- heading('Bitbucket');
28
- field('Workspace', all.bitbucket.workspace);
29
- field('Default repo', all.bitbucket.defaultRepo);
30
- field('Email', all.bitbucket.email);
31
- field('Token', mask(all.bitbucket.token));
32
-
33
- heading('Jira');
34
- field('Base URL', all.jira.baseUrl);
35
- field('Token', mask(all.jira.token));
36
- field('Default project', all.jira.defaultProject);
37
-
38
- heading('Confluence');
39
- field('Base URL', all.confluence.baseUrl);
40
- field('Email', all.confluence.email);
41
- field('Token', mask(all.confluence.token));
42
- field('Default space', all.confluence.defaultSpace);
22
+ for (const [section, definition] of Object.entries(schema)) {
23
+ heading(definition.title);
24
+ for (const [key, property] of Object.entries(definition.properties)) {
25
+ const value = all[section]?.[key] ?? '';
26
+ field(property.title, property.writeOnly ? mask(value) : value);
27
+ }
28
+ }
43
29
 
44
30
  console.log('');
45
31
  divider();
@@ -51,11 +37,8 @@ config
51
37
  .command('set <key> <value>')
52
38
  .description('Set a config value (e.g. bitbucket.workspace my-ws)')
53
39
  .action((key, value) => {
54
- const validKeys = [
55
- 'bitbucket.workspace', 'bitbucket.defaultRepo', 'bitbucket.email', 'bitbucket.token',
56
- 'jira.baseUrl', 'jira.token', 'jira.defaultProject',
57
- 'confluence.baseUrl', 'confluence.email', 'confluence.token', 'confluence.defaultSpace',
58
- ];
40
+ const validKeys = Object.entries(schema).flatMap(([section, definition]) =>
41
+ Object.keys(definition.properties).map((key) => `${section}.${key}`));
59
42
  if (!validKeys.includes(key)) {
60
43
  error(`Invalid key: ${key}`);
61
44
  console.log(`Valid keys: ${validKeys.join(', ')}`);
@@ -57,30 +57,25 @@ confluence.command('pages')
57
57
  .alias('ls')
58
58
  .description('List pages in a space')
59
59
  .option('-s, --space <key>', 'Space key (overrides default)')
60
- .option('--space-id <id>', 'Space ID (skip key lookup)')
60
+ .option('--space-id <id>', 'Space ID (instead of a key)')
61
61
  .action(async function (opts) {
62
62
  const mode = getOutputMode(this);
63
63
  const s = mode === 'rich' ? spinner('Fetching pages...').start() : null;
64
64
  try {
65
- let spaceId = opts.spaceId;
66
- if (!spaceId) {
67
- const spaceKey = opts.space || get('confluence.defaultSpace');
68
- if (!spaceKey) {
69
- if (s) s.error({ text: 'No space specified' });
70
- console.error('Specify --space <key>, --space-id <id>, or set confluence.defaultSpace');
71
- process.exitCode = 1;
72
- return;
73
- }
74
- const spaces = await listSpaces();
75
- const match = spaces.find((sp) => sp.key === spaceKey);
76
- if (!match) {
77
- if (s) s.error({ text: `Space "${spaceKey}" not found` });
78
- process.exitCode = 1;
79
- return;
80
- }
81
- spaceId = match.id;
65
+ const spaceId = opts.spaceId;
66
+ const spaceKey = spaceId ? undefined : opts.space || get('confluence.defaultSpace');
67
+ if (!spaceId && !spaceKey) {
68
+ if (s) s.error({ text: 'No space specified' });
69
+ console.error('Specify --space <key>, --space-id <id>, or set confluence.defaultSpace');
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+ const pages = await listSpacePages({ spaceId, spaceKey });
74
+ if (!pages) {
75
+ if (s) s.error({ text: `Space "${spaceId || spaceKey}" not found` });
76
+ process.exitCode = 1;
77
+ return;
82
78
  }
83
- const pages = await listSpacePages(spaceId);
84
79
  if (s) s.success({ text: `${pages.length} page(s) found` });
85
80
  if (mode === 'json') outputJson(curatePagesJson(pages));
86
81
  else if (mode === 'plain') plainPages(pages);
@@ -1,135 +1,135 @@
1
- import { Command } from 'commander';
2
- import {
3
- getIssue,
4
- getIssueComments,
5
- searchIssues,
6
- getMyIssues,
7
- listQueues,
8
- searchByQueue,
9
- } from '../clients/serviceDesk.js';
10
- import {
11
- formatIssueView, formatIssueComments, formatIssueSearch,
12
- plainIssueView, plainIssueComments, plainIssueSearch,
13
- curateIssueJson, curateCommentsJson, curateSearchJson,
14
- formatQueueList, plainQueueList, curateQueuesJson,
15
- } from '../formatters/serviceDesk.js';
16
- import { spinner } from '../utils/output.js';
17
- import { handleError } from '../utils/errors.js';
18
- import { getOutputMode, outputJson } from '../utils/mode.js';
19
-
20
- const sd = new Command('sd')
21
- .alias('servicedesk')
22
- .description('Jira Service Desk Cloud tickets');
23
-
24
- sd.command('view <key>')
25
- .alias('read')
26
- .description('View ticket details')
27
- .action(async function (key) {
28
- const mode = getOutputMode(this);
29
- const s = mode === 'rich' ? spinner(`Fetching ${key}...`).start() : null;
30
- try {
31
- const issue = await getIssue(key);
32
- if (s) s.success({ text: key });
33
- if (mode === 'json') outputJson(curateIssueJson(issue));
34
- else if (mode === 'plain') plainIssueView(issue);
35
- else formatIssueView(issue);
36
- } catch (err) {
37
- if (s) s.error({ text: `Failed to fetch ${key}` });
38
- handleError(err, `sd view ${key}`);
39
- }
40
- });
41
-
42
- sd.command('comments <key>')
43
- .description('List ticket comments')
44
- .action(async function (key) {
45
- const mode = getOutputMode(this);
46
- const s = mode === 'rich' ? spinner(`Fetching comments for ${key}...`).start() : null;
47
- try {
48
- const comments = await getIssueComments(key);
49
- if (s) s.success({ text: `${comments.length} comment(s)` });
50
- if (mode === 'json') outputJson(curateCommentsJson(comments));
51
- else if (mode === 'plain') plainIssueComments(comments);
52
- else formatIssueComments(comments);
53
- } catch (err) {
54
- if (s) s.error({ text: `Failed to fetch comments for ${key}` });
55
- handleError(err, `sd comments ${key}`);
56
- }
57
- });
58
-
59
- sd.command('search <jql>')
60
- .alias('find')
61
- .description('Search Service Desk tickets with JQL')
62
- .option('-n, --per-page <number>', 'Results per page', '20')
63
- .option('--next <token>', 'Continuation token from a previous page')
64
- .action(async function (jql, opts) {
65
- const mode = getOutputMode(this);
66
- const perPage = parseInt(opts.perPage, 10);
67
- const s = mode === 'rich' ? spinner('Searching...').start() : null;
68
- try {
69
- const result = await searchIssues(jql, perPage, opts.next || '');
70
- if (s) s.success({ text: `${result.issues.length} issue(s) returned` });
71
- if (mode === 'json') outputJson(curateSearchJson(result));
72
- else if (mode === 'plain') plainIssueSearch(result);
73
- else formatIssueSearch(result);
74
- } catch (err) {
75
- if (s) s.error({ text: 'Search failed' });
76
- handleError(err, `sd search "${jql}"`);
77
- }
78
- });
79
-
80
- sd.command('queues')
81
- .description('List Service Desk queues for the default project')
82
- .action(async function () {
83
- const mode = getOutputMode(this);
84
- const s = mode === 'rich' ? spinner('Fetching queues...').start() : null;
85
- try {
86
- const queues = await listQueues();
87
- if (s) s.success({ text: `${queues.length} queue(s)` });
88
- if (mode === 'json') outputJson(curateQueuesJson(queues));
89
- else if (mode === 'plain') plainQueueList(queues);
90
- else formatQueueList(queues);
91
- } catch (err) {
92
- if (s) s.error({ text: 'Failed to fetch queues' });
93
- handleError(err, 'sd queues');
94
- }
95
- });
96
-
97
- sd.command('queue <nameOrId>')
98
- .description('Show issues in a Service Desk queue (by name or id)')
99
- .option('-n, --per-page <number>', 'Results per page', '20')
100
- .option('--next <token>', 'Continuation token from a previous page')
101
- .action(async function (nameOrId, opts) {
102
- const mode = getOutputMode(this);
103
- const perPage = parseInt(opts.perPage, 10);
104
- const s = mode === 'rich' ? spinner(`Fetching queue "${nameOrId}"...`).start() : null;
105
- try {
106
- const result = await searchByQueue(nameOrId, perPage, opts.next || '');
107
- if (s) s.success({ text: `${result.issues.length} issue(s) in "${result.queue.name}"` });
108
- if (mode === 'json') outputJson(curateSearchJson(result));
109
- else if (mode === 'plain') plainIssueSearch(result);
110
- else formatIssueSearch(result);
111
- } catch (err) {
112
- if (s) s.error({ text: `Failed to fetch queue "${nameOrId}"` });
113
- handleError(err, `sd queue ${nameOrId}`);
114
- }
115
- });
116
-
117
- sd.command('mine')
118
- .alias('my')
119
- .description('Show my open Service Desk tickets')
120
- .action(async function () {
121
- const mode = getOutputMode(this);
122
- const s = mode === 'rich' ? spinner('Fetching my tickets...').start() : null;
123
- try {
124
- const result = await getMyIssues();
125
- if (s) s.success({ text: `${result.issues.length} ticket(s)` });
126
- if (mode === 'json') outputJson(curateSearchJson(result));
127
- else if (mode === 'plain') plainIssueSearch(result);
128
- else formatIssueSearch(result);
129
- } catch (err) {
130
- if (s) s.error({ text: 'Failed to fetch tickets' });
131
- handleError(err, 'sd my tickets');
132
- }
133
- });
134
-
135
- export default sd;
1
+ import { Command } from 'commander';
2
+ import {
3
+ getIssue,
4
+ getIssueComments,
5
+ searchIssues,
6
+ getMyIssues,
7
+ listQueues,
8
+ searchByQueue,
9
+ } from '../clients/serviceDesk.js';
10
+ import {
11
+ formatIssueView, formatIssueComments, formatIssueSearch,
12
+ plainIssueView, plainIssueComments, plainIssueSearch,
13
+ curateIssueJson, curateCommentsJson, curateSearchJson,
14
+ formatQueueList, plainQueueList, curateQueuesJson,
15
+ } from '../formatters/serviceDesk.js';
16
+ import { spinner } from '../utils/output.js';
17
+ import { handleError } from '../utils/errors.js';
18
+ import { getOutputMode, outputJson } from '../utils/mode.js';
19
+
20
+ const sd = new Command('sd')
21
+ .alias('servicedesk')
22
+ .description('Jira Service Desk Cloud tickets');
23
+
24
+ sd.command('view <key>')
25
+ .alias('read')
26
+ .description('View ticket details')
27
+ .action(async function (key) {
28
+ const mode = getOutputMode(this);
29
+ const s = mode === 'rich' ? spinner(`Fetching ${key}...`).start() : null;
30
+ try {
31
+ const issue = await getIssue(key);
32
+ if (s) s.success({ text: key });
33
+ if (mode === 'json') outputJson(curateIssueJson(issue));
34
+ else if (mode === 'plain') plainIssueView(issue);
35
+ else formatIssueView(issue);
36
+ } catch (err) {
37
+ if (s) s.error({ text: `Failed to fetch ${key}` });
38
+ handleError(err, `sd view ${key}`);
39
+ }
40
+ });
41
+
42
+ sd.command('comments <key>')
43
+ .description('List ticket comments')
44
+ .action(async function (key) {
45
+ const mode = getOutputMode(this);
46
+ const s = mode === 'rich' ? spinner(`Fetching comments for ${key}...`).start() : null;
47
+ try {
48
+ const comments = await getIssueComments(key);
49
+ if (s) s.success({ text: `${comments.length} comment(s)` });
50
+ if (mode === 'json') outputJson(curateCommentsJson(comments));
51
+ else if (mode === 'plain') plainIssueComments(comments);
52
+ else formatIssueComments(comments);
53
+ } catch (err) {
54
+ if (s) s.error({ text: `Failed to fetch comments for ${key}` });
55
+ handleError(err, `sd comments ${key}`);
56
+ }
57
+ });
58
+
59
+ sd.command('search <jql>')
60
+ .alias('find')
61
+ .description('Search Service Desk tickets with JQL')
62
+ .option('-n, --per-page <number>', 'Results per page', '20')
63
+ .option('--next <token>', 'Continuation token from a previous page')
64
+ .action(async function (jql, opts) {
65
+ const mode = getOutputMode(this);
66
+ const perPage = parseInt(opts.perPage, 10);
67
+ const s = mode === 'rich' ? spinner('Searching...').start() : null;
68
+ try {
69
+ const result = await searchIssues(jql, perPage, opts.next || '');
70
+ if (s) s.success({ text: `${result.issues.length} issue(s) returned` });
71
+ if (mode === 'json') outputJson(curateSearchJson(result));
72
+ else if (mode === 'plain') plainIssueSearch(result);
73
+ else formatIssueSearch(result);
74
+ } catch (err) {
75
+ if (s) s.error({ text: 'Search failed' });
76
+ handleError(err, `sd search "${jql}"`);
77
+ }
78
+ });
79
+
80
+ sd.command('queues')
81
+ .description('List Service Desk queues for the default project')
82
+ .action(async function () {
83
+ const mode = getOutputMode(this);
84
+ const s = mode === 'rich' ? spinner('Fetching queues...').start() : null;
85
+ try {
86
+ const queues = await listQueues();
87
+ if (s) s.success({ text: `${queues.length} queue(s)` });
88
+ if (mode === 'json') outputJson(curateQueuesJson(queues));
89
+ else if (mode === 'plain') plainQueueList(queues);
90
+ else formatQueueList(queues);
91
+ } catch (err) {
92
+ if (s) s.error({ text: 'Failed to fetch queues' });
93
+ handleError(err, 'sd queues');
94
+ }
95
+ });
96
+
97
+ sd.command('queue <nameOrId>')
98
+ .description('Show issues in a Service Desk queue (by name or id)')
99
+ .option('-n, --per-page <number>', 'Results per page', '20')
100
+ .option('--next <token>', 'Continuation token from a previous page')
101
+ .action(async function (nameOrId, opts) {
102
+ const mode = getOutputMode(this);
103
+ const perPage = parseInt(opts.perPage, 10);
104
+ const s = mode === 'rich' ? spinner(`Fetching queue "${nameOrId}"...`).start() : null;
105
+ try {
106
+ const result = await searchByQueue(nameOrId, perPage, opts.next || '');
107
+ if (s) s.success({ text: `${result.issues.length} issue(s) in "${result.queue.name}"` });
108
+ if (mode === 'json') outputJson(curateSearchJson(result));
109
+ else if (mode === 'plain') plainIssueSearch(result);
110
+ else formatIssueSearch(result);
111
+ } catch (err) {
112
+ if (s) s.error({ text: `Failed to fetch queue "${nameOrId}"` });
113
+ handleError(err, `sd queue ${nameOrId}`);
114
+ }
115
+ });
116
+
117
+ sd.command('mine')
118
+ .alias('my')
119
+ .description('Show my open Service Desk tickets')
120
+ .action(async function () {
121
+ const mode = getOutputMode(this);
122
+ const s = mode === 'rich' ? spinner('Fetching my tickets...').start() : null;
123
+ try {
124
+ const result = await getMyIssues();
125
+ if (s) s.success({ text: `${result.issues.length} ticket(s)` });
126
+ if (mode === 'json') outputJson(curateSearchJson(result));
127
+ else if (mode === 'plain') plainIssueSearch(result);
128
+ else formatIssueSearch(result);
129
+ } catch (err) {
130
+ if (s) s.error({ text: 'Failed to fetch tickets' });
131
+ handleError(err, 'sd my tickets');
132
+ }
133
+ });
134
+
135
+ export default sd;
@@ -2,40 +2,44 @@ import Conf from 'conf';
2
2
  import * as p from '@clack/prompts';
3
3
  import pc from 'picocolors';
4
4
 
5
- const schema = {
5
+ export const schema = {
6
6
  bitbucket: {
7
7
  type: 'object',
8
+ title: 'Bitbucket',
8
9
  properties: {
9
- workspace: { type: 'string' },
10
- defaultRepo: { type: 'string' },
11
- email: { type: 'string' },
12
- token: { type: 'string' },
10
+ workspace: { type: 'string', title: 'Workspace' },
11
+ defaultRepo: { type: 'string', title: 'Default repo' },
12
+ email: { type: 'string', title: 'Email' },
13
+ token: { type: 'string', title: 'Token', writeOnly: true },
13
14
  },
14
15
  },
15
16
  jira: {
16
17
  type: 'object',
18
+ title: 'Jira',
17
19
  properties: {
18
- baseUrl: { type: 'string' },
19
- token: { type: 'string' },
20
- defaultProject: { type: 'string' },
20
+ baseUrl: { type: 'string', title: 'Base URL' },
21
+ token: { type: 'string', title: 'Token', writeOnly: true },
22
+ defaultProject: { type: 'string', title: 'Default project' },
21
23
  },
22
24
  },
23
25
  confluence: {
24
26
  type: 'object',
27
+ title: 'Confluence',
25
28
  properties: {
26
- baseUrl: { type: 'string' },
27
- email: { type: 'string' },
28
- token: { type: 'string' },
29
- defaultSpace: { type: 'string' },
29
+ baseUrl: { type: 'string', title: 'Base URL' },
30
+ email: { type: 'string', title: 'Email' },
31
+ token: { type: 'string', title: 'Token', writeOnly: true },
32
+ defaultSpace: { type: 'string', title: 'Default space' },
30
33
  },
31
34
  },
32
35
  serviceDesk: {
33
36
  type: 'object',
37
+ title: 'Service Desk',
34
38
  properties: {
35
- baseUrl: { type: 'string' },
36
- email: { type: 'string' },
37
- token: { type: 'string' },
38
- defaultProject: { type: 'string' },
39
+ baseUrl: { type: 'string', title: 'Base URL' },
40
+ email: { type: 'string', title: 'Email' },
41
+ token: { type: 'string', title: 'Token', writeOnly: true },
42
+ defaultProject: { type: 'string', title: 'Default project' },
39
43
  },
40
44
  },
41
45
  };
@@ -132,17 +136,17 @@ export async function runSetup() {
132
136
 
133
137
  const confluence = await p.group({
134
138
  baseUrl: () => p.text({
135
- message: 'Confluence Cloud base URL',
139
+ message: 'Confluence base URL (Cloud or Server/Data Center)',
136
140
  placeholder: 'https://company.atlassian.net',
137
141
  initialValue: config.get('confluence.baseUrl') || '',
138
142
  }),
139
143
  email: () => p.text({
140
- message: 'Confluence account email',
144
+ message: 'Confluence account email (Server/Data Center: username, or empty for a personal access token)',
141
145
  placeholder: 'you@company.com',
142
146
  initialValue: config.get('confluence.email') || '',
143
147
  }),
144
148
  token: () => p.password({
145
- message: 'Confluence API token',
149
+ message: 'Confluence API token (Server/Data Center: personal access token or password)',
146
150
  }),
147
151
  defaultSpace: () => p.text({
148
152
  message: 'Confluence default space key',