rafcode 2.5.0-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.
@@ -49,6 +49,7 @@ import {
49
49
  removeWorktree,
50
50
  computeWorktreeBaseDir,
51
51
  pullMainBranch,
52
+ resolveWorktreeProjectByIdentifier,
52
53
  } from '../core/worktree.js';
53
54
 
54
55
  interface PlanCommandOptions {
@@ -690,54 +691,58 @@ async function runResumeCommand(identifier: string, model?: string): Promise<voi
690
691
  process.exit(1);
691
692
  }
692
693
 
693
- // First, try to resolve the project from main repo
694
+ // Try to resolve the project from worktrees first, then fall back to main repo
695
+ const repoBasename = getRepoBasename();
694
696
  const rafDir = getRafDir();
695
- const mainResolution = resolveProjectIdentifierWithDetails(rafDir, identifier);
696
-
697
- if (!mainResolution.path) {
698
- if (mainResolution.error === 'ambiguous' && mainResolution.matches) {
699
- logger.error(`Ambiguous project name: ${identifier}`);
700
- logger.error('Multiple projects match:');
701
- for (const match of mainResolution.matches) {
702
- logger.error(` - ${match.folder}`);
697
+
698
+ let projectPath: string | undefined;
699
+ let resumeCwd: string | undefined;
700
+ let folderName: string | undefined;
701
+
702
+ // 1. Try worktree resolution first (if we're in a git repo)
703
+ if (repoBasename) {
704
+ const worktreeResolution = resolveWorktreeProjectByIdentifier(repoBasename, identifier);
705
+
706
+ if (worktreeResolution) {
707
+ // Found in worktree - validate and use it
708
+ const wtValidation = validateWorktree(worktreeResolution.worktreeRoot, '');
709
+
710
+ if (wtValidation.isValidWorktree) {
711
+ folderName = worktreeResolution.folder;
712
+ const repoRoot = getRepoRoot()!;
713
+ const rafRelativePath = path.relative(repoRoot, rafDir);
714
+ projectPath = path.join(worktreeResolution.worktreeRoot, rafRelativePath, folderName);
715
+ resumeCwd = worktreeResolution.worktreeRoot;
716
+ logger.info(`Resuming session in worktree: ${resumeCwd}`);
717
+ } else {
718
+ logger.warn(`Worktree found but invalid: ${worktreeResolution.worktreeRoot}`);
719
+ logger.warn('Falling back to main repo resolution.');
720
+ // Fall through to main repo resolution
703
721
  }
704
- logger.error('Please specify the project ID or full folder name.');
705
- } else {
706
- logger.error(`Project not found: ${identifier}`);
707
722
  }
708
- process.exit(1);
709
723
  }
710
724
 
711
- const projectPath = mainResolution.path;
712
- const folderName = path.basename(projectPath);
713
-
714
- // Determine if this is a worktree project by checking if a worktree exists
715
- let resumeCwd = projectPath; // Default to main repo project path
716
- const repoBasename = getRepoBasename();
717
-
718
- if (repoBasename) {
719
- const worktreeBaseDir = computeWorktreeBaseDir(repoBasename);
725
+ // 2. If not found in worktree (or invalid), try main repo
726
+ if (!projectPath) {
727
+ const mainResolution = resolveProjectIdentifierWithDetails(rafDir, identifier);
720
728
 
721
- // Check if a worktree exists for this project
722
- if (fs.existsSync(worktreeBaseDir)) {
723
- const entries = fs.readdirSync(worktreeBaseDir, { withFileTypes: true });
724
- const worktreeEntry = entries.find(
725
- (entry) => entry.isDirectory() && entry.name === folderName
726
- );
727
-
728
- if (worktreeEntry) {
729
- // Worktree exists - use it as the CWD
730
- const worktreePath = path.join(worktreeBaseDir, worktreeEntry.name);
731
- const wtValidation = validateWorktree(worktreePath, '');
732
- if (wtValidation.isValidWorktree) {
733
- resumeCwd = worktreePath;
734
- logger.info(`Resuming session in worktree: ${worktreePath}`);
735
- } else {
736
- logger.warn(`Worktree found but invalid: ${worktreePath}`);
737
- logger.warn('Falling back to main repo path.');
729
+ if (!mainResolution.path) {
730
+ if (mainResolution.error === 'ambiguous' && mainResolution.matches) {
731
+ logger.error(`Ambiguous project name: ${identifier}`);
732
+ logger.error('Multiple projects match:');
733
+ for (const match of mainResolution.matches) {
734
+ logger.error(` - ${match.folder}`);
738
735
  }
736
+ logger.error('Please specify the project ID or full folder name.');
737
+ } else {
738
+ logger.error(`Project not found: ${identifier}`);
739
739
  }
740
+ process.exit(1);
740
741
  }
742
+
743
+ projectPath = mainResolution.path;
744
+ folderName = path.basename(projectPath);
745
+ resumeCwd = projectPath; // Use main repo project path as CWD
741
746
  }
742
747
 
743
748
  logger.info(`Project: ${folderName}`);
package/src/index.ts CHANGED
@@ -4,7 +4,6 @@ import { Command } from 'commander';
4
4
  import { createPlanCommand } from './commands/plan.js';
5
5
  import { createDoCommand } from './commands/do.js';
6
6
  import { createStatusCommand } from './commands/status.js';
7
- import { createMigrateCommand } from './commands/migrate.js';
8
7
  import { createConfigCommand } from './commands/config.js';
9
8
  import { getVersion } from './utils/version.js';
10
9
 
@@ -18,7 +17,6 @@ program
18
17
  program.addCommand(createPlanCommand());
19
18
  program.addCommand(createDoCommand());
20
19
  program.addCommand(createStatusCommand());
21
- program.addCommand(createMigrateCommand());
22
20
  program.addCommand(createConfigCommand());
23
21
 
24
22
  program.parse();
@@ -200,7 +200,9 @@ This is rarely needed — prefer using the \`effort\` label so the user's config
200
200
 
201
201
  After creating all new plan files:
202
202
  1. Provide a summary of:
203
- - The new tasks you've created
203
+ - The new tasks you've created, including the effort level for each task. Example format:
204
+ - Task 02: add-caching (effort: medium)
205
+ - Task 03: update-docs (effort: low)
204
206
  - How they relate to existing tasks
205
207
  - Total task count in the project
206
208
  2. Display this exit message to the user:
@@ -168,7 +168,10 @@ or for multiple dependencies:
168
168
  ### Step 5: Confirm Completion
169
169
 
170
170
  After creating all plan files:
171
- 1. Provide a summary of the tasks you've created
171
+ 1. Provide a summary of the tasks you've created, including the effort level for each task. Example:
172
+ - Task 01: setup-database (effort: low)
173
+ - Task 02: implement-auth (effort: medium)
174
+ - Task 03: refactor-api (effort: high)
172
175
  2. Display this exit message to the user:
173
176
 
174
177
  \`\`\`
@@ -138,11 +138,6 @@ export interface StatusCommandOptions {
138
138
  json?: boolean;
139
139
  }
140
140
 
141
- export interface MigrateCommandOptions {
142
- dryRun?: boolean;
143
- worktree?: boolean;
144
- }
145
-
146
141
  /** Per-model token usage breakdown from stream-json result event. */
147
142
  export interface ModelTokenUsage {
148
143
  inputTokens: number;
@@ -0,0 +1,153 @@
1
+ import { jest } from '@jest/globals';
2
+
3
+ /**
4
+ * Tests for worktree resolution in the --resume command.
5
+ *
6
+ * When `raf plan --resume <identifier>` is run, it should:
7
+ * 1. Search worktrees first
8
+ * 2. Fall back to main repo if not found in worktree
9
+ * 3. Auto-detect worktree mode without requiring --worktree flag
10
+ */
11
+
12
+ describe('Plan Resume - Worktree Resolution Logic', () => {
13
+ describe('resolution flow logic', () => {
14
+ it('should prioritize worktree over main repo when both exist', () => {
15
+ // This test verifies the conceptual resolution order used in runResumeCommand:
16
+ // 1. Try worktree resolution via resolveWorktreeProjectByIdentifier()
17
+ // 2. If found and valid → use worktree path and worktree root as CWD
18
+ // 3. If not found → fall back to main repo resolution
19
+
20
+ // Simulating the flow:
21
+ const worktreeFound = true;
22
+ const worktreeValid = true;
23
+
24
+ let useWorktree = false;
25
+ let useMainRepo = false;
26
+
27
+ // Step 1: Try worktree
28
+ if (worktreeFound && worktreeValid) {
29
+ useWorktree = true;
30
+ }
31
+
32
+ // Step 2: Fall back to main repo if worktree not used
33
+ if (!useWorktree) {
34
+ useMainRepo = true;
35
+ }
36
+
37
+ expect(useWorktree).toBe(true);
38
+ expect(useMainRepo).toBe(false);
39
+ });
40
+
41
+ it('should fall back to main repo when worktree is invalid', () => {
42
+ const worktreeFound = true;
43
+ const worktreeValid = false;
44
+
45
+ let useWorktree = false;
46
+ let useMainRepo = false;
47
+
48
+ if (worktreeFound && worktreeValid) {
49
+ useWorktree = true;
50
+ }
51
+
52
+ if (!useWorktree) {
53
+ useMainRepo = true;
54
+ }
55
+
56
+ expect(useWorktree).toBe(false);
57
+ expect(useMainRepo).toBe(true);
58
+ });
59
+
60
+ it('should fall back to main repo when worktree not found', () => {
61
+ const worktreeFound = false;
62
+
63
+ let useWorktree = false;
64
+ let useMainRepo = false;
65
+
66
+ if (worktreeFound) {
67
+ useWorktree = true;
68
+ }
69
+
70
+ if (!useWorktree) {
71
+ useMainRepo = true;
72
+ }
73
+
74
+ expect(useWorktree).toBe(false);
75
+ expect(useMainRepo).toBe(true);
76
+ });
77
+ });
78
+
79
+ describe('resumeCwd determination', () => {
80
+ it('should set resumeCwd to worktree root when project found in valid worktree', () => {
81
+ // Simulated values
82
+ const worktreeRoot = '/Users/user/.raf/worktrees/RAF/ahwvrz-legacy-sunset';
83
+ const projectPath = '/Users/user/.raf/worktrees/RAF/ahwvrz-legacy-sunset/RAF/ahwvrz-legacy-sunset';
84
+
85
+ const worktreeValid = true;
86
+ let resumeCwd: string | undefined;
87
+
88
+ if (worktreeValid) {
89
+ resumeCwd = worktreeRoot;
90
+ }
91
+
92
+ expect(resumeCwd).toBe(worktreeRoot);
93
+ });
94
+
95
+ it('should set resumeCwd to project path when using main repo', () => {
96
+ const mainRepoProjectPath = '/Users/user/myapp/RAF/ahwvrz-legacy-sunset';
97
+
98
+ const worktreeFound = false;
99
+ let resumeCwd: string | undefined;
100
+
101
+ if (!worktreeFound) {
102
+ resumeCwd = mainRepoProjectPath;
103
+ }
104
+
105
+ expect(resumeCwd).toBe(mainRepoProjectPath);
106
+ });
107
+ });
108
+
109
+ describe('variable initialization', () => {
110
+ it('should handle undefined variables correctly when worktree is found', () => {
111
+ let projectPath: string | undefined;
112
+ let resumeCwd: string | undefined;
113
+ let folderName: string | undefined;
114
+
115
+ // Simulate worktree resolution
116
+ const worktreeResolution = {
117
+ folder: 'ahwvrz-legacy-sunset',
118
+ worktreeRoot: '/Users/user/.raf/worktrees/RAF/ahwvrz-legacy-sunset',
119
+ };
120
+
121
+ if (worktreeResolution) {
122
+ folderName = worktreeResolution.folder;
123
+ projectPath = `/path/to/${folderName}`;
124
+ resumeCwd = worktreeResolution.worktreeRoot;
125
+ }
126
+
127
+ expect(folderName).toBeDefined();
128
+ expect(projectPath).toBeDefined();
129
+ expect(resumeCwd).toBeDefined();
130
+ });
131
+
132
+ it('should handle undefined variables correctly when falling back to main repo', () => {
133
+ let projectPath: string | undefined;
134
+ let resumeCwd: string | undefined;
135
+ let folderName: string | undefined;
136
+
137
+ // Worktree resolution returns null
138
+ const worktreeResolution = null;
139
+
140
+ // Skip worktree assignment since it's null
141
+ if (!projectPath && !worktreeResolution) {
142
+ // Main repo resolution
143
+ projectPath = '/Users/user/myapp/RAF/ahwvrz-legacy-sunset';
144
+ folderName = 'ahwvrz-legacy-sunset';
145
+ resumeCwd = projectPath;
146
+ }
147
+
148
+ expect(folderName).toBeDefined();
149
+ expect(projectPath).toBeDefined();
150
+ expect(resumeCwd).toBeDefined();
151
+ });
152
+ });
153
+ });
@@ -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
- }