@gitset-dev/cli 2.0.2 → 2.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/index.js +8 -0
- package/lib/ai/providers/gemini.js +2 -0
- package/lib/cli-issue.js +8 -11
- package/lib/cli-pr.js +8 -11
- package/lib/label-picker.js +117 -0
- package/lib/prompts/context.js +24 -0
- package/lib/prompts/defaults.js +6 -2
- package/lib/prompts/index.js +3 -1
- package/package.json +1 -1
- package/src/commands/feedback.js +98 -0
package/index.js
CHANGED
|
@@ -17,6 +17,8 @@ function showHelp() {
|
|
|
17
17
|
|
|
18
18
|
${theme.bold('First time?')} Run ${theme.accent('gitset config')} — an interactive wizard sets up your AI provider in under a minute.
|
|
19
19
|
|
|
20
|
+
${theme.bold('Found a bug or have a suggestion?')} Run ${theme.accent('gitset feedback')} anytime.
|
|
21
|
+
|
|
20
22
|
${theme.bold('AI commands')} (use your own provider key):
|
|
21
23
|
commit Generate a commit message from staged changes
|
|
22
24
|
pr Generate a pull-request description from the branch diff
|
|
@@ -35,6 +37,7 @@ ${theme.bold('Local tools')} (no AI):
|
|
|
35
37
|
status Show git + provider status
|
|
36
38
|
template Manage local templates (~/.gitset/templates), edit <tool> to customize
|
|
37
39
|
init Scaffold local templates
|
|
40
|
+
feedback Report a bug, suggest a feature, or share feedback
|
|
38
41
|
|
|
39
42
|
${theme.bold('Setup')}:
|
|
40
43
|
config interactive setup wizard
|
|
@@ -125,6 +128,9 @@ Manage local templates in ~/.gitset/templates.
|
|
|
125
128
|
path print the templates directory`,
|
|
126
129
|
init: `Usage: gitset init
|
|
127
130
|
Scaffold the local template directory.`,
|
|
131
|
+
feedback: `Usage: gitset feedback
|
|
132
|
+
Interactively submit a bug report, feature suggestion, or general feedback.
|
|
133
|
+
Filed as a GitHub issue in gitset-dev/gitset via your own \`gh\` auth.`,
|
|
128
134
|
config: `Usage: gitset config [set|list|remove|theme|path]
|
|
129
135
|
(no args) interactive setup wizard
|
|
130
136
|
set interactive wizard, or: set <provider> --key <key> [--model m] [--base-url u] [--default]
|
|
@@ -200,6 +206,8 @@ async function main() {
|
|
|
200
206
|
code = await require('./src/commands/labelspack').runLabelspackCommand(rest); break;
|
|
201
207
|
case 'dependabot':
|
|
202
208
|
code = (await require('./src/commands/dependabot-resolver')(null, rest)) || 0; break;
|
|
209
|
+
case 'feedback':
|
|
210
|
+
code = (await require('./src/commands/feedback')()) || 0; break;
|
|
203
211
|
|
|
204
212
|
case 'auth': case 'verify': case 'logout':
|
|
205
213
|
code = deprecatedAuth(command); break;
|
|
@@ -21,6 +21,8 @@ function create({ apiKey, model, timeoutMs = 60_000 }) {
|
|
|
21
21
|
temperature: req.temperature ?? 0.7,
|
|
22
22
|
...(req.maxTokens ? { maxOutputTokens: req.maxTokens } : {}),
|
|
23
23
|
...(req.system ? { systemInstruction: req.system } : {}),
|
|
24
|
+
|
|
25
|
+
thinkingConfig: { thinkingBudget: req.thinkingBudget ?? 0 },
|
|
24
26
|
};
|
|
25
27
|
const ac = req.signal ? { abortSignal: req.signal } : {};
|
|
26
28
|
return { config: cfg, http: { ...ac, timeout: timeoutMs } };
|
package/lib/cli-issue.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const { execFileSync } = require('child_process');
|
|
13
13
|
const readline = require('readline');
|
|
14
14
|
const genLocal = require('./generate-local');
|
|
15
|
-
const {
|
|
15
|
+
const { pickLabels } = require('./label-picker');
|
|
16
16
|
const theme = require('../src/utils/theme');
|
|
17
17
|
|
|
18
18
|
const out = (s = '') => console.log(s);
|
|
@@ -106,6 +106,7 @@ async function runIssueCommand(argv) {
|
|
|
106
106
|
for (;;) {
|
|
107
107
|
out();
|
|
108
108
|
out(theme.bold(`Title: ${title}`));
|
|
109
|
+
if (selectedLabels.length) out(theme.dim(`Labels: ${selectedLabels.map((l) => l.name).join(', ')}`));
|
|
109
110
|
out(theme.accent(result.text.slice(0, 1500) + (result.text.length > 1500 ? '\n… (truncated)' : '')));
|
|
110
111
|
out();
|
|
111
112
|
out(theme.dim(`tip: edit ~/.gitset/templates/issue.md (or run \`gitset template edit issue\`) to change the format`));
|
|
@@ -115,16 +116,12 @@ async function runIssueCommand(argv) {
|
|
|
115
116
|
if (ch === 'c') return create(result.text);
|
|
116
117
|
if (ch === 'l') {
|
|
117
118
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const pick = await ask('Apply which? (comma numbers, "all", or blank to skip): ');
|
|
125
|
-
if (pick.toLowerCase() === 'all') selectedLabels = sug;
|
|
126
|
-
else { const idx = pick.split(',').map((s) => parseInt(s.trim(), 10) - 1).filter((n) => n >= 0 && n < sug.length); selectedLabels = idx.map((i) => sug[i]); }
|
|
127
|
-
if (selectedLabels.length) out(theme.success(`✓ labels: ${selectedLabels.map((l) => l.name).join(', ')}`));
|
|
119
|
+
const chosen = await pickLabels({ title, body: result.text, provider: getFlag(argv, '--provider'), model: getFlag(argv, '--model') });
|
|
120
|
+
if (chosen.length) {
|
|
121
|
+
const byName = new Map(selectedLabels.map((l) => [l.name.toLowerCase(), l]));
|
|
122
|
+
for (const l of chosen) byName.set(l.name.toLowerCase(), l);
|
|
123
|
+
selectedLabels = [...byName.values()];
|
|
124
|
+
}
|
|
128
125
|
} catch (e) { errln(e.message); }
|
|
129
126
|
continue;
|
|
130
127
|
}
|
package/lib/cli-pr.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const { execFileSync } = require('child_process');
|
|
13
13
|
const readline = require('readline');
|
|
14
14
|
const genLocal = require('./generate-local');
|
|
15
|
-
const {
|
|
15
|
+
const { pickLabels } = require('./label-picker');
|
|
16
16
|
const theme = require('../src/utils/theme');
|
|
17
17
|
|
|
18
18
|
const out = (s = '') => console.log(s);
|
|
@@ -130,6 +130,7 @@ async function runPrCommand(argv) {
|
|
|
130
130
|
for (;;) {
|
|
131
131
|
out();
|
|
132
132
|
out(theme.bold(`${title} ${theme.dim(`(${base} ← ${head})`)}`));
|
|
133
|
+
if (selectedLabels.length) out(theme.dim(`Labels: ${selectedLabels.map((l) => l.name).join(', ')}`));
|
|
133
134
|
out(theme.accent(result.text.slice(0, 1500) + (result.text.length > 1500 ? '\n… (truncated)' : '')));
|
|
134
135
|
out();
|
|
135
136
|
out(theme.dim(`tip: edit ~/.gitset/templates/pr.md (or run \`gitset template edit pr\`) to change the format`));
|
|
@@ -139,16 +140,12 @@ async function runPrCommand(argv) {
|
|
|
139
140
|
if (ch === 'c') return create(result.text);
|
|
140
141
|
if (ch === 'l') {
|
|
141
142
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const pick = await ask('Apply which? (comma numbers, "all", or blank to skip): ');
|
|
149
|
-
if (pick.toLowerCase() === 'all') selectedLabels = sug;
|
|
150
|
-
else { const idx = pick.split(',').map((s) => parseInt(s.trim(), 10) - 1).filter((n) => n >= 0 && n < sug.length); selectedLabels = idx.map((i) => sug[i]); }
|
|
151
|
-
if (selectedLabels.length) out(theme.success(`✓ labels: ${selectedLabels.map((l) => l.name).join(', ')}`));
|
|
143
|
+
const chosen = await pickLabels({ title, body: result.text, provider: getFlag(argv, '--provider'), model: getFlag(argv, '--model') });
|
|
144
|
+
if (chosen.length) {
|
|
145
|
+
const byName = new Map(selectedLabels.map((l) => [l.name.toLowerCase(), l]));
|
|
146
|
+
for (const l of chosen) byName.set(l.name.toLowerCase(), l);
|
|
147
|
+
selectedLabels = [...byName.values()];
|
|
148
|
+
}
|
|
152
149
|
} catch (e) { errln(e.message); }
|
|
153
150
|
continue;
|
|
154
151
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared label-selection UI for `gitset issue` / `gitset pr`'s [l]abels
|
|
5
|
+
* step. Three explicit sources, so it's always clear whether a pick
|
|
6
|
+
* applies something that already exists in the repo, asks the AI (which
|
|
7
|
+
* marks each suggestion new vs. existing), or pulls from your saved
|
|
8
|
+
* label pack (`gitset labelspack`).
|
|
9
|
+
*/
|
|
10
|
+
const { execFileSync } = require('child_process');
|
|
11
|
+
const readline = require('readline');
|
|
12
|
+
const { suggestLabels } = require('./labels-ai');
|
|
13
|
+
const { getLabels } = require('../src/utils/labels');
|
|
14
|
+
const theme = require('../src/utils/theme');
|
|
15
|
+
|
|
16
|
+
const out = (s = '') => console.log(s);
|
|
17
|
+
const errln = (s) => console.error(`${theme.error('✗')} ${s}`);
|
|
18
|
+
|
|
19
|
+
function ask(q) {
|
|
20
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
21
|
+
return new Promise((r) => rl.question(q, (a) => { rl.close(); r(a.trim()); }));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getRepoLabels() {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(execFileSync(
|
|
27
|
+
'gh', ['label', 'list', '--limit', '200', '--json', 'name,color,description'],
|
|
28
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] },
|
|
29
|
+
));
|
|
30
|
+
} catch { return []; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function tagExisting(candidates, repoLabels) {
|
|
34
|
+
const names = new Set(repoLabels.map((l) => l.name.toLowerCase()));
|
|
35
|
+
return candidates.map((l) => ({ ...l, existsInRepo: names.has(String(l.name).toLowerCase()) }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printChoices(list) {
|
|
39
|
+
list.forEach((l, i) => {
|
|
40
|
+
const tag = l.existsInRepo === true ? theme.dim(' (existing)')
|
|
41
|
+
: l.existsInRepo === false ? theme.accent(' (new)') : '';
|
|
42
|
+
const desc = l.description ? theme.dim(` — ${l.description}`) : '';
|
|
43
|
+
out(` [${i + 1}] ${l.name}${tag}${desc}`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function pickFrom(list) {
|
|
48
|
+
if (list.length === 0) return [];
|
|
49
|
+
printChoices(list);
|
|
50
|
+
const pick = await ask('Apply which? (comma numbers, "all", or blank to skip): ');
|
|
51
|
+
if (pick.toLowerCase() === 'all') return list;
|
|
52
|
+
const idx = pick.split(',').map((s) => parseInt(s.trim(), 10) - 1).filter((n) => n >= 0 && n < list.length);
|
|
53
|
+
return idx.map((i) => list[i]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @returns {Promise<Array<{name,color,description}>>} labels picked this round ([] to skip)
|
|
58
|
+
*/
|
|
59
|
+
async function pickLabels({ title, body, provider, model }) {
|
|
60
|
+
const repoLabels = getRepoLabels();
|
|
61
|
+
|
|
62
|
+
for (;;) {
|
|
63
|
+
out();
|
|
64
|
+
out(theme.bold('Labels — where from?'));
|
|
65
|
+
out(` ${theme.key('1', ' Existing repo labels')} ${theme.dim(`(${repoLabels.length} in this repo)`)}`);
|
|
66
|
+
out(` ${theme.key('2', ' AI suggestions')} ${theme.dim('(tags each pick new vs. existing)')}`);
|
|
67
|
+
out(` ${theme.key('3', ' Your label pack')} ${theme.dim('(gitset labelspack)')}`);
|
|
68
|
+
const src = (await ask(` ${theme.key('q', 'uit / skip')} > `)).toLowerCase();
|
|
69
|
+
|
|
70
|
+
if (src === '1') {
|
|
71
|
+
if (repoLabels.length === 0) {
|
|
72
|
+
out(theme.warn('This repo has no labels yet — try 2 (AI) or 3 (your pack) instead.'));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const chosen = await pickFrom(repoLabels.map((l) => ({ ...l, existsInRepo: true })));
|
|
76
|
+
if (chosen.length) out(theme.success(`✓ labels: ${chosen.map((l) => l.name).join(', ')}`));
|
|
77
|
+
return chosen;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (src === '2') {
|
|
81
|
+
process.stderr.write(theme.dim('… asking the AI for label suggestions\n'));
|
|
82
|
+
let sug;
|
|
83
|
+
try {
|
|
84
|
+
sug = await suggestLabels({
|
|
85
|
+
title, body, existing: repoLabels.map((l) => l.name).join(', '), provider, model,
|
|
86
|
+
});
|
|
87
|
+
} catch (e) {
|
|
88
|
+
errln(e.message || 'AI label suggestion failed.');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (sug.length === 0) {
|
|
92
|
+
out(theme.warn("The AI didn't return usable suggestions. Try again, or use 1/3 instead."));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const chosen = await pickFrom(tagExisting(sug, repoLabels));
|
|
96
|
+
if (chosen.length) out(theme.success(`✓ labels: ${chosen.map((l) => l.name).join(', ')}`));
|
|
97
|
+
return chosen;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (src === '3') {
|
|
101
|
+
const { source, labels } = getLabels();
|
|
102
|
+
if (labels.length === 0) {
|
|
103
|
+
out(theme.warn('Your label pack is empty. Run `gitset labelspack --init` first.'));
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
out(theme.dim(`(from ${source})`));
|
|
107
|
+
const chosen = await pickFrom(tagExisting(labels, repoLabels));
|
|
108
|
+
if (chosen.length) out(theme.success(`✓ labels: ${chosen.map((l) => l.name).join(', ')}`));
|
|
109
|
+
return chosen;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (src === 'q' || src === '') return [];
|
|
113
|
+
out(theme.warn('Unrecognized option.'));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { pickLabels };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function currentDateLine() {
|
|
4
|
+
const now = new Date();
|
|
5
|
+
const iso = now.toISOString().slice(0, 10);
|
|
6
|
+
const natural = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
|
7
|
+
return `${natural} (${iso})`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function dateAwarenessNote() {
|
|
11
|
+
return `Today's real date is ${currentDateLine()}. Treat this as "today"/"now" for any ` +
|
|
12
|
+
'date, year, or "released on" text you generate. Do not use a date from your training ' +
|
|
13
|
+
'data or default to a past year out of habit — only reference a different date when the ' +
|
|
14
|
+
'content specifically and verifiably describes a past event.';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function constraintAdherenceNote() {
|
|
18
|
+
return 'If the input below contains explicit constraints or exclusions (e.g. "do not ' +
|
|
19
|
+
'mention X", "do not name Y", a required format, a forbidden word), treat them as hard ' +
|
|
20
|
+
'requirements — re-check your output against each one before finishing, not just your ' +
|
|
21
|
+
'first draft.';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { currentDateLine, dateAwarenessNote, constraintAdherenceNote };
|
package/lib/prompts/defaults.js
CHANGED
|
@@ -86,7 +86,7 @@ const readme = {
|
|
|
86
86
|
const release = {
|
|
87
87
|
id: 'release',
|
|
88
88
|
build(ctx = {}) {
|
|
89
|
-
const { tag = '', commits = '', mode = 'summary', template = '', instruction = '', previous = '' } = ctx;
|
|
89
|
+
const { tag = '', repo = '', commits = '', mode = 'summary', template = '', instruction = '', previous = '' } = ctx;
|
|
90
90
|
return {
|
|
91
91
|
system:
|
|
92
92
|
'You write release notes in GitHub-flavored Markdown. Group changes ' +
|
|
@@ -95,8 +95,12 @@ const release = {
|
|
|
95
95
|
'with NO preamble, NO sign-off, and NO conversational text such as ' +
|
|
96
96
|
'"Here\'s a summary" or "Here are the release notes". ' +
|
|
97
97
|
'When a template/style example is provided, follow its structure, section ' +
|
|
98
|
-
'headings, ordering, and tone closely — treat it as the format to reproduce.'
|
|
98
|
+
'headings, ordering, and tone closely — treat it as the format to reproduce. ' +
|
|
99
|
+
'Never invent a repository path (e.g. placeholders like "your-project/your-repo" ' +
|
|
100
|
+
'or "user/repo") for a "Full Changelog" or compare link — only include such a link ' +
|
|
101
|
+
'when the real repository is given below, using that exact path; otherwise omit it.',
|
|
99
102
|
user: [
|
|
103
|
+
repo && `Repository: ${clip(repo, 200)}`,
|
|
100
104
|
tag && `Release: ${clip(tag, 100)}`,
|
|
101
105
|
`Mode: ${mode}`,
|
|
102
106
|
template && `Reproduce the structure, sections, and tone of this template/example closely:\n${clip(template, 12000)}`,
|
package/lib/prompts/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const defaults = require('./defaults');
|
|
5
|
+
const { dateAwarenessNote, constraintAdherenceNote } = require('./context');
|
|
5
6
|
|
|
6
7
|
let _cache = null;
|
|
7
8
|
|
|
@@ -61,7 +62,8 @@ function getPrompt(key, ctx = {}) {
|
|
|
61
62
|
if (!out || typeof out.system !== 'string' || typeof out.user !== 'string') {
|
|
62
63
|
throw new Error(`Prompt "${key}" must return { system: string, user: string }`);
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
+
|
|
66
|
+
return { ...out, system: `${out.system}\n\n${dateAwarenessNote()}\n\n${constraintAdherenceNote()}` };
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
function toAIRequest(key, ctx = {}, extra = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitset-dev/cli",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Gitset CLI — drafts commits, PRs, issues, READMEs and release notes with your own AI key. Fully local: your code and keys never touch a Gitset server. Draft. Refine. Ship.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const { execFileSync } = require('child_process');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const { log, askQuestion, selectOption } = require('../utils/ui');
|
|
4
|
+
|
|
5
|
+
const REPO = 'gitset-dev/gitset';
|
|
6
|
+
|
|
7
|
+
const TYPE_LABELS = {
|
|
8
|
+
bug: ['user-feedback', 'bug'],
|
|
9
|
+
suggestion: ['user-feedback', 'feature-request'],
|
|
10
|
+
qos: ['user-feedback'],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const TYPE_TITLES = {
|
|
14
|
+
bug: 'Bug Report',
|
|
15
|
+
suggestion: 'Feature Suggestion',
|
|
16
|
+
qos: 'Quality of Service Feedback',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function ghAvailable() {
|
|
20
|
+
try { execFileSync('gh', ['--version'], { stdio: 'ignore' }); return true; }
|
|
21
|
+
catch { return false; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ensureLabel(name, color, description) {
|
|
25
|
+
try {
|
|
26
|
+
execFileSync('gh', ['label', 'create', name, '--repo', REPO, '--color', color, '--description', description], { stdio: 'ignore' });
|
|
27
|
+
} catch { }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildBody(message, tool, version, includeSystemInfo) {
|
|
31
|
+
const lines = [message.trim(), ''];
|
|
32
|
+
if (includeSystemInfo) {
|
|
33
|
+
lines.push(
|
|
34
|
+
'<details>',
|
|
35
|
+
'<summary>Metadata</summary>',
|
|
36
|
+
'',
|
|
37
|
+
'- Source: cli',
|
|
38
|
+
tool ? `- Tool/command: ${tool}` : null,
|
|
39
|
+
`- Version: ${version}`,
|
|
40
|
+
`- OS: ${os.platform()}-${os.arch()}`,
|
|
41
|
+
'',
|
|
42
|
+
'</details>',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return lines.filter((l) => l !== null).join('\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = async function commandFeedback() {
|
|
49
|
+
console.log('\n=== Gitset Feedback ===\n');
|
|
50
|
+
|
|
51
|
+
if (!ghAvailable()) {
|
|
52
|
+
log('The `gh` CLI is required to submit feedback (https://cli.github.com).', 'red');
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const type = await selectOption('What kind of feedback would you like to share?', [
|
|
57
|
+
{ label: 'Bug Report', value: 'bug' },
|
|
58
|
+
{ label: 'Feature Suggestion', value: 'suggestion' },
|
|
59
|
+
{ label: 'Quality of Service / General Feedback', value: 'qos' },
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
let title = '';
|
|
63
|
+
while (!title) {
|
|
64
|
+
title = (await askQuestion('Short title for your feedback: ')).slice(0, 120);
|
|
65
|
+
if (!title) log('A title is required.', 'red');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tool = await askQuestion('Which tool or command does this relate to? (optional): ');
|
|
69
|
+
|
|
70
|
+
let message = '';
|
|
71
|
+
while (!message) {
|
|
72
|
+
message = await askQuestion('Please describe your feedback: ');
|
|
73
|
+
if (!message) log('A description is required.', 'red');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const version = require('../../package.json').version;
|
|
77
|
+
const consent = await askQuestion(`May we include your CLI version (${version}) and OS automatically? (Y/n): `);
|
|
78
|
+
const includeSystemInfo = consent.trim().toLowerCase() !== 'n';
|
|
79
|
+
|
|
80
|
+
ensureLabel('user-feedback', 'D4A5A5', 'Submitted via the in-app/CLI feedback tool (issue #20).');
|
|
81
|
+
|
|
82
|
+
const issueTitle = `[${TYPE_TITLES[type]}] ${title}`;
|
|
83
|
+
const body = buildBody(message, tool, version, includeSystemInfo);
|
|
84
|
+
const labels = TYPE_LABELS[type];
|
|
85
|
+
|
|
86
|
+
const args = ['issue', 'create', '--repo', REPO, '--title', issueTitle, '--body', body];
|
|
87
|
+
for (const l of labels) args.push('--label', l);
|
|
88
|
+
|
|
89
|
+
log('\nSubmitting feedback...', 'dim');
|
|
90
|
+
try {
|
|
91
|
+
const url = execFileSync('gh', args, { encoding: 'utf8' }).trim();
|
|
92
|
+
log(`\n✔ Feedback successfully submitted! View your issue here: ${url}`, 'green');
|
|
93
|
+
return 0;
|
|
94
|
+
} catch (e) {
|
|
95
|
+
log('Failed to submit feedback (is `gh` authenticated?).', 'red');
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
};
|