geeto 0.3.9 → 0.4.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 +1 -1
- package/README.md +104 -107
- package/lib/api/copilot-sdk.d.ts +3 -0
- package/lib/api/copilot-sdk.d.ts.map +1 -1
- package/lib/api/copilot-sdk.js +44 -3
- package/lib/api/copilot-sdk.js.map +1 -1
- package/lib/api/copilot.d.ts +1 -0
- package/lib/api/copilot.d.ts.map +1 -1
- package/lib/api/copilot.js +13 -1
- package/lib/api/copilot.js.map +1 -1
- package/lib/api/gemini-sdk.d.ts +3 -0
- package/lib/api/gemini-sdk.d.ts.map +1 -1
- package/lib/api/gemini-sdk.js +46 -3
- package/lib/api/gemini-sdk.js.map +1 -1
- package/lib/api/gemini.d.ts +1 -0
- package/lib/api/gemini.d.ts.map +1 -1
- package/lib/api/gemini.js +13 -1
- package/lib/api/gemini.js.map +1 -1
- package/lib/api/openrouter-sdk.d.ts +3 -0
- package/lib/api/openrouter-sdk.d.ts.map +1 -1
- package/lib/api/openrouter-sdk.js +46 -3
- package/lib/api/openrouter-sdk.js.map +1 -1
- package/lib/api/openrouter.d.ts +1 -0
- package/lib/api/openrouter.d.ts.map +1 -1
- package/lib/api/openrouter.js +13 -1
- package/lib/api/openrouter.js.map +1 -1
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -1
- package/lib/utils/git-ai.d.ts +2 -0
- package/lib/utils/git-ai.d.ts.map +1 -1
- package/lib/utils/git-ai.js +17 -0
- package/lib/utils/git-ai.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/workflows/release.d.ts.map +1 -1
- package/lib/workflows/release.js +518 -9
- package/lib/workflows/release.js.map +1 -1
- package/lib/workflows/repo-settings.d.ts +9 -0
- package/lib/workflows/repo-settings.d.ts.map +1 -0
- package/lib/workflows/repo-settings.js +425 -0
- package/lib/workflows/repo-settings.js.map +1 -0
- package/package.json +16 -5
package/lib/workflows/release.js
CHANGED
|
@@ -12,6 +12,33 @@ import { chooseModelForProvider, generateReleaseNotesWithProvider, getAIProvider
|
|
|
12
12
|
import { log } from '../utils/logging.js';
|
|
13
13
|
import { loadState } from '../utils/state.js';
|
|
14
14
|
// ─── Helpers ───
|
|
15
|
+
/**
|
|
16
|
+
* Normalize markdown spacing for consistent markdownlint-friendly output.
|
|
17
|
+
* Ensures: one blank line after ### and #### headings, one blank line between sections,
|
|
18
|
+
* no double blank lines, trailing newline.
|
|
19
|
+
*/
|
|
20
|
+
const normalizeReleaseMarkdown = (md) => {
|
|
21
|
+
const lines = md.split('\n');
|
|
22
|
+
const result = [];
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const line = lines[i] ?? '';
|
|
25
|
+
const nextLine = lines[i + 1] ?? '';
|
|
26
|
+
result.push(line);
|
|
27
|
+
// After a heading (### or ####), ensure exactly one blank line before content
|
|
28
|
+
if ((line.startsWith('###') || line.startsWith('####')) && nextLine.trim() !== '') {
|
|
29
|
+
result.push('');
|
|
30
|
+
}
|
|
31
|
+
// After a bullet line, if next line is a heading, ensure blank line
|
|
32
|
+
if (line.startsWith('-') && (nextLine.startsWith('###') || nextLine.startsWith('####'))) {
|
|
33
|
+
result.push('');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Collapse multiple blank lines into one
|
|
37
|
+
return result
|
|
38
|
+
.join('\n')
|
|
39
|
+
.replaceAll(/\n{3,}/g, '\n\n')
|
|
40
|
+
.trim();
|
|
41
|
+
};
|
|
15
42
|
const parseSemver = (version) => {
|
|
16
43
|
const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
17
44
|
if (!match)
|
|
@@ -223,10 +250,448 @@ const generateChangelogEntry = (version, commits, prevVersion) => {
|
|
|
223
250
|
: [];
|
|
224
251
|
return [...header, ...breakingSection, ...featureSection, ...fixSection, ...otherSection].join('\n');
|
|
225
252
|
};
|
|
253
|
+
// ─── Sync GitHub Releases for existing tags ───
|
|
254
|
+
const getExistingGithubReleases = () => {
|
|
255
|
+
try {
|
|
256
|
+
const output = execSilent('gh release list --limit 100 --json tagName --jq ".[].tagName"').trim();
|
|
257
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const handleSyncReleases = async () => {
|
|
264
|
+
const line = '─'.repeat(56);
|
|
265
|
+
// Check if gh CLI is available
|
|
266
|
+
try {
|
|
267
|
+
execSilent('gh --version');
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
log.error('GitHub CLI (gh) is not installed. Install it: https://cli.github.com');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
console.log('');
|
|
274
|
+
const spinner = log.spinner();
|
|
275
|
+
spinner.start('Checking GitHub releases...');
|
|
276
|
+
const localTags = getExistingTags();
|
|
277
|
+
const ghReleases = getExistingGithubReleases();
|
|
278
|
+
const missingTags = localTags.filter((t) => !ghReleases.includes(t));
|
|
279
|
+
spinner.succeed(`Found ${localTags.length} tags, ${ghReleases.length} GitHub releases`);
|
|
280
|
+
if (missingTags.length === 0) {
|
|
281
|
+
console.log('');
|
|
282
|
+
log.success('All tags have GitHub Releases! Nothing to sync.');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
console.log('');
|
|
286
|
+
log.info(`${colors.bright}${missingTags.length}${colors.reset} tags missing GitHub Releases:`);
|
|
287
|
+
for (const tag of missingTags) {
|
|
288
|
+
console.log(` ${colors.yellow}${tag}${colors.reset}`);
|
|
289
|
+
}
|
|
290
|
+
console.log('');
|
|
291
|
+
const action = await select('What do you want to do?', [
|
|
292
|
+
{ label: 'Create releases for all missing tags', value: 'all' },
|
|
293
|
+
{ label: 'Select which tags to release', value: 'select' },
|
|
294
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
295
|
+
]);
|
|
296
|
+
if (action === 'cancel')
|
|
297
|
+
return;
|
|
298
|
+
let tagsToRelease = missingTags;
|
|
299
|
+
if (action === 'select') {
|
|
300
|
+
const { multiSelect } = await import('../cli/menu.js');
|
|
301
|
+
const choices = missingTags.map((t) => ({ label: t, value: t }));
|
|
302
|
+
const selected = await multiSelect('Select tags to create releases for:', choices);
|
|
303
|
+
if (selected.length === 0) {
|
|
304
|
+
log.info('No tags selected.');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
tagsToRelease = selected;
|
|
308
|
+
}
|
|
309
|
+
// Choose release notes mode: AI or template
|
|
310
|
+
console.log('');
|
|
311
|
+
const notesMode = await select('How should release notes be generated?', [
|
|
312
|
+
{ label: 'AI-generated (recommended)', value: 'ai' },
|
|
313
|
+
{ label: 'Auto-generate (template-based)', value: 'auto' },
|
|
314
|
+
]);
|
|
315
|
+
// AI setup if needed
|
|
316
|
+
let useAI = notesMode === 'ai';
|
|
317
|
+
let language = 'en';
|
|
318
|
+
let aiProvider = 'copilot';
|
|
319
|
+
let copilotModel;
|
|
320
|
+
let openrouterModel;
|
|
321
|
+
let geminiModel;
|
|
322
|
+
if (useAI) {
|
|
323
|
+
language = (await select('Release notes language:', [
|
|
324
|
+
{ label: 'English', value: 'en' },
|
|
325
|
+
{ label: 'Indonesian (Bahasa Indonesia)', value: 'id' },
|
|
326
|
+
]));
|
|
327
|
+
// Check saved config
|
|
328
|
+
const savedState = loadState();
|
|
329
|
+
if (savedState?.aiProvider &&
|
|
330
|
+
savedState.aiProvider !== 'manual' &&
|
|
331
|
+
(savedState.copilotModel || savedState.openrouterModel || savedState.geminiModel)) {
|
|
332
|
+
aiProvider = savedState.aiProvider;
|
|
333
|
+
copilotModel = savedState.copilotModel;
|
|
334
|
+
openrouterModel = savedState.openrouterModel;
|
|
335
|
+
geminiModel = savedState.geminiModel;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
let providerChosen = false;
|
|
339
|
+
while (!providerChosen) {
|
|
340
|
+
aiProvider = (await select('Choose AI Provider:', [
|
|
341
|
+
{ label: 'GitHub Copilot (Recommended)', value: 'copilot' },
|
|
342
|
+
{ label: 'Gemini', value: 'gemini' },
|
|
343
|
+
{ label: 'OpenRouter', value: 'openrouter' },
|
|
344
|
+
]));
|
|
345
|
+
const chosen = await chooseModelForProvider(aiProvider, undefined, 'Back to AI provider menu');
|
|
346
|
+
if (!chosen || chosen === 'back')
|
|
347
|
+
continue;
|
|
348
|
+
switch (aiProvider) {
|
|
349
|
+
case 'gemini': {
|
|
350
|
+
geminiModel = chosen;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case 'copilot': {
|
|
354
|
+
copilotModel = chosen;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case 'openrouter': {
|
|
358
|
+
openrouterModel = chosen;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
providerChosen = true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Preview and confirm
|
|
367
|
+
console.log('');
|
|
368
|
+
console.log(`${colors.cyan}┌${line}┐${colors.reset}`);
|
|
369
|
+
console.log(`${colors.cyan}│${colors.reset} ${colors.bright}Sync Plan${colors.reset}`);
|
|
370
|
+
console.log(`${colors.cyan}├${line}┤${colors.reset}`);
|
|
371
|
+
for (const tag of tagsToRelease) {
|
|
372
|
+
const ver = tag.replace(/^v/, '');
|
|
373
|
+
console.log(`${colors.cyan}│${colors.reset} ${colors.green}+${colors.reset} Create release for ${colors.yellow}${ver}${colors.reset}`);
|
|
374
|
+
}
|
|
375
|
+
console.log(`${colors.cyan}│${colors.reset} ${colors.gray}Mode: ${useAI ? 'AI-generated' : 'Template-based'}${colors.reset}`);
|
|
376
|
+
console.log(`${colors.cyan}└${line}┘${colors.reset}`);
|
|
377
|
+
console.log('');
|
|
378
|
+
const proceed = confirm(`Create ${tagsToRelease.length} GitHub Releases?`);
|
|
379
|
+
if (!proceed)
|
|
380
|
+
return;
|
|
381
|
+
// Create releases one by one
|
|
382
|
+
const allTags = getExistingTags();
|
|
383
|
+
let successCount = 0;
|
|
384
|
+
for (const tag of tagsToRelease) {
|
|
385
|
+
const ver = tag.replace(/^v/, '');
|
|
386
|
+
const tagIdx = allTags.indexOf(tag);
|
|
387
|
+
const prevTag = allTags[tagIdx + 1];
|
|
388
|
+
const commits = getCommitsSinceTag(prevTag);
|
|
389
|
+
const commitList = commits.map((c) => c.subject).join('\n');
|
|
390
|
+
let releaseBody;
|
|
391
|
+
if (useAI && commits.length > 0) {
|
|
392
|
+
console.log('');
|
|
393
|
+
const aiSpinner = log.spinner();
|
|
394
|
+
const modelDisplay = getModelValue(copilotModel ?? openrouterModel ?? geminiModel ?? '');
|
|
395
|
+
aiSpinner.start(`Generating notes for ${tag} with ${getAIProviderShortName(aiProvider)}` +
|
|
396
|
+
(modelDisplay ? ` (${modelDisplay})` : '') +
|
|
397
|
+
'...');
|
|
398
|
+
const aiResult = await generateReleaseNotesWithProvider(aiProvider, commitList, language, undefined, copilotModel, openrouterModel, geminiModel);
|
|
399
|
+
if (aiResult) {
|
|
400
|
+
aiSpinner.succeed(`Notes generated for ${tag}`);
|
|
401
|
+
releaseBody = normalizeReleaseMarkdown(aiResult);
|
|
402
|
+
// Preview notes for user review
|
|
403
|
+
console.log('');
|
|
404
|
+
console.log(`${colors.cyan}┌${'─'.repeat(56)}┐${colors.reset}`);
|
|
405
|
+
console.log(`${colors.cyan}│${colors.reset} ${colors.bright}Release Notes — ${tag}${colors.reset}`);
|
|
406
|
+
console.log(`${colors.cyan}├${'─'.repeat(56)}┤${colors.reset}`);
|
|
407
|
+
for (const noteLine of releaseBody.split('\n')) {
|
|
408
|
+
console.log(`${colors.cyan}│${colors.reset} ${noteLine}`);
|
|
409
|
+
}
|
|
410
|
+
console.log(`${colors.cyan}└${'─'.repeat(56)}┘${colors.reset}`);
|
|
411
|
+
console.log('');
|
|
412
|
+
const reviewAction = await select(`Publish release for ${tag}?`, [
|
|
413
|
+
{ label: 'Yes, publish', value: 'accept' },
|
|
414
|
+
{ label: 'Skip this tag', value: 'skip' },
|
|
415
|
+
]);
|
|
416
|
+
if (reviewAction === 'skip')
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
aiSpinner.fail(`AI failed for ${tag}, using template`);
|
|
421
|
+
useAI = false;
|
|
422
|
+
releaseBody = generateReleaseMd(ver, commits, prevTag?.replace(/^v/, '') ?? '0.0.0')
|
|
423
|
+
.replace(/^## .*\n+/, '')
|
|
424
|
+
.replace(/\n---\n*$/, '')
|
|
425
|
+
.trim();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
releaseBody = generateReleaseMd(ver, commits, prevTag?.replace(/^v/, '') ?? '0.0.0')
|
|
430
|
+
.replace(/^## .*\n+/, '')
|
|
431
|
+
.replace(/\n---\n*$/, '')
|
|
432
|
+
.trim();
|
|
433
|
+
}
|
|
434
|
+
console.log('');
|
|
435
|
+
const releaseSpinner = log.spinner();
|
|
436
|
+
// Ensure tag exists on remote before creating GitHub Release
|
|
437
|
+
releaseSpinner.start(`Pushing tag ${colors.yellow}${tag}${colors.reset} to remote...`);
|
|
438
|
+
try {
|
|
439
|
+
await execAsync(`git push origin ${tag} --no-verify`, true);
|
|
440
|
+
releaseSpinner.succeed(`Tag ${tag} pushed to remote`);
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
// Tag might already exist on remote — that's fine, continue
|
|
444
|
+
}
|
|
445
|
+
console.log('');
|
|
446
|
+
const createSpinner = log.spinner();
|
|
447
|
+
createSpinner.start(`Creating release ${colors.yellow}${tag}${colors.reset}...`);
|
|
448
|
+
const os = await import('node:os');
|
|
449
|
+
const tempFile = `${os.tmpdir()}/geeto-sync-${Date.now()}.md`;
|
|
450
|
+
writeFileSync(tempFile, releaseBody, 'utf8');
|
|
451
|
+
try {
|
|
452
|
+
await execAsync(`gh release create ${tag} --title "${tag}" --notes-file "${tempFile}"`, true);
|
|
453
|
+
createSpinner.succeed(`Release ${tag} created`);
|
|
454
|
+
successCount++;
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
const stderr = error.stderr?.trim();
|
|
458
|
+
createSpinner.fail(`Failed to create release for ${tag}`);
|
|
459
|
+
if (stderr)
|
|
460
|
+
log.error(` ${stderr.split('\n')[0]}`);
|
|
461
|
+
}
|
|
462
|
+
// Cleanup temp file
|
|
463
|
+
try {
|
|
464
|
+
const { unlinkSync } = await import('node:fs');
|
|
465
|
+
unlinkSync(tempFile);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
/* ignore */
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
console.log('');
|
|
472
|
+
if (successCount === tagsToRelease.length) {
|
|
473
|
+
log.success(`All ${successCount} GitHub Releases created!`);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
log.warn(`${successCount}/${tagsToRelease.length} releases created`);
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
// ─── Delete GitHub Releases ───
|
|
480
|
+
const handleDeleteReleases = async () => {
|
|
481
|
+
// Check if gh CLI is available
|
|
482
|
+
try {
|
|
483
|
+
execSilent('gh --version');
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
log.error('GitHub CLI (gh) is not installed. Install it: https://cli.github.com');
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
console.log('');
|
|
490
|
+
const spinner = log.spinner();
|
|
491
|
+
spinner.start('Fetching GitHub releases...');
|
|
492
|
+
const ghReleases = getExistingGithubReleases();
|
|
493
|
+
spinner.succeed(`Found ${ghReleases.length} GitHub releases`);
|
|
494
|
+
if (ghReleases.length === 0) {
|
|
495
|
+
console.log('');
|
|
496
|
+
log.info('No GitHub Releases to delete.');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
console.log('');
|
|
500
|
+
const { multiSelect } = await import('../cli/menu.js');
|
|
501
|
+
const choices = ghReleases.map((t) => ({ label: t, value: t }));
|
|
502
|
+
const selected = await multiSelect('Select releases to delete:', choices);
|
|
503
|
+
if (selected.length === 0) {
|
|
504
|
+
log.info('No releases selected.');
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
console.log('');
|
|
508
|
+
const alsoDeleteTag = confirm('Also delete the associated git tags?');
|
|
509
|
+
console.log('');
|
|
510
|
+
const proceed = confirm(`Delete ${selected.length} GitHub Release(s)${alsoDeleteTag ? ' + tags' : ''}?`);
|
|
511
|
+
if (!proceed)
|
|
512
|
+
return;
|
|
513
|
+
let successCount = 0;
|
|
514
|
+
for (const release of selected) {
|
|
515
|
+
console.log('');
|
|
516
|
+
const releaseSpinner = log.spinner();
|
|
517
|
+
releaseSpinner.start(`Deleting release ${colors.yellow}${release}${colors.reset}...`);
|
|
518
|
+
try {
|
|
519
|
+
await execAsync(`gh release delete ${release} --yes`, true);
|
|
520
|
+
if (alsoDeleteTag) {
|
|
521
|
+
try {
|
|
522
|
+
await execAsync(`git tag -d ${release}`, true);
|
|
523
|
+
await execAsync(`git push origin --delete ${release} --no-verify`, true);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
/* Tag deletion is best-effort */
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
releaseSpinner.succeed(`Release ${release} deleted${alsoDeleteTag ? ' + tag' : ''}`);
|
|
530
|
+
successCount++;
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
const stderr = error.stderr?.trim();
|
|
534
|
+
releaseSpinner.fail(`Failed to delete ${release}`);
|
|
535
|
+
if (stderr)
|
|
536
|
+
log.error(` ${stderr.split('\n')[0]}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
console.log('');
|
|
540
|
+
if (successCount === selected.length) {
|
|
541
|
+
log.success(`All ${successCount} releases deleted!`);
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
log.warn(`${successCount}/${selected.length} releases deleted`);
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
// ─── Recover missing tags ───
|
|
548
|
+
const handleRecoverTags = async () => {
|
|
549
|
+
const line = '─'.repeat(56);
|
|
550
|
+
console.log('');
|
|
551
|
+
const spinner = log.spinner();
|
|
552
|
+
spinner.start('Scanning release commits...');
|
|
553
|
+
// Find all release commits: "chore(release): vX.Y.Z"
|
|
554
|
+
let gitLog;
|
|
555
|
+
try {
|
|
556
|
+
gitLog = exec('git log --all --oneline --grep="^chore(release): v" --format="%H %s"', true);
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
spinner.fail('Failed to scan git log');
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const releasePattern = /^([a-f0-9]+) chore\(release\): v(.+)$/;
|
|
563
|
+
const releaseCommits = [];
|
|
564
|
+
for (const logLine of gitLog.split('\n').filter(Boolean)) {
|
|
565
|
+
const match = logLine.match(releasePattern);
|
|
566
|
+
if (match?.[1] && match[2]) {
|
|
567
|
+
releaseCommits.push({
|
|
568
|
+
hash: match[1],
|
|
569
|
+
version: match[2],
|
|
570
|
+
tag: `v${match[2]}`,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (releaseCommits.length === 0) {
|
|
575
|
+
spinner.fail('No release commits found');
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// Compare with existing tags
|
|
579
|
+
const existingTags = new Set(getExistingTags());
|
|
580
|
+
const missingTags = releaseCommits.filter((rc) => !existingTags.has(rc.tag));
|
|
581
|
+
spinner.succeed(`Found ${releaseCommits.length} release commits, ${existingTags.size} tags`);
|
|
582
|
+
if (missingTags.length === 0) {
|
|
583
|
+
console.log('');
|
|
584
|
+
log.success('All release commits have matching tags! Nothing to recover.');
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
// Show missing tags
|
|
588
|
+
console.log('');
|
|
589
|
+
log.info(`${colors.bright}${missingTags.length}${colors.reset} tags missing (release commit exists but no tag):`);
|
|
590
|
+
for (const mt of missingTags) {
|
|
591
|
+
console.log(` ${colors.yellow}${mt.tag}${colors.reset} ${colors.gray}← ${mt.hash.slice(0, 7)}${colors.reset}`);
|
|
592
|
+
}
|
|
593
|
+
console.log('');
|
|
594
|
+
const action = await select('What do you want to do?', [
|
|
595
|
+
{ label: 'Recover all missing tags', value: 'all' },
|
|
596
|
+
{ label: 'Select which tags to recover', value: 'select' },
|
|
597
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
598
|
+
]);
|
|
599
|
+
if (action === 'cancel')
|
|
600
|
+
return;
|
|
601
|
+
let tagsToRecover = missingTags;
|
|
602
|
+
if (action === 'select') {
|
|
603
|
+
const { multiSelect } = await import('../cli/menu.js');
|
|
604
|
+
const choices = missingTags.map((mt) => ({
|
|
605
|
+
label: `${mt.tag} (${mt.hash.slice(0, 7)})`,
|
|
606
|
+
value: mt.tag,
|
|
607
|
+
}));
|
|
608
|
+
const selected = await multiSelect('Select tags to recover:', choices);
|
|
609
|
+
if (selected.length === 0) {
|
|
610
|
+
log.info('No tags selected.');
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
tagsToRecover = missingTags.filter((mt) => selected.includes(mt.tag));
|
|
614
|
+
}
|
|
615
|
+
// Preview
|
|
616
|
+
console.log('');
|
|
617
|
+
console.log(`${colors.cyan}┌${line}┐${colors.reset}`);
|
|
618
|
+
console.log(`${colors.cyan}│${colors.reset} ${colors.bright}Recovery Plan${colors.reset}`);
|
|
619
|
+
console.log(`${colors.cyan}├${line}┤${colors.reset}`);
|
|
620
|
+
for (const mt of tagsToRecover) {
|
|
621
|
+
console.log(`${colors.cyan}│${colors.reset} ${colors.green}+${colors.reset} ${colors.yellow}${mt.tag}${colors.reset} → commit ${colors.gray}${mt.hash.slice(0, 7)}${colors.reset}`);
|
|
622
|
+
}
|
|
623
|
+
console.log(`${colors.cyan}└${line}┘${colors.reset}`);
|
|
624
|
+
console.log('');
|
|
625
|
+
const proceed = confirm(`Create ${tagsToRecover.length} tags?`);
|
|
626
|
+
if (!proceed)
|
|
627
|
+
return;
|
|
628
|
+
let successCount = 0;
|
|
629
|
+
for (const mt of tagsToRecover) {
|
|
630
|
+
console.log('');
|
|
631
|
+
const tagSpinner = log.spinner();
|
|
632
|
+
tagSpinner.start(`Creating tag ${colors.yellow}${mt.tag}${colors.reset}...`);
|
|
633
|
+
try {
|
|
634
|
+
exec(`git tag -a ${mt.tag} ${mt.hash} -m "Release ${mt.tag}"`, true);
|
|
635
|
+
tagSpinner.succeed(`Tag ${mt.tag} created`);
|
|
636
|
+
successCount++;
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
640
|
+
tagSpinner.fail(`Failed to create ${mt.tag}`);
|
|
641
|
+
log.error(` ${errMsg.split('\n')[0]}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
console.log('');
|
|
645
|
+
if (successCount === tagsToRecover.length) {
|
|
646
|
+
log.success(`All ${successCount} tags recovered!`);
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
log.warn(`${successCount}/${tagsToRecover.length} tags recovered`);
|
|
650
|
+
}
|
|
651
|
+
// Offer to push tags to remote
|
|
652
|
+
if (successCount > 0) {
|
|
653
|
+
console.log('');
|
|
654
|
+
const pushTags = confirm('Push recovered tags to remote?');
|
|
655
|
+
if (pushTags) {
|
|
656
|
+
console.log('');
|
|
657
|
+
const pushSpinner = log.spinner();
|
|
658
|
+
pushSpinner.start('Pushing tags to remote...');
|
|
659
|
+
try {
|
|
660
|
+
await execAsync('git push --tags --no-verify', true);
|
|
661
|
+
pushSpinner.succeed('Tags pushed to remote');
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
const stderr = error.stderr?.trim();
|
|
665
|
+
pushSpinner.fail('Failed to push tags');
|
|
666
|
+
if (stderr)
|
|
667
|
+
log.error(` ${stderr.split('\n')[0]}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
};
|
|
226
672
|
// ─── Main handler ───
|
|
227
673
|
export const handleRelease = async () => {
|
|
228
674
|
log.banner();
|
|
229
675
|
log.step(`${colors.cyan}Release / Tag Manager${colors.reset}\n`);
|
|
676
|
+
// Main menu: create new release, sync, or manage releases
|
|
677
|
+
const mode = await select('What do you want to do?', [
|
|
678
|
+
{ label: 'Create a new release', value: 'create' },
|
|
679
|
+
{ label: 'Sync GitHub Releases for existing tags', value: 'sync' },
|
|
680
|
+
{ label: 'Recover missing tags from release commits', value: 'recover' },
|
|
681
|
+
{ label: 'Delete GitHub Releases', value: 'delete' },
|
|
682
|
+
]);
|
|
683
|
+
if (mode === 'sync') {
|
|
684
|
+
await handleSyncReleases();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (mode === 'recover') {
|
|
688
|
+
await handleRecoverTags();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (mode === 'delete') {
|
|
692
|
+
await handleDeleteReleases();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
230
695
|
const currentVersion = getCurrentVersion();
|
|
231
696
|
const semver = parseSemver(currentVersion);
|
|
232
697
|
if (!semver) {
|
|
@@ -366,10 +831,6 @@ export const handleRelease = async () => {
|
|
|
366
831
|
copilotModel = savedState.copilotModel;
|
|
367
832
|
openrouterModel = savedState.openrouterModel;
|
|
368
833
|
geminiModel = savedState.geminiModel;
|
|
369
|
-
const modelDisplay = getModelValue(copilotModel ?? openrouterModel ?? geminiModel ?? '');
|
|
370
|
-
log.info(`Using saved AI config: ${getAIProviderShortName(aiProvider)}` +
|
|
371
|
-
(modelDisplay ? ` (${modelDisplay})` : ''));
|
|
372
|
-
console.log('');
|
|
373
834
|
}
|
|
374
835
|
else {
|
|
375
836
|
// No saved config — ask user to pick provider + model
|
|
@@ -541,7 +1002,9 @@ export const handleRelease = async () => {
|
|
|
541
1002
|
month: 'long',
|
|
542
1003
|
day: 'numeric',
|
|
543
1004
|
});
|
|
544
|
-
|
|
1005
|
+
const normalizedNotes = normalizeReleaseMarkdown(aiReleaseNotes);
|
|
1006
|
+
newEntry =
|
|
1007
|
+
[`## v${newVersion} — ${date}`, '', normalizedNotes, '', '---', ''].join('\n') + '\n';
|
|
545
1008
|
}
|
|
546
1009
|
else {
|
|
547
1010
|
newEntry = generateReleaseMd(newVersion, commits, currentVersion);
|
|
@@ -631,8 +1094,8 @@ export const handleRelease = async () => {
|
|
|
631
1094
|
// 5. Push
|
|
632
1095
|
console.log('');
|
|
633
1096
|
const pushChoice = await select('Push to remote?', [
|
|
634
|
-
{ label: 'Push
|
|
635
|
-
{ label: 'Push
|
|
1097
|
+
{ label: 'Push release + tag', value: 'both' },
|
|
1098
|
+
{ label: 'Push release only', value: 'commit' },
|
|
636
1099
|
{ label: 'Skip pushing', value: 'skip' },
|
|
637
1100
|
]);
|
|
638
1101
|
if (pushChoice === 'both' || pushChoice === 'commit') {
|
|
@@ -645,13 +1108,13 @@ export const handleRelease = async () => {
|
|
|
645
1108
|
try {
|
|
646
1109
|
await execAsync(`git push`, true);
|
|
647
1110
|
if (pushChoice === 'both') {
|
|
648
|
-
await execAsync(`git push origin v${newVersion}`, true);
|
|
1111
|
+
await execAsync(`git push origin v${newVersion} --no-verify`, true);
|
|
649
1112
|
}
|
|
650
1113
|
clearInterval(interval);
|
|
651
1114
|
progressBar.update(100);
|
|
652
1115
|
progressBar.complete();
|
|
653
1116
|
console.log('');
|
|
654
|
-
log.success(pushChoice === 'both' ? 'Pushed
|
|
1117
|
+
log.success(pushChoice === 'both' ? 'Pushed release + tag' : 'Pushed release');
|
|
655
1118
|
}
|
|
656
1119
|
catch {
|
|
657
1120
|
clearInterval(interval);
|
|
@@ -660,6 +1123,49 @@ export const handleRelease = async () => {
|
|
|
660
1123
|
log.error('Failed to push');
|
|
661
1124
|
}
|
|
662
1125
|
}
|
|
1126
|
+
// 6. Create GitHub Release (if tag was pushed and gh CLI is available)
|
|
1127
|
+
let ghReleaseCreated = false;
|
|
1128
|
+
if (pushChoice === 'both') {
|
|
1129
|
+
try {
|
|
1130
|
+
execSilent('gh --version');
|
|
1131
|
+
// gh CLI is available — create a GitHub Release
|
|
1132
|
+
// Build release body from AI notes or template
|
|
1133
|
+
const releaseBody = aiReleaseNotes
|
|
1134
|
+
? normalizeReleaseMarkdown(aiReleaseNotes)
|
|
1135
|
+
: generateReleaseMd(newVersion, commits, currentVersion)
|
|
1136
|
+
.replace(/^## .*\n+/, '')
|
|
1137
|
+
.replace(/\n---\n*$/, '')
|
|
1138
|
+
.trim();
|
|
1139
|
+
// Write to temp file to avoid shell quoting issues
|
|
1140
|
+
const os = await import('node:os');
|
|
1141
|
+
const tempFile = `${os.tmpdir()}/geeto-release-${Date.now()}.md`;
|
|
1142
|
+
writeFileSync(tempFile, releaseBody, 'utf8');
|
|
1143
|
+
const releaseSpinner = log.spinner();
|
|
1144
|
+
releaseSpinner.start('Creating GitHub Release...');
|
|
1145
|
+
try {
|
|
1146
|
+
await execAsync(`gh release create v${newVersion} --title "v${newVersion}" --notes-file "${tempFile}"`, true);
|
|
1147
|
+
releaseSpinner.succeed('GitHub Release created');
|
|
1148
|
+
ghReleaseCreated = true;
|
|
1149
|
+
}
|
|
1150
|
+
catch (error) {
|
|
1151
|
+
const stderr = error.stderr?.trim();
|
|
1152
|
+
releaseSpinner.fail('Failed to create GitHub Release');
|
|
1153
|
+
if (stderr)
|
|
1154
|
+
log.error(` ${stderr.split('\n')[0]}`);
|
|
1155
|
+
}
|
|
1156
|
+
// Cleanup temp file
|
|
1157
|
+
try {
|
|
1158
|
+
const { unlinkSync } = await import('node:fs');
|
|
1159
|
+
unlinkSync(tempFile);
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
/* ignore cleanup errors */
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
catch {
|
|
1166
|
+
// gh CLI not available — skip silently
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
663
1169
|
// Summary
|
|
664
1170
|
console.log('');
|
|
665
1171
|
console.log(`${colors.cyan}┌${line}┐${colors.reset}`);
|
|
@@ -669,6 +1175,9 @@ export const handleRelease = async () => {
|
|
|
669
1175
|
console.log(`${colors.cyan}│${colors.reset} ${colors.green}✓${colors.reset} RELEASE.MD generated ${colors.gray}(user-facing)${colors.reset}`);
|
|
670
1176
|
console.log(`${colors.cyan}│${colors.reset} ${colors.green}✓${colors.reset} CHANGELOG.md updated ${colors.gray}(developer-facing)${colors.reset}`);
|
|
671
1177
|
console.log(`${colors.cyan}│${colors.reset} ${colors.green}✓${colors.reset} Tag v${newVersion} created`);
|
|
1178
|
+
if (ghReleaseCreated) {
|
|
1179
|
+
console.log(`${colors.cyan}│${colors.reset} ${colors.green}✓${colors.reset} GitHub Release published`);
|
|
1180
|
+
}
|
|
672
1181
|
console.log(`${colors.cyan}└${line}┘${colors.reset}`);
|
|
673
1182
|
};
|
|
674
1183
|
//# sourceMappingURL=release.js.map
|