@gitset-dev/cli 2.0.1 → 2.1.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/README.md CHANGED
@@ -33,7 +33,7 @@ nowhere else. It never contacts a Gitset server.
33
33
  npm install -g @gitset-dev/cli
34
34
  ```
35
35
 
36
- Requires Node.js 18+. Commands that publish to GitHub (`--create`,
36
+ Requires Node.js 20+. Commands that publish to GitHub (`--create`,
37
37
  `--apply`) use the [GitHub CLI](https://cli.github.com) (`gh`) with your
38
38
  existing login.
39
39
 
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,17 @@
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
+ module.exports = { currentDateLine, dateAwarenessNote };
@@ -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 } = 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()}` };
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.1",
3
+ "version": "2.1.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": {
@@ -12,15 +12,9 @@
12
12
  "!lib/__tests__",
13
13
  "src/"
14
14
  ],
15
- "packageManager": "pnpm@9.15.0",
16
15
  "engines": {
17
16
  "node": ">=20"
18
17
  },
19
- "scripts": {
20
- "sync:ai": "node scripts/sync-ai.js",
21
- "test": "node --test lib/__tests__/*.test.js",
22
- "start": "node index.js"
23
- },
24
18
  "keywords": [
25
19
  "git",
26
20
  "cli",
@@ -50,5 +44,10 @@
50
44
  },
51
45
  "bugs": {
52
46
  "url": "https://github.com/gitset-dev/gitset-cli-v2/issues"
47
+ },
48
+ "scripts": {
49
+ "sync:ai": "node scripts/sync-ai.js",
50
+ "test": "node --test lib/__tests__/*.test.js",
51
+ "start": "node index.js"
53
52
  }
54
- }
53
+ }