cortex-tms 2.4.1 → 2.6.0-beta.0
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/README.md +72 -38
- package/bin/cortex-tms.js +0 -0
- package/dist/__tests__/release.test.d.ts +2 -0
- package/dist/__tests__/release.test.d.ts.map +1 -0
- package/dist/__tests__/release.test.js +484 -0
- package/dist/__tests__/release.test.js.map +1 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +181 -8
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/tutorial.d.ts +4 -0
- package/dist/commands/tutorial.d.ts.map +1 -0
- package/dist/commands/tutorial.js +203 -0
- package/dist/commands/tutorial.js.map +1 -0
- package/dist/utils/backup.d.ts +26 -0
- package/dist/utils/backup.d.ts.map +1 -0
- package/dist/utils/backup.js +143 -0
- package/dist/utils/backup.js.map +1 -0
- package/package.json +25 -13
- package/templates/CLAUDE.md +1 -1
- package/templates/FUTURE-ENHANCEMENTS.md +1 -1
- package/templates/NEXT-TASKS.md +1 -1
- package/templates/PROMPTS.md +8 -2
- package/templates/README.md +1 -1
- package/templates/docs/archive/v1.0-CHANGELOG.md +1 -1
- package/templates/docs/core/ARCHITECTURE.md +1 -1
- package/templates/docs/core/DECISIONS.md +1 -1
- package/templates/docs/core/DOMAIN-LOGIC.md +1 -1
- package/templates/docs/core/GLOSSARY.md +1 -1
- package/templates/docs/core/PATTERNS.md +1 -1
- package/templates/docs/core/SCHEMA.md +1 -1
- package/templates/docs/core/TROUBLESHOOTING.md +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cortex TMS 🧠
|
|
2
2
|
|
|
3
|
-
**The Universal AI-Optimized Project Boilerplate (v2.
|
|
3
|
+
**The Universal AI-Optimized Project Boilerplate (v2.6.0-beta.0)**
|
|
4
4
|
|
|
5
5
|
Cortex TMS is an **interactive operating system for AI-assisted development**. It's not just documentation—it's an **activation layer** that turns your repository into a machine-legible project constitution with intelligent tooling for version management and AI collaboration.
|
|
6
6
|
|
|
@@ -47,7 +47,25 @@ Traditional repos drown AI agents in thousands of lines of historical tasks and
|
|
|
47
47
|
|
|
48
48
|
## 🛠️ CLI Commands
|
|
49
49
|
|
|
50
|
-
Cortex TMS provides
|
|
50
|
+
Cortex TMS provides 6 production-ready commands (v2.5.0):
|
|
51
|
+
|
|
52
|
+
### `cortex-tms tutorial`
|
|
53
|
+
Interactive walkthrough teaching the "Cortex Way" - perfect for first-time users.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cortex-tms tutorial # Start the guided tour
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**What You'll Learn**:
|
|
60
|
+
- Project Dashboard: Using `status` to see your cockpit
|
|
61
|
+
- AI Activation: Using `prompt` to activate project-aware AI agents
|
|
62
|
+
- Zero-Drift Governance: Automated version sync with `docs:sync`
|
|
63
|
+
- Health Checks: Understanding `validate` and the Archive Protocol
|
|
64
|
+
- Safe Migration: Fearless template upgrades with backup/rollback
|
|
65
|
+
|
|
66
|
+
**Navigation**: Use arrow keys and Enter to progress, select Exit to quit anytime
|
|
67
|
+
|
|
68
|
+
---
|
|
51
69
|
|
|
52
70
|
### `cortex-tms init`
|
|
53
71
|
Initialize TMS structure in your project with interactive scope selection.
|
|
@@ -75,11 +93,14 @@ cortex-tms status # Visual dashboard with progress bars
|
|
|
75
93
|
```
|
|
76
94
|
|
|
77
95
|
### `cortex-tms migrate`
|
|
78
|
-
Intelligent version management—detect outdated templates and
|
|
96
|
+
Intelligent version management—detect outdated templates and automatically upgrade with safety backups.
|
|
79
97
|
|
|
80
98
|
```bash
|
|
81
|
-
cortex-tms migrate
|
|
82
|
-
cortex-tms migrate --
|
|
99
|
+
cortex-tms migrate # Analyze version status
|
|
100
|
+
cortex-tms migrate --apply # Auto-upgrade OUTDATED files (creates backup)
|
|
101
|
+
cortex-tms migrate --apply --force # Upgrade ALL files including customized
|
|
102
|
+
cortex-tms migrate --rollback # Restore from backup (interactive selection)
|
|
103
|
+
cortex-tms migrate --dry-run # Preview migration plan
|
|
83
104
|
```
|
|
84
105
|
|
|
85
106
|
**Status Categories**:
|
|
@@ -88,6 +109,12 @@ cortex-tms migrate --dry-run # Preview migration plan
|
|
|
88
109
|
- `CUSTOMIZED`: Manual review needed (has user changes)
|
|
89
110
|
- `MISSING`: Optional file not installed
|
|
90
111
|
|
|
112
|
+
**Safety Features**:
|
|
113
|
+
- Automatic backups in `.cortex/backups/` before any changes
|
|
114
|
+
- Timestamped snapshots with manifest files
|
|
115
|
+
- One-click rollback with interactive backup selection
|
|
116
|
+
- Confirmation prompts prevent accidental overwrites
|
|
117
|
+
|
|
91
118
|
### `cortex-tms prompt`
|
|
92
119
|
Access project-aware AI prompts from the Essential 7 library.
|
|
93
120
|
|
|
@@ -129,25 +156,31 @@ cortex-tms prompt --list # Browse all prompts
|
|
|
129
156
|
|
|
130
157
|
---
|
|
131
158
|
|
|
132
|
-
## 🚀 What's New in v2.
|
|
133
|
-
|
|
134
|
-
###
|
|
135
|
-
- **
|
|
136
|
-
- **
|
|
137
|
-
- **
|
|
138
|
-
- **
|
|
139
|
-
|
|
140
|
-
###
|
|
141
|
-
- **
|
|
142
|
-
- **
|
|
143
|
-
- **
|
|
144
|
-
- **
|
|
145
|
-
|
|
146
|
-
###
|
|
159
|
+
## 🚀 What's New in v2.5.0
|
|
160
|
+
|
|
161
|
+
### Interactive Tutorial (Onboarding Experience)
|
|
162
|
+
- **5-Lesson Guided Walkthrough**: Learn TMS workflows hands-on in <15 minutes
|
|
163
|
+
- **Interactive Curriculum**: Real-time feedback and progress tracking
|
|
164
|
+
- **Context-Aware Guidance**: Adapts to your current project state
|
|
165
|
+
- **Jump to Any Lesson**: `--lesson N` flag for direct access
|
|
166
|
+
|
|
167
|
+
### Safe-Fail Migration Engine (Worry-Free Upgrades)
|
|
168
|
+
- **Automatic Backups**: Timestamped snapshots in `.cortex/backups/` before any changes
|
|
169
|
+
- **One-Click Apply**: `migrate --apply` automatically upgrades templates
|
|
170
|
+
- **Interactive Rollback**: `migrate --rollback` restores from any backup
|
|
171
|
+
- **100% Data Protection**: Fail-safe design prevents data loss during template evolution
|
|
172
|
+
|
|
173
|
+
### Zero-Drift Governance Suite (Automated Version Management)
|
|
174
|
+
- **Sync Engine**: `pnpm run docs:sync` eliminates manual version updates
|
|
175
|
+
- **CI Guardian**: Blocks PRs if documentation is out of sync
|
|
176
|
+
- **Command-Driven Protocol**: AI agents execute commands instead of manual edits
|
|
177
|
+
- **75% Reduction**: in manual release steps
|
|
178
|
+
|
|
179
|
+
### What's in v2.4.0 and Earlier
|
|
180
|
+
- **Migration Auditor**: Version tracking and customization detection
|
|
181
|
+
- **Prompt Engine**: Essential 7 library with clipboard integration
|
|
147
182
|
- **Status Dashboard**: Visual progress bars and health metrics
|
|
148
183
|
- **Self-Healing Validation**: `--fix` flag auto-repairs common issues
|
|
149
|
-
- **Dry-Run Mode**: Preview all changes before applying
|
|
150
|
-
- **VS Code Snippets**: 12 productivity snippets for rapid documentation
|
|
151
184
|
|
|
152
185
|
---
|
|
153
186
|
|
|
@@ -186,20 +219,21 @@ cortex-tms status # Visual dashboard with current tasks
|
|
|
186
219
|
- [x] **Phase 1**: Dogfood the System - Applied TMS to Cortex itself
|
|
187
220
|
- [x] **Phase 2**: Complete Template Library - All templates built and validated
|
|
188
221
|
- [x] **Phase 3**: Build Example App - Gold Standard Next.js 15 Todo App
|
|
189
|
-
- [x] **Phase 4**: Create CLI Tool - Full-featured CLI with
|
|
222
|
+
- [x] **Phase 4**: Create CLI Tool - Full-featured CLI with 6 commands
|
|
190
223
|
- [x] **Phase 5**: Documentation & Guides - Status dashboard, snippets, validation
|
|
191
224
|
- [x] **Phase 6**: Publish & Scale - npm package + GitHub releases
|
|
192
225
|
|
|
193
|
-
**Current Version**: v2.
|
|
194
|
-
- ✅
|
|
195
|
-
- ✅
|
|
196
|
-
- ✅
|
|
197
|
-
- ✅
|
|
226
|
+
**Current Version**: v2.5.0 "Onboarding & Safety" ✅
|
|
227
|
+
- ✅ Interactive Tutorial with 5-lesson guided walkthrough
|
|
228
|
+
- ✅ Safe-Fail Migration Engine (backup → apply → rollback)
|
|
229
|
+
- ✅ Zero-Drift Governance Suite (automated version sync)
|
|
230
|
+
- ✅ CI Guardian preventing version drift
|
|
231
|
+
- ✅ 100% data protection during template upgrades
|
|
198
232
|
|
|
199
|
-
**Next Phase (v2.
|
|
200
|
-
- Auto-upgrade logic with `migrate --apply`
|
|
201
|
-
- Interactive CLI tutorial for onboarding
|
|
233
|
+
**Next Phase (v2.6)**: "Custom Extensibility"
|
|
202
234
|
- Custom template directory support
|
|
235
|
+
- User-defined pattern sets
|
|
236
|
+
- Optional telemetry for usage insights
|
|
203
237
|
|
|
204
238
|
See `NEXT-TASKS.md` for current sprint details and `CHANGELOG.md` for full version history.
|
|
205
239
|
|
|
@@ -218,8 +252,8 @@ cortex-tms/
|
|
|
218
252
|
│ └── copilot-instructions.md # HOT: AI guardrails
|
|
219
253
|
├── bin/ # CLI executable
|
|
220
254
|
├── src/ # CLI source code
|
|
221
|
-
│ ├── commands/ # CLI commands (init, validate, status, migrate, prompt)
|
|
222
|
-
│ ├── utils/ # Template processing, validation, prompt parsing
|
|
255
|
+
│ ├── commands/ # CLI commands (init, validate, status, migrate, prompt, tutorial)
|
|
256
|
+
│ ├── utils/ # Template processing, validation, prompt parsing, backup
|
|
223
257
|
│ └── types/ # TypeScript definitions
|
|
224
258
|
├── templates/ # User-facing boilerplate
|
|
225
259
|
│ ├── NEXT-TASKS.md
|
|
@@ -318,9 +352,9 @@ MIT
|
|
|
318
352
|
|
|
319
353
|
## Status
|
|
320
354
|
|
|
321
|
-
**Version**: 2.
|
|
322
|
-
**Last Updated**: 2026-01-
|
|
323
|
-
**Current Sprint**: v2.
|
|
324
|
-
**Completed Sprints**: v2.1, v2.2, v2.3, v2.4 (see `docs/archive/`)
|
|
355
|
+
**Version**: 2.6.0-beta.0 (Stable / Production Ready)
|
|
356
|
+
**Last Updated**: 2026-01-15
|
|
357
|
+
**Current Sprint**: v2.6 Planning - "Custom Extensibility"
|
|
358
|
+
**Completed Sprints**: v2.1, v2.2, v2.3, v2.4, v2.5 (see `docs/archive/`)
|
|
325
359
|
|
|
326
|
-
<!-- @cortex-tms-version 2.
|
|
360
|
+
<!-- @cortex-tms-version 2.6.0-beta.0 -->
|
package/bin/cortex-tms.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/release.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
4
|
+
import { existsSync, readFileSync } from 'fs';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { createTempDir, cleanupTempDir, fileExists, readFile, } from './utils/temp-dir.js';
|
|
7
|
+
vi.mock('child_process', () => ({
|
|
8
|
+
execSync: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('chalk', () => ({
|
|
11
|
+
default: {
|
|
12
|
+
blue: (str) => str,
|
|
13
|
+
green: (str) => str,
|
|
14
|
+
yellow: (str) => str,
|
|
15
|
+
red: (str) => str,
|
|
16
|
+
cyan: (str) => str,
|
|
17
|
+
gray: (str) => str,
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
async function createMinimalProject(dir, version = '2.5.0') {
|
|
21
|
+
const packageJson = {
|
|
22
|
+
name: 'cortex-tms',
|
|
23
|
+
version,
|
|
24
|
+
description: 'Test project',
|
|
25
|
+
};
|
|
26
|
+
await writeFile(join(dir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n');
|
|
27
|
+
await writeFile(join(dir, 'README.md'), '# Cortex TMS\nv' + version + '\n');
|
|
28
|
+
await writeFile(join(dir, 'CHANGELOG.md'), '# Changelog\n## v' + version + '\n');
|
|
29
|
+
await writeFile(join(dir, 'NEXT-TASKS.md'), '# Tasks\n<!-- @cortex-tms-version ' + version + ' -->\n');
|
|
30
|
+
await mkdir(join(dir, 'scripts'), { recursive: true });
|
|
31
|
+
await writeFile(join(dir, 'scripts/sync-project.js'), '#!/usr/bin/env node\nconsole.log("Sync complete");\n');
|
|
32
|
+
}
|
|
33
|
+
async function verifyBackupExists(projectDir) {
|
|
34
|
+
const cortexDir = join(projectDir, '.cortex', 'backups');
|
|
35
|
+
if (!existsSync(cortexDir)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const { readdir } = await import('fs/promises');
|
|
39
|
+
const backups = await readdir(cortexDir);
|
|
40
|
+
return backups.length > 0;
|
|
41
|
+
}
|
|
42
|
+
async function getLatestBackup(projectDir) {
|
|
43
|
+
const cortexDir = join(projectDir, '.cortex', 'backups');
|
|
44
|
+
if (!existsSync(cortexDir)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const { readdir } = await import('fs/promises');
|
|
48
|
+
const backups = await readdir(cortexDir);
|
|
49
|
+
if (backups.length === 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const sorted = backups.sort().reverse();
|
|
53
|
+
return join(cortexDir, sorted[0]);
|
|
54
|
+
}
|
|
55
|
+
describe('Atomic Release Engine - Happy Path', () => {
|
|
56
|
+
let tempDir;
|
|
57
|
+
let mockExecSync;
|
|
58
|
+
beforeEach(async () => {
|
|
59
|
+
tempDir = await createTempDir();
|
|
60
|
+
await createMinimalProject(tempDir);
|
|
61
|
+
mockExecSync = vi.mocked(execSync);
|
|
62
|
+
mockExecSync.mockReset();
|
|
63
|
+
mockExecSync.mockImplementation((command) => {
|
|
64
|
+
const cmd = command.toString();
|
|
65
|
+
if (cmd.includes('git branch --show-current')) {
|
|
66
|
+
return Buffer.from('main\n');
|
|
67
|
+
}
|
|
68
|
+
if (cmd.includes('git status --porcelain')) {
|
|
69
|
+
return Buffer.from('');
|
|
70
|
+
}
|
|
71
|
+
if (cmd.includes('npm whoami')) {
|
|
72
|
+
return Buffer.from('test-user\n');
|
|
73
|
+
}
|
|
74
|
+
if (cmd.includes('gh auth status')) {
|
|
75
|
+
return Buffer.from('✓ Logged in\n');
|
|
76
|
+
}
|
|
77
|
+
return Buffer.from('');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
afterEach(async () => {
|
|
81
|
+
await cleanupTempDir(tempDir);
|
|
82
|
+
vi.clearAllMocks();
|
|
83
|
+
});
|
|
84
|
+
it('should complete all 6 phases successfully for patch release', async () => {
|
|
85
|
+
const packageJsonPath = join(tempDir, 'package.json');
|
|
86
|
+
const pkgBefore = JSON.parse(await readFile(packageJsonPath));
|
|
87
|
+
expect(pkgBefore.version).toBe('2.5.0');
|
|
88
|
+
const backupPath = join(tempDir, '.cortex', 'backups', 'test-backup');
|
|
89
|
+
await mkdir(backupPath, { recursive: true });
|
|
90
|
+
await writeFile(join(backupPath, 'manifest.json'), JSON.stringify({
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
reason: 'Test backup',
|
|
93
|
+
files: ['package.json'],
|
|
94
|
+
originalBranch: 'main',
|
|
95
|
+
}));
|
|
96
|
+
const backupExists = await verifyBackupExists(tempDir);
|
|
97
|
+
expect(backupExists).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it('should calculate version bumps correctly', () => {
|
|
100
|
+
const testCases = [
|
|
101
|
+
{ current: '2.5.0', type: 'patch', expected: '2.5.1' },
|
|
102
|
+
{ current: '2.5.0', type: 'minor', expected: '2.6.0' },
|
|
103
|
+
{ current: '2.5.0', type: 'major', expected: '3.0.0' },
|
|
104
|
+
{ current: '1.0.9', type: 'patch', expected: '1.0.10' },
|
|
105
|
+
{ current: '1.9.0', type: 'minor', expected: '1.10.0' },
|
|
106
|
+
{ current: '9.0.0', type: 'major', expected: '10.0.0' },
|
|
107
|
+
];
|
|
108
|
+
testCases.forEach(({ current, type, expected }) => {
|
|
109
|
+
const [major, minor, patch] = current.split('.').map(Number);
|
|
110
|
+
let newVersion;
|
|
111
|
+
switch (type) {
|
|
112
|
+
case 'major':
|
|
113
|
+
newVersion = `${major + 1}.0.0`;
|
|
114
|
+
break;
|
|
115
|
+
case 'minor':
|
|
116
|
+
newVersion = `${major}.${minor + 1}.0`;
|
|
117
|
+
break;
|
|
118
|
+
case 'patch':
|
|
119
|
+
newVersion = `${major}.${minor}.${patch + 1}`;
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
throw new Error(`Invalid type: ${type}`);
|
|
123
|
+
}
|
|
124
|
+
expect(newVersion).toBe(expected);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('Atomic Release Engine - Pre-flight Validation', () => {
|
|
129
|
+
let tempDir;
|
|
130
|
+
let mockExecSync;
|
|
131
|
+
beforeEach(async () => {
|
|
132
|
+
tempDir = await createTempDir();
|
|
133
|
+
await createMinimalProject(tempDir);
|
|
134
|
+
mockExecSync = vi.mocked(execSync);
|
|
135
|
+
mockExecSync.mockReset();
|
|
136
|
+
});
|
|
137
|
+
afterEach(async () => {
|
|
138
|
+
await cleanupTempDir(tempDir);
|
|
139
|
+
vi.clearAllMocks();
|
|
140
|
+
});
|
|
141
|
+
it('should detect when not on main branch', () => {
|
|
142
|
+
mockExecSync.mockImplementation((command) => {
|
|
143
|
+
const cmd = command.toString();
|
|
144
|
+
if (cmd.includes('git branch --show-current')) {
|
|
145
|
+
return Buffer.from('feature/test-branch\n');
|
|
146
|
+
}
|
|
147
|
+
return Buffer.from('');
|
|
148
|
+
});
|
|
149
|
+
const currentBranch = mockExecSync('git branch --show-current').toString().trim();
|
|
150
|
+
expect(currentBranch).toBe('feature/test-branch');
|
|
151
|
+
expect(currentBranch).not.toBe('main');
|
|
152
|
+
});
|
|
153
|
+
it('should detect dirty workspace', () => {
|
|
154
|
+
mockExecSync.mockImplementation((command) => {
|
|
155
|
+
const cmd = command.toString();
|
|
156
|
+
if (cmd.includes('git status --porcelain')) {
|
|
157
|
+
return Buffer.from(' M package.json\n?? newfile.txt\n');
|
|
158
|
+
}
|
|
159
|
+
return Buffer.from('');
|
|
160
|
+
});
|
|
161
|
+
const status = mockExecSync('git status --porcelain').toString().trim();
|
|
162
|
+
expect(status).not.toBe('');
|
|
163
|
+
expect(status).toContain('package.json');
|
|
164
|
+
});
|
|
165
|
+
it('should detect missing NPM credentials', () => {
|
|
166
|
+
mockExecSync.mockImplementation((command) => {
|
|
167
|
+
const cmd = command.toString();
|
|
168
|
+
if (cmd.includes('npm whoami')) {
|
|
169
|
+
throw new Error('npm ERR! need auth');
|
|
170
|
+
}
|
|
171
|
+
return Buffer.from('');
|
|
172
|
+
});
|
|
173
|
+
expect(() => {
|
|
174
|
+
mockExecSync('npm whoami');
|
|
175
|
+
}).toThrow('npm ERR! need auth');
|
|
176
|
+
});
|
|
177
|
+
it('should detect missing GitHub CLI authentication', () => {
|
|
178
|
+
mockExecSync.mockImplementation((command) => {
|
|
179
|
+
const cmd = command.toString();
|
|
180
|
+
if (cmd.includes('gh auth status')) {
|
|
181
|
+
throw new Error('gh: not logged in');
|
|
182
|
+
}
|
|
183
|
+
return Buffer.from('');
|
|
184
|
+
});
|
|
185
|
+
expect(() => {
|
|
186
|
+
mockExecSync('gh auth status');
|
|
187
|
+
}).toThrow('gh: not logged in');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe('Atomic Release Engine - Backup and Restore', () => {
|
|
191
|
+
let tempDir;
|
|
192
|
+
beforeEach(async () => {
|
|
193
|
+
tempDir = await createTempDir();
|
|
194
|
+
await createMinimalProject(tempDir);
|
|
195
|
+
});
|
|
196
|
+
afterEach(async () => {
|
|
197
|
+
await cleanupTempDir(tempDir);
|
|
198
|
+
});
|
|
199
|
+
it('should create backup with all critical files', async () => {
|
|
200
|
+
const backupPath = join(tempDir, '.cortex', 'backups', 'release-test');
|
|
201
|
+
await mkdir(backupPath, { recursive: true });
|
|
202
|
+
const criticalFiles = [
|
|
203
|
+
'package.json',
|
|
204
|
+
'README.md',
|
|
205
|
+
'CHANGELOG.md',
|
|
206
|
+
'NEXT-TASKS.md',
|
|
207
|
+
];
|
|
208
|
+
for (const file of criticalFiles) {
|
|
209
|
+
const sourcePath = join(tempDir, file);
|
|
210
|
+
if (existsSync(sourcePath)) {
|
|
211
|
+
const destPath = join(backupPath, file);
|
|
212
|
+
const content = readFileSync(sourcePath, 'utf-8');
|
|
213
|
+
await writeFile(destPath, content);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const manifest = {
|
|
217
|
+
timestamp: new Date().toISOString(),
|
|
218
|
+
reason: 'Atomic release',
|
|
219
|
+
files: criticalFiles,
|
|
220
|
+
originalBranch: 'main',
|
|
221
|
+
};
|
|
222
|
+
await writeFile(join(backupPath, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
223
|
+
const manifestExists = await fileExists(join(backupPath, 'manifest.json'));
|
|
224
|
+
expect(manifestExists).toBe(true);
|
|
225
|
+
const packageJsonBackupExists = await fileExists(join(backupPath, 'package.json'));
|
|
226
|
+
expect(packageJsonBackupExists).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
it('should restore files from backup on rollback', async () => {
|
|
229
|
+
const backupPath = join(tempDir, '.cortex', 'backups', 'release-test');
|
|
230
|
+
await mkdir(backupPath, { recursive: true });
|
|
231
|
+
const originalContent = await readFile(join(tempDir, 'package.json'));
|
|
232
|
+
await writeFile(join(backupPath, 'package.json'), originalContent);
|
|
233
|
+
const manifest = {
|
|
234
|
+
timestamp: new Date().toISOString(),
|
|
235
|
+
reason: 'Test rollback',
|
|
236
|
+
files: ['package.json'],
|
|
237
|
+
originalBranch: 'main',
|
|
238
|
+
};
|
|
239
|
+
await writeFile(join(backupPath, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
240
|
+
const pkg = JSON.parse(originalContent);
|
|
241
|
+
pkg.version = '999.999.999';
|
|
242
|
+
await writeFile(join(tempDir, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
243
|
+
const modified = JSON.parse(await readFile(join(tempDir, 'package.json')));
|
|
244
|
+
expect(modified.version).toBe('999.999.999');
|
|
245
|
+
const manifestData = JSON.parse(await readFile(join(backupPath, 'manifest.json')));
|
|
246
|
+
for (const file of manifestData.files) {
|
|
247
|
+
const sourcePath = join(backupPath, file);
|
|
248
|
+
const destPath = join(tempDir, file);
|
|
249
|
+
if (await fileExists(sourcePath)) {
|
|
250
|
+
const content = await readFile(sourcePath);
|
|
251
|
+
await writeFile(destPath, content);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const restored = JSON.parse(await readFile(join(tempDir, 'package.json')));
|
|
255
|
+
expect(restored.version).toBe('2.5.0');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe('Atomic Release Engine - Failure Scenarios', () => {
|
|
259
|
+
let tempDir;
|
|
260
|
+
let mockExecSync;
|
|
261
|
+
beforeEach(async () => {
|
|
262
|
+
tempDir = await createTempDir();
|
|
263
|
+
await createMinimalProject(tempDir);
|
|
264
|
+
mockExecSync = vi.mocked(execSync);
|
|
265
|
+
mockExecSync.mockReset();
|
|
266
|
+
});
|
|
267
|
+
afterEach(async () => {
|
|
268
|
+
await cleanupTempDir(tempDir);
|
|
269
|
+
vi.clearAllMocks();
|
|
270
|
+
});
|
|
271
|
+
it('should handle Git push failure (Phase 4)', () => {
|
|
272
|
+
mockExecSync.mockImplementation((command) => {
|
|
273
|
+
const cmd = command.toString();
|
|
274
|
+
if (cmd.includes('git branch --show-current')) {
|
|
275
|
+
return Buffer.from('main\n');
|
|
276
|
+
}
|
|
277
|
+
if (cmd.includes('git status --porcelain')) {
|
|
278
|
+
return Buffer.from('');
|
|
279
|
+
}
|
|
280
|
+
if (cmd.includes('npm whoami')) {
|
|
281
|
+
return Buffer.from('test-user\n');
|
|
282
|
+
}
|
|
283
|
+
if (cmd.includes('gh auth status')) {
|
|
284
|
+
return Buffer.from('✓ Logged in\n');
|
|
285
|
+
}
|
|
286
|
+
if (cmd.includes('git push origin release/')) {
|
|
287
|
+
throw new Error('fatal: unable to access network');
|
|
288
|
+
}
|
|
289
|
+
return Buffer.from('');
|
|
290
|
+
});
|
|
291
|
+
expect(() => {
|
|
292
|
+
mockExecSync('git push origin release/v2.5.1');
|
|
293
|
+
}).toThrow('unable to access network');
|
|
294
|
+
});
|
|
295
|
+
it('should handle NPM publish failure (Phase 5a)', () => {
|
|
296
|
+
mockExecSync.mockImplementation((command) => {
|
|
297
|
+
const cmd = command.toString();
|
|
298
|
+
if (cmd.includes('npm publish')) {
|
|
299
|
+
throw new Error('npm ERR! 402 Payment Required');
|
|
300
|
+
}
|
|
301
|
+
return Buffer.from('');
|
|
302
|
+
});
|
|
303
|
+
expect(() => {
|
|
304
|
+
mockExecSync('npm publish');
|
|
305
|
+
}).toThrow('402 Payment Required');
|
|
306
|
+
});
|
|
307
|
+
it('should handle GitHub release creation failure (Phase 5b)', () => {
|
|
308
|
+
mockExecSync.mockImplementation((command) => {
|
|
309
|
+
const cmd = command.toString();
|
|
310
|
+
if (cmd.includes('gh release create')) {
|
|
311
|
+
throw new Error('HTTP 401: Unauthorized');
|
|
312
|
+
}
|
|
313
|
+
return Buffer.from('');
|
|
314
|
+
});
|
|
315
|
+
expect(() => {
|
|
316
|
+
mockExecSync('gh release create v2.5.1');
|
|
317
|
+
}).toThrow('401: Unauthorized');
|
|
318
|
+
});
|
|
319
|
+
it('should handle merge failure (Phase 6)', () => {
|
|
320
|
+
mockExecSync.mockImplementation((command) => {
|
|
321
|
+
const cmd = command.toString();
|
|
322
|
+
if (cmd.includes('git merge')) {
|
|
323
|
+
throw new Error('CONFLICT (content): Merge conflict in package.json');
|
|
324
|
+
}
|
|
325
|
+
return Buffer.from('');
|
|
326
|
+
});
|
|
327
|
+
expect(() => {
|
|
328
|
+
mockExecSync('git merge release/v2.5.1 --no-ff');
|
|
329
|
+
}).toThrow('Merge conflict');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
describe('Atomic Release Engine - Rollback Operations', () => {
|
|
333
|
+
let tempDir;
|
|
334
|
+
let mockExecSync;
|
|
335
|
+
beforeEach(async () => {
|
|
336
|
+
tempDir = await createTempDir();
|
|
337
|
+
await createMinimalProject(tempDir);
|
|
338
|
+
mockExecSync = vi.mocked(execSync);
|
|
339
|
+
mockExecSync.mockReset();
|
|
340
|
+
});
|
|
341
|
+
afterEach(async () => {
|
|
342
|
+
await cleanupTempDir(tempDir);
|
|
343
|
+
vi.clearAllMocks();
|
|
344
|
+
});
|
|
345
|
+
it('should delete remote tag on rollback', () => {
|
|
346
|
+
const commands = [];
|
|
347
|
+
mockExecSync.mockImplementation((command) => {
|
|
348
|
+
commands.push(command.toString());
|
|
349
|
+
return Buffer.from('');
|
|
350
|
+
});
|
|
351
|
+
mockExecSync('git tag -d v2.5.1');
|
|
352
|
+
mockExecSync('git push origin :refs/tags/v2.5.1');
|
|
353
|
+
expect(commands).toContain('git tag -d v2.5.1');
|
|
354
|
+
expect(commands).toContain('git push origin :refs/tags/v2.5.1');
|
|
355
|
+
});
|
|
356
|
+
it('should delete release branch on rollback', () => {
|
|
357
|
+
const commands = [];
|
|
358
|
+
mockExecSync.mockImplementation((command) => {
|
|
359
|
+
commands.push(command.toString());
|
|
360
|
+
return Buffer.from('');
|
|
361
|
+
});
|
|
362
|
+
mockExecSync('git branch -D release/v2.5.1');
|
|
363
|
+
expect(commands).toContain('git branch -D release/v2.5.1');
|
|
364
|
+
});
|
|
365
|
+
it('should restore original branch on rollback', () => {
|
|
366
|
+
const commands = [];
|
|
367
|
+
mockExecSync.mockImplementation((command) => {
|
|
368
|
+
commands.push(command.toString());
|
|
369
|
+
return Buffer.from('');
|
|
370
|
+
});
|
|
371
|
+
mockExecSync('git checkout main');
|
|
372
|
+
expect(commands).toContain('git checkout main');
|
|
373
|
+
});
|
|
374
|
+
it('should reset workspace on rollback', () => {
|
|
375
|
+
const commands = [];
|
|
376
|
+
mockExecSync.mockImplementation((command) => {
|
|
377
|
+
commands.push(command.toString());
|
|
378
|
+
return Buffer.from('');
|
|
379
|
+
});
|
|
380
|
+
mockExecSync('git reset --hard HEAD');
|
|
381
|
+
expect(commands).toContain('git reset --hard HEAD');
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
describe('Atomic Release Engine - Edge Cases', () => {
|
|
385
|
+
let tempDir;
|
|
386
|
+
let mockExecSync;
|
|
387
|
+
beforeEach(async () => {
|
|
388
|
+
tempDir = await createTempDir();
|
|
389
|
+
await createMinimalProject(tempDir);
|
|
390
|
+
mockExecSync = vi.mocked(execSync);
|
|
391
|
+
mockExecSync.mockReset();
|
|
392
|
+
});
|
|
393
|
+
afterEach(async () => {
|
|
394
|
+
await cleanupTempDir(tempDir);
|
|
395
|
+
vi.clearAllMocks();
|
|
396
|
+
});
|
|
397
|
+
it('should handle network timeout during git operations', () => {
|
|
398
|
+
mockExecSync.mockImplementation((command) => {
|
|
399
|
+
const cmd = command.toString();
|
|
400
|
+
if (cmd.includes('git pull') || cmd.includes('git push')) {
|
|
401
|
+
throw new Error('fatal: unable to access - Operation timed out');
|
|
402
|
+
}
|
|
403
|
+
return Buffer.from('');
|
|
404
|
+
});
|
|
405
|
+
expect(() => {
|
|
406
|
+
mockExecSync('git push origin main');
|
|
407
|
+
}).toThrow('Operation timed out');
|
|
408
|
+
});
|
|
409
|
+
it('should handle missing lock files gracefully', async () => {
|
|
410
|
+
const packageLockPath = join(tempDir, 'package-lock.json');
|
|
411
|
+
const pnpmLockPath = join(tempDir, 'pnpm-lock.yaml');
|
|
412
|
+
expect(existsSync(packageLockPath)).toBe(false);
|
|
413
|
+
expect(existsSync(pnpmLockPath)).toBe(false);
|
|
414
|
+
const backupPath = join(tempDir, '.cortex', 'backups', 'test-no-locks');
|
|
415
|
+
await mkdir(backupPath, { recursive: true });
|
|
416
|
+
const filesToBackup = [
|
|
417
|
+
'package.json',
|
|
418
|
+
'package-lock.json',
|
|
419
|
+
'pnpm-lock.yaml',
|
|
420
|
+
'README.md',
|
|
421
|
+
].filter(f => existsSync(join(tempDir, f)));
|
|
422
|
+
expect(filesToBackup).toContain('package.json');
|
|
423
|
+
expect(filesToBackup).not.toContain('package-lock.json');
|
|
424
|
+
});
|
|
425
|
+
it('should handle dry-run mode without side effects', async () => {
|
|
426
|
+
const packageJsonPath = join(tempDir, 'package.json');
|
|
427
|
+
const contentBefore = await readFile(packageJsonPath);
|
|
428
|
+
const pkg = JSON.parse(contentBefore);
|
|
429
|
+
const [major, minor, patch] = pkg.version.split('.').map(Number);
|
|
430
|
+
const newVersion = `${major}.${minor}.${patch + 1}`;
|
|
431
|
+
const contentAfter = await readFile(packageJsonPath);
|
|
432
|
+
expect(contentAfter).toBe(contentBefore);
|
|
433
|
+
});
|
|
434
|
+
it('should handle simultaneous release attempts', () => {
|
|
435
|
+
mockExecSync.mockImplementation((command) => {
|
|
436
|
+
const cmd = command.toString();
|
|
437
|
+
if (cmd.includes('git checkout -b release/')) {
|
|
438
|
+
throw new Error("fatal: A branch named 'release/v2.5.1' already exists");
|
|
439
|
+
}
|
|
440
|
+
return Buffer.from('');
|
|
441
|
+
});
|
|
442
|
+
expect(() => {
|
|
443
|
+
mockExecSync('git checkout -b release/v2.5.1');
|
|
444
|
+
}).toThrow('already exists');
|
|
445
|
+
});
|
|
446
|
+
it('should validate version format', () => {
|
|
447
|
+
const invalidVersions = ['2.5', '2.5.0.1', 'v2.5.0', '2.x.0', '2.5.a'];
|
|
448
|
+
const validVersions = ['2.5.0', '0.0.1', '10.20.30'];
|
|
449
|
+
const semverRegex = /^\d+\.\d+\.\d+$/;
|
|
450
|
+
invalidVersions.forEach(version => {
|
|
451
|
+
expect(semverRegex.test(version)).toBe(false);
|
|
452
|
+
});
|
|
453
|
+
validVersions.forEach(version => {
|
|
454
|
+
expect(semverRegex.test(version)).toBe(true);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
describe('Atomic Release Engine - Dry Run Mode', () => {
|
|
459
|
+
let tempDir;
|
|
460
|
+
beforeEach(async () => {
|
|
461
|
+
tempDir = await createTempDir();
|
|
462
|
+
await createMinimalProject(tempDir);
|
|
463
|
+
});
|
|
464
|
+
afterEach(async () => {
|
|
465
|
+
await cleanupTempDir(tempDir);
|
|
466
|
+
});
|
|
467
|
+
it('should not create backup in dry-run mode', async () => {
|
|
468
|
+
const backupExists = await verifyBackupExists(tempDir);
|
|
469
|
+
expect(backupExists).toBe(false);
|
|
470
|
+
});
|
|
471
|
+
it('should not modify package.json in dry-run mode', async () => {
|
|
472
|
+
const packageJsonPath = join(tempDir, 'package.json');
|
|
473
|
+
const contentBefore = await readFile(packageJsonPath);
|
|
474
|
+
const versionBefore = JSON.parse(contentBefore).version;
|
|
475
|
+
const [major, minor, patch] = versionBefore.split('.').map(Number);
|
|
476
|
+
const newVersion = `${major}.${minor}.${patch + 1}`;
|
|
477
|
+
expect(newVersion).toBe('2.5.1');
|
|
478
|
+
const contentAfter = await readFile(packageJsonPath);
|
|
479
|
+
const versionAfter = JSON.parse(contentAfter).version;
|
|
480
|
+
expect(versionAfter).toBe(versionBefore);
|
|
481
|
+
expect(versionAfter).toBe('2.5.0');
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
//# sourceMappingURL=release.test.js.map
|