rafcode 2.4.1-0 → 2.5.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/CLAUDE.md +4 -4
- package/RAF/ahwqwq-model-whisperer/decisions.md +22 -0
- package/RAF/ahwqwq-model-whisperer/input.md +5 -0
- package/RAF/ahwqwq-model-whisperer/outcomes/01-show-model-on-task-line.md +49 -0
- package/RAF/ahwqwq-model-whisperer/outcomes/02-use-claude-cost-estimation.md +107 -0
- package/RAF/ahwqwq-model-whisperer/outcomes/03-add-plan-resume-flag.md +87 -0
- package/RAF/ahwqwq-model-whisperer/plans/01-show-model-on-task-line.md +45 -0
- package/RAF/ahwqwq-model-whisperer/plans/02-use-claude-cost-estimation.md +115 -0
- package/RAF/ahwqwq-model-whisperer/plans/03-add-plan-resume-flag.md +70 -0
- package/RAF/ahwvrz-legacy-sunset/decisions.md +10 -0
- package/RAF/ahwvrz-legacy-sunset/input.md +10 -0
- package/RAF/ahwvrz-legacy-sunset/outcomes/01-remove-migrate-command.md +30 -0
- package/RAF/ahwvrz-legacy-sunset/outcomes/02-fix-resume-worktree-resolution.md +62 -0
- package/RAF/ahwvrz-legacy-sunset/plans/01-remove-migrate-command.md +65 -0
- package/RAF/ahwvrz-legacy-sunset/plans/02-fix-resume-worktree-resolution.md +72 -0
- package/README.md +0 -17
- package/dist/commands/do.js +13 -15
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +98 -2
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +8 -0
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +72 -0
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/parsers/stream-renderer.d.ts +2 -0
- package/dist/parsers/stream-renderer.d.ts.map +1 -1
- package/dist/parsers/stream-renderer.js +2 -0
- package/dist/parsers/stream-renderer.js.map +1 -1
- package/dist/prompts/amend.d.ts.map +1 -1
- package/dist/prompts/amend.js +3 -1
- package/dist/prompts/amend.js.map +1 -1
- package/dist/prompts/planning.d.ts.map +1 -1
- package/dist/prompts/planning.js +4 -1
- package/dist/prompts/planning.js.map +1 -1
- package/dist/types/config.d.ts +4 -28
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +0 -24
- package/dist/types/config.js.map +1 -1
- package/dist/utils/config.d.ts +1 -26
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +2 -98
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/terminal-symbols.d.ts +7 -16
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +16 -42
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +4 -30
- package/dist/utils/token-tracker.d.ts.map +1 -1
- package/dist/utils/token-tracker.js +17 -98
- package/dist/utils/token-tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/do.ts +14 -15
- package/src/commands/plan.ts +112 -1
- package/src/core/claude-runner.ts +81 -0
- package/src/index.ts +0 -2
- package/src/parsers/stream-renderer.ts +4 -0
- package/src/prompts/amend.ts +3 -1
- package/src/prompts/config-docs.md +1 -72
- package/src/prompts/planning.ts +4 -1
- package/src/types/config.ts +4 -57
- package/src/utils/config.ts +2 -112
- package/src/utils/terminal-symbols.ts +16 -46
- package/src/utils/token-tracker.ts +19 -113
- package/tests/unit/claude-runner.test.ts +1 -0
- package/tests/unit/config-command.test.ts +4 -13
- package/tests/unit/config.test.ts +6 -148
- package/tests/unit/plan-resume-worktree-resolution.test.ts +153 -0
- package/tests/unit/stream-renderer.test.ts +82 -0
- package/tests/unit/terminal-symbols.test.ts +86 -124
- package/tests/unit/token-tracker.test.ts +159 -679
- package/src/commands/migrate.ts +0 -269
- package/tests/unit/migrate-command.test.ts +0 -197
package/src/commands/migrate.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import { getRafDir, encodeBase26 } from '../utils/paths.js';
|
|
5
|
-
import { logger } from '../utils/logger.js';
|
|
6
|
-
import { getRepoBasename, computeWorktreeBaseDir } from '../core/worktree.js';
|
|
7
|
-
import type { MigrateCommandOptions } from '../types/config.js';
|
|
8
|
-
|
|
9
|
-
/** 3-char base36 legacy prefix: e.g., "007", "01a", "1zz" */
|
|
10
|
-
const LEGACY_3CHAR_PATTERN = /^([0-9a-z]{3})-(.+)$/;
|
|
11
|
-
|
|
12
|
-
/** 6-char base36 legacy prefix with at least one digit: e.g., "021h44", "00j3k1" */
|
|
13
|
-
const LEGACY_6CHAR_PATTERN = /^([0-9a-z]{6})-(.+)$/;
|
|
14
|
-
|
|
15
|
-
/** Already-migrated base26 prefix: all lowercase letters, no digits */
|
|
16
|
-
const BASE26_PATTERN = /^[a-z]{6}-/;
|
|
17
|
-
|
|
18
|
-
export interface MigrationEntry {
|
|
19
|
-
oldName: string;
|
|
20
|
-
newName: string;
|
|
21
|
-
oldPath: string;
|
|
22
|
-
newPath: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check if a 6-char prefix contains at least one digit (meaning it's legacy base36, not base26).
|
|
27
|
-
*/
|
|
28
|
-
function hasDigit(str: string): boolean {
|
|
29
|
-
return /[0-9]/.test(str);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Decode a base36 string of any length back to a number.
|
|
34
|
-
*/
|
|
35
|
-
function decodeBase36(str: string): number {
|
|
36
|
-
return parseInt(str, 36);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Scan a directory for legacy project folders and compute migrations.
|
|
41
|
-
* Returns an array of migration entries (old name -> new name).
|
|
42
|
-
*/
|
|
43
|
-
export function detectMigrations(dirPath: string): MigrationEntry[] {
|
|
44
|
-
if (!fs.existsSync(dirPath)) {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
49
|
-
const migrations: MigrationEntry[] = [];
|
|
50
|
-
|
|
51
|
-
for (const entry of entries) {
|
|
52
|
-
if (!entry.isDirectory()) continue;
|
|
53
|
-
|
|
54
|
-
const name = entry.name;
|
|
55
|
-
|
|
56
|
-
// Skip already-migrated base26 folders
|
|
57
|
-
if (BASE26_PATTERN.test(name)) continue;
|
|
58
|
-
|
|
59
|
-
// Try 3-char base36 pattern first
|
|
60
|
-
const match3 = name.match(LEGACY_3CHAR_PATTERN);
|
|
61
|
-
if (match3 && match3[1] && match3[2]) {
|
|
62
|
-
const prefix = match3[1];
|
|
63
|
-
const projectName = match3[2];
|
|
64
|
-
const numericValue = decodeBase36(prefix);
|
|
65
|
-
const newPrefix = encodeBase26(numericValue);
|
|
66
|
-
const newName = `${newPrefix}-${projectName}`;
|
|
67
|
-
|
|
68
|
-
migrations.push({
|
|
69
|
-
oldName: name,
|
|
70
|
-
newName,
|
|
71
|
-
oldPath: path.join(dirPath, name),
|
|
72
|
-
newPath: path.join(dirPath, newName),
|
|
73
|
-
});
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Try 6-char base36 pattern (must have at least one digit to distinguish from base26)
|
|
78
|
-
const match6 = name.match(LEGACY_6CHAR_PATTERN);
|
|
79
|
-
if (match6 && match6[1] && match6[2]) {
|
|
80
|
-
const prefix = match6[1];
|
|
81
|
-
if (!hasDigit(prefix)) continue; // Pure letters = already base26
|
|
82
|
-
|
|
83
|
-
const projectName = match6[2];
|
|
84
|
-
const numericValue = decodeBase36(prefix);
|
|
85
|
-
const newPrefix = encodeBase26(numericValue);
|
|
86
|
-
const newName = `${newPrefix}-${projectName}`;
|
|
87
|
-
|
|
88
|
-
migrations.push({
|
|
89
|
-
oldName: name,
|
|
90
|
-
newName,
|
|
91
|
-
oldPath: path.join(dirPath, name),
|
|
92
|
-
newPath: path.join(dirPath, newName),
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return migrations;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Check for collisions in a set of migrations (two old IDs mapping to the same base26 ID).
|
|
102
|
-
* Returns an array of collision descriptions if found.
|
|
103
|
-
*/
|
|
104
|
-
function findCollisions(migrations: MigrationEntry[]): string[] {
|
|
105
|
-
const targetNames = new Map<string, string[]>();
|
|
106
|
-
for (const m of migrations) {
|
|
107
|
-
const existing = targetNames.get(m.newName) ?? [];
|
|
108
|
-
existing.push(m.oldName);
|
|
109
|
-
targetNames.set(m.newName, existing);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const collisions: string[] = [];
|
|
113
|
-
for (const [newName, oldNames] of targetNames) {
|
|
114
|
-
if (oldNames.length > 1) {
|
|
115
|
-
collisions.push(`${oldNames.join(', ')} -> ${newName}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return collisions;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Check if a target name already exists on disk (collision with existing folder).
|
|
123
|
-
*/
|
|
124
|
-
function findExistingConflicts(migrations: MigrationEntry[]): string[] {
|
|
125
|
-
const conflicts: string[] = [];
|
|
126
|
-
for (const m of migrations) {
|
|
127
|
-
if (fs.existsSync(m.newPath)) {
|
|
128
|
-
conflicts.push(`${m.oldName} -> ${m.newName} (target already exists)`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return conflicts;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Execute migrations: rename folders on disk.
|
|
136
|
-
* Returns count of successful renames.
|
|
137
|
-
*/
|
|
138
|
-
function executeMigrations(migrations: MigrationEntry[]): { succeeded: number; errors: string[] } {
|
|
139
|
-
let succeeded = 0;
|
|
140
|
-
const errors: string[] = [];
|
|
141
|
-
|
|
142
|
-
for (const m of migrations) {
|
|
143
|
-
try {
|
|
144
|
-
fs.renameSync(m.oldPath, m.newPath);
|
|
145
|
-
succeeded++;
|
|
146
|
-
} catch (error) {
|
|
147
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
148
|
-
errors.push(`Failed to rename ${m.oldName}: ${msg}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return { succeeded, errors };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function createMigrateCommand(): Command {
|
|
156
|
-
const command = new Command('migrate-project-ids-base26')
|
|
157
|
-
.description('Rename project folders from legacy base36 IDs to base26 encoding')
|
|
158
|
-
.option('--dry-run', 'Preview changes without renaming')
|
|
159
|
-
.option('--worktree', 'Also migrate worktree project directories')
|
|
160
|
-
.action(async (options?: MigrateCommandOptions) => {
|
|
161
|
-
await runMigrateCommand(options);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
return command;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function runMigrateCommand(options?: MigrateCommandOptions): Promise<void> {
|
|
168
|
-
const dryRun = options?.dryRun ?? false;
|
|
169
|
-
const includeWorktree = options?.worktree ?? false;
|
|
170
|
-
|
|
171
|
-
// Collect directories to scan
|
|
172
|
-
const dirsToScan: Array<{ label: string; path: string }> = [];
|
|
173
|
-
|
|
174
|
-
// Main RAF directory
|
|
175
|
-
const rafDir = getRafDir();
|
|
176
|
-
dirsToScan.push({ label: 'RAF', path: rafDir });
|
|
177
|
-
|
|
178
|
-
// Worktree directories
|
|
179
|
-
if (includeWorktree) {
|
|
180
|
-
const repoBasename = getRepoBasename();
|
|
181
|
-
if (repoBasename) {
|
|
182
|
-
const worktreeBaseDir = computeWorktreeBaseDir(repoBasename);
|
|
183
|
-
if (fs.existsSync(worktreeBaseDir)) {
|
|
184
|
-
const worktreeEntries = fs.readdirSync(worktreeBaseDir, { withFileTypes: true });
|
|
185
|
-
for (const entry of worktreeEntries) {
|
|
186
|
-
if (entry.isDirectory()) {
|
|
187
|
-
const wtRafDir = path.join(worktreeBaseDir, entry.name, 'RAF');
|
|
188
|
-
if (fs.existsSync(wtRafDir)) {
|
|
189
|
-
dirsToScan.push({ label: `worktree:${entry.name}`, path: wtRafDir });
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
} else {
|
|
195
|
-
logger.warn('Not in a git repository — skipping worktree scan');
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Detect all migrations
|
|
200
|
-
let totalMigrations: MigrationEntry[] = [];
|
|
201
|
-
const migrationsByDir: Array<{ label: string; migrations: MigrationEntry[] }> = [];
|
|
202
|
-
|
|
203
|
-
for (const dir of dirsToScan) {
|
|
204
|
-
const migrations = detectMigrations(dir.path);
|
|
205
|
-
if (migrations.length > 0) {
|
|
206
|
-
migrationsByDir.push({ label: dir.label, migrations });
|
|
207
|
-
totalMigrations = totalMigrations.concat(migrations);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (totalMigrations.length === 0) {
|
|
212
|
-
logger.info('No legacy project folders found. Nothing to migrate.');
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Check for collisions
|
|
217
|
-
const collisions = findCollisions(totalMigrations);
|
|
218
|
-
if (collisions.length > 0) {
|
|
219
|
-
logger.error('Collision detected — multiple old IDs map to the same base26 ID:');
|
|
220
|
-
for (const c of collisions) {
|
|
221
|
-
logger.error(` ${c}`);
|
|
222
|
-
}
|
|
223
|
-
process.exit(1);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Check for existing conflicts (target folder already exists)
|
|
227
|
-
const conflicts = findExistingConflicts(totalMigrations);
|
|
228
|
-
if (conflicts.length > 0) {
|
|
229
|
-
logger.error('Target folder already exists:');
|
|
230
|
-
for (const c of conflicts) {
|
|
231
|
-
logger.error(` ${c}`);
|
|
232
|
-
}
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Print summary
|
|
237
|
-
if (dryRun) {
|
|
238
|
-
logger.info('Dry run — no changes will be made:');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
for (const group of migrationsByDir) {
|
|
242
|
-
if (migrationsByDir.length > 1) {
|
|
243
|
-
logger.info(`\n${group.label}:`);
|
|
244
|
-
}
|
|
245
|
-
for (const m of group.migrations) {
|
|
246
|
-
logger.info(` ${m.oldName} -> ${m.newName}`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (dryRun) {
|
|
251
|
-
logger.info(`\n${totalMigrations.length} folder${totalMigrations.length === 1 ? '' : 's'} would be renamed.`);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Execute migrations
|
|
256
|
-
const { succeeded, errors } = executeMigrations(totalMigrations);
|
|
257
|
-
|
|
258
|
-
if (errors.length > 0) {
|
|
259
|
-
for (const e of errors) {
|
|
260
|
-
logger.error(e);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
logger.success(`${succeeded} folder${succeeded === 1 ? '' : 's'} renamed.`);
|
|
265
|
-
if (errors.length > 0) {
|
|
266
|
-
logger.error(`${errors.length} rename${errors.length === 1 ? '' : 's'} failed.`);
|
|
267
|
-
process.exit(1);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import * as os from 'node:os';
|
|
4
|
-
import { detectMigrations, type MigrationEntry } from '../../src/commands/migrate.js';
|
|
5
|
-
import { encodeBase26 } from '../../src/utils/paths.js';
|
|
6
|
-
|
|
7
|
-
describe('migrate-project-ids-base26', () => {
|
|
8
|
-
let tempDir: string;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'raf-migrate-test-'));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe('detectMigrations', () => {
|
|
19
|
-
it('should detect 3-char base36 legacy folders', () => {
|
|
20
|
-
fs.mkdirSync(path.join(tempDir, '007-my-project'));
|
|
21
|
-
fs.mkdirSync(path.join(tempDir, '01a-feature'));
|
|
22
|
-
|
|
23
|
-
const migrations = detectMigrations(tempDir);
|
|
24
|
-
|
|
25
|
-
expect(migrations).toHaveLength(2);
|
|
26
|
-
|
|
27
|
-
// 007 in base36 = 7, encodeBase26(7) = "aaaaah"
|
|
28
|
-
const m007 = migrations.find(m => m.oldName === '007-my-project');
|
|
29
|
-
expect(m007).toBeDefined();
|
|
30
|
-
expect(m007!.newName).toBe(`${encodeBase26(parseInt('007', 36))}-my-project`);
|
|
31
|
-
expect(m007!.newName).toBe('aaaaah-my-project');
|
|
32
|
-
|
|
33
|
-
// 01a in base36 = 46, encodeBase26(46) = "aaaabu"
|
|
34
|
-
const m01a = migrations.find(m => m.oldName === '01a-feature');
|
|
35
|
-
expect(m01a).toBeDefined();
|
|
36
|
-
expect(m01a!.newName).toBe(`${encodeBase26(parseInt('01a', 36))}-feature`);
|
|
37
|
-
expect(m01a!.newName).toBe('aaaabu-feature');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should detect 6-char base36 legacy folders with digits', () => {
|
|
41
|
-
fs.mkdirSync(path.join(tempDir, '021h44-letterjam'));
|
|
42
|
-
fs.mkdirSync(path.join(tempDir, '00j3k1-fix-stuff'));
|
|
43
|
-
|
|
44
|
-
const migrations = detectMigrations(tempDir);
|
|
45
|
-
|
|
46
|
-
expect(migrations).toHaveLength(2);
|
|
47
|
-
|
|
48
|
-
const m1 = migrations.find(m => m.oldName === '021h44-letterjam');
|
|
49
|
-
expect(m1).toBeDefined();
|
|
50
|
-
expect(m1!.newName).toBe(`${encodeBase26(parseInt('021h44', 36))}-letterjam`);
|
|
51
|
-
|
|
52
|
-
const m2 = migrations.find(m => m.oldName === '00j3k1-fix-stuff');
|
|
53
|
-
expect(m2).toBeDefined();
|
|
54
|
-
expect(m2!.newName).toBe(`${encodeBase26(parseInt('00j3k1', 36))}-fix-stuff`);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should skip already-migrated base26 folders', () => {
|
|
58
|
-
fs.mkdirSync(path.join(tempDir, 'abcdef-my-project'));
|
|
59
|
-
fs.mkdirSync(path.join(tempDir, 'aaaaab-another'));
|
|
60
|
-
|
|
61
|
-
const migrations = detectMigrations(tempDir);
|
|
62
|
-
expect(migrations).toHaveLength(0);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should skip non-directory entries', () => {
|
|
66
|
-
fs.writeFileSync(path.join(tempDir, '007-not-a-dir'), 'file');
|
|
67
|
-
|
|
68
|
-
const migrations = detectMigrations(tempDir);
|
|
69
|
-
expect(migrations).toHaveLength(0);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should return empty array for non-existent directory', () => {
|
|
73
|
-
const migrations = detectMigrations(path.join(tempDir, 'nonexistent'));
|
|
74
|
-
expect(migrations).toHaveLength(0);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should return empty array when no legacy folders exist', () => {
|
|
78
|
-
fs.mkdirSync(path.join(tempDir, 'abcdef-project'));
|
|
79
|
-
fs.mkdirSync(path.join(tempDir, 'random-folder'));
|
|
80
|
-
|
|
81
|
-
const migrations = detectMigrations(tempDir);
|
|
82
|
-
expect(migrations).toHaveLength(0);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should handle mixed legacy and migrated folders', () => {
|
|
86
|
-
fs.mkdirSync(path.join(tempDir, '007-old-project'));
|
|
87
|
-
fs.mkdirSync(path.join(tempDir, 'abcdef-new-project'));
|
|
88
|
-
fs.mkdirSync(path.join(tempDir, '021h44-medium-project'));
|
|
89
|
-
|
|
90
|
-
const migrations = detectMigrations(tempDir);
|
|
91
|
-
expect(migrations).toHaveLength(2);
|
|
92
|
-
expect(migrations.map(m => m.oldName).sort()).toEqual(['007-old-project', '021h44-medium-project']);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should produce correct paths', () => {
|
|
96
|
-
fs.mkdirSync(path.join(tempDir, '007-my-project'));
|
|
97
|
-
|
|
98
|
-
const migrations = detectMigrations(tempDir);
|
|
99
|
-
expect(migrations[0]!.oldPath).toBe(path.join(tempDir, '007-my-project'));
|
|
100
|
-
expect(migrations[0]!.newPath).toBe(path.join(tempDir, 'aaaaah-my-project'));
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should handle 3-char base36 edge cases', () => {
|
|
104
|
-
// "000" = 0
|
|
105
|
-
fs.mkdirSync(path.join(tempDir, '000-zero'));
|
|
106
|
-
// "zzz" = 36^3 - 1 = 46655
|
|
107
|
-
fs.mkdirSync(path.join(tempDir, 'zzz-max'));
|
|
108
|
-
|
|
109
|
-
const migrations = detectMigrations(tempDir);
|
|
110
|
-
|
|
111
|
-
// "zzz" contains no digits but matches 3-char pattern
|
|
112
|
-
// Wait — "zzz" has no digits. The 3-char pattern is [0-9a-z]{3}
|
|
113
|
-
// so "zzz" matches the 3-char pattern (3 chars, all lowercase letters/digits)
|
|
114
|
-
// It gets detected as a legacy 3-char folder
|
|
115
|
-
|
|
116
|
-
const m000 = migrations.find(m => m.oldName === '000-zero');
|
|
117
|
-
expect(m000).toBeDefined();
|
|
118
|
-
expect(m000!.newName).toBe('aaaaaa-zero');
|
|
119
|
-
|
|
120
|
-
const mZzz = migrations.find(m => m.oldName === 'zzz-max');
|
|
121
|
-
expect(mZzz).toBeDefined();
|
|
122
|
-
expect(mZzz!.newName).toBe(`${encodeBase26(46655)}-max`);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should not match folders without a hyphen after the prefix', () => {
|
|
126
|
-
fs.mkdirSync(path.join(tempDir, '007'));
|
|
127
|
-
|
|
128
|
-
const migrations = detectMigrations(tempDir);
|
|
129
|
-
expect(migrations).toHaveLength(0);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should handle 6-char all-letter prefix that is not pure a-z', () => {
|
|
133
|
-
// "abcde1" has a digit, so it's legacy
|
|
134
|
-
fs.mkdirSync(path.join(tempDir, 'abcde1-mixed'));
|
|
135
|
-
|
|
136
|
-
const migrations = detectMigrations(tempDir);
|
|
137
|
-
expect(migrations).toHaveLength(1);
|
|
138
|
-
expect(migrations[0]!.oldName).toBe('abcde1-mixed');
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe('migration execution (integration)', () => {
|
|
143
|
-
it('should rename folders when executed', () => {
|
|
144
|
-
fs.mkdirSync(path.join(tempDir, '007-my-project'));
|
|
145
|
-
fs.writeFileSync(path.join(tempDir, '007-my-project', 'input.md'), 'test');
|
|
146
|
-
|
|
147
|
-
const migrations = detectMigrations(tempDir);
|
|
148
|
-
expect(migrations).toHaveLength(1);
|
|
149
|
-
|
|
150
|
-
// Simulate what executeMigrations does
|
|
151
|
-
const m = migrations[0]!;
|
|
152
|
-
fs.renameSync(m.oldPath, m.newPath);
|
|
153
|
-
|
|
154
|
-
// Old folder should not exist
|
|
155
|
-
expect(fs.existsSync(path.join(tempDir, '007-my-project'))).toBe(false);
|
|
156
|
-
// New folder should exist with contents
|
|
157
|
-
expect(fs.existsSync(path.join(tempDir, 'aaaaah-my-project'))).toBe(true);
|
|
158
|
-
expect(fs.readFileSync(path.join(tempDir, 'aaaaah-my-project', 'input.md'), 'utf-8')).toBe('test');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should preserve folder contents during rename', () => {
|
|
162
|
-
const oldDir = path.join(tempDir, '021h44-letterjam');
|
|
163
|
-
fs.mkdirSync(oldDir);
|
|
164
|
-
fs.mkdirSync(path.join(oldDir, 'plans'));
|
|
165
|
-
fs.mkdirSync(path.join(oldDir, 'outcomes'));
|
|
166
|
-
fs.writeFileSync(path.join(oldDir, 'input.md'), 'requirements');
|
|
167
|
-
fs.writeFileSync(path.join(oldDir, 'plans', '01-task.md'), 'plan');
|
|
168
|
-
|
|
169
|
-
const migrations = detectMigrations(tempDir);
|
|
170
|
-
const m = migrations[0]!;
|
|
171
|
-
fs.renameSync(m.oldPath, m.newPath);
|
|
172
|
-
|
|
173
|
-
expect(fs.existsSync(path.join(m.newPath, 'input.md'))).toBe(true);
|
|
174
|
-
expect(fs.existsSync(path.join(m.newPath, 'plans', '01-task.md'))).toBe(true);
|
|
175
|
-
expect(fs.readFileSync(path.join(m.newPath, 'input.md'), 'utf-8')).toBe('requirements');
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
describe('encoding correctness', () => {
|
|
180
|
-
it('3-char base36 IDs produce small base26 values', () => {
|
|
181
|
-
// 007 base36 = 7
|
|
182
|
-
expect(encodeBase26(7)).toBe('aaaaah');
|
|
183
|
-
// 023 base36 = 75
|
|
184
|
-
expect(encodeBase26(parseInt('023', 36))).toBe('aaaacx');
|
|
185
|
-
// 100 base36 = 1296
|
|
186
|
-
expect(encodeBase26(parseInt('100', 36))).toBe('aaabxw');
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('6-char base36 epoch IDs produce reasonable base26 values', () => {
|
|
190
|
-
// These are large numbers (seconds since epoch)
|
|
191
|
-
const val = parseInt('021h44', 36);
|
|
192
|
-
const encoded = encodeBase26(val);
|
|
193
|
-
expect(encoded).toHaveLength(6);
|
|
194
|
-
expect(/^[a-z]{6}$/.test(encoded)).toBe(true);
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
});
|