@gitset-dev/cli 2.1.0 → 2.3.2

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,8 @@ ${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
41
+ auth Check GitHub access, or sync org permissions (auth sync)
38
42
 
39
43
  ${theme.bold('Setup')}:
40
44
  config interactive setup wizard
@@ -112,7 +116,9 @@ Manage & apply a label pack (local pack + gh).
112
116
  --apply create/update the labels in the current repo via gh
113
117
  --init write the default pack to ~/.gitset/labels.md
114
118
  --add --name <x> --color <hex> --description "..." add a label
115
- --sync pull the repo's labels into the local pack`,
119
+ --from-repo import the current repo's labels (name, color,
120
+ description) into the pack; --yes to overwrite
121
+ without asking (--sync is an alias)`,
116
122
  dependabot: `Usage: gitset dependabot [--resolve] [--dry-run] [--save-dev]
117
123
  Resolve Dependabot alerts for the current repo (local + gh).`,
118
124
  status: `Usage: gitset status
@@ -125,6 +131,15 @@ Manage local templates in ~/.gitset/templates.
125
131
  path print the templates directory`,
126
132
  init: `Usage: gitset init
127
133
  Scaffold the local template directory.`,
134
+ feedback: `Usage: gitset feedback
135
+ Interactively submit a bug report, feature suggestion, or general feedback.
136
+ Filed as a GitHub issue in gitset-dev/gitset via your own \`gh\` auth.`,
137
+ auth: `Usage: gitset auth [status|sync]
138
+ status (default) show your GitHub CLI auth status and access guidance
139
+ sync re-run GitHub authorization to approve additional
140
+ organizations or scopes (wraps \`gh auth refresh\`)
141
+ Gitset uses your own gh authentication locally — there is no Gitset account.
142
+ Web app permissions: gitset.dev/dashboard → GitHub connection.`,
128
143
  config: `Usage: gitset config [set|list|remove|theme|path]
129
144
  (no args) interactive setup wizard
130
145
  set interactive wizard, or: set <provider> --key <key> [--model m] [--base-url u] [--default]
@@ -200,8 +215,13 @@ async function main() {
200
215
  code = await require('./src/commands/labelspack').runLabelspackCommand(rest); break;
201
216
  case 'dependabot':
202
217
  code = (await require('./src/commands/dependabot-resolver')(null, rest)) || 0; break;
218
+ case 'feedback':
219
+ code = (await require('./src/commands/feedback')()) || 0; break;
220
+
221
+ case 'auth':
222
+ code = require('./src/commands/auth')(rest) || 0; break;
203
223
 
204
- case 'auth': case 'verify': case 'logout':
224
+ case 'verify': case 'logout':
205
225
  code = deprecatedAuth(command); break;
206
226
 
207
227
  case 'help': case '--help': case '-h': case undefined:
@@ -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 } };
@@ -0,0 +1,127 @@
1
+ // VENDORED from gitset-core-v2/lib/manifest — do not edit here. Run `pnpm sync:ai`.
2
+ 'use strict';
3
+
4
+ const MANIFESTS = [
5
+ { type: 'npm', file: 'package.json', label: 'package.json (npm)' },
6
+ { type: 'cargo', file: 'Cargo.toml', label: 'Cargo.toml (Cargo)' },
7
+ { type: 'python', file: 'pyproject.toml', label: 'pyproject.toml (Python)' },
8
+ { type: 'gemspec', file: null, label: '*.gemspec (RubyGems)' },
9
+ ];
10
+
11
+ function listSupported() {
12
+ return MANIFESTS.map(({ type, label }) => ({ type, label }));
13
+ }
14
+
15
+ function detectManifestFile(fileNames) {
16
+ const names = Array.isArray(fileNames) ? fileNames : [];
17
+ for (const m of MANIFESTS) {
18
+ if (m.file && names.includes(m.file)) return { type: m.type, file: m.file };
19
+ }
20
+ const gem = names.find((f) => f.endsWith('.gemspec'));
21
+ if (gem) return { type: 'gemspec', file: gem };
22
+ return null;
23
+ }
24
+
25
+ function normalizeVersion(tagName) {
26
+ if (typeof tagName !== 'string') return null;
27
+ const trimmed = tagName.trim();
28
+ return trimmed.replace(/^v/i, '') || null;
29
+ }
30
+
31
+ function detectIndent(text) {
32
+ const m = text.match(/^[ \t]+(?=")/m);
33
+ return m ? m[0] : ' ';
34
+ }
35
+
36
+ function findTomlSection(content, sectionName) {
37
+ const escaped = sectionName.replace(/\./g, '\\.');
38
+ const headerRe = new RegExp(`^\\[${escaped}\\][ \\t]*$`, 'm');
39
+ const m = headerRe.exec(content);
40
+ if (!m) return null;
41
+ const bodyStart = m.index + m[0].length;
42
+ const rest = content.slice(bodyStart);
43
+ const next = rest.match(/^\[[^\]]+\][ \t]*$/m);
44
+ const bodyEnd = next ? bodyStart + next.index : content.length;
45
+ return { bodyStart, bodyEnd };
46
+ }
47
+
48
+ const TOML_VERSION_LINE_RE = /^([ \t]*version[ \t]*=[ \t]*)(["'])([^"']+)\2([ \t]*)$/m;
49
+
50
+ function readTomlSectionVersion(content, sectionName) {
51
+ const sec = findTomlSection(content, sectionName);
52
+ if (!sec) return null;
53
+ const body = content.slice(sec.bodyStart, sec.bodyEnd);
54
+ const m = TOML_VERSION_LINE_RE.exec(body);
55
+ return m ? m[3] : null;
56
+ }
57
+
58
+ function bumpTomlSectionVersion(content, sectionName, newVersion) {
59
+ const sec = findTomlSection(content, sectionName);
60
+ if (!sec) return null;
61
+ const body = content.slice(sec.bodyStart, sec.bodyEnd);
62
+ if (!TOML_VERSION_LINE_RE.test(body)) return null;
63
+ const newBody = body.replace(TOML_VERSION_LINE_RE, (_full, pre, quote, _old, trail) => `${pre}${quote}${newVersion}${quote}${trail}`);
64
+ return content.slice(0, sec.bodyStart) + newBody + content.slice(sec.bodyEnd);
65
+ }
66
+
67
+ const GEMSPEC_VERSION_LINE_RE = /^([ \t]*[\w.]+\.version[ \t]*=[ \t]*)(["'])([^"']+)\2([ \t]*)$/m;
68
+
69
+ function readGemspecVersion(content) {
70
+ const m = GEMSPEC_VERSION_LINE_RE.exec(content);
71
+ return m ? m[3] : null;
72
+ }
73
+
74
+ function bumpGemspecVersion(content, newVersion) {
75
+ if (!GEMSPEC_VERSION_LINE_RE.test(content)) return null;
76
+ return content.replace(GEMSPEC_VERSION_LINE_RE, (_full, pre, quote, _old, trail) => `${pre}${quote}${newVersion}${quote}${trail}`);
77
+ }
78
+
79
+ function readVersion(type, content) {
80
+ switch (type) {
81
+ case 'npm': {
82
+ const parsed = JSON.parse(content);
83
+ return typeof parsed.version === 'string' ? parsed.version : null;
84
+ }
85
+ case 'cargo':
86
+ return readTomlSectionVersion(content, 'package');
87
+ case 'python':
88
+ return readTomlSectionVersion(content, 'project') || readTomlSectionVersion(content, 'tool.poetry');
89
+ case 'gemspec':
90
+ return readGemspecVersion(content);
91
+ default:
92
+ throw new Error(`Unsupported manifest type: ${type}`);
93
+ }
94
+ }
95
+
96
+ function bumpNpmVersion(content, newVersion) {
97
+ const parsed = JSON.parse(content);
98
+ if (typeof parsed.version !== 'string') return null;
99
+ parsed.version = newVersion;
100
+ const indent = detectIndent(content);
101
+ let out = JSON.stringify(parsed, null, indent);
102
+ if (content.endsWith('\n')) out += '\n';
103
+ return out;
104
+ }
105
+
106
+ function bumpVersion(type, content, newVersion) {
107
+ switch (type) {
108
+ case 'npm':
109
+ return bumpNpmVersion(content, newVersion);
110
+ case 'cargo':
111
+ return bumpTomlSectionVersion(content, 'package', newVersion);
112
+ case 'python':
113
+ return bumpTomlSectionVersion(content, 'project', newVersion) || bumpTomlSectionVersion(content, 'tool.poetry', newVersion);
114
+ case 'gemspec':
115
+ return bumpGemspecVersion(content, newVersion);
116
+ default:
117
+ throw new Error(`Unsupported manifest type: ${type}`);
118
+ }
119
+ }
120
+
121
+ module.exports = {
122
+ listSupported,
123
+ detectManifestFile,
124
+ normalizeVersion,
125
+ readVersion,
126
+ bumpVersion,
127
+ };
@@ -14,4 +14,11 @@ function dateAwarenessNote() {
14
14
  'content specifically and verifiably describes a past event.';
15
15
  }
16
16
 
17
- module.exports = { currentDateLine, dateAwarenessNote };
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 };
@@ -2,7 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  const defaults = require('./defaults');
5
- const { dateAwarenessNote } = require('./context');
5
+ const { dateAwarenessNote, constraintAdherenceNote } = require('./context');
6
6
 
7
7
  let _cache = null;
8
8
 
@@ -63,7 +63,7 @@ function getPrompt(key, ctx = {}) {
63
63
  throw new Error(`Prompt "${key}" must return { system: string, user: string }`);
64
64
  }
65
65
 
66
- return { ...out, system: `${out.system}\n\n${dateAwarenessNote()}` };
66
+ return { ...out, system: `${out.system}\n\n${dateAwarenessNote()}\n\n${constraintAdherenceNote()}` };
67
67
  }
68
68
 
69
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.1.0",
3
+ "version": "2.3.2",
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,54 @@
1
+ const { execFileSync } = require('child_process');
2
+ const { log } = require('../utils/ui');
3
+ const theme = require('../utils/theme');
4
+
5
+ function ghAvailable() {
6
+ try { execFileSync('gh', ['--version'], { stdio: 'ignore' }); return true; }
7
+ catch { return false; }
8
+ }
9
+
10
+ function showStatus() {
11
+ console.log(`\n=== Gitset Auth (via GitHub CLI) ===\n`);
12
+ try {
13
+ execFileSync('gh', ['auth', 'status'], { stdio: 'inherit' });
14
+ } catch {
15
+ log('\nYou are not logged in with the GitHub CLI. Run: gh auth login', 'yellow');
16
+ }
17
+ console.log(`
18
+ ${theme.bold('How Gitset access works')}
19
+ Locally, Gitset uses your own ${theme.accent('gh')} authentication for every GitHub
20
+ action (issues, PRs, releases, labels). No Gitset account is involved.
21
+
22
+ ${theme.bold('Missing an organization or repository?')}
23
+ - Local CLI: run ${theme.accent('gitset auth sync')} to re-run GitHub's authorization
24
+ and approve additional organizations or scopes.
25
+ - Web app: open ${theme.accent('https://gitset.dev/dashboard')} → GitHub connection →
26
+ Manage organization access.
27
+ `);
28
+ return 0;
29
+ }
30
+
31
+ function sync() {
32
+ console.log(`\n=== Gitset Auth Sync ===\n`);
33
+ log('Re-running GitHub authorization. Approve any missing organizations when your browser opens.', 'dim');
34
+ try {
35
+ execFileSync('gh', ['auth', 'refresh'], { stdio: 'inherit' });
36
+ log('\n✔ GitHub access refreshed. Newly granted organizations are now available.', 'green');
37
+ return 0;
38
+ } catch {
39
+ log('\nAuthorization was not completed. You can retry with `gitset auth sync` or run `gh auth refresh` directly.', 'red');
40
+ return 1;
41
+ }
42
+ }
43
+
44
+ module.exports = function commandAuth(rest = []) {
45
+ if (!ghAvailable()) {
46
+ log('The `gh` CLI is required (https://cli.github.com). Gitset uses your own gh authentication — no Gitset account exists.', 'red');
47
+ return 1;
48
+ }
49
+ const sub = (rest[0] || 'status').toLowerCase();
50
+ if (sub === 'sync' || sub === 'refresh') return sync();
51
+ if (sub === 'status') return showStatus();
52
+ log(`Unknown subcommand "${sub}". Use: gitset auth [status|sync]`, 'red');
53
+ return 1;
54
+ };
@@ -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
+ };
@@ -4,14 +4,15 @@
4
4
  * `gitset labelspack` — manage & apply a label pack. Local pack +
5
5
  * the user's `gh` CLI only. No backend, injection-safe (arg-array exec).
6
6
  *
7
- * gitset labelspack list the pack
8
- * gitset labelspack --apply create/update those labels in the repo via gh
9
- * gitset labelspack --init write the default pack to ~/.gitset/labels.md
7
+ * gitset labelspack list the pack
8
+ * gitset labelspack --apply create/update those labels in the repo via gh
9
+ * gitset labelspack --init write the default pack to ~/.gitset/labels.md
10
+ * gitset labelspack --from-repo import the current repo's labels into the pack
10
11
  * gitset labelspack --add --name x --color hex --description "..."
11
12
  */
12
13
  const { execFileSync } = require('child_process');
13
14
  const readline = require('readline');
14
- const { getLabels, addLabel, writeDefaultPack, getRepoLabels } = require('../utils/labels');
15
+ const { getLabels, addLabel, writeDefaultPack, writePack, getRepoLabels } = require('../utils/labels');
15
16
 
16
17
  const tty = () => process.stdout.isTTY;
17
18
  const c = (code, s) => (tty() ? `\x1b[${code}m${s}\x1b[0m` : s);
@@ -35,6 +36,36 @@ async function runLabelspackCommand(argv) {
35
36
  return 0;
36
37
  }
37
38
 
39
+ if (argv.includes('--from-repo') || argv.includes('--sync')) {
40
+ if (!ghAvailable()) {
41
+ console.error(`${c('31', '✗')} GitHub CLI \`gh\` not found / not authenticated.`);
42
+ return 1;
43
+ }
44
+ const repoLabels = getRepoLabels();
45
+ if (repoLabels.length === 0) {
46
+ console.error(`${c('31', '✗')} No labels found — run this inside a repository your \`gh\` account can access.`);
47
+ return 1;
48
+ }
49
+ const { source, labels: current } = getLabels();
50
+ if (source.startsWith('local') && !argv.includes('--yes') && !argv.includes('-y')) {
51
+ if (!process.stdin.isTTY) {
52
+ console.error(`${c('31', '✗')} A label pack already exists (${current.length} labels). Pass --yes to overwrite.`);
53
+ return 1;
54
+ }
55
+ const ok = (await ask(`Overwrite your existing pack (${current.length} labels) with ${repoLabels.length} labels from this repo? [y/N] `)).toLowerCase();
56
+ if (ok !== 'y') { console.log('Aborted.'); return 0; }
57
+ }
58
+ const imported = repoLabels.map((l) => ({
59
+ name: l.name,
60
+ color: String(l.color || '').replace('#', ''),
61
+ description: l.description || '',
62
+ }));
63
+ const f = writePack(imported);
64
+ console.log(`${c('32', '✓')} Imported ${imported.length} labels from this repository → ${f}`);
65
+ console.log(c('90', 'Apply them to any other repo: cd <repo> && gitset labelspack --apply'));
66
+ return 0;
67
+ }
68
+
38
69
  if (argv.includes('--add')) {
39
70
  let name = flag(argv, '--name');
40
71
  let color = flag(argv, '--color');
@@ -53,7 +84,7 @@ async function runLabelspackCommand(argv) {
53
84
 
54
85
  const { source, labels } = getLabels();
55
86
 
56
- if (!argv.includes('--apply') && !argv.includes('--sync')) {
87
+ if (!argv.includes('--apply')) {
57
88
  console.log(c('1', `\nLabel pack (${source}) — ${labels.length} labels\n`));
58
89
  for (const l of labels) {
59
90
  console.log(` ${c('36', l.name)}${l.description ? ` ${c('90', l.description)}` : ''}`);
@@ -4,6 +4,7 @@ const path = require('path');
4
4
  const os = require('os');
5
5
  const { log, askQuestion, selectOption } = require('../utils/ui');
6
6
  const genLocal = require('../../lib/generate-local');
7
+ const manifestLib = require('../../lib/manifest');
7
8
 
8
9
  function execCommand(cmd) {
9
10
  try {
@@ -37,6 +38,90 @@ function commitsToText(commits) {
37
38
  return commits.map(c => `- ${c.hash || ''} ${c.message || ''} (${c.author || ''})`).join('\n');
38
39
  }
39
40
 
41
+ async function syncManifestVersion(tagName) {
42
+ const newVersion = manifestLib.normalizeVersion(tagName);
43
+ if (!newVersion) return;
44
+
45
+ let fileNames;
46
+ try {
47
+ fileNames = fs.readdirSync(process.cwd());
48
+ } catch {
49
+ return;
50
+ }
51
+
52
+ const found = manifestLib.detectManifestFile(fileNames);
53
+ if (!found) return;
54
+
55
+ const filePath = path.join(process.cwd(), found.file);
56
+ let content;
57
+ try {
58
+ content = fs.readFileSync(filePath, 'utf8');
59
+ } catch {
60
+ return;
61
+ }
62
+
63
+ let oldVersion;
64
+ try {
65
+ oldVersion = manifestLib.readVersion(found.type, content);
66
+ } catch (err) {
67
+ log(`\nCould not parse ${found.file}, skipping version sync (${err.message}).`, 'yellow');
68
+ return;
69
+ }
70
+
71
+ if (!oldVersion) {
72
+ log(`\n${found.file} has no static top-level version field, skipping version sync.`, 'dim');
73
+ return;
74
+ }
75
+
76
+ if (oldVersion === newVersion) return;
77
+
78
+ let newContent;
79
+ try {
80
+ newContent = manifestLib.bumpVersion(found.type, content, newVersion);
81
+ } catch (err) {
82
+ log(`\nCould not bump ${found.file}, skipping version sync (${err.message}).`, 'yellow');
83
+ return;
84
+ }
85
+ if (!newContent) return;
86
+
87
+ log(`\n${found.file} version differs from this release:`, 'cyan');
88
+ log(` - version: ${oldVersion}`, 'red');
89
+ log(` + version: ${newVersion}`, 'green');
90
+ log(' Only the top-level version field changes — dependency versions are left untouched.', 'dim');
91
+
92
+ const bumpChoice = await selectOption(`Update ${found.file} to ${newVersion} as part of this release?`, [
93
+ { label: `Yes, update ${found.file}`, value: 'yes' },
94
+ { label: 'No, skip', value: 'no' },
95
+ ]);
96
+ if (bumpChoice !== 'yes') return;
97
+
98
+ try {
99
+ fs.writeFileSync(filePath, newContent);
100
+ execFileSync('git', ['add', found.file], { stdio: 'pipe' });
101
+ execFileSync('git', ['commit', '-m', `chore: bump version to ${newVersion}`], { stdio: 'pipe' });
102
+ log(`Committed: chore: bump version to ${newVersion}`, 'green');
103
+ } catch (err) {
104
+ log(`Failed to commit the version bump: ${err.message}`, 'red');
105
+ return;
106
+ }
107
+
108
+ log('\nThis commit needs to be pushed before creating the release, otherwise the release tag will not include it.', 'dim');
109
+ const pushChoice = await selectOption('Push this commit now?', [
110
+ { label: 'Yes, push', value: 'yes' },
111
+ { label: 'No, I\'ll push it myself', value: 'no' },
112
+ ]);
113
+ if (pushChoice === 'yes') {
114
+ try {
115
+ execFileSync('git', ['push'], { stdio: 'inherit' });
116
+ log('Pushed.', 'green');
117
+ } catch {
118
+ log('Failed to push. Push manually before the release tag will reflect this version.', 'red');
119
+ }
120
+ } else {
121
+ log('Not pushed — the release tag will not include this version bump until you push it.', 'yellow');
122
+ }
123
+ }
124
+
40
125
  module.exports = async function commandRelease(options = {}) {
41
126
  console.log('\n=== Gitset Release Manager (BYOAI) ===\n');
42
127
 
@@ -163,7 +248,10 @@ module.exports = async function commandRelease(options = {}) {
163
248
  const notesFile = 'RELEASE_NOTES.md';
164
249
  fs.writeFileSync(notesFile, currentNotes);
165
250
  log(`Saved notes to ${notesFile}`, 'dim');
166
- log('Creating release via gh CLI…', 'cyan');
251
+
252
+ await syncManifestVersion(tagName);
253
+
254
+ log('\nCreating release via gh CLI…', 'cyan');
167
255
  try {
168
256
  execFileSync('gh', ['release', 'create', tagName, '--title', tagName, '--notes-file', notesFile], { stdio: 'inherit' });
169
257
  log('\nRelease created successfully!', 'green');
@@ -96,7 +96,13 @@ function getRepoLabels() {
96
96
  }
97
97
  }
98
98
 
99
+ function writePack(labels) {
100
+ fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
101
+ fs.writeFileSync(LABELS_FILE, serializePack(labels));
102
+ return LABELS_FILE;
103
+ }
104
+
99
105
  module.exports = {
100
106
  LABELS_FILE, DEFAULT_LABELS,
101
- getLabels, addLabel, writeDefaultPack, getRepoLabels, parseLabelsYaml,
107
+ getLabels, addLabel, writeDefaultPack, writePack, getRepoLabels, parseLabelsYaml,
102
108
  };
package/src/utils/ui.js CHANGED
@@ -73,29 +73,31 @@ function askSecret(query) {
73
73
  stdin.pause();
74
74
  }
75
75
  function onData(chunk) {
76
- const ch = String(chunk);
77
- if (ch === '\r' || ch === '\n') {
78
- cleanup();
79
- process.stdout.write('\n');
80
- resolve(value.trim());
81
- return;
82
- }
83
- if (ch === '\u0003') { // Ctrl+C
84
- cleanup();
85
- process.stdout.write('\n');
86
- reject(new Error('Cancelled.'));
87
- return;
88
- }
89
- if (ch === '\u007f' || ch === '\b') { // backspace (DEL on most terminals, BS on some)
90
- if (value.length) {
91
- value = value.slice(0, -1);
92
- process.stdout.write('\b \b');
76
+ const cleaned = String(chunk).replace(/\x1b\[[0-9;]*[a-zA-Z~]/g, '');
77
+ for (const c of cleaned) {
78
+ if (c === '\r' || c === '\n') {
79
+ cleanup();
80
+ process.stdout.write('\n');
81
+ resolve(value.trim());
82
+ return;
83
+ }
84
+ if (c === '\u0003') { // Ctrl+C
85
+ cleanup();
86
+ process.stdout.write('\n');
87
+ reject(new Error('Cancelled.'));
88
+ return;
89
+ }
90
+ if (c === '\u007f' || c === '\b') { // backspace (DEL on most terminals, BS on some)
91
+ if (value.length) {
92
+ value = value.slice(0, -1);
93
+ process.stdout.write('\b \b');
94
+ }
95
+ continue;
93
96
  }
94
- return;
97
+ if (c.charCodeAt(0) < 32) continue; // ignore other control bytes
98
+ value += c;
99
+ process.stdout.write('*');
95
100
  }
96
- if (ch.charCodeAt(0) < 32) return; // ignore other control/escape bytes
97
- value += ch;
98
- process.stdout.write('*');
99
101
  }
100
102
  stdin.on('data', onData);
101
103
  });