@codebakers/cli 3.3.18 → 3.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebakers/cli",
3
- "version": "3.3.18",
3
+ "version": "3.4.0",
4
4
  "description": "CodeBakers CLI - Production patterns for AI-assisted development",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,234 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ const PRE_COMMIT_SCRIPT = `#!/bin/sh
6
+ # CodeBakers Pre-Commit Hook - Session Enforcement
7
+ # Blocks commits unless AI called discover_patterns and validate_complete
8
+
9
+ # Run the validation script
10
+ node "$(dirname "$0")/validate-session.js"
11
+ exit $?
12
+ `;
13
+
14
+ const VALIDATE_SESSION_SCRIPT = `#!/usr/bin/env node
15
+ /**
16
+ * CodeBakers Pre-Commit Validation
17
+ * Blocks commits unless a valid session exists
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const RED = '\\x1b[31m';
24
+ const GREEN = '\\x1b[32m';
25
+ const YELLOW = '\\x1b[33m';
26
+ const CYAN = '\\x1b[36m';
27
+ const RESET = '\\x1b[0m';
28
+
29
+ function log(color, message) {
30
+ console.log(color + message + RESET);
31
+ }
32
+
33
+ async function validateSession() {
34
+ const cwd = process.cwd();
35
+ const stateFile = path.join(cwd, '.codebakers.json');
36
+
37
+ // Check if this is a CodeBakers project
38
+ if (!fs.existsSync(stateFile)) {
39
+ return { valid: true, reason: 'not-codebakers-project' };
40
+ }
41
+
42
+ let state;
43
+ try {
44
+ state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
45
+ } catch (error) {
46
+ return { valid: false, reason: 'invalid-state-file' };
47
+ }
48
+
49
+ // Check if v6.0 server-enforced mode
50
+ if (!state.serverEnforced) {
51
+ return { valid: true, reason: 'legacy-project' };
52
+ }
53
+
54
+ // Check for session token (means discover_patterns was called)
55
+ const sessionToken = state.currentSessionToken;
56
+ if (!sessionToken) {
57
+ // Check if there's a recent passed validation
58
+ const lastValidation = state.lastValidation;
59
+ if (!lastValidation || !lastValidation.passed) {
60
+ return {
61
+ valid: false,
62
+ reason: 'no-session',
63
+ message: 'No active CodeBakers session.\\nAI must call discover_patterns before writing code.'
64
+ };
65
+ }
66
+ }
67
+
68
+ // Check session expiry
69
+ const sessionExpiry = state.sessionExpiresAt;
70
+ if (sessionExpiry && new Date(sessionExpiry) < new Date()) {
71
+ return {
72
+ valid: false,
73
+ reason: 'session-expired',
74
+ message: 'CodeBakers session has expired.\\nAI must call discover_patterns again.'
75
+ };
76
+ }
77
+
78
+ // Check if validation was completed
79
+ const lastValidation = state.lastValidation;
80
+ if (!lastValidation) {
81
+ return {
82
+ valid: false,
83
+ reason: 'no-validation',
84
+ message: 'No validation completed.\\nAI must call validate_complete before committing.'
85
+ };
86
+ }
87
+
88
+ // Check if validation passed
89
+ if (!lastValidation.passed) {
90
+ const issues = lastValidation.issues?.map(i => i.message || i).join(', ') || 'Unknown issues';
91
+ return {
92
+ valid: false,
93
+ reason: 'validation-failed',
94
+ message: 'Validation failed: ' + issues + '\\nAI must fix issues and call validate_complete again.'
95
+ };
96
+ }
97
+
98
+ // Check if validation is recent (within last 30 minutes)
99
+ const validationTime = new Date(lastValidation.timestamp);
100
+ const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000);
101
+ if (validationTime < thirtyMinutesAgo) {
102
+ return {
103
+ valid: false,
104
+ reason: 'validation-stale',
105
+ message: 'Validation is stale (older than 30 minutes).\\nAI must call validate_complete again.'
106
+ };
107
+ }
108
+
109
+ return { valid: true, reason: 'session-valid' };
110
+ }
111
+
112
+ async function main() {
113
+ console.log('');
114
+ log(CYAN, ' 🍪 CodeBakers Pre-Commit Validation');
115
+ console.log('');
116
+
117
+ const result = await validateSession();
118
+
119
+ if (result.valid) {
120
+ if (result.reason === 'not-codebakers-project') {
121
+ log(GREEN, ' ✓ Not a CodeBakers project - commit allowed');
122
+ } else if (result.reason === 'legacy-project') {
123
+ log(GREEN, ' ✓ Legacy project (pre-6.0) - commit allowed');
124
+ } else {
125
+ log(GREEN, ' ✓ Valid CodeBakers session - commit allowed');
126
+ }
127
+ console.log('');
128
+ process.exit(0);
129
+ } else {
130
+ log(RED, ' ✗ Commit blocked: ' + result.reason);
131
+ console.log('');
132
+ if (result.message) {
133
+ log(YELLOW, ' ' + result.message.split('\\n').join('\\n '));
134
+ }
135
+ console.log('');
136
+ log(CYAN, ' How to fix:');
137
+ log(RESET, ' 1. AI must call discover_patterns before writing code');
138
+ log(RESET, ' 2. AI must call validate_complete before saying "done"');
139
+ log(RESET, ' 3. Both tools must pass for commits to be allowed');
140
+ console.log('');
141
+ log(YELLOW, ' To bypass (not recommended): git commit --no-verify');
142
+ console.log('');
143
+ process.exit(1);
144
+ }
145
+ }
146
+
147
+ main().catch(error => {
148
+ log(RED, ' Error: ' + error.message);
149
+ process.exit(1);
150
+ });
151
+ `;
152
+
153
+ export async function installPrecommit(): Promise<void> {
154
+ console.log(chalk.blue('\n CodeBakers Pre-Commit Hook Installation\n'));
155
+
156
+ const cwd = process.cwd();
157
+
158
+ // Check if this is a git repository
159
+ const gitDir = join(cwd, '.git');
160
+ if (!existsSync(gitDir)) {
161
+ console.log(chalk.red(' ✗ Not a git repository'));
162
+ console.log(chalk.gray(' Initialize git first: git init\n'));
163
+ process.exit(1);
164
+ }
165
+
166
+ // Check if this is a CodeBakers project
167
+ const stateFile = join(cwd, '.codebakers.json');
168
+ if (!existsSync(stateFile)) {
169
+ console.log(chalk.yellow(' ⚠️ No .codebakers.json found'));
170
+ console.log(chalk.gray(' Run codebakers upgrade first to enable server enforcement\n'));
171
+ }
172
+
173
+ // Create hooks directory if it doesn't exist
174
+ const hooksDir = join(gitDir, 'hooks');
175
+ if (!existsSync(hooksDir)) {
176
+ mkdirSync(hooksDir, { recursive: true });
177
+ }
178
+
179
+ // Write the pre-commit hook
180
+ const preCommitPath = join(hooksDir, 'pre-commit');
181
+ writeFileSync(preCommitPath, PRE_COMMIT_SCRIPT);
182
+
183
+ // Make it executable (Unix only, Windows ignores this)
184
+ try {
185
+ chmodSync(preCommitPath, '755');
186
+ } catch {
187
+ // Windows doesn't support chmod
188
+ }
189
+
190
+ console.log(chalk.green(' ✓ Created pre-commit hook'));
191
+
192
+ // Write the validation script
193
+ const validatePath = join(hooksDir, 'validate-session.js');
194
+ writeFileSync(validatePath, VALIDATE_SESSION_SCRIPT);
195
+
196
+ console.log(chalk.green(' ✓ Created validation script'));
197
+
198
+ // Check if husky is being used
199
+ const huskyDir = join(cwd, '.husky');
200
+ if (existsSync(huskyDir)) {
201
+ // Also install in husky
202
+ const huskyPreCommit = join(huskyDir, 'pre-commit');
203
+ let huskyContent = '';
204
+
205
+ if (existsSync(huskyPreCommit)) {
206
+ huskyContent = readFileSync(huskyPreCommit, 'utf-8');
207
+ if (!huskyContent.includes('validate-session')) {
208
+ huskyContent += '\n# CodeBakers session enforcement\nnode .git/hooks/validate-session.js\n';
209
+ writeFileSync(huskyPreCommit, huskyContent);
210
+ console.log(chalk.green(' ✓ Added to existing husky pre-commit'));
211
+ } else {
212
+ console.log(chalk.gray(' ✓ Husky hook already configured'));
213
+ }
214
+ } else {
215
+ huskyContent = '#!/usr/bin/env sh\n. "$(dirname -- "$0")/_/husky.sh"\n\n# CodeBakers session enforcement\nnode .git/hooks/validate-session.js\n';
216
+ writeFileSync(huskyPreCommit, huskyContent);
217
+ try {
218
+ chmodSync(huskyPreCommit, '755');
219
+ } catch {
220
+ // Windows
221
+ }
222
+ console.log(chalk.green(' ✓ Created husky pre-commit hook'));
223
+ }
224
+ }
225
+
226
+ console.log(chalk.green('\n ✅ Pre-commit hook installed!\n'));
227
+ console.log(chalk.cyan(' What this does:'));
228
+ console.log(chalk.gray(' - Blocks commits unless AI called discover_patterns'));
229
+ console.log(chalk.gray(' - Blocks commits unless AI called validate_complete'));
230
+ console.log(chalk.gray(' - Requires validation to pass before committing'));
231
+ console.log(chalk.gray(' - Validation expires after 30 minutes\n'));
232
+ console.log(chalk.yellow(' To bypass (not recommended):'));
233
+ console.log(chalk.gray(' git commit --no-verify\n'));
234
+ }
@@ -1,16 +1,135 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { existsSync, writeFileSync, mkdirSync } from 'fs';
3
+ import { existsSync, writeFileSync, mkdirSync, readFileSync, rmSync, readdirSync, copyFileSync, chmodSync } from 'fs';
4
4
  import { join } from 'path';
5
5
  import { getApiKey, getApiUrl } from '../config.js';
6
6
  import { checkForUpdates, getCliVersion } from '../lib/api.js';
7
7
 
8
+ // Pre-commit hook script for session enforcement
9
+ const PRE_COMMIT_SCRIPT = `#!/bin/sh
10
+ # CodeBakers Pre-Commit Hook - Session Enforcement
11
+ # Blocks commits unless AI called discover_patterns and validate_complete
12
+ node "$(dirname "$0")/validate-session.js"
13
+ exit $?
14
+ `;
15
+
16
+ const VALIDATE_SESSION_SCRIPT = `#!/usr/bin/env node
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const RED = '\\x1b[31m', GREEN = '\\x1b[32m', YELLOW = '\\x1b[33m', CYAN = '\\x1b[36m', RESET = '\\x1b[0m';
20
+ function log(c, m) { console.log(c + m + RESET); }
21
+
22
+ async function validate() {
23
+ const stateFile = path.join(process.cwd(), '.codebakers.json');
24
+ if (!fs.existsSync(stateFile)) return { valid: true, reason: 'not-codebakers' };
25
+
26
+ let state;
27
+ try { state = JSON.parse(fs.readFileSync(stateFile, 'utf-8')); }
28
+ catch { return { valid: false, reason: 'invalid-state' }; }
29
+
30
+ if (!state.serverEnforced) return { valid: true, reason: 'legacy' };
31
+
32
+ const v = state.lastValidation;
33
+ if (!v) return { valid: false, reason: 'no-validation', msg: 'AI must call validate_complete before commit' };
34
+ if (!v.passed) return { valid: false, reason: 'failed', msg: 'Validation failed - fix issues first' };
35
+
36
+ const age = Date.now() - new Date(v.timestamp).getTime();
37
+ if (age > 30 * 60 * 1000) return { valid: false, reason: 'stale', msg: 'Validation expired - call validate_complete again' };
38
+
39
+ return { valid: true, reason: 'ok' };
40
+ }
41
+
42
+ async function main() {
43
+ console.log(''); log(CYAN, ' 🍪 CodeBakers Pre-Commit');
44
+ const r = await validate();
45
+ if (r.valid) { log(GREEN, ' ✓ Commit allowed'); console.log(''); process.exit(0); }
46
+ else { log(RED, ' ✗ Blocked: ' + r.reason); if (r.msg) log(YELLOW, ' ' + r.msg);
47
+ console.log(''); log(YELLOW, ' Bypass: git commit --no-verify'); console.log(''); process.exit(1); }
48
+ }
49
+ main().catch(e => { log(RED, ' Error: ' + e.message); process.exit(1); });
50
+ `;
51
+
52
+ /**
53
+ * Install pre-commit hook for session enforcement
54
+ */
55
+ function installPrecommitHook(cwd: string): void {
56
+ const gitDir = join(cwd, '.git');
57
+ if (!existsSync(gitDir)) {
58
+ console.log(chalk.gray(' ⏭️ Skipping pre-commit hook (not a git repo)'));
59
+ return;
60
+ }
61
+
62
+ const hooksDir = join(gitDir, 'hooks');
63
+ if (!existsSync(hooksDir)) {
64
+ mkdirSync(hooksDir, { recursive: true });
65
+ }
66
+
67
+ // Write pre-commit hook
68
+ const preCommitPath = join(hooksDir, 'pre-commit');
69
+ writeFileSync(preCommitPath, PRE_COMMIT_SCRIPT);
70
+ try { chmodSync(preCommitPath, '755'); } catch { /* Windows */ }
71
+
72
+ // Write validation script
73
+ const validatePath = join(hooksDir, 'validate-session.js');
74
+ writeFileSync(validatePath, VALIDATE_SESSION_SCRIPT);
75
+
76
+ console.log(chalk.green(' ✓ Installed pre-commit hook (blocks commits without validation)'));
77
+ }
78
+
8
79
  interface ContentResponse {
9
80
  version: string;
10
81
  router: string;
11
82
  modules: Record<string, string>;
12
83
  }
13
84
 
85
+ // Ultra-minimal CLAUDE.md for v6.0 - ALL instructions come from server
86
+ const MINIMAL_CLAUDE_MD = `# CodeBakers v6.0
87
+
88
+ **MANDATORY: Before writing ANY code, call the \`discover_patterns\` MCP tool.**
89
+
90
+ \`\`\`
91
+ Tool: discover_patterns
92
+ Args: { task: "description of what you're building" }
93
+ \`\`\`
94
+
95
+ The server will return:
96
+ - All coding patterns and rules you must follow
97
+ - Test requirements
98
+ - Validation instructions
99
+
100
+ **You cannot write code without calling this tool first. The server tracks compliance.**
101
+
102
+ ---
103
+ *CodeBakers v6.0 - Server-Enforced*
104
+ `;
105
+
106
+ // Ultra-minimal .cursorrules for v6.0 - ALL instructions come from server
107
+ const MINIMAL_CURSORRULES = `# CodeBakers v6.0
108
+
109
+ MANDATORY: Before writing ANY code, call the discover_patterns MCP tool.
110
+
111
+ Tool: discover_patterns
112
+ Args: { task: "description of what you're building" }
113
+
114
+ The server returns all patterns, rules, and test requirements.
115
+ You cannot write code without calling this tool first.
116
+ `;
117
+
118
+ /**
119
+ * Compare semantic versions (returns true if v1 < v2)
120
+ */
121
+ function isVersionLessThan(v1: string, v2: string): boolean {
122
+ const parts1 = v1.split('.').map(Number);
123
+ const parts2 = v2.split('.').map(Number);
124
+ for (let i = 0; i < 3; i++) {
125
+ const p1 = parts1[i] || 0;
126
+ const p2 = parts2[i] || 0;
127
+ if (p1 < p2) return true;
128
+ if (p1 > p2) return false;
129
+ }
130
+ return false;
131
+ }
132
+
14
133
  interface ConfirmData {
15
134
  version: string;
16
135
  moduleCount: number;
@@ -37,6 +156,131 @@ async function confirmDownload(apiUrl: string, apiKey: string, data: ConfirmData
37
156
  }
38
157
  }
39
158
 
159
+ /**
160
+ * Get current installed version from .claude/.version.json
161
+ */
162
+ function getCurrentVersion(cwd: string): string | null {
163
+ const versionFile = join(cwd, '.claude', '.version.json');
164
+ const codebakersFile = join(cwd, '.codebakers.json');
165
+
166
+ try {
167
+ if (existsSync(versionFile)) {
168
+ const data = JSON.parse(readFileSync(versionFile, 'utf-8'));
169
+ return data.version || null;
170
+ }
171
+ if (existsSync(codebakersFile)) {
172
+ const data = JSON.parse(readFileSync(codebakersFile, 'utf-8'));
173
+ return data.version || null;
174
+ }
175
+ } catch {
176
+ // Ignore errors
177
+ }
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * Backup old files before migration
183
+ */
184
+ function backupOldFiles(cwd: string): void {
185
+ const backupDir = join(cwd, '.codebakers', 'backup', new Date().toISOString().replace(/[:.]/g, '-'));
186
+ mkdirSync(backupDir, { recursive: true });
187
+
188
+ // Backup CLAUDE.md
189
+ const claudeMd = join(cwd, 'CLAUDE.md');
190
+ if (existsSync(claudeMd)) {
191
+ copyFileSync(claudeMd, join(backupDir, 'CLAUDE.md'));
192
+ }
193
+
194
+ // Backup .cursorrules
195
+ const cursorrules = join(cwd, '.cursorrules');
196
+ if (existsSync(cursorrules)) {
197
+ copyFileSync(cursorrules, join(backupDir, '.cursorrules'));
198
+ }
199
+
200
+ // Backup .claude folder
201
+ const claudeDir = join(cwd, '.claude');
202
+ if (existsSync(claudeDir)) {
203
+ const claudeBackup = join(backupDir, '.claude');
204
+ mkdirSync(claudeBackup, { recursive: true });
205
+ const files = readdirSync(claudeDir);
206
+ for (const file of files) {
207
+ const src = join(claudeDir, file);
208
+ const dest = join(claudeBackup, file);
209
+ try {
210
+ copyFileSync(src, dest);
211
+ } catch {
212
+ // Ignore copy errors
213
+ }
214
+ }
215
+ }
216
+
217
+ console.log(chalk.gray(` Backup saved to: ${backupDir}`));
218
+ }
219
+
220
+ /**
221
+ * Migrate to v6.0 server-enforced patterns
222
+ */
223
+ function migrateToV6(cwd: string): void {
224
+ console.log(chalk.yellow('\n 📦 Migrating to v6.0 Server-Enforced Patterns...\n'));
225
+
226
+ // Backup old files
227
+ console.log(chalk.gray(' Backing up old files...'));
228
+ backupOldFiles(cwd);
229
+
230
+ // Replace CLAUDE.md with minimal version
231
+ const claudeMd = join(cwd, 'CLAUDE.md');
232
+ writeFileSync(claudeMd, MINIMAL_CLAUDE_MD);
233
+ console.log(chalk.green(' ✓ Updated CLAUDE.md (minimal server-enforced version)'));
234
+
235
+ // Replace .cursorrules with minimal version
236
+ const cursorrules = join(cwd, '.cursorrules');
237
+ writeFileSync(cursorrules, MINIMAL_CURSORRULES);
238
+ console.log(chalk.green(' ✓ Updated .cursorrules (minimal server-enforced version)'));
239
+
240
+ // Delete .claude folder (patterns now come from server)
241
+ const claudeDir = join(cwd, '.claude');
242
+ if (existsSync(claudeDir)) {
243
+ try {
244
+ rmSync(claudeDir, { recursive: true, force: true });
245
+ console.log(chalk.green(' ✓ Removed .claude/ folder (patterns now server-side)'));
246
+ } catch (error) {
247
+ console.log(chalk.yellow(' ⚠️ Could not remove .claude/ folder - please delete manually'));
248
+ }
249
+ }
250
+
251
+ // Create .codebakers directory if it doesn't exist
252
+ const codebakersDir = join(cwd, '.codebakers');
253
+ if (!existsSync(codebakersDir)) {
254
+ mkdirSync(codebakersDir, { recursive: true });
255
+ }
256
+
257
+ // Update version in .codebakers.json
258
+ const stateFile = join(cwd, '.codebakers.json');
259
+ let state: Record<string, unknown> = {};
260
+ if (existsSync(stateFile)) {
261
+ try {
262
+ state = JSON.parse(readFileSync(stateFile, 'utf-8'));
263
+ } catch {
264
+ // Ignore errors
265
+ }
266
+ }
267
+ state.version = '6.0';
268
+ state.migratedAt = new Date().toISOString();
269
+ state.serverEnforced = true;
270
+ writeFileSync(stateFile, JSON.stringify(state, null, 2));
271
+
272
+ // Auto-install pre-commit hook for enforcement
273
+ installPrecommitHook(cwd);
274
+
275
+ console.log(chalk.green('\n ✅ Migration to v6.0 complete!\n'));
276
+ console.log(chalk.cyan(' What changed:'));
277
+ console.log(chalk.gray(' - Patterns are now fetched from server in real-time'));
278
+ console.log(chalk.gray(' - discover_patterns creates a server-tracked session'));
279
+ console.log(chalk.gray(' - validate_complete verifies with server before completion'));
280
+ console.log(chalk.gray(' - Pre-commit hook blocks commits without validation'));
281
+ console.log(chalk.gray(' - No local pattern files needed\n'));
282
+ }
283
+
40
284
  /**
41
285
  * Upgrade CodeBakers patterns to the latest version
42
286
  */
@@ -46,9 +290,10 @@ export async function upgrade(): Promise<void> {
46
290
  const cwd = process.cwd();
47
291
  const claudeMdPath = join(cwd, 'CLAUDE.md');
48
292
  const claudeDir = join(cwd, '.claude');
293
+ const codebakersJson = join(cwd, '.codebakers.json');
49
294
 
50
295
  // Check if this is a CodeBakers project
51
- if (!existsSync(claudeMdPath) && !existsSync(claudeDir)) {
296
+ if (!existsSync(claudeMdPath) && !existsSync(claudeDir) && !existsSync(codebakersJson)) {
52
297
  console.log(chalk.yellow(' No CodeBakers installation found in this directory.\n'));
53
298
  console.log(chalk.gray(' Run `codebakers install` to set up patterns first.\n'));
54
299
  return;
@@ -72,11 +317,71 @@ export async function upgrade(): Promise<void> {
72
317
  return;
73
318
  }
74
319
 
75
- // Fetch latest patterns
76
- const spinner = ora('Fetching latest patterns...').start();
320
+ // Check current version and determine if migration is needed
321
+ const currentVersion = getCurrentVersion(cwd);
322
+ const spinner = ora('Checking server for latest version...').start();
77
323
 
78
324
  try {
79
325
  const apiUrl = getApiUrl();
326
+
327
+ // Check latest version from server
328
+ const versionResponse = await fetch(`${apiUrl}/api/content/version`, {
329
+ method: 'GET',
330
+ headers: {
331
+ 'Authorization': `Bearer ${apiKey}`,
332
+ },
333
+ });
334
+
335
+ if (!versionResponse.ok) {
336
+ throw new Error('Failed to check version');
337
+ }
338
+
339
+ const versionData = await versionResponse.json();
340
+ const latestVersion = versionData.version;
341
+
342
+ spinner.succeed(`Latest version: v${latestVersion}`);
343
+
344
+ // Check if we need to migrate to v6.0
345
+ const needsV6Migration = currentVersion && isVersionLessThan(currentVersion, '6.0') &&
346
+ !isVersionLessThan(latestVersion, '6.0');
347
+
348
+ if (needsV6Migration || (!currentVersion && !isVersionLessThan(latestVersion, '6.0'))) {
349
+ // Need to migrate to v6.0 server-enforced patterns
350
+ migrateToV6(cwd);
351
+
352
+ // Confirm migration to server
353
+ confirmDownload(apiUrl, apiKey, {
354
+ version: '6.0',
355
+ moduleCount: 0, // No local modules in v6
356
+ cliVersion: getCliVersion(),
357
+ command: 'upgrade-v6-migration',
358
+ }).catch(() => {});
359
+
360
+ return;
361
+ }
362
+
363
+ // For v6.0+, just confirm the installation is up to date
364
+ const stateFile = join(cwd, '.codebakers.json');
365
+ let state: Record<string, unknown> = {};
366
+ if (existsSync(stateFile)) {
367
+ try {
368
+ state = JSON.parse(readFileSync(stateFile, 'utf-8'));
369
+ } catch {
370
+ // Ignore
371
+ }
372
+ }
373
+
374
+ if (state.serverEnforced) {
375
+ // Already on v6.0+ server-enforced mode
376
+ console.log(chalk.green('\n ✅ Already using v6.0 server-enforced patterns!\n'));
377
+ console.log(chalk.gray(' Patterns are fetched from server in real-time.'));
378
+ console.log(chalk.gray(' No local updates needed.\n'));
379
+ return;
380
+ }
381
+
382
+ // Legacy upgrade for pre-6.0 versions (fetch full content)
383
+ const contentSpinner = ora('Fetching latest patterns...').start();
384
+
80
385
  const response = await fetch(`${apiUrl}/api/content`, {
81
386
  method: 'GET',
82
387
  headers: {
@@ -91,7 +396,7 @@ export async function upgrade(): Promise<void> {
91
396
 
92
397
  const content: ContentResponse = await response.json();
93
398
 
94
- spinner.succeed(`Patterns v${content.version} downloaded`);
399
+ contentSpinner.succeed(`Patterns v${content.version} downloaded`);
95
400
 
96
401
  // Count what we're updating
97
402
  const moduleCount = Object.keys(content.modules).length;
@@ -124,6 +429,10 @@ export async function upgrade(): Promise<void> {
124
429
  updatedAt: new Date().toISOString(),
125
430
  cliVersion: getCliVersion(),
126
431
  };
432
+
433
+ if (!existsSync(claudeDir)) {
434
+ mkdirSync(claudeDir, { recursive: true });
435
+ }
127
436
  writeFileSync(join(claudeDir, '.version.json'), JSON.stringify(versionInfo, null, 2));
128
437
  console.log(chalk.green(' ✓ Version info saved'));
129
438
 
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import { install } from './commands/install.js';
7
7
  import { status } from './commands/status.js';
8
8
  import { uninstall } from './commands/uninstall.js';
9
9
  import { installHook, uninstallHook } from './commands/install-hook.js';
10
+ import { installPrecommit } from './commands/install-precommit.js';
10
11
  import { doctor } from './commands/doctor.js';
11
12
  import { init } from './commands/init.js';
12
13
  import { serve } from './commands/serve.js';
@@ -471,6 +472,11 @@ program
471
472
  .description('Remove the CodeBakers hook from Claude Code')
472
473
  .action(uninstallHook);
473
474
 
475
+ program
476
+ .command('install-precommit')
477
+ .description('Install git pre-commit hook for session enforcement (v6.0)')
478
+ .action(installPrecommit);
479
+
474
480
  program
475
481
  .command('doctor')
476
482
  .description('Check if CodeBakers is set up correctly')