@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 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 { suggestLabels } = require('./labels-ai');
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
- let existing = '';
119
- try { existing = JSON.parse(execFileSync('gh', ['label', 'list', '--limit', '100', '--json', 'name'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] })).map((x) => x.name).join(', '); } catch { }
120
- process.stderr.write(theme.dim('… suggesting labels\n'));
121
- const sug = await suggestLabels({ title, body: result.text, existing, provider: getFlag(argv, '--provider'), model: getFlag(argv, '--model') });
122
- if (sug.length === 0) { out(theme.warn('No label suggestions.')); continue; }
123
- out('Suggested: ' + sug.map((l, i) => `[${i + 1}] ${l.name}`).join(' '));
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 { suggestLabels } = require('./labels-ai');
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
- let existing = '';
143
- try { existing = JSON.parse(execFileSync('gh', ['label', 'list', '--limit', '100', '--json', 'name'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] })).map((x) => x.name).join(', '); } catch { }
144
- process.stderr.write(theme.dim('… suggesting labels\n'));
145
- const sug = await suggestLabels({ title, body: result.text, existing, provider: getFlag(argv, '--provider'), model: getFlag(argv, '--model') });
146
- if (sug.length === 0) { out(theme.warn('No label suggestions.')); continue; }
147
- out('Suggested: ' + sug.map((l, i) => `[${i + 1}] ${l.name}`).join(' '));
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 };
@@ -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)}`,
@@ -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
- return out;
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.2",
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
+ };