@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 +1 -1
- 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 +17 -0
- package/lib/prompts/defaults.js +6 -2
- package/lib/prompts/index.js +3 -1
- package/package.json +7 -8
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
|
|
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 {
|
|
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,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 };
|
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 } = 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()}` };
|
|
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.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
|
+
}
|