@gitset-dev/cli 1.2.1 → 2.0.1
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/{LICENSE.md → LICENSE} +1 -1
- package/README.md +80 -159
- package/index.js +230 -0
- package/lib/ai/capabilities.js +81 -0
- package/lib/ai/errors.js +115 -0
- package/lib/ai/index.js +98 -0
- package/lib/ai/providers/anthropic.js +62 -0
- package/lib/ai/providers/gemini.js +77 -0
- package/lib/ai/providers/mock.js +41 -0
- package/lib/ai/providers/openai-compatible.js +72 -0
- package/lib/ai/retry.js +31 -0
- package/lib/cli-commit.js +143 -0
- package/lib/cli-config.js +207 -0
- package/lib/cli-issue.js +144 -0
- package/lib/cli-pr.js +168 -0
- package/lib/cli-readme.js +153 -0
- package/lib/commit-local.js +67 -0
- package/lib/config.js +134 -0
- package/lib/generate-local.js +59 -0
- package/lib/labels-ai.js +41 -0
- package/lib/prompts/defaults.js +191 -0
- package/lib/prompts/index.js +79 -0
- package/lib/setup-wizard.js +115 -0
- package/package.json +34 -35
- package/src/commands/dependabot-resolver.js +314 -0
- package/src/commands/gitignore.js +110 -0
- package/src/commands/init.js +38 -0
- package/src/commands/labelspack.js +94 -0
- package/src/commands/license.js +208 -0
- package/src/commands/release.js +180 -0
- package/src/commands/repo.js +176 -0
- package/src/commands/status.js +56 -0
- package/src/commands/template.js +92 -0
- package/src/commands/tree.js +77 -0
- package/src/utils/dependabot-analyzer.js +101 -0
- package/src/utils/labels.js +102 -0
- package/src/utils/theme.js +70 -0
- package/src/utils/ui.js +173 -0
- package/.github/workflows/npm_publish.yml +0 -38
- package/src/index.js +0 -505
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `gitset license` — fully local, offline license generator.
|
|
5
|
+
* No backend. Standard public license texts embedded; {year}/{owner}
|
|
6
|
+
* substituted. Replaces the old remote /api/licenses path.
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { execFileSync } = require('child_process');
|
|
11
|
+
const readline = require('readline');
|
|
12
|
+
|
|
13
|
+
const tty = () => process.stdout.isTTY;
|
|
14
|
+
const c = (code, s) => (tty() ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
15
|
+
const ask = (q) => new Promise((r) => {
|
|
16
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
17
|
+
rl.question(q, (a) => { rl.close(); r(a.trim()); });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const MIT = `MIT License
|
|
21
|
+
|
|
22
|
+
Copyright (c) {year} {owner}
|
|
23
|
+
|
|
24
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
25
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
26
|
+
in the Software without restriction, including without limitation the rights
|
|
27
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
28
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
29
|
+
furnished to do so, subject to the following conditions:
|
|
30
|
+
|
|
31
|
+
The above copyright notice and this permission notice shall be included in all
|
|
32
|
+
copies or substantial portions of the Software.
|
|
33
|
+
|
|
34
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
35
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
36
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
37
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
38
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
39
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
40
|
+
SOFTWARE.
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const ISC = `ISC License
|
|
44
|
+
|
|
45
|
+
Copyright (c) {year} {owner}
|
|
46
|
+
|
|
47
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
48
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
49
|
+
copyright notice and this permission notice appear in all copies.
|
|
50
|
+
|
|
51
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
52
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
53
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
54
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
55
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
56
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
57
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const BSD2 = `BSD 2-Clause License
|
|
61
|
+
|
|
62
|
+
Copyright (c) {year}, {owner}
|
|
63
|
+
|
|
64
|
+
Redistribution and use in source and binary forms, with or without
|
|
65
|
+
modification, are permitted provided that the following conditions are met:
|
|
66
|
+
|
|
67
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
68
|
+
this list of conditions and the following disclaimer.
|
|
69
|
+
|
|
70
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
71
|
+
this list of conditions and the following disclaimer in the documentation
|
|
72
|
+
and/or other materials provided with the distribution.
|
|
73
|
+
|
|
74
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
75
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
76
|
+
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
77
|
+
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY
|
|
78
|
+
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
|
79
|
+
DAMAGE.
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
const BSD3 = `BSD 3-Clause License
|
|
83
|
+
|
|
84
|
+
Copyright (c) {year}, {owner}
|
|
85
|
+
|
|
86
|
+
Redistribution and use in source and binary forms, with or without
|
|
87
|
+
modification, are permitted provided that the following conditions are met:
|
|
88
|
+
|
|
89
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
90
|
+
this list of conditions and the following disclaimer.
|
|
91
|
+
|
|
92
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
93
|
+
this list of conditions and the following disclaimer in the documentation
|
|
94
|
+
and/or other materials provided with the distribution.
|
|
95
|
+
|
|
96
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
97
|
+
may be used to endorse or promote products derived from this software
|
|
98
|
+
without specific prior written permission.
|
|
99
|
+
|
|
100
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
101
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
102
|
+
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
103
|
+
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY
|
|
104
|
+
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
|
105
|
+
DAMAGE.
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
const UNLICENSE = `This is free and unencumbered software released into the public domain.
|
|
109
|
+
|
|
110
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or distribute
|
|
111
|
+
this software, either in source code form or as a compiled binary, for any
|
|
112
|
+
purpose, commercial or non-commercial, and by any means.
|
|
113
|
+
|
|
114
|
+
In jurisdictions that recognize copyright laws, the author or authors of this
|
|
115
|
+
software dedicate any and all copyright interest in the software to the public
|
|
116
|
+
domain.
|
|
117
|
+
|
|
118
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
119
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
120
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
121
|
+
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
122
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
123
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
124
|
+
|
|
125
|
+
For more information, please refer to <https://unlicense.org>
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
const APACHE2_NOTICE = ` Apache License
|
|
129
|
+
Version 2.0, January 2004
|
|
130
|
+
|
|
131
|
+
Copyright (c) {year} {owner}
|
|
132
|
+
|
|
133
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
134
|
+
you may not use this file except in compliance with the License.
|
|
135
|
+
You may obtain a copy of the License at
|
|
136
|
+
|
|
137
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
138
|
+
|
|
139
|
+
Unless required by applicable law or agreed to in writing, software
|
|
140
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
141
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
142
|
+
See the License for the specific language governing permissions and
|
|
143
|
+
limitations under the License.
|
|
144
|
+
|
|
145
|
+
NOTE: This is the standard Apache-2.0 short notice. Include the full license
|
|
146
|
+
text from https://www.apache.org/licenses/LICENSE-2.0.txt as required.
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
const LICENSES = {
|
|
150
|
+
mit: { name: 'MIT License', requiresOwner: true, text: MIT },
|
|
151
|
+
isc: { name: 'ISC License', requiresOwner: true, text: ISC },
|
|
152
|
+
'bsd-2-clause': { name: 'BSD 2-Clause', requiresOwner: true, text: BSD2 },
|
|
153
|
+
'bsd-3-clause': { name: 'BSD 3-Clause', requiresOwner: true, text: BSD3 },
|
|
154
|
+
'apache-2.0': { name: 'Apache License 2.0', requiresOwner: true, text: APACHE2_NOTICE },
|
|
155
|
+
unlicense: { name: 'The Unlicense', requiresOwner: false, text: UNLICENSE },
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
function listIds() { return Object.keys(LICENSES); }
|
|
159
|
+
|
|
160
|
+
function getLicense(id, { owner = '', year = new Date().getFullYear() } = {}) {
|
|
161
|
+
const lic = LICENSES[String(id).toLowerCase()];
|
|
162
|
+
if (!lic) return null;
|
|
163
|
+
return lic.text.replace(/\{year\}/g, String(year)).replace(/\{owner\}/g, owner || 'the authors');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function flag(argv, name) {
|
|
167
|
+
const i = argv.indexOf(name);
|
|
168
|
+
return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[i + 1] : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function runLicenseCommand(argv) {
|
|
172
|
+
const printList = () => {
|
|
173
|
+
console.log('Available licenses:');
|
|
174
|
+
for (const k of listIds()) console.log(` ${k.padEnd(16)} ${LICENSES[k].name}`);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (argv.includes('--list')) { printList(); return 0; }
|
|
178
|
+
|
|
179
|
+
let id = flag(argv, '--id') || (argv[0] && !argv[0].startsWith('-') ? argv[0] : null);
|
|
180
|
+
if (!id) {
|
|
181
|
+
printList();
|
|
182
|
+
if (!process.stdin.isTTY) { console.error(`${c('31', '✗')} Specify a license: gitset license --id mit --owner "Name"`); return 1; }
|
|
183
|
+
id = (await ask('\nLicense id: ')).toLowerCase();
|
|
184
|
+
}
|
|
185
|
+
const lic = LICENSES[String(id).toLowerCase()];
|
|
186
|
+
if (!lic) { console.error(`${c('31', '✗')} Unknown license "${id}". Try --list.`); return 1; }
|
|
187
|
+
|
|
188
|
+
let owner = flag(argv, '--owner') || '';
|
|
189
|
+
if (lic.requiresOwner && !owner) {
|
|
190
|
+
let guess = '';
|
|
191
|
+
try { guess = execFileSync('git', ['config', 'user.name'], { encoding: 'utf8' }).trim(); } catch { }
|
|
192
|
+
if (!process.stdin.isTTY) owner = guess;
|
|
193
|
+
else owner = (await ask(`Copyright holder${guess ? ` (${guess})` : ''}: `)) || guess;
|
|
194
|
+
if (!owner) { console.error(`${c('31', '✗')} This license needs a copyright holder (--owner).`); return 1; }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const content = getLicense(id, { owner });
|
|
198
|
+
const target = path.join(process.cwd(), 'LICENSE');
|
|
199
|
+
if (fs.existsSync(target) && !argv.includes('--force')) {
|
|
200
|
+
if (!process.stdin.isTTY) { console.error(`${c('31', '✗')} LICENSE exists. Use --force.`); return 1; }
|
|
201
|
+
if ((await ask('LICENSE exists. Overwrite? [y/N] ')).toLowerCase() !== 'y') { console.log('Aborted.'); return 0; }
|
|
202
|
+
}
|
|
203
|
+
fs.writeFileSync(target, content);
|
|
204
|
+
console.log(`${c('32', '✓')} Wrote LICENSE (${lic.name})`);
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { runLicenseCommand, getLicense, listIds, LICENSES };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const { execSync, execFileSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { log, askQuestion, selectOption } = require('../utils/ui');
|
|
6
|
+
const genLocal = require('../../lib/generate-local');
|
|
7
|
+
|
|
8
|
+
function execCommand(cmd) {
|
|
9
|
+
try {
|
|
10
|
+
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
11
|
+
} catch (err) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getTags() {
|
|
17
|
+
const tagsOutput = execCommand('git tag --sort=-creatordate');
|
|
18
|
+
return tagsOutput ? tagsOutput.split('\n').filter(Boolean) : [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getCommits(from, to) {
|
|
22
|
+
const cmd = `git log ${from}..${to} --pretty=format:"%h|%an|%s"`;
|
|
23
|
+
const output = execCommand(cmd);
|
|
24
|
+
if (!output) return [];
|
|
25
|
+
return output.split('\n').filter(Boolean).map(line => {
|
|
26
|
+
const [hash, author, message] = line.split('|');
|
|
27
|
+
return { hash, author, message };
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getCurrentBranch() {
|
|
32
|
+
return execCommand('git rev-parse --abbrev-ref HEAD') || 'HEAD';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function commitsToText(commits) {
|
|
36
|
+
if (!commits || commits.length === 0) return '';
|
|
37
|
+
return commits.map(c => `- ${c.hash || ''} ${c.message || ''} (${c.author || ''})`).join('\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = async function commandRelease(options = {}) {
|
|
41
|
+
console.log('\n=== Gitset Release Manager (BYOAI) ===\n');
|
|
42
|
+
|
|
43
|
+
let fromRef = options.from;
|
|
44
|
+
let toRef = options.to || getCurrentBranch();
|
|
45
|
+
|
|
46
|
+
if (!fromRef) {
|
|
47
|
+
const tags = getTags();
|
|
48
|
+
if (tags.length > 0) {
|
|
49
|
+
const useLatestTag = await selectOption('Select previous tag (From):', [
|
|
50
|
+
{ label: `Latest Tag (${tags[0]})`, value: tags[0] },
|
|
51
|
+
{ label: 'Select from list', value: 'list' },
|
|
52
|
+
{ label: 'Manual Input', value: 'manual' },
|
|
53
|
+
{ label: 'No previous tag (Initial Release)', value: 'initial' }
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
if (useLatestTag === 'list') {
|
|
57
|
+
fromRef = await selectOption('Select tag:', tags.slice(0, 10).map(t => ({ label: t, value: t })));
|
|
58
|
+
} else if (useLatestTag === 'manual') {
|
|
59
|
+
fromRef = await askQuestion('Enter start reference (tag/sha): ');
|
|
60
|
+
} else if (useLatestTag === 'initial') {
|
|
61
|
+
fromRef = null;
|
|
62
|
+
} else {
|
|
63
|
+
fromRef = useLatestTag;
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
log('No tags found. Assuming initial release.', 'yellow');
|
|
67
|
+
fromRef = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
log(`\nAnalyzing commits from ${fromRef ? fromRef : 'start'} to ${toRef}...`, 'dim');
|
|
72
|
+
let commits = [];
|
|
73
|
+
if (fromRef) {
|
|
74
|
+
commits = getCommits(fromRef, toRef);
|
|
75
|
+
} else {
|
|
76
|
+
const output = execCommand(`git log ${toRef} --pretty=format:"%h|%an|%s"`);
|
|
77
|
+
if (output) {
|
|
78
|
+
commits = output.split('\n').filter(Boolean).map(line => {
|
|
79
|
+
const [hash, author, message] = line.split('|');
|
|
80
|
+
return { hash, author, message };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (commits.length === 0) {
|
|
86
|
+
log('No commits found in range.', 'yellow');
|
|
87
|
+
const proceed = await selectOption('Proceed anyway?', [
|
|
88
|
+
{ label: 'Yes, use Manual Mode', value: 'manual' },
|
|
89
|
+
{ label: 'Abort', value: 'abort' }
|
|
90
|
+
]);
|
|
91
|
+
if (proceed === 'abort') return 0;
|
|
92
|
+
} else {
|
|
93
|
+
log(`Found ${commits.length} commits.`, 'green');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let tagName = options.version;
|
|
97
|
+
while (!tagName) {
|
|
98
|
+
tagName = await askQuestion('Enter new tag name (e.g. v1.2.0): ');
|
|
99
|
+
if (!tagName) log('Tag name is required.', 'red');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let currentNotes = '';
|
|
103
|
+
|
|
104
|
+
const generate = async (instr = '') => {
|
|
105
|
+
log('\nGenerating release notes…', 'cyan');
|
|
106
|
+
try {
|
|
107
|
+
const result = await genLocal.generate({
|
|
108
|
+
tool: 'release',
|
|
109
|
+
ctx: {
|
|
110
|
+
tag: tagName,
|
|
111
|
+
commits: commitsToText(commits),
|
|
112
|
+
mode: commits.length === 0 ? 'manual' : 'summary',
|
|
113
|
+
instruction: instr,
|
|
114
|
+
previous: instr ? currentNotes : '',
|
|
115
|
+
},
|
|
116
|
+
provider: options.provider,
|
|
117
|
+
model: options.model,
|
|
118
|
+
maxTokens: 4096,
|
|
119
|
+
interactive: true,
|
|
120
|
+
});
|
|
121
|
+
currentNotes = result.text;
|
|
122
|
+
log(`\n--- Release Notes (via ${result.provider}) ---\n`, 'green');
|
|
123
|
+
console.log(currentNotes);
|
|
124
|
+
log('\n-------------------------------\n', 'green');
|
|
125
|
+
return true;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err instanceof genLocal.AIError) {
|
|
128
|
+
log(`AI provider error (${err.code}): ${err.message}`, 'red');
|
|
129
|
+
} else {
|
|
130
|
+
log(err.message, 'red');
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (!(await generate())) return 2;
|
|
137
|
+
|
|
138
|
+
while (true) {
|
|
139
|
+
const action = await selectOption('What would you like to do?', [
|
|
140
|
+
{ label: 'Create Release (GitHub)', value: 'create' },
|
|
141
|
+
{ label: 'Refine with AI', value: 'refine' },
|
|
142
|
+
{ label: 'Edit Manually (Open Editor)', value: 'edit' },
|
|
143
|
+
{ label: 'Abort', value: 'abort' }
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
if (action === 'abort') return 0;
|
|
147
|
+
|
|
148
|
+
if (action === 'refine') {
|
|
149
|
+
const instruction = await askQuestion('Enter refinement instruction: ');
|
|
150
|
+
if (instruction) await generate(instruction);
|
|
151
|
+
} else if (action === 'edit') {
|
|
152
|
+
const tempFile = path.join(os.tmpdir(), 'RELEASE_NOTES.md');
|
|
153
|
+
fs.writeFileSync(tempFile, currentNotes);
|
|
154
|
+
log(`Opening ${tempFile} in default editor…`, 'dim');
|
|
155
|
+
try {
|
|
156
|
+
execFileSync(process.env.EDITOR || 'vi', [tempFile], { stdio: 'inherit' });
|
|
157
|
+
currentNotes = fs.readFileSync(tempFile, 'utf8');
|
|
158
|
+
log('Notes updated from editor.', 'green');
|
|
159
|
+
} catch (e) {
|
|
160
|
+
log('Failed to open editor. You can edit the file manually.', 'red');
|
|
161
|
+
}
|
|
162
|
+
} else if (action === 'create') {
|
|
163
|
+
const notesFile = 'RELEASE_NOTES.md';
|
|
164
|
+
fs.writeFileSync(notesFile, currentNotes);
|
|
165
|
+
log(`Saved notes to ${notesFile}`, 'dim');
|
|
166
|
+
log('Creating release via gh CLI…', 'cyan');
|
|
167
|
+
try {
|
|
168
|
+
execFileSync('gh', ['release', 'create', tagName, '--title', tagName, '--notes-file', notesFile], { stdio: 'inherit' });
|
|
169
|
+
log('\nRelease created successfully!', 'green');
|
|
170
|
+
try { fs.unlinkSync(notesFile); } catch { }
|
|
171
|
+
return 0;
|
|
172
|
+
} catch (e) {
|
|
173
|
+
log('\nFailed to create release with gh CLI.', 'red');
|
|
174
|
+
log('Ensure "gh" is installed and authenticated.', 'dim');
|
|
175
|
+
log(`Your notes are saved in ${notesFile}`, 'yellow');
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `gitset repo <about|license|backup>` — local, BYOAI, no backend.
|
|
5
|
+
*
|
|
6
|
+
* about : AI description + topics from local repo context, applied via `gh`
|
|
7
|
+
* license : delegates to the local license generator
|
|
8
|
+
* backup : writes a SECURE local GitHub Actions mirror workflow. We NEVER
|
|
9
|
+
* handle the user's PAT (the old remote/PAT flow was insecure and
|
|
10
|
+
* half-broken — removed). The user sets one repo secret in GitHub.
|
|
11
|
+
*/
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execFileSync } = require('child_process');
|
|
15
|
+
const readline = require('readline');
|
|
16
|
+
const genLocal = require('../../lib/generate-local');
|
|
17
|
+
const { runLicenseCommand } = require('./license');
|
|
18
|
+
|
|
19
|
+
const tty = () => process.stdout.isTTY;
|
|
20
|
+
const c = (code, s) => (tty() ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
21
|
+
const ask = (q) => new Promise((r) => {
|
|
22
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
23
|
+
rl.question(q, (a) => { rl.close(); r(a.trim()); });
|
|
24
|
+
});
|
|
25
|
+
const flag = (argv, n) => {
|
|
26
|
+
const i = argv.indexOf(n);
|
|
27
|
+
return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[i + 1] : null;
|
|
28
|
+
};
|
|
29
|
+
function git(args) {
|
|
30
|
+
try { return execFileSync('git', args, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(); }
|
|
31
|
+
catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
function parseJsonish(text) {
|
|
34
|
+
let t = String(text).trim().replace(/^```[a-z]*\n?/i, '').replace(/\n?```$/i, '');
|
|
35
|
+
const a = t.indexOf('{'); const b = t.lastIndexOf('}');
|
|
36
|
+
if (a !== -1 && b !== -1) { try { return JSON.parse(t.slice(a, b + 1)); } catch { } }
|
|
37
|
+
return { description: t.slice(0, 350), topics: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function aboutCmd(argv) {
|
|
41
|
+
const remote = git(['config', '--get', 'remote.origin.url']);
|
|
42
|
+
let owner = '', name = '';
|
|
43
|
+
const m = remote && remote.match(/[:/]([^/]+)\/([^/.]+)(?:\.git)?$/);
|
|
44
|
+
if (m) { owner = m[1]; name = m[2]; }
|
|
45
|
+
if (!name) { console.error(`${c('31', '✗')} No git remote origin found.`); return 1; }
|
|
46
|
+
console.log(`${c('1', `${owner}/${name}`)}`);
|
|
47
|
+
|
|
48
|
+
let ctx = '';
|
|
49
|
+
for (const f of ['README.md', 'package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod']) {
|
|
50
|
+
if (fs.existsSync(f)) ctx += `\n--- ${f} ---\n${fs.readFileSync(f, 'utf8').slice(0, 6000)}\n`;
|
|
51
|
+
}
|
|
52
|
+
if (!ctx && !process.stdin.isTTY) { console.error(`${c('31', '✗')} No README/manifest; run interactively to describe it.`); return 1; }
|
|
53
|
+
if (!ctx) ctx = await ask('Briefly describe the project: ');
|
|
54
|
+
|
|
55
|
+
async function gen(previous, instruction) {
|
|
56
|
+
process.stderr.write(c('90', '… generating about\n'));
|
|
57
|
+
const r = await genLocal.generate({
|
|
58
|
+
tool: 'about',
|
|
59
|
+
ctx: { context: ctx, previous: previous || '', instruction: instruction || '' },
|
|
60
|
+
provider: flag(argv, '--provider'), model: flag(argv, '--model'), maxTokens: 1024,
|
|
61
|
+
});
|
|
62
|
+
return parseJsonish(r.text);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let data;
|
|
66
|
+
try { data = await gen(); }
|
|
67
|
+
catch (e) {
|
|
68
|
+
if (e instanceof genLocal.AIError) { console.error(`${c('31', '✗')} AI provider error (${e.code}): ${e.message}`); return 2; }
|
|
69
|
+
console.error(`${c('31', '✗')} ${e.message}`); return /gitset config/.test(e.message || '') ? 1 : 2;
|
|
70
|
+
}
|
|
71
|
+
let { description = '', topics = [] } = data;
|
|
72
|
+
|
|
73
|
+
const nonInteractive = !process.stdin.isTTY || argv.includes('--yes') || argv.includes('-y');
|
|
74
|
+
for (;;) {
|
|
75
|
+
console.log(`\n${c('1', 'Description:')} ${description}`);
|
|
76
|
+
console.log(`${c('1', 'Topics:')} ${(topics || []).join(', ')}`);
|
|
77
|
+
if (nonInteractive) {
|
|
78
|
+
if (!argv.includes('--apply')) return 0;
|
|
79
|
+
}
|
|
80
|
+
const action = nonInteractive ? 'apply'
|
|
81
|
+
: (await ask(`\n${c('1', '[a]')}pply to GitHub ${c('1', '[r]')}efine ${c('1', '[e]')}dit ${c('1', '[q]')}uit > `)).toLowerCase();
|
|
82
|
+
|
|
83
|
+
if (action === 'q' || action === '') return 0;
|
|
84
|
+
if (action === 'a' || action === 'apply') {
|
|
85
|
+
try {
|
|
86
|
+
if (description) execFileSync('gh', ['repo', 'edit', `${owner}/${name}`, '--description', description], { stdio: 'ignore' });
|
|
87
|
+
if (topics && topics.length) {
|
|
88
|
+
const args = ['repo', 'edit', `${owner}/${name}`];
|
|
89
|
+
for (const t of topics) args.push('--add-topic', String(t).trim());
|
|
90
|
+
execFileSync('gh', args, { stdio: 'ignore' });
|
|
91
|
+
}
|
|
92
|
+
console.log(`${c('32', '✓')} Applied to ${owner}/${name}`);
|
|
93
|
+
return 0;
|
|
94
|
+
} catch {
|
|
95
|
+
console.error(`${c('31', '✗')} gh repo edit failed (is gh installed & authenticated?).`);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (action === 'e') {
|
|
100
|
+
description = (await ask(`Description: `)) || description;
|
|
101
|
+
const t = await ask('Topics (comma separated): ');
|
|
102
|
+
if (t) topics = t.split(',').map((x) => x.trim()).filter(Boolean);
|
|
103
|
+
}
|
|
104
|
+
if (action === 'r') {
|
|
105
|
+
const instr = await ask('Refinement instruction: ');
|
|
106
|
+
if (instr) { try { ({ description = description, topics = topics } = await gen(JSON.stringify({ description, topics }), instr)); } catch (e) { console.error(`${c('31', '✗')} ${e.message}`); } }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const BACKUP_WORKFLOW = (schedule) => `name: Gitset Backup Mirror
|
|
112
|
+
# Pushes this repository to a mirror you control.
|
|
113
|
+
# Set repo secret BACKUP_MIRROR_URL, e.g.
|
|
114
|
+
# https://<user>:<TOKEN>@github.com/<user>/<repo>-backup.git
|
|
115
|
+
# (Create the token yourself in GitHub → Settings → Developer settings.
|
|
116
|
+
# Gitset never sees or stores it.)
|
|
117
|
+
on:
|
|
118
|
+
schedule:
|
|
119
|
+
- cron: '${schedule}'
|
|
120
|
+
workflow_dispatch:
|
|
121
|
+
permissions:
|
|
122
|
+
contents: read
|
|
123
|
+
jobs:
|
|
124
|
+
mirror:
|
|
125
|
+
runs-on: ubuntu-latest
|
|
126
|
+
steps:
|
|
127
|
+
- uses: actions/checkout@v4
|
|
128
|
+
with: { fetch-depth: 0 }
|
|
129
|
+
- name: Push mirror
|
|
130
|
+
run: |
|
|
131
|
+
if [ -z "\${{ secrets.BACKUP_MIRROR_URL }}" ]; then
|
|
132
|
+
echo "::error::Set the BACKUP_MIRROR_URL repo secret."; exit 1
|
|
133
|
+
fi
|
|
134
|
+
git push --mirror "\${{ secrets.BACKUP_MIRROR_URL }}"
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
async function backupCmd(argv) {
|
|
138
|
+
const schedules = { daily: '0 0 * * *', weekly: '0 0 * * 0', monthly: '0 0 1 * *' };
|
|
139
|
+
const sched = schedules[flag(argv, '--schedule') || 'daily'] || schedules.daily;
|
|
140
|
+
const dir = path.join(process.cwd(), '.github', 'workflows');
|
|
141
|
+
const file = path.join(dir, 'gitset-backup.yml');
|
|
142
|
+
if (fs.existsSync(file) && !argv.includes('--force')) {
|
|
143
|
+
console.error(`${c('31', '✗')} ${path.relative(process.cwd(), file)} exists. Use --force.`);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
147
|
+
fs.writeFileSync(file, BACKUP_WORKFLOW(sched));
|
|
148
|
+
console.log(`${c('32', '✓')} Wrote ${path.relative(process.cwd(), file)} (schedule: ${sched})`);
|
|
149
|
+
console.log(c('1', '\nNext (you control the credentials — Gitset never touches them):'));
|
|
150
|
+
console.log(' 1. Create an empty mirror repo + a token with repo scope.');
|
|
151
|
+
console.log(' 2. Repo → Settings → Secrets → Actions → add BACKUP_MIRROR_URL');
|
|
152
|
+
console.log(' = https://<user>:<TOKEN>@github.com/<user>/<repo>-backup.git');
|
|
153
|
+
console.log(' 3. git add .github/workflows/gitset-backup.yml && commit && push\n');
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function runRepoCommand(argv) {
|
|
158
|
+
const sub = argv[0] && !argv[0].startsWith('-') ? argv[0]
|
|
159
|
+
: argv.includes('--about') ? 'about'
|
|
160
|
+
: argv.includes('--license') ? 'license'
|
|
161
|
+
: argv.includes('--backup') ? 'backup' : null;
|
|
162
|
+
|
|
163
|
+
if (sub === 'about') return aboutCmd(argv.slice(1));
|
|
164
|
+
if (sub === 'license') return runLicenseCommand(argv.filter((a) => a !== 'license'));
|
|
165
|
+
if (sub === 'backup') return backupCmd(argv.slice(1));
|
|
166
|
+
|
|
167
|
+
console.log(`${c('1', 'gitset repo')} — repository tools (local, BYOAI)
|
|
168
|
+
|
|
169
|
+
gitset repo about AI description + topics, applied via gh
|
|
170
|
+
gitset repo license generate a LICENSE file (offline)
|
|
171
|
+
gitset repo backup write a secure mirror-backup GitHub Actions workflow
|
|
172
|
+
`);
|
|
173
|
+
return sub ? 1 : 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { runRepoCommand };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `gitset status` — local environment status. No backend, no gitset_key.
|
|
5
|
+
* Shows git state + configured BYOAI providers + vendored AI core.
|
|
6
|
+
*/
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { execFileSync } = require('child_process');
|
|
10
|
+
const config = require('../../lib/config');
|
|
11
|
+
|
|
12
|
+
const tty = () => process.stdout.isTTY;
|
|
13
|
+
const c = (code, s) => (tty() ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
14
|
+
const git = (args) => {
|
|
15
|
+
try { return execFileSync('git', args, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(); }
|
|
16
|
+
catch { return null; }
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function runStatusCommand() {
|
|
20
|
+
console.log(c('1', '\nGitset — status\n'));
|
|
21
|
+
|
|
22
|
+
const inRepo = git(['rev-parse', '--is-inside-work-tree']) === 'true';
|
|
23
|
+
if (inRepo) {
|
|
24
|
+
const branch = git(['branch', '--show-current'])
|
|
25
|
+
|| git(['symbolic-ref', '--short', 'HEAD'])
|
|
26
|
+
|| 'HEAD (detached)';
|
|
27
|
+
const porcelain = git(['status', '--porcelain']) || '';
|
|
28
|
+
const lines = porcelain ? porcelain.split('\n') : [];
|
|
29
|
+
const staged = lines.filter((l) => l[0] && l[0] !== ' ' && l[0] !== '?').length;
|
|
30
|
+
const modified = lines.filter((l) => l[1] && l[1] !== ' ').length;
|
|
31
|
+
const untracked = lines.filter((l) => l.startsWith('??')).length;
|
|
32
|
+
console.log(`${c('36', 'git')} branch ${c('1', branch)} · ${staged} staged · ${modified} modified · ${untracked} untracked`);
|
|
33
|
+
} else {
|
|
34
|
+
console.log(`${c('36', 'git')} ${c('33', 'not a git repository')}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let providers = [];
|
|
38
|
+
try { providers = config.list(); } catch { providers = []; }
|
|
39
|
+
if (providers.length === 0) {
|
|
40
|
+
console.log(`${c('36', 'ai')} ${c('33', 'no provider configured')} — run: gitset config set <provider> --key <key>`);
|
|
41
|
+
} else {
|
|
42
|
+
for (const p of providers) {
|
|
43
|
+
const def = p.isDefault ? c('32', ' (default)') : '';
|
|
44
|
+
const key = p.keyLast4 ? `••••${p.keyLast4}` : c('33', 'no key');
|
|
45
|
+
console.log(`${c('36', 'ai')} ${p.provider}${def} — ${key}${p.model ? ` · ${p.model}` : ''}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
console.log(`${c('36', 'config')} ${config.FILE}`);
|
|
49
|
+
|
|
50
|
+
const aiOk = fs.existsSync(path.join(__dirname, '..', '..', 'lib', 'ai', 'index.js'));
|
|
51
|
+
console.log(`${c('36', 'core')} vendored lib/ai ${aiOk ? c('32', 'present') : c('31', 'MISSING (run: pnpm sync:ai)')}`);
|
|
52
|
+
console.log();
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { runStatusCommand };
|