@wipcomputer/wip-ai-devops-toolbox 1.9.61 → 1.9.63

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/CHANGELOG.md CHANGED
@@ -31,6 +31,56 @@
31
31
 
32
32
 
33
33
 
34
+
35
+ ## 1.9.63 (2026-03-29)
36
+
37
+ # Release Notes: wip-ai-devops-toolbox v1.9.63
38
+
39
+ **Fix all wip-release errors: branch cleanup crashes, shell injection, stale remote refs.**
40
+
41
+ ## The story
42
+
43
+ Every wip-release run produced errors: "fatal: Not a valid object name +", "remote ref does not exist", and shell injection risks from branch names passed through execSync template strings. These were dismissed as "non-blocking" but they cluttered every release output and masked real problems.
44
+
45
+ Root cause: branch cleanup code (sections 10 and 11) used `execSync` with template strings, which breaks on branch names with special characters and allows shell injection. Also tried to delete remote branches that GitHub already deleted during PR merge.
46
+
47
+ Fix: replaced all `execSync` template strings with `execFileSync` array args (safe from injection). Added character validation to skip branches with special chars. Wrapped remote delete in try/catch since GitHub PR merge already handles deletion.
48
+
49
+ ## Issues closed
50
+
51
+ - #231 (continued: release pipeline reliability)
52
+
53
+ ## How to verify
54
+
55
+ ```bash
56
+ wip-release patch --dry-run
57
+ # Should show no "fatal" or "Not a valid object name" errors
58
+ # Guard tests: cd tools/wip-branch-guard && bash test.sh
59
+ ```
60
+
61
+ ## 1.9.62 (2026-03-29)
62
+
63
+ # Release Notes: wip-ai-devops-toolbox v1.9.62
64
+
65
+ **Fix wip-release leaving dirty state on main after every release.**
66
+
67
+ ## The story
68
+
69
+ wip-release writes to 15+ files during a release (root package.json, 12 sub-tool package.json files, SKILL.md, CHANGELOG.md, product docs, trashed release notes). But gitCommitAndTag() only staged 3 files (package.json, CHANGELOG.md, SKILL.md). The other 12+ files were left modified on disk, uncommitted. This blocked git pull on the next operation and required manual `git checkout -- .` every time.
70
+
71
+ Fix: stage all files that wip-release modifies. Sub-tool package.json files, product docs (ai/product/), and trashed release notes (_trash/) are now included in the release commit.
72
+
73
+ ## Issues closed
74
+
75
+ - #231 (wip-release rollback version bumps on failure)
76
+
77
+ ## How to verify
78
+
79
+ ```bash
80
+ wip-release patch
81
+ git status
82
+ # Should show clean working tree after release
83
+ ```
34
84
 
35
85
  ## 1.9.61 (2026-03-29)
36
86
 
package/SKILL.md CHANGED
@@ -5,7 +5,7 @@ license: MIT
5
5
  interface: [cli, module, mcp, skill, hook, plugin]
6
6
  metadata:
7
7
  display-name: "WIP AI DevOps Toolbox"
8
- version: "1.9.61"
8
+ version: "1.9.63"
9
9
  homepage: "https://github.com/wipcomputer/wip-ai-devops-toolbox"
10
10
  author: "Parker Todd Brooks"
11
11
  category: dev-tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ai-devops-toolbox",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
4
4
  "type": "module",
5
5
  "description": "The complete AI DevOps toolkit for AI-assisted development teams.",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/deploy-public",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
4
4
  "description": "Private-to-public repo sync. Excludes ai/ folder, creates PR, merges, cleans up branches.",
5
5
  "bin": {
6
6
  "deploy-public": "./deploy-public.sh"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/post-merge-rename",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
4
4
  "description": "Post-merge branch renaming. Appends --merged-YYYY-MM-DD to preserve history.",
5
5
  "bin": {
6
6
  "post-merge-rename": "./post-merge-rename.sh"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-branch-guard",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
4
4
  "description": "PreToolUse hook that blocks all writes on main branch. Forces agents to work on branches or worktrees.",
5
5
  "type": "module",
6
6
  "main": "guard.mjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-file-guard",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
4
4
  "type": "module",
5
5
  "description": "Hook that blocks destructive edits to protected identity files. For Claude Code CLI and OpenClaw.",
6
6
  "main": "guard.mjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-license-guard",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
4
4
  "description": "License compliance for your own repos. Ensures correct copyright, dual-license blocks, and LICENSE files.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-license-hook",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
4
4
  "description": "License rug-pull detection and dependency license compliance for open source projects",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-readme-format",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
4
4
  "description": "Reformat any repo's README to follow the WIP Computer standard. Agent-first, human-readable.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -149,12 +149,37 @@ function trashReleaseNotes(repoPath) {
149
149
 
150
150
  function gitCommitAndTag(repoPath, newVersion, notes) {
151
151
  const msg = `v${newVersion}: ${notes || 'Release'}`;
152
- // Stage known files (ignore missing ones)
152
+ // Stage ALL files that wip-release modifies:
153
+ // - Root: package.json, CHANGELOG.md, SKILL.md
154
+ // - Sub-tools: tools/*/package.json
155
+ // - Product docs: ai/product/plans-prds/roadmap.md, ai/product/readme-first-product.md
156
+ // - Trashed release notes: _trash/RELEASE-NOTES-*.md
157
+ // Using git add -A on specific paths instead of listing each file (#231)
153
158
  for (const f of ['package.json', 'CHANGELOG.md', 'SKILL.md']) {
154
159
  if (existsSync(join(repoPath, f))) {
155
160
  execFileSync('git', ['add', f], { cwd: repoPath, stdio: 'pipe' });
156
161
  }
157
162
  }
163
+ // Stage sub-tool package.json files
164
+ const toolsDir = join(repoPath, 'tools');
165
+ if (existsSync(toolsDir)) {
166
+ for (const sub of readdirSync(toolsDir, { withFileTypes: true })) {
167
+ if (!sub.isDirectory()) continue;
168
+ const subPkg = join('tools', sub.name, 'package.json');
169
+ if (existsSync(join(repoPath, subPkg))) {
170
+ execFileSync('git', ['add', subPkg], { cwd: repoPath, stdio: 'pipe' });
171
+ }
172
+ }
173
+ }
174
+ // Stage product docs and trashed release notes
175
+ const aiProduct = join(repoPath, 'ai', 'product');
176
+ if (existsSync(aiProduct)) {
177
+ execFileSync('git', ['add', 'ai/product/'], { cwd: repoPath, stdio: 'pipe' });
178
+ }
179
+ const trash = join(repoPath, '_trash');
180
+ if (existsSync(trash)) {
181
+ execFileSync('git', ['add', '_trash/'], { cwd: repoPath, stdio: 'pipe' });
182
+ }
158
183
  // Use execFileSync to avoid shell injection via notes.
159
184
  // --no-verify: wip-release legitimately commits on main (version bump + changelog).
160
185
  // The pre-commit hook blocks all commits on main, but wip-release is the one exception.
@@ -1544,31 +1569,33 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
1544
1569
  .filter(b => b && b !== 'main' && b !== 'master' && !b.startsWith('*') && !b.includes('--merged-'));
1545
1570
 
1546
1571
  if (merged.length > 0) {
1572
+ const current = execSync('git branch --show-current', { cwd: repoPath, encoding: 'utf8' }).trim();
1547
1573
  console.log(` Scanning ${merged.length} merged branch(es) for rename...`);
1548
1574
  for (const branch of merged) {
1549
- const current = execSync('git branch --show-current', { cwd: repoPath, encoding: 'utf8' }).trim();
1550
1575
  if (branch === current) continue;
1576
+ // Skip branches with characters that break git commands
1577
+ if (/[+\s~^:?*\[\]]/.test(branch)) continue;
1551
1578
 
1552
1579
  let mergeDate;
1553
1580
  try {
1554
- const mergeBase = execSync(`git merge-base main ${branch}`, { cwd: repoPath, encoding: 'utf8' }).trim();
1555
- mergeDate = execSync(
1556
- `git log main --format="%ai" --ancestry-path ${mergeBase}..main`,
1557
- { cwd: repoPath, encoding: 'utf8' }
1558
- ).trim().split('\n').pop().split(' ')[0];
1581
+ // Use execFileSync (array args) instead of execSync (shell string) to avoid injection
1582
+ const mergeBase = execFileSync('git', ['merge-base', 'main', branch], { cwd: repoPath, encoding: 'utf8' }).trim();
1583
+ const logOutput = execFileSync('git', ['log', 'main', '--format=%ai', '--ancestry-path', `${mergeBase}..main`], { cwd: repoPath, encoding: 'utf8' }).trim();
1584
+ if (logOutput) mergeDate = logOutput.split('\n').pop().split(' ')[0];
1559
1585
  } catch {}
1560
1586
  if (!mergeDate) {
1561
1587
  try {
1562
- mergeDate = execSync(`git log ${branch} -1 --format="%ai"`, { cwd: repoPath, encoding: 'utf8' }).trim().split(' ')[0];
1588
+ mergeDate = execFileSync('git', ['log', branch, '-1', '--format=%ai'], { cwd: repoPath, encoding: 'utf8' }).trim().split(' ')[0];
1563
1589
  } catch {}
1564
1590
  }
1565
1591
  if (!mergeDate) continue;
1566
1592
 
1567
1593
  const newName = `${branch}--merged-${mergeDate}`;
1568
1594
  try {
1569
- execSync(`git branch -m "${branch}" "${newName}"`, { cwd: repoPath, stdio: 'pipe' });
1570
- execSync(`git push origin "${newName}"`, { cwd: repoPath, stdio: 'pipe' });
1571
- execSync(`git push origin --delete "${branch}"`, { cwd: repoPath, stdio: 'pipe' });
1595
+ execFileSync('git', ['branch', '-m', branch, newName], { cwd: repoPath, stdio: 'pipe' });
1596
+ execFileSync('git', ['push', 'origin', newName], { cwd: repoPath, stdio: 'pipe' });
1597
+ // Remote branch may already be deleted by GitHub PR merge. That's fine.
1598
+ try { execFileSync('git', ['push', 'origin', '--delete', branch], { cwd: repoPath, stdio: 'pipe' }); } catch {}
1572
1599
  console.log(` ✓ Renamed: ${branch} -> ${newName}`);
1573
1600
  } catch (e) {
1574
1601
  console.log(` ! Could not rename ${branch}: ${e.message}`);
@@ -1610,8 +1637,8 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
1610
1637
 
1611
1638
  for (let i = KEEP_COUNT; i < branches.length; i++) {
1612
1639
  try {
1613
- execSync(`git push origin --delete "${branches[i]}"`, { cwd: repoPath, stdio: 'pipe' });
1614
- execSync(`git branch -d "${branches[i]}" 2>/dev/null || true`, { cwd: repoPath, stdio: 'pipe', shell: true });
1640
+ execFileSync('git', ['push', 'origin', '--delete', branches[i]], { cwd: repoPath, stdio: 'pipe' });
1641
+ try { execFileSync('git', ['branch', '-d', branches[i]], { cwd: repoPath, stdio: 'pipe' }); } catch {}
1615
1642
  pruned++;
1616
1643
  } catch {}
1617
1644
  }
@@ -1634,11 +1661,12 @@ export async function release({ repoPath, level, notes, notesSource, dryRun, noP
1634
1661
  let staleCleaned = 0;
1635
1662
  for (const branch of allRemote) {
1636
1663
  if (branch === current) continue;
1664
+ if (/[+\s~^:?*\[\]]/.test(branch)) continue;
1637
1665
  try {
1638
- execSync(`git merge-base --is-ancestor origin/${branch} origin/main`, { cwd: repoPath, stdio: 'pipe' });
1666
+ execFileSync('git', ['merge-base', '--is-ancestor', `origin/${branch}`, 'origin/main'], { cwd: repoPath, stdio: 'pipe' });
1639
1667
  // If we get here, branch is fully merged
1640
- execSync(`git push origin --delete "${branch}"`, { cwd: repoPath, stdio: 'pipe' });
1641
- execSync(`git branch -d "${branch}" 2>/dev/null || true`, { cwd: repoPath, stdio: 'pipe', shell: true });
1668
+ try { execFileSync('git', ['push', 'origin', '--delete', branch], { cwd: repoPath, stdio: 'pipe' }); } catch {}
1669
+ try { execFileSync('git', ['branch', '-d', branch], { cwd: repoPath, stdio: 'pipe' }); } catch {}
1642
1670
  staleCleaned++;
1643
1671
  } catch {}
1644
1672
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-release",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-repo-init",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
4
4
  "description": "Scaffold the standard ai/ directory structure in any repo",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-repo-permissions-hook",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
4
4
  "type": "module",
5
5
  "description": "Repo visibility guard. Blocks repos from going public without a -private counterpart.",
6
6
  "main": "core.mjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-repos",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
4
4
  "type": "module",
5
5
  "description": "Repo manifest reconciler. Single source of truth for repo organization. Like prettier for folder structure.",
6
6
  "main": "core.mjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/universal-installer",
3
- "version": "1.9.61",
3
+ "version": "1.9.63",
4
4
  "type": "module",
5
5
  "description": "The Universal Interface specification for agent-native software. Teaches your AI how to build repos with every interface: CLI, Module, MCP Server, OpenClaw Plugin, Skill, Claude Code Hook.",
6
6
  "main": "detect.mjs",