@wipcomputer/wip-release 1.9.8 → 1.9.10

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.
Files changed (3) hide show
  1. package/cli.js +31 -14
  2. package/core.mjs +31 -1
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -20,18 +20,26 @@ const dryRun = args.includes('--dry-run');
20
20
  const noPublish = args.includes('--no-publish');
21
21
  const skipProductCheck = args.includes('--skip-product-check');
22
22
  const skipStaleCheck = args.includes('--skip-stale-check');
23
+ const skipWorktreeCheck = args.includes('--skip-worktree-check');
23
24
  const notesFilePath = flag('notes-file');
24
25
  let notes = flag('notes');
25
26
  let notesSource = notes ? 'flag' : 'none'; // track where notes came from
26
27
 
27
- // Auto-detect RELEASE-NOTES-v{version}.md if no --notes or --notes-file provided.
28
- // Also supports explicit --notes-file for custom paths.
28
+ // Release notes priority (highest wins):
29
+ // 1. --notes-file=path Explicit file path (always wins)
30
+ // 2. RELEASE-NOTES-v{ver}.md In repo root (always wins over --notes flag)
31
+ // 3. ai/dev-updates/YYYY-MM-DD* Today's dev update (wins over --notes flag if longer)
32
+ // 4. --notes="text" Flag fallback (only if nothing better exists)
33
+ //
34
+ // Rule: written release notes on disk ALWAYS beat a CLI one-liner.
35
+ // The --notes flag is a fallback, not an override.
29
36
  {
30
37
  const { readFileSync, existsSync } = await import('node:fs');
31
38
  const { resolve, join } = await import('node:path');
39
+ const flagNotes = notes; // save original flag value for fallback
32
40
 
33
41
  if (notesFilePath) {
34
- // Explicit --notes-file
42
+ // 1. Explicit --notes-file (highest priority)
35
43
  const resolved = resolve(notesFilePath);
36
44
  if (!existsSync(resolved)) {
37
45
  console.error(` ✗ Notes file not found: ${resolved}`);
@@ -39,8 +47,8 @@ let notesSource = notes ? 'flag' : 'none'; // track where notes came from
39
47
  }
40
48
  notes = readFileSync(resolved, 'utf8').trim();
41
49
  notesSource = 'file';
42
- } else if (!notes && level) {
43
- // Auto-detect: compute the next version and look for RELEASE-NOTES-v{version}.md
50
+ } else if (level) {
51
+ // 2. Auto-detect RELEASE-NOTES-v{version}.md (ALWAYS checks, even if --notes provided)
44
52
  try {
45
53
  const { detectCurrentVersion, bumpSemver } = await import('./core.mjs');
46
54
  const cwd = process.cwd();
@@ -49,15 +57,19 @@ let notesSource = notes ? 'flag' : 'none'; // track where notes came from
49
57
  const dashed = newVersion.replace(/\./g, '-');
50
58
  const autoFile = join(cwd, `RELEASE-NOTES-v${dashed}.md`);
51
59
  if (existsSync(autoFile)) {
52
- notes = readFileSync(autoFile, 'utf8').trim();
60
+ const fileContent = readFileSync(autoFile, 'utf8').trim();
61
+ if (flagNotes && flagNotes !== fileContent) {
62
+ console.log(` ! --notes flag ignored: RELEASE-NOTES-v${dashed}.md takes priority`);
63
+ }
64
+ notes = fileContent;
53
65
  notesSource = 'file';
54
66
  console.log(` ✓ Found RELEASE-NOTES-v${dashed}.md`);
55
67
  }
56
68
  } catch {}
57
69
  }
58
70
 
59
- // Auto-detect dev update from ai/dev-updates/ if notes are missing or thin
60
- if (level && (!notes || notes.length < 100)) {
71
+ // 3. Auto-detect dev update from ai/dev-updates/ (wins over --notes flag if longer)
72
+ if (level && (!notes || (notesSource === 'flag' && notes.length < 200))) {
61
73
  try {
62
74
  const { readdirSync } = await import('node:fs');
63
75
  const devUpdatesDir = join(process.cwd(), 'ai', 'dev-updates');
@@ -72,6 +84,9 @@ let notesSource = notes ? 'flag' : 'none'; // track where notes came from
72
84
  const devUpdatePath = join(devUpdatesDir, todayFiles[0]);
73
85
  const devUpdateContent = readFileSync(devUpdatePath, 'utf8').trim();
74
86
  if (devUpdateContent.length > (notes || '').length) {
87
+ if (flagNotes) {
88
+ console.log(` ! --notes flag ignored: dev update takes priority`);
89
+ }
75
90
  notes = devUpdateContent;
76
91
  notesSource = 'dev-update';
77
92
  console.log(` ✓ Found dev update: ai/dev-updates/${todayFiles[0]}`);
@@ -101,13 +116,14 @@ Flags:
101
116
  --no-publish Bump + tag only, skip npm/GitHub
102
117
  --skip-product-check Skip product docs check (dev update, roadmap, readme-first)
103
118
  --skip-stale-check Skip stale remote branch check
119
+ --skip-worktree-check Skip worktree guard (allow release from worktree)
104
120
 
105
- Release notes:
106
- Auto-detects notes from three sources (first match wins):
107
- 1. --notes-file=path Explicit file path
108
- 2. RELEASE-NOTES-v{ver}.md In repo root (e.g. RELEASE-NOTES-v1-7-4.md)
109
- 3. ai/dev-updates/YYYY-MM-DD* Today's dev update files (most recent first)
110
- Write dev updates as you work. wip-release picks them up automatically.
121
+ Release notes (highest priority wins, files ALWAYS beat --notes flag):
122
+ 1. --notes-file=path Explicit file path (always wins)
123
+ 2. RELEASE-NOTES-v{ver}.md In repo root (wins over --notes)
124
+ 3. ai/dev-updates/YYYY-MM-DD* Today's dev update (wins over --notes if longer)
125
+ 4. --notes="text" Fallback only (use for repos without release notes files)
126
+ Written notes on disk always take priority over a CLI one-liner.
111
127
 
112
128
  Pipeline:
113
129
  1. Bump package.json version
@@ -130,6 +146,7 @@ release({
130
146
  noPublish,
131
147
  skipProductCheck,
132
148
  skipStaleCheck,
149
+ skipWorktreeCheck,
133
150
  }).catch(err => {
134
151
  console.error(` ✗ ${err.message}`);
135
152
  process.exit(1);
package/core.mjs CHANGED
@@ -552,7 +552,7 @@ export function checkStaleBranches(repoPath, level) {
552
552
  /**
553
553
  * Run the full release pipeline.
554
554
  */
555
- export async function release({ repoPath, level, notes, notesSource, dryRun, noPublish, skipProductCheck, skipStaleCheck }) {
555
+ export async function release({ repoPath, level, notes, notesSource, dryRun, noPublish, skipProductCheck, skipStaleCheck, skipWorktreeCheck }) {
556
556
  repoPath = repoPath || process.cwd();
557
557
  const currentVersion = detectCurrentVersion(repoPath);
558
558
  const newVersion = bumpSemver(currentVersion, level);
@@ -562,6 +562,36 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
562
562
  console.log(` ${repoName}: ${currentVersion} -> ${newVersion} (${level})`);
563
563
  console.log(` ${'─'.repeat(40)}`);
564
564
 
565
+ // -1. Worktree guard: block releases from linked worktrees
566
+ if (!skipWorktreeCheck) {
567
+ try {
568
+ const gitDir = execFileSync('git', ['rev-parse', '--git-dir'], {
569
+ cwd: repoPath, encoding: 'utf8'
570
+ }).trim();
571
+
572
+ // Linked worktrees have "/worktrees/" in their git-dir path
573
+ if (gitDir.includes('/worktrees/')) {
574
+ // Get the main working tree path from `git worktree list`
575
+ const worktreeList = execFileSync('git', ['worktree', 'list', '--porcelain'], {
576
+ cwd: repoPath, encoding: 'utf8'
577
+ });
578
+ const mainWorktree = worktreeList.split('\n')
579
+ .find(line => line.startsWith('worktree '));
580
+ const mainPath = mainWorktree ? mainWorktree.replace('worktree ', '') : '(unknown)';
581
+
582
+ console.log(` \u2717 wip-release must run from the main working tree, not a worktree.`);
583
+ console.log(` Current: ${repoPath}`);
584
+ console.log(` Main working tree: ${mainPath}`);
585
+ console.log(` Switch to the main working tree and run again.`);
586
+ console.log('');
587
+ return { currentVersion, newVersion, dryRun: false, failed: true };
588
+ }
589
+ console.log(' \u2713 Running from main working tree');
590
+ } catch {
591
+ // Git command failed... skip check gracefully
592
+ }
593
+ }
594
+
565
595
  // 0. License compliance gate
566
596
  const configPath = join(repoPath, '.license-guard.json');
567
597
  if (existsSync(configPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-release",
3
- "version": "1.9.8",
3
+ "version": "1.9.10",
4
4
  "type": "module",
5
5
  "description": "One-command release pipeline. Bumps version, updates changelog + SKILL.md, publishes to npm + GitHub.",
6
6
  "main": "core.mjs",